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