import { XMLParser } from 'fast-xml-parser'; export type DatRom = { name: string; size?: number; crc?: string; md5?: string; sha1?: string; }; export type DatGame = { name: string; roms: DatRom[]; }; export type DatDatabase = { games: DatGame[]; }; function ensureArray(v: T | T[] | undefined): T[] { if (v === undefined || v === null) return []; return Array.isArray(v) ? v : [v]; } function normalizeHex(v?: string): string | undefined { if (!v) return undefined; return v.trim().toLowerCase(); } export function parseDat(xml: string): DatDatabase { const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', trimValues: true, }); const parsed = parser.parse(xml as any) as any; const datafile = parsed?.datafile ?? parsed; const rawGames = ensureArray(datafile?.game); const games: DatGame[] = rawGames.map((g: any) => { // game name may be an attribute or a child node const nameAttr = g?.name ?? g?.['@_name'] ?? (g?.$?.name as any); const romNodes = ensureArray(g?.rom); const roms: DatRom[] = romNodes.map((r: any) => { const rname = r?.name ?? r?.['@_name'] ?? r?.['@name']; const sizeRaw = r?.size ?? r?.['@_size']; const parsedSize = sizeRaw != null ? Number(sizeRaw) : undefined; return { name: String(rname ?? ''), size: typeof parsedSize === 'number' && !Number.isNaN(parsedSize) ? parsedSize : undefined, crc: normalizeHex(r?.crc ?? r?.['@_crc'] ?? r?.CRC ?? r?.['CRC']), md5: normalizeHex(r?.md5 ?? r?.['@_md5'] ?? r?.MD5 ?? r?.['MD5']), sha1: normalizeHex(r?.sha1 ?? r?.['@_sha1'] ?? r?.SHA1 ?? r?.['SHA1']), }; }); return { name: String(nameAttr ?? ''), roms, }; }); return { games }; } export function verifyHashesAgainstDat( datDb: DatDatabase, hashes: { crc?: string; md5?: string; sha1?: string; size?: number } ): { gameName: string; romName: string; matchedOn: 'crc' | 'md5' | 'sha1' | 'size' } | null { const cmp = { crc: normalizeHex(hashes.crc), md5: normalizeHex(hashes.md5), sha1: normalizeHex(hashes.sha1), size: hashes.size, } as any; for (const g of datDb.games) { for (const r of g.roms) { if (cmp.crc && r.crc && cmp.crc === r.crc) { return { gameName: g.name, romName: r.name, matchedOn: 'crc' }; } if (cmp.md5 && r.md5 && cmp.md5 === r.md5) { return { gameName: g.name, romName: r.name, matchedOn: 'md5' }; } if (cmp.sha1 && r.sha1 && cmp.sha1 === r.sha1) { return { gameName: g.name, romName: r.name, matchedOn: 'sha1' }; } if (cmp.size !== undefined && r.size !== undefined && Number(cmp.size) === Number(r.size)) { return { gameName: g.name, romName: r.name, matchedOn: 'size' }; } } } return null; }