Files
Perler_beads/tools/import-cached-map.cjs
2026-10-20 13:13:03 +08:00

50 lines
4.8 KiB
JavaScript

// Offline extraction only. Validate the decoder against an existing known package.
const fs = require('fs'), path = require('path'), crypto = require('crypto'), zlib = require('zlib');
const root = path.resolve(__dirname, '..'), app = 'wxe12810fe39ff34db';
const [input, group] = process.argv.slice(2);
if (!input || !/^map_(19|21)$/.test(group)) throw Error('Expected explicit cached package path and map_19 or map_21');
const sha = b => crypto.createHash('sha256').update(b).digest('hex');
function decrypt(b) {
if (b.toString('ascii', 0, 6) !== 'V1MMWX' || b.length < 1030) throw Error('Unexpected encrypted package');
const dec = crypto.createDecipheriv('aes-256-cbc', crypto.pbkdf2Sync(app, 'saltiest', 1000, 32, 'sha1'), 'the iv: 16 bytes');
dec.setAutoPadding(false);
const head = Buffer.concat([dec.update(b.subarray(6, 1030)), dec.final()]).subarray(0, 1023);
const tail = Buffer.from(b.subarray(1030));
for (let i = 0; i < tail.length; i++) tail[i] ^= app.charCodeAt(app.length - 2);
return Buffer.concat([head, tail]);
}
const packageRoot = path.join(root, 'recovered', app, 'v11', 'packages');
const control = path.join(packageRoot, '_res_map_1_.wxapkg');
if (!decrypt(fs.readFileSync(control)).equals(fs.readFileSync(control + '.dec'))) throw Error('Known decoder control failed');
const bytes = fs.readFileSync(path.resolve(input)), decoded = decrypt(bytes);
if (decoded[0] !== 0xbe || decoded[13] !== 0xed || decoded.length !== 14 + decoded.readUInt32BE(5) + decoded.readUInt32BE(9)) throw Error('Invalid package header');
const count = decoded.readUInt32BE(14), indexEnd = 14 + decoded.readUInt32BE(5), members = [];
let cursor = 18;
for (let i = 0; i < count; i++) {
if (cursor + 4 > indexEnd) throw Error('Index overflow');
const n = decoded.readUInt32BE(cursor); cursor += 4;
if (!n || cursor + n + 8 > indexEnd) throw Error('Invalid member name length');
const name = decoded.toString('utf8', cursor, cursor + n); cursor += n;
const offset = decoded.readUInt32BE(cursor), length = decoded.readUInt32BE(cursor + 4); cursor += 8;
if (!new RegExp(`^/res/${group}/(?:level[0-9_]+\\.json|game\\.js|[^/]+\\.txt)$`).test(name) || offset < indexEnd || offset + length > decoded.length) throw Error('Unexpected member: ' + name);
members.push({ name, offset, length, bytes: decoded.subarray(offset, offset + length) });
}
if (cursor !== indexEnd) throw Error('Index size mismatch');
const levels = members.filter(m => m.name.endsWith('.json')).map(m => {
let data; try { data = JSON.parse(m.bytes.toString()); } catch { data = JSON.parse(zlib.brotliDecompressSync(m.bytes)); }
const board = typeof data.cubeData === 'string' ? JSON.parse(data.cubeData) : data.cubeData;
const cars = typeof data.cannon_order === 'string' ? JSON.parse(data.cannon_order) : data.cannon_order;
if (!Array.isArray(board) || !board.length || !board.every(r => Array.isArray(r) && r.length === board[0].length && r.every(v => Number.isInteger(v) && v >= 0)) || !Array.isArray(cars)) throw Error('Invalid map: ' + m.name);
return { member: m, data, normalized: Buffer.from(JSON.stringify(data)), rows: board.length, cols: board[0].length, pixels: board.reduce((s, r) => s + r.filter(v => v !== 7000).length, 0), fields: Object.keys(data) };
});
// Preflight every write before creating any output; never overwrite a differing file.
const writes = [{ file: path.join(packageRoot, `_res_${group}_.wxapkg`), bytes }, { file: path.join(packageRoot, `_res_${group}_.wxapkg.dec`), bytes: decoded }];
for (const m of members) writes.push({ file: path.join(root, 'recovered', app, 'v11', 'unpacked', `_res_${group}_.wxapkg.unpacked`, m.name.slice(1)), bytes: m.bytes });
for (const l of levels) for (const dir of ['assets/resources/levels', 'preview/resources/levels']) writes.push({ file: path.join(root, dir, path.basename(l.member.name)), bytes: l.normalized });
for (const w of writes) if (fs.existsSync(w.file) && !fs.readFileSync(w.file).equals(w.bytes)) throw Error('Refuse differing existing file: ' + w.file);
for (const w of writes) { fs.mkdirSync(path.dirname(w.file), { recursive: true }); if (!fs.existsSync(w.file)) fs.writeFileSync(w.file, w.bytes, { flag: 'wx' }); }
const report = { at: new Date().toISOString(), source: path.resolve(input), group, packageSha256: sha(bytes), decodedSha256: sha(decoded), knownControlMatches: true, memberCount: count, levels: levels.map(l => ({ resource: path.basename(l.member.name, '.json'), rawSha256: sha(l.member.bytes), normalizedSha256: sha(l.normalized), rows: l.rows, cols: l.cols, pixels: l.pixels, fields: l.fields })) };
const reportFile = path.join(root, 'artifacts/home-levels-20261018', `${group}-import.json`);
fs.writeFileSync(reportFile, JSON.stringify(report, null, 2));
console.log(JSON.stringify({ group, memberCount: count, importedLevels: levels.length, packageSha256: report.packageSha256, reportFile }));