97 lines
4.7 KiB
TypeScript
97 lines
4.7 KiB
TypeScript
/**
|
|
* Round-trip check for the MPQ reader against an archive written by
|
|
* `scripts/make-mpq-fixture.ts`.
|
|
*
|
|
* The archive's contents are known byte for byte, so this is a two-sided test:
|
|
* the reader must list exactly the packed names, report exactly the packed sizes
|
|
* and flags, and return exactly the packed bytes — for members stored raw *and*
|
|
* for members stored as zlib sectors, which are the two read paths that exist.
|
|
*
|
|
* Usage: node scripts/verify-mpq-roundtrip.ts <fixture-directory>
|
|
*/
|
|
import { readFile } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
|
import { memorySource } from '../src/mpq/source.ts'
|
|
|
|
const dir = process.argv[2]
|
|
if (dir === undefined) {
|
|
console.error('usage: node scripts/verify-mpq-roundtrip.ts <fixture-directory>')
|
|
process.exit(2)
|
|
}
|
|
|
|
const archive = await MpqArchive.open(memorySource(new Uint8Array(await readFile(join(dir, 'fixture.mpq'))), 'fixture.mpq'))
|
|
const names = await archive.listFiles()
|
|
const problems: string[] = []
|
|
|
|
// `listFiles()` returns the names the archive publishes — the members named by
|
|
// `(listfile)` — which by design is not the listfile itself.
|
|
const published = [
|
|
'actor.dc6',
|
|
'data/global/excel/armor.txt', 'data/global/excel/experience.txt',
|
|
'data/global/excel/magicprefix.txt', 'data/global/excel/magicsuffix.txt',
|
|
'data/global/excel/misc.txt', 'data/global/excel/monstats.txt', 'data/global/excel/npcs.txt',
|
|
'data/global/excel/quests.txt', 'data/global/excel/skills.txt', 'data/global/excel/weapons.txt',
|
|
'data/local/string.tbl',
|
|
'fixture.dc6', 'fixture.ds1', 'fixture.dt1', 'palette.pal',
|
|
]
|
|
const listed = [...names].sort()
|
|
if (JSON.stringify(listed) !== JSON.stringify(published)) {
|
|
problems.push(`published names: ${JSON.stringify(listed)} != ${JSON.stringify(published)}`)
|
|
}
|
|
// Every member, the listfile included, must be present as a block.
|
|
const expectedNames = ['(listfile)', ...published]
|
|
if (archive.files().length !== expectedNames.length) {
|
|
problems.push(`occupied blocks: ${String(archive.files().length)} != ${String(expectedNames.length)}`)
|
|
}
|
|
|
|
// `(listfile)` is synthesised by the packer rather than read from disk, so its
|
|
// expected bytes are rebuilt here the same way the packer built them.
|
|
const synthetic = new TextEncoder().encode(`${[
|
|
'fixture.ds1', 'fixture.dt1', 'palette.pal', 'fixture.dc6', 'actor.dc6',
|
|
'data/global/excel/monstats.txt', 'data/global/excel/experience.txt',
|
|
'data/global/excel/weapons.txt', 'data/global/excel/armor.txt', 'data/global/excel/misc.txt',
|
|
'data/global/excel/magicprefix.txt', 'data/global/excel/magicsuffix.txt',
|
|
'data/global/excel/skills.txt', 'data/global/excel/npcs.txt', 'data/global/excel/quests.txt',
|
|
'data/local/string.tbl',
|
|
].join('\r\n')}\r\n`)
|
|
|
|
let comparedBytes = 0
|
|
for (const name of expectedNames) {
|
|
// Table members are stored under a directory path; on disk they live in the
|
|
// same tree, so the member name doubles as a relative path.
|
|
const onDisk = name === '(listfile)'
|
|
? synthetic
|
|
: new Uint8Array(await readFile(join(dir, name)))
|
|
const entry = archive.find(name)
|
|
if (entry === undefined) { problems.push(`${name}: not found by the reader`); continue }
|
|
if (entry.fileSize !== onDisk.byteLength) {
|
|
problems.push(`${name}: reported size ${String(entry.fileSize)} != ${String(onDisk.byteLength)}`)
|
|
}
|
|
const decoded = await archive.read(entry)
|
|
if (decoded.byteLength !== onDisk.byteLength) {
|
|
problems.push(`${name}: decoded ${String(decoded.byteLength)} bytes != ${String(onDisk.byteLength)}`)
|
|
continue
|
|
}
|
|
let firstDifference = -1
|
|
for (let at = 0; at < onDisk.byteLength; at += 1) {
|
|
if (decoded[at] !== onDisk[at]) { firstDifference = at; break }
|
|
}
|
|
if (firstDifference !== -1) {
|
|
problems.push(`${name}: byte ${String(firstDifference)} differs (${String(decoded[firstDifference])} != ${String(onDisk[firstDifference])})`)
|
|
continue
|
|
}
|
|
comparedBytes += decoded.byteLength
|
|
const flags = entry.flags >>> 0
|
|
const compressed = (flags & 0x00000200) !== 0
|
|
console.log(` ${name.padEnd(14)} ${String(decoded.byteLength).padStart(7)} bytes ${compressed ? 'zlib sectors' : 'stored'} flags 0x${flags.toString(16)}`)
|
|
}
|
|
|
|
console.log(`archive ${join(dir, 'fixture.mpq')}`)
|
|
console.log(`members ${String(archive.files().length)} blocks, ${String(names.length)} published names`)
|
|
console.log(`compared ${String(comparedBytes)} bytes across ${String(expectedNames.length)} members`)
|
|
console.log(`problems ${String(problems.length)}`)
|
|
for (const problem of problems.slice(0, 8)) console.log(` - ${problem}`)
|
|
console.log(problems.length === 0 ? 'RESULT reader round-trips the packed archive exactly' : 'RESULT FAILED')
|
|
process.exit(problems.length === 0 ? 0 : 1)
|