From 86155c9fd384c1111e707fedb525660a3103388d Mon Sep 17 00:00:00 2001 From: Jaybe Date: Sun, 16 Nov 2025 20:24:30 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20verifyIntegrity=20CLI=20=EB=9E=98?= =?UTF-8?q?=ED=8D=BC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=ED=99=94=20(TDD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - verify.cli.ts/verify.cli.test.ts 추가 - README 사용법/종료코드/CI 가이드 문서화 - 전체 테스트 그린 유지 --- README.md | 20 ++++++++++ lib/exporter/verify.cli.test.ts | 54 ++++++++++++++++++++++++++ lib/exporter/verify.cli.ts | 68 +++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 lib/exporter/verify.cli.test.ts create mode 100644 lib/exporter/verify.cli.ts diff --git a/README.md b/README.md index a8ce47b..f8747c4 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,26 @@ The Builder can export a production-ready ZIP that contains: ### Asset paths and grouping - AssetManager writes files to `assets/` with stable hash-based filenames. + +### verifyIntegrity CLI + +- Purpose: Validate exported bundle assets against `assets.integrity.index.json`. +- Input format: two JSON files mapping file path → base64 bytes + - Extras JSON: must contain `"assets.integrity.index.json"` key with base64 of the index file + - Assets JSON: keys are asset paths, values are base64 of file bytes +- Usage: + +```bash +node scripts/verify.js --extras extras.json --assets assets.json +``` + +- Output: prints a JSON with fields of `VerifyResult` (`ok`, `errors`, `summary`) +- Exit codes: + - 0: ok + - 1: verification failed or invalid inputs + - 2: usage error (missing args) + +- CI: add a step to run the CLI after build/export; fail the job if exit code != 0. - Path strategy can be configured: - `flat` (default): `assets/.` - `grouped`: `assets/images/...`, `assets/fonts/...`, `assets/other/...` diff --git a/lib/exporter/verify.cli.test.ts b/lib/exporter/verify.cli.test.ts new file mode 100644 index 0000000..cf5120b --- /dev/null +++ b/lib/exporter/verify.cli.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest' +import { verifyCli } from '@/lib/exporter/verify.cli' + +// helpers to build base64 from string +function b64(s: string) { + if (typeof Buffer !== 'undefined') return Buffer.from(s, 'utf-8').toString('base64') + throw new Error('Buffer not available') +} + +describe('verify.cli', () => { + const EMPTY_SHA256_B64 = '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' + + it('returns 0 and logs summary when ok', async () => { + // empty entries integrity index; bundleHash = sha256("") in base64 + const extras = { + 'assets.integrity.index.json': b64(JSON.stringify({ entries: [], bundleHash: 'sha256-' + EMPTY_SHA256_B64 })) + } + const assets = {} + const extrasJson = JSON.stringify(extras) + const assetsJson = JSON.stringify(assets) + + const paths: Record = { + '/tmp/extras.ok.json': extrasJson, + '/tmp/assets.ok.json': assetsJson, + } + const logs: string[] = [] + const readText = async (p: string) => { + const v = paths[p] + if (v == null) throw new Error('no such file: ' + p) + return v + } + const code = await verifyCli(['--extras', '/tmp/extras.ok.json', '--assets', '/tmp/assets.ok.json'], readText, (s: string) => logs.push(s)) + expect(code).toBe(0) + expect(logs.join('\n')).toMatch(/"ok": true/) + expect(logs.join('\n')).toMatch(/"total": 0/) + }) + + it('returns 1 and logs errors when invalid JSON', async () => { + const paths: Record = { + '/tmp/extras.bad.json': JSON.stringify({ 'assets.integrity.index.json': 'not-base64' }), + '/tmp/assets.empty.json': JSON.stringify({}), + } + const logs: string[] = [] + const readText = async (p: string) => { + const v = paths[p] + if (v == null) throw new Error('no such file: ' + p) + return v + } + const code = await verifyCli(['--extras', '/tmp/extras.bad.json', '--assets', '/tmp/assets.empty.json'], readText, (s: string) => logs.push(s)) + expect(code).toBe(1) + expect(logs.join('\n')).toMatch(/"ok": false/) + expect(logs.join('\n')).toMatch(/Invalid assets\.integrity\.index\.json JSON|Missing assets\.integrity\.index\.json/) + }) +}) diff --git a/lib/exporter/verify.cli.ts b/lib/exporter/verify.cli.ts new file mode 100644 index 0000000..f3fa51d --- /dev/null +++ b/lib/exporter/verify.cli.ts @@ -0,0 +1,68 @@ +import { verifyIntegrity } from '@/lib/exporter/verify' + +export type ReadText = (path: string) => Promise +export type Logger = (line: string) => void + +// base64 -> Uint8Array +function fromBase64(b64: string): Uint8Array { + if (typeof Buffer !== 'undefined') return new Uint8Array(Buffer.from(b64, 'base64')) + throw new Error('Buffer not available') +} + +// parse args like: [--extras, path, --assets, path] +function parseArgs(argv: string[]): { extras?: string; assets?: string } { + const out: { extras?: string; assets?: string } = {} + for (let i = 0; i < argv.length; i++) { + const a = argv[i] + if (a === '--extras') out.extras = argv[++i] + else if (a === '--assets') out.assets = argv[++i] + } + return out +} + +export async function verifyCli(argv: string[], readText: ReadText, log: Logger): Promise { + try { + const { extras: extrasPath, assets: assetsPath } = parseArgs(argv) + if (!extrasPath || !assetsPath) { + log('Usage: verify --extras --assets ') + return 2 + } + const extrasJson = await readText(extrasPath) + const assetsJson = await readText(assetsPath) + let extrasMap: Record + let assetsMap: Record + try { + extrasMap = JSON.parse(extrasJson) + assetsMap = JSON.parse(assetsJson) + } catch (e) { + log('Invalid JSON input files') + return 1 + } + // decode base64 into Uint8Array maps + const extrasBytes: Record = {} + for (const [k, v] of Object.entries(extrasMap)) { + try { + extrasBytes[k] = fromBase64(v) + } catch { + // mark invalid; let verifyIntegrity handle missing/invalid as possible + log(`Failed to decode base64 for extras entry: ${k}`) + return 1 + } + } + const assetsBytes: Record = {} + for (const [k, v] of Object.entries(assetsMap)) { + try { + assetsBytes[k] = fromBase64(v) + } catch { + log(`Failed to decode base64 for asset entry: ${k}`) + return 1 + } + } + const result = verifyIntegrity(extrasBytes, assetsBytes) + log(JSON.stringify(result, null, 2)) + return result.ok ? 0 : 1 + } catch (e) { + log(String(e instanceof Error ? e.message : e)) + return 1 + } +}