import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { gunzipSync } from 'node:zlib'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; export const gameDbRoot = fileURLToPath(new URL('../data/game-db/', import.meta.url)); export const sha256 = bytes => createHash('sha256').update(bytes).digest('hex'); export function openGameDb(pointer, root = gameDbRoot) { if (!/^snapshots\/[a-zA-Z0-9]{64}$/.test(pointer.snapshot ?? '')) throw new Error('Invalid game DB snapshot path'); const directory = path.join(root, pointer.snapshot); const bytes = readFileSync(path.join(directory, 'manifest.json')); if (sha256(bytes) !== pointer.manifestSha256) throw new Error('Game DB manifest hash mismatch'); const manifest = JSON.parse(bytes); if (manifest.schemaVersion !== 1 || manifest.kind !== 'android-master-db' || manifest.locale !== 'kor' || pointer.snapshot !== `snapshots/${manifest.masterDataVersion}` || !Object.keys(manifest.tables ?? {}).length) { throw new Error('Invalid game DB manifest'); } return { ...manifest, ...pointer, directory }; } export function sourceMetadata(source) { return { kind: source.kind, masterDataVersion: source.masterDataVersion, locale: source.locale, capturedAt: source.capturedAt, manifestSha256: source.manifestSha256 }; } export function prepareGameDbSource(locale = 'kor', { review = false } = {}) { if (locale !== 'kor') throw new Error(`Current game DB contains Korean only; no external fallback for ${locale}`); const pointer = JSON.parse(readFileSync(path.join(gameDbRoot, 'current.json'), 'utf8')); const source = openGameDb(pointer); if (!review) { let report; try { report = JSON.parse(readFileSync(path.join(gameDbRoot, 'review/latest.json'), 'utf8')); } catch { throw new Error('Run npm run data:sync and report its changes before data:apply.'); } if (report.target.manifestSha256 !== source.manifestSha256) throw new Error('Game DB changed after review; run data:sync again.'); } return source; } export function readGameDbText(source, filename) { const name = filename.replace(/\.json$/, ''); const table = source.tables[name]; if (!table || table.file !== `${name}.json.gz` || !/^[A-Za-z0-9_]+$/.test(name)) throw new Error(`Missing game DB table: ${filename}`); const packed = readFileSync(path.join(source.directory, table.file)); if (sha256(packed) !== table.gzipSha256) throw new Error(`Game DB archive hash mismatch: ${name}`); const raw = gunzipSync(packed); if (sha256(raw) !== table.sha256) throw new Error(`Game DB table hash mismatch: ${name}`); const rows = JSON.parse(raw); if (!Array.isArray(rows) || rows.length !== table.rows) throw new Error(`Game DB row count mismatch: ${name}`); return raw.toString('utf8'); } export function readGameDbTable(locale, filename) { return JSON.parse(readGameDbText(prepareGameDbSource(locale), filename)); }