78 lines
2.8 KiB
TypeScript
78 lines
2.8 KiB
TypeScript
/**
|
|
* MPQ inspection CLI (Node-side tooling).
|
|
*
|
|
* Usage:
|
|
* node scripts/inspect-mpq.ts <archive> [header|list|hist|extract <name> <out>]
|
|
*
|
|
* `header` prints the parsed v1 header, `list` the name list, `hist` the
|
|
* compression masks the archive actually uses (which decoders are required),
|
|
* and `extract` decodes one member to disk for external verification.
|
|
*/
|
|
import { writeFile } from 'node:fs/promises'
|
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
|
import { fileSource } from '../src/mpq/file-source.ts'
|
|
|
|
const [path, command = 'header', ...rest] = process.argv.slice(2)
|
|
|
|
if (path === undefined) {
|
|
console.error('usage: node scripts/inspect-mpq.ts <archive> [header|list|hist|extract <name> <out>]')
|
|
process.exit(2)
|
|
}
|
|
|
|
const archive = await MpqArchive.open(await fileSource(path))
|
|
|
|
switch (command) {
|
|
case 'header': {
|
|
const h = archive.header
|
|
console.log(`archive ${path}`)
|
|
console.log(`headerSize ${String(h.headerSize)}`)
|
|
console.log(`archiveSize ${String(h.archiveSize)}`)
|
|
console.log(`format v${String(h.formatVersion + 1)}`)
|
|
console.log(`sectorSize ${String(h.sectorSize)}`)
|
|
console.log(`hashTable @${String(h.hashTableOffset)} (${String(h.hashTableEntries)} entries)`)
|
|
console.log(`blockTable @${String(h.blockTableOffset)} (${String(h.blockTableEntries)} entries)`)
|
|
const occupied = archive.files().length
|
|
console.log(`files ${String(occupied)} occupied block slots`)
|
|
for (const [flag, count] of archive.flagHistogram()) {
|
|
console.log(` ${flag.padEnd(12)} ${String(count)}`)
|
|
}
|
|
break
|
|
}
|
|
case 'list': {
|
|
const names = await archive.listFiles()
|
|
console.log(`${String(names.length)} names`)
|
|
for (const name of names.slice(0, 40)) console.log(` ${name}`)
|
|
if (names.length > 40) console.log(` … ${String(names.length - 40)} more`)
|
|
break
|
|
}
|
|
case 'hist': {
|
|
const hist = await archive.compressionHistogram(await archive.nameIndex())
|
|
const total = [...hist.values()].reduce((a, b) => a + b, 0)
|
|
console.log(`compression masks over ${String(total)} files`)
|
|
for (const [mask, count] of [...hist].sort((a, b) => b[1] - a[1])) {
|
|
console.log(` ${String(count).padStart(6)} ${mask}`)
|
|
}
|
|
break
|
|
}
|
|
case 'extract': {
|
|
const [name, out] = rest
|
|
if (name === undefined || out === undefined) {
|
|
console.error('usage: … extract <name> <out>')
|
|
process.exit(2)
|
|
}
|
|
const file = archive.find(name)
|
|
if (file === undefined) {
|
|
console.error(`no such file: ${name}`)
|
|
process.exit(1)
|
|
}
|
|
const data = await archive.read(file)
|
|
await writeFile(out, data)
|
|
console.log(`wrote ${out} (${String(data.byteLength)} bytes, expected ${String(file.fileSize)})`)
|
|
break
|
|
}
|
|
default: {
|
|
console.error(`unknown command: ${command}`)
|
|
process.exit(2)
|
|
}
|
|
}
|