scripts/build-zip-fixtures.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
// Writes tests/fixtures/zip/*.zip: one small kit of synthetic files archived
// by real tools, so the zip reader is tested against what people send rather
// than only against tests/helpers/zip.ts. macOS only (zip, ditto, python3).
// Prints each archive's SHA-256 for allowedBinaryFiles in
// scripts/lib/source-export-policy.mjs, which must be updated after a run.
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { crc32, deflateSync } from 'node:zlib';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const OUT = join(ROOT, 'tests', 'fixtures', 'zip');
const FIXED_TIME = new Date('2026-01-01T00:00:00Z');
const TOOL_ENV = { ...process.env, TZ: 'UTC' };
const KIT = {
'kit/notes.md': '# Synthetic plan\n\nThis file is synthetic test content.\n',
'kit/data/table.csv': 'region,units\nnorth,12\nsouth,7\n',
'kit/report.pdf': textPdf('Synthetic report: 42 units'),
'kit/logo.png': png(2, 2),
'kit/.DS_Store': Buffer.concat([Buffer.from('\0\0\0\u0001Bud1'), Buffer.alloc(24)]),
};
const PYTHON_WRITER = `
import os, sys, zipfile
out = sys.argv[1]
names = ['kit/notes.md', 'kit/data/table.csv', 'kit/report.pdf', 'kit/logo.png']
def write(name, compression, comment=b'', entries=names):
with zipfile.ZipFile(os.path.join(out, name), 'w', compression) as archive:
archive.comment = comment
for entry in entries:
info = zipfile.ZipInfo(entry, date_time=(2026, 1, 1, 0, 0, 0))
info.compress_type = compression
with open(entry, 'rb') as source:
archive.writestr(info, source.read())
write('python-stored.zip', zipfile.ZIP_STORED)
write('python-deflated.zip', zipfile.ZIP_DEFLATED)
write('python-comment.zip', zipfile.ZIP_DEFLATED, comment=b'Synthetic archive comment.')
write('python-empty.zip', zipfile.ZIP_DEFLATED, entries=[])
`;
const work = mkdtempSync(join(tmpdir(), 'chickpea-zip-fixtures-'));
try {
const source = join(work, 'source');
for (const [path, content] of Object.entries(KIT)) {
mkdirSync(dirname(join(source, path)), { recursive: true });
writeFileSync(join(source, path), content);
}
// An extended attribute makes ditto write an AppleDouble file under __MACOSX/, as Finder does.
execFileSync('/usr/bin/xattr', ['-w', 'com.example.synthetic', 'fixture', join(source, 'kit/notes.md')]);
for (const path of [...Object.keys(KIT), 'kit/data', 'kit']) utimesSync(join(source, path), FIXED_TIME, FIXED_TIME);
mkdirSync(OUT, { recursive: true });
const run = (command, args) => execFileSync(command, args, { cwd: source, env: TOOL_ENV, stdio: 'inherit' });
const output = (name) => {
const path = join(OUT, name);
rmSync(path, { force: true });
return path;
};
run('/usr/bin/zip', ['-r', '-q', output('zip-r.zip'), 'kit']);
run('/usr/bin/zip', ['-r', '-q', '-P', 'synthetic', output('zip-encrypted.zip'), 'kit']);
run('/usr/bin/ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', 'kit', output('ditto.zip')]);
run('python3', ['-I', '-c', PYTHON_WRITER, OUT]);
for (const name of ['ditto.zip', 'python-comment.zip', 'python-deflated.zip', 'python-empty.zip', 'python-stored.zip', 'zip-encrypted.zip', 'zip-r.zip']) {
const hash = createHash('sha256').update(readFileSync(join(OUT, name))).digest('hex');
console.log(` [exportPath('tests', 'fixtures', 'zip', '${name}'), '${hash}'],`);
}
} finally {
rmSync(work, { recursive: true, force: true });
}
function textPdf(text) {
const stream = `BT /F1 12 Tf 72 720 Td (${text}) Tj ET`;
const objects = [
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids [4 0 R] /Count 1 >>',
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 3 0 R >> >> /Contents 5 0 R >>',
`<< /Length ${stream.length} >>\nstream\n${stream}\nendstream`,
];
let body = '%PDF-1.4\n';
const offsets = objects.map((object, index) => {
const offset = body.length;
body += `${index + 1} 0 obj\n${object}\nendobj\n`;
return offset;
});
const xref = body.length;
body += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
body += offsets.map((offset) => `${String(offset).padStart(10, '0')} 00000 n \n`).join('');
body += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
return Buffer.from(body, 'latin1');
}
function png(width, height) {
const chunk = (type, data) => {
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length);
const checksum = Buffer.alloc(4);
checksum.writeUInt32BE(crc32(Buffer.concat([Buffer.from(type), data])));
return Buffer.concat([length, Buffer.from(type), data, checksum]);
};
const header = Buffer.alloc(13);
header.writeUInt32BE(width, 0);
header.writeUInt32BE(height, 4);
header[8] = 8;
header[9] = 2;
const row = Buffer.concat([Buffer.from([0]), Buffer.alloc(width * 3, 0x7f)]);
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk('IHDR', header),
chunk('IDAT', deflateSync(Buffer.concat(Array.from({ length: height }, () => row)))),
chunk('IEND', Buffer.alloc(0)),
]);
}