explodorama / files / tools / audit-art.mjs
audit-art.mjs
| 1 | // Art provenance check (2026-11-01): is every file under assets/kenney* really Kenney's? |
| 2 | // Hashes each file and looks for the same bytes anywhere in a folder of unpacked Kenney packs. |
| 3 | // Files that match are "verbatim"; the rest are listed, so each can be explained in |
| 4 | // docs/art-provenance.md (a scaled copy, a converted sound). The packs are not in the repo. |
| 5 | // |
| 6 | // node tools/audit-art.mjs [folder of unpacked packs, default ~/code/kenney] [--table] |
| 7 | // |
| 8 | // --table prints the verbatim files as markdown rows (file | pack | pack path). |
| 9 | import { createHash } from 'node:crypto' |
| 10 | import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' |
| 11 | import { homedir } from 'node:os' |
| 12 | import { join, relative } from 'node:path' |
| 13 | import { fileURLToPath } from 'node:url' |
| 14 | |
| 15 | const args = process.argv.slice(2) |
| 16 | const table = args.includes('--table') |
| 17 | const packs = args.find((a) => !a.startsWith('--')) ?? join(homedir(), 'code/kenney') |
| 18 | const repo = fileURLToPath(new URL('..', import.meta.url)) |
| 19 | if (!existsSync(packs)) { |
| 20 | console.error(`No folder of Kenney packs at ${packs}; pass its path as the first argument.`) |
| 21 | process.exit(1) |
| 22 | } |
| 23 | |
| 24 | const walk = (dir) => |
| 25 | readdirSync(dir).flatMap((n) => { |
| 26 | const p = join(dir, n) |
| 27 | return statSync(p).isDirectory() ? walk(p) : [p] |
| 28 | }) |
| 29 | const md5 = (p) => createHash('md5').update(readFileSync(p)).digest('hex') |
| 30 | |
| 31 | const byHash = new Map() |
| 32 | for (const p of walk(packs).filter((f) => !f.endsWith('.zip'))) byHash.set(md5(p), relative(packs, p)) |
| 33 | |
| 34 | const verbatim = [] |
| 35 | const other = [] |
| 36 | for (const p of walk(join(repo, 'assets')).sort()) { |
| 37 | const rel = relative(repo, p) |
| 38 | if (/composed\/|\.DS_Store|License\.txt|license\.txt/i.test(rel)) continue |
| 39 | const hit = byHash.get(md5(p)) |
| 40 | if (hit) verbatim.push([rel, hit]) |
| 41 | else other.push(rel) |
| 42 | } |
| 43 | |
| 44 | if (table) { |
| 45 | for (const [rel, hit] of verbatim) { |
| 46 | const [pack, ...rest] = hit.split('/') |
| 47 | console.log(`| ${rel.replace('assets/', '')} | ${pack.replace('kenney_', '')} | ${rest.join('/')} |`) |
| 48 | } |
| 49 | } else { |
| 50 | console.log(`${verbatim.length} files are byte-identical to a file in ${packs}`) |
| 51 | console.log(`${other.length} are not (explain each in docs/art-provenance.md):`) |
| 52 | for (const rel of other) console.log(' ' + rel) |
| 53 | } |