feat: add datVerifier and tests
- Añade `datVerifier` con `parseDat` y `verifyHashesAgainstDat` - Añade tests y fixture XML para validar matching por CRC/MD5/SHA1/size - Añade `fast-xml-parser` en `backend/package.json` (devDependency)
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.0.0",
|
"@types/node": "^18.0.0",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.0.0",
|
||||||
|
"fast-xml-parser": "^5.3.5",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prisma": "6.19.2",
|
"prisma": "6.19.2",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
|
|||||||
@@ -1,23 +1,100 @@
|
|||||||
/**
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
* Servicio: datVerifier
|
|
||||||
*
|
export type DatRom = {
|
||||||
* Encargado de parsear ficheros DAT (XML de listas de ROMs) y de verificar si
|
name: string;
|
||||||
* un ROM (por tamaño / CRC / MD5 / SHA1) coincide con una entrada del DAT.
|
size?: number;
|
||||||
*
|
crc?: string;
|
||||||
* Actualmente este archivo contiene stubs mínimos: `parseDat` y
|
md5?: string;
|
||||||
* `verifyRomAgainstDat` devuelven valores vacíos para permitir que las pruebas
|
sha1?: string;
|
||||||
* unitarias/integración opcionales se salten cuando `INTEGRATION` no está set.
|
};
|
||||||
* En fases posteriores se sustituirá por un parseador XML completo y lógica de
|
|
||||||
* matching detallada.
|
export type DatGame = {
|
||||||
*/
|
name: string;
|
||||||
export function parseDat(_xml: string): any {
|
roms: DatRom[];
|
||||||
// Stub: el parseo completo no se implementa en esta fase.
|
};
|
||||||
return {};
|
|
||||||
|
export type DatDatabase = {
|
||||||
|
games: DatGame[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function ensureArray<T>(v: T | T[] | undefined): T[] {
|
||||||
|
if (v === undefined || v === null) return [];
|
||||||
|
return Array.isArray(v) ? v : [v];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyRomAgainstDat(_romMeta: any, _parsedDat: any): Promise<any> {
|
function normalizeHex(v?: string): string | undefined {
|
||||||
// Stub: verificación mínima para que los tests de integración puedan ser saltados.
|
if (!v) return undefined;
|
||||||
return {};
|
return v.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { parseDat, verifyRomAgainstDat };
|
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;
|
||||||
|
}
|
||||||
|
|||||||
17
backend/tests/fixtures/sample.dat.xml
vendored
Normal file
17
backend/tests/fixtures/sample.dat.xml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<datafile>
|
||||||
|
<header>
|
||||||
|
<name>Sample DAT for tests</name>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<game name="Game Alpha">
|
||||||
|
<rom name="alpha1.bin" size="1000" crc="ABCD1234" md5="11111111111111111111111111111111" sha1="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />
|
||||||
|
<rom name="alpha2.bin" size="2000" crc="BEEFCAFE" md5="22222222222222222222222222222222" sha1="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" />
|
||||||
|
</game>
|
||||||
|
|
||||||
|
<game name="Game Beta">
|
||||||
|
<rom name="beta1.rom" size="3000" crc="DEADBEEF" md5="33333333333333333333333333333333" sha1="cccccccccccccccccccccccccccccccccccccccc" />
|
||||||
|
<rom name="beta2.rom" size="4000" md5="44444444444444444444444444444444" sha1="dddddddddddddddddddddddddddddddddddddddd" />
|
||||||
|
</game>
|
||||||
|
|
||||||
|
</datafile>
|
||||||
@@ -1,35 +1,73 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { parseDat, verifyRomAgainstDat } from '../../src/services/datVerifier';
|
import path from 'path';
|
||||||
|
|
||||||
const fixturesDir = path.join(__dirname, '..', 'fixtures');
|
import { parseDat, verifyHashesAgainstDat, DatDatabase } from '../../src/services/datVerifier';
|
||||||
const datPath = path.join(fixturesDir, 'dats', 'sample-no-intro.dat.xml');
|
|
||||||
const simpleRom = path.join(fixturesDir, 'simple-rom.bin');
|
const FIXTURE = path.resolve('tests/fixtures/sample.dat.xml');
|
||||||
|
|
||||||
// Ejecutar siempre las pruebas de datVerifier. Dependencias externas (p.ej.
|
|
||||||
// binarios para formatos específicos) deben estar instaladas en el entorno
|
|
||||||
// donde se intente ejecutar las pruebas completas.
|
|
||||||
describe('services/datVerifier', () => {
|
describe('services/datVerifier', () => {
|
||||||
it('parsea DAT xml', () => {
|
it('parseDat parses simple DAT XML', () => {
|
||||||
const xml = fs.readFileSync(datPath, 'utf8');
|
const xml = fs.readFileSync(FIXTURE, 'utf8');
|
||||||
const parsed = parseDat(xml);
|
const dat: DatDatabase = parseDat(xml);
|
||||||
expect(parsed).toBeDefined();
|
|
||||||
|
expect(dat).toBeTruthy();
|
||||||
|
expect(Array.isArray(dat.games)).toBe(true);
|
||||||
|
expect(dat.games.length).toBe(2);
|
||||||
|
|
||||||
|
const g0 = dat.games[0];
|
||||||
|
expect(g0.name).toBe('Game Alpha');
|
||||||
|
expect(g0.roms.length).toBeGreaterThan(0);
|
||||||
|
expect(g0.roms[0].name).toBe('alpha1.bin');
|
||||||
|
expect(g0.roms[0].crc).toBeDefined();
|
||||||
|
expect(g0.roms[0].md5).toBeDefined();
|
||||||
|
|
||||||
|
const g1 = dat.games[1];
|
||||||
|
expect(g1.name).toBe('Game Beta');
|
||||||
|
expect(g1.roms.some((r) => r.name === 'beta2.rom')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('verifica rom contra DAT', async () => {
|
it('verifyHashesAgainstDat finds match by CRC', () => {
|
||||||
const stats = fs.statSync(simpleRom);
|
const xml = fs.readFileSync(FIXTURE, 'utf8');
|
||||||
const romMeta = {
|
const dat = parseDat(xml);
|
||||||
filename: 'simple-rom.bin',
|
|
||||||
size: stats.size,
|
|
||||||
md5: 'placeholder',
|
|
||||||
sha1: 'placeholder',
|
|
||||||
crc32: 'placeholder',
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const xml = fs.readFileSync(datPath, 'utf8');
|
const match = verifyHashesAgainstDat(dat, { crc: 'DEADBEEF' });
|
||||||
const parsed = parseDat(xml);
|
expect(match).not.toBeNull();
|
||||||
const res = await verifyRomAgainstDat(romMeta, parsed);
|
expect(match?.gameName).toBe('Game Beta');
|
||||||
expect(res).toBeDefined();
|
expect(match?.romName).toBe('beta1.rom');
|
||||||
|
expect(match?.matchedOn).toBe('crc');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verifyHashesAgainstDat finds match by MD5, SHA1 and size', () => {
|
||||||
|
const xml = fs.readFileSync(FIXTURE, 'utf8');
|
||||||
|
const dat = parseDat(xml);
|
||||||
|
|
||||||
|
const md5match = verifyHashesAgainstDat(dat, { md5: '11111111111111111111111111111111' });
|
||||||
|
expect(md5match).not.toBeNull();
|
||||||
|
expect(md5match?.gameName).toBe('Game Alpha');
|
||||||
|
expect(md5match?.romName).toBe('alpha1.bin');
|
||||||
|
expect(md5match?.matchedOn).toBe('md5');
|
||||||
|
|
||||||
|
const sha1match = verifyHashesAgainstDat(dat, {
|
||||||
|
sha1: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||||
|
});
|
||||||
|
expect(sha1match).not.toBeNull();
|
||||||
|
expect(sha1match?.gameName).toBe('Game Alpha');
|
||||||
|
expect(sha1match?.romName).toBe('alpha2.bin');
|
||||||
|
expect(sha1match?.matchedOn).toBe('sha1');
|
||||||
|
|
||||||
|
const sizematch = verifyHashesAgainstDat(dat, { size: 4000 });
|
||||||
|
expect(sizematch).not.toBeNull();
|
||||||
|
expect(sizematch?.gameName).toBe('Game Beta');
|
||||||
|
expect(sizematch?.romName).toBe('beta2.rom');
|
||||||
|
expect(sizematch?.matchedOn).toBe('size');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verifyHashesAgainstDat returns null when no match', () => {
|
||||||
|
const xml = fs.readFileSync(FIXTURE, 'utf8');
|
||||||
|
const dat = parseDat(xml);
|
||||||
|
|
||||||
|
const noMatch = verifyHashesAgainstDat(dat, { md5: 'ffffffffffffffffffffffffffffffff' });
|
||||||
|
expect(noMatch).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
29
plans/gestor-coleccion-plan-phase-4-complete.md
Normal file
29
plans/gestor-coleccion-plan-phase-4-complete.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
## Phase 4 Complete: DAT verifier
|
||||||
|
|
||||||
|
TL;DR: Implementado `datVerifier` para parsear archivos DAT (XML) y verificar hashes de ROMs (CRC/MD5/SHA1/size). Se añadieron tests TDD y una fixture XML; los tests específicos pasan y se aplicó un parche menor de calidad.
|
||||||
|
|
||||||
|
**Files created/changed:**
|
||||||
|
|
||||||
|
- backend/src/services/datVerifier.ts
|
||||||
|
- backend/tests/services/datVerifier.spec.ts
|
||||||
|
- backend/tests/fixtures/sample.dat.xml
|
||||||
|
- backend/package.json (se añadió `fast-xml-parser` en devDependencies)
|
||||||
|
|
||||||
|
**Functions created/changed:**
|
||||||
|
|
||||||
|
- `parseDat(xml: string): DatDatabase` — parsea y normaliza la estructura DAT a un modelo en memoria.
|
||||||
|
- `verifyHashesAgainstDat(datDb: DatDatabase, hashes): {gameName, romName, matchedOn} | null` — verifica hashes contra el DAT y devuelve la coincidencia.
|
||||||
|
|
||||||
|
**Tests created/changed:**
|
||||||
|
|
||||||
|
- `backend/tests/services/datVerifier.spec.ts` — cubre parsing, match por CRC/MD5/SHA1/size y ausencia de match.
|
||||||
|
- `backend/tests/fixtures/sample.dat.xml` — fixture usada por las pruebas.
|
||||||
|
|
||||||
|
**Review Status:** APPROVED with minor recommendations
|
||||||
|
|
||||||
|
**Git Commit Message:**
|
||||||
|
feat: add datVerifier and tests
|
||||||
|
|
||||||
|
- Añade `datVerifier` con `parseDat` y `verifyHashesAgainstDat`
|
||||||
|
- Añade tests y fixture XML para validar matching por CRC/MD5/SHA1/size
|
||||||
|
- Añade `fast-xml-parser` en `backend/package.json` (devDependency)
|
||||||
19
yarn.lock
19
yarn.lock
@@ -1992,6 +1992,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"fast-xml-parser@npm:^5.3.5":
|
||||||
|
version: 5.3.5
|
||||||
|
resolution: "fast-xml-parser@npm:5.3.5"
|
||||||
|
dependencies:
|
||||||
|
strnum: "npm:^2.1.2"
|
||||||
|
bin:
|
||||||
|
fxparser: src/cli/cli.js
|
||||||
|
checksum: 10c0/ac6232de821c8292436c53a58fc583f073cc5a73d14310b956391512e325e1ef65b950a1d41f5f2715b0d4d363fac2e483d7df1748b344647ea7c7f219a5d2f4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"fastify-plugin@npm:^4.0.0, fastify-plugin@npm:^4.2.1":
|
"fastify-plugin@npm:^4.0.0, fastify-plugin@npm:^4.2.1":
|
||||||
version: 4.5.1
|
version: 4.5.1
|
||||||
resolution: "fastify-plugin@npm:4.5.1"
|
resolution: "fastify-plugin@npm:4.5.1"
|
||||||
@@ -3335,6 +3346,7 @@ __metadata:
|
|||||||
"@types/node": "npm:^18.0.0"
|
"@types/node": "npm:^18.0.0"
|
||||||
dotenv: "npm:^16.0.0"
|
dotenv: "npm:^16.0.0"
|
||||||
eslint: "npm:^8.0.0"
|
eslint: "npm:^8.0.0"
|
||||||
|
fast-xml-parser: "npm:^5.3.5"
|
||||||
fastify: "npm:^4.28.0"
|
fastify: "npm:^4.28.0"
|
||||||
pino: "npm:^8.0.0"
|
pino: "npm:^8.0.0"
|
||||||
prettier: "npm:^2.8.0"
|
prettier: "npm:^2.8.0"
|
||||||
@@ -3773,6 +3785,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"strnum@npm:^2.1.2":
|
||||||
|
version: 2.1.2
|
||||||
|
resolution: "strnum@npm:2.1.2"
|
||||||
|
checksum: 10c0/4e04753b793540d79cd13b2c3e59e298440477bae2b853ab78d548138385193b37d766d95b63b7046475d68d44fb1fca692f0a3f72b03f4168af076c7b246df9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"supports-color@npm:^7.1.0":
|
"supports-color@npm:^7.1.0":
|
||||||
version: 7.2.0
|
version: 7.2.0
|
||||||
resolution: "supports-color@npm:7.2.0"
|
resolution: "supports-color@npm:7.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user