Files
model-viewer/tools/img.js
T
2026-09-01 01:04:57 +01:00

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 };