64 lines
1.9 KiB
JavaScript
64 lines
1.9 KiB
JavaScript
// IMG archive reader.
|
|
//
|
|
// San Andreas ships VER2 archives: the 'VER2' tag, a u32 entry count, then one
|
|
// 32 byte directory entry per file -
|
|
// u32 offset, in 2048 byte sectors
|
|
// u16 streaming size, u16 size in archive (zero on retail, so prefer the former)
|
|
// char name[24], null padded
|
|
// Bodies follow the directory, each starting on a sector boundary.
|
|
|
|
const fs = require('fs');
|
|
|
|
const SECTOR = 2048;
|
|
|
|
class Img {
|
|
constructor(file) {
|
|
this.fd = fs.openSync(file, 'r');
|
|
this.path = file;
|
|
|
|
const header = Buffer.alloc(8);
|
|
fs.readSync(this.fd, header, 0, 8, 0);
|
|
if (header.toString('ascii', 0, 4) !== 'VER2') {
|
|
throw new Error(`${file} is not a VER2 img archive`);
|
|
}
|
|
const count = header.readUInt32LE(4);
|
|
|
|
const dir = Buffer.alloc(count * 32);
|
|
fs.readSync(this.fd, dir, 0, dir.length, 8);
|
|
|
|
// Lower cased, because the ide names and the archive names disagree on case.
|
|
this.entries = new Map();
|
|
for (let i = 0; i < count; i++) {
|
|
const at = i * 32;
|
|
const name = dir.toString('ascii', at + 8, at + 32).split('\0')[0];
|
|
const sectors = dir.readUInt16LE(at + 4) || dir.readUInt16LE(at + 6);
|
|
this.entries.set(name.toLowerCase(), {
|
|
name,
|
|
offset: dir.readUInt32LE(at) * SECTOR,
|
|
size: sectors * SECTOR,
|
|
});
|
|
}
|
|
}
|
|
|
|
has(name) {
|
|
return this.entries.has(name.toLowerCase());
|
|
}
|
|
|
|
// Entry bodies are padded out to the sector size, and every reader downstream
|
|
// walks chunk headers rather than trusting the length, so the padding is
|
|
// harmless and gets written as-is.
|
|
read(name) {
|
|
const entry = this.entries.get(name.toLowerCase());
|
|
if (!entry) return null;
|
|
const buffer = Buffer.alloc(entry.size);
|
|
fs.readSync(this.fd, buffer, 0, entry.size, entry.offset);
|
|
return buffer;
|
|
}
|
|
|
|
close() {
|
|
fs.closeSync(this.fd);
|
|
}
|
|
}
|
|
|
|
module.exports = { Img };
|