tests/zip.test.ts
Rama main · réplica de lectura del repositorio original.
// The ZIP writer (shared/zip.ts): the format's fixed fields, the CRC, the
// data descriptor, the UTF-8 flag, and the streaming contract.
//
// The reader below is deliberately tiny and deliberately independent of the
// writer: it walks the central directory the way `unzip` does (end record →
// directory → local headers), so an archive that satisfies it is one the
// real tools open. A test that asserted the writer against its own
// constants would prove only that the constants were typed twice.
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { crc32, dosDateTime, ZipWriter, zipSync } from "../shared/zip.ts";
const enc = new TextEncoder();
const dec = new TextDecoder();
interface Entry {
name: string;
crc: number;
size: number;
flags: number;
method: number;
offset: number;
data: Uint8Array;
}
/** A minimal central-directory reader: enough to prove the archive is
* well-formed and to hand back each entry's bytes. */
function readZip(bytes: Uint8Array): Entry[] {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
// The end record is the last 22 bytes when there is no comment.
const endAt = bytes.length - 22;
assert.equal(view.getUint32(endAt, true), 0x06054b50, "end-of-central-directory signature");
const count = view.getUint16(endAt + 10, true);
const dirSize = view.getUint32(endAt + 12, true);
const dirAt = view.getUint32(endAt + 16, true);
assert.equal(dirAt + dirSize, endAt, "the directory runs up to the end record");
const entries: Entry[] = [];
let at = dirAt;
for (let i = 0; i < count; i++) {
assert.equal(view.getUint32(at, true), 0x02014b50, "central header signature");
const flags = view.getUint16(at + 8, true);
const method = view.getUint16(at + 10, true);
const crc = view.getUint32(at + 16, true);
const csize = view.getUint32(at + 20, true);
const size = view.getUint32(at + 24, true);
const nameLen = view.getUint16(at + 28, true);
const extraLen = view.getUint16(at + 30, true);
const commentLen = view.getUint16(at + 32, true);
const offset = view.getUint32(at + 42, true);
const name = dec.decode(bytes.subarray(at + 46, at + 46 + nameLen));
assert.equal(csize, size, "STORE: compressed size equals size");
// Now the local header the offset points at.
assert.equal(view.getUint32(offset, true), 0x04034b50, "local header signature");
const localNameLen = view.getUint16(offset + 26, true);
const localExtraLen = view.getUint16(offset + 28, true);
assert.equal(dec.decode(bytes.subarray(offset + 30, offset + 30 + localNameLen)), name);
const dataAt = offset + 30 + localNameLen + localExtraLen;
const data = bytes.subarray(dataAt, dataAt + size);
// With bit 3 set, the local header's CRC is zero and the descriptor after
// the data carries the truth; the SIZE is written up front whenever the
// writer knew it (a sequential reader needs it to find the entry's end)
// and is zero only for a truly streamed entry.
if (flags & (1 << 3)) {
assert.equal(view.getUint32(offset + 14, true), 0, "local crc deferred");
const localSize = view.getUint32(offset + 18, true);
assert.ok(localSize === 0 || localSize === size, "local size is the entry's or deferred");
const descAt = dataAt + size;
assert.equal(view.getUint32(descAt, true), 0x08074b50, "data descriptor signature");
assert.equal(view.getUint32(descAt + 4, true), crc, "descriptor crc equals central crc");
assert.equal(view.getUint32(descAt + 8, true), size);
assert.equal(view.getUint32(descAt + 12, true), size);
}
entries.push({ name, crc, size, flags, method, offset, data });
at += 46 + nameLen + extraLen + commentLen;
}
return entries;
}
describe("crc32", () => {
it("matches the published check value", () => {
// The CRC-32 of "123456789" is the constant every implementation is
// checked against.
assert.equal(crc32(enc.encode("123456789")), 0xcbf43926);
assert.equal(crc32(new Uint8Array(0)), 0);
});
it("is the same whether fed whole or in pieces", () => {
const whole = enc.encode("The quick brown fox jumps over the lazy dog");
const a = crc32(whole);
let running = 0;
for (let i = 0; i < whole.length; i += 7) running = crc32(whole.subarray(i, i + 7), running);
assert.equal(running, a);
});
});
describe("dosDateTime", () => {
it("keeps a modern mtime to the (even) second", () => {
const { time, date } = dosDateTime(new Date(2026, 8, 13, 14, 30, 45).getTime());
assert.equal(date >> 9, 2026 - 1980);
assert.equal((date >> 5) & 0xf, 9);
assert.equal(date & 0x1f, 13);
assert.equal(time >> 11, 14);
assert.equal((time >> 5) & 0x3f, 30);
assert.equal((time & 0x1f) * 2, 44);
});
it("dates a file older than the format to 1980-01-01", () => {
assert.deepEqual(dosDateTime(0), { time: 0, date: (1 << 5) | 1 });
assert.deepEqual(dosDateTime(Number.NaN), { time: 0, date: (1 << 5) | 1 });
});
});
describe("ZipWriter", () => {
it("writes an archive a directory walk reads back byte for byte", () => {
const files = [
{ name: "Ideas.md", data: enc.encode("# Ideas\n\n[[Other]]\n") },
{ name: "Media/pixel.png", data: new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0, 1, 2, 3]) },
{ name: "empty.md", data: new Uint8Array(0) },
];
const entries = readZip(zipSync(files));
assert.deepEqual(
entries.map((e) => e.name),
files.map((f) => f.name),
);
for (let i = 0; i < files.length; i++) {
assert.deepEqual(entries[i].data, files[i].data, files[i].name);
assert.equal(entries[i].crc, crc32(files[i].data));
assert.equal(entries[i].size, files[i].data.length);
assert.equal(entries[i].method, 0, "STORE");
}
});
it("marks every name UTF-8 so an Arabic path survives", () => {
const name = "ملاحظات/فكرة جديدة.md";
const [entry] = readZip(zipSync([{ name, data: enc.encode("مرحبا") }]));
assert.equal(entry.name, name);
assert.ok(entry.flags & (1 << 11), "general-purpose bit 11 set");
assert.equal(dec.decode(entry.data), "مرحبا");
});
it("streams: chunks of any size, the descriptor written after the data", () => {
const chunks: Uint8Array[] = [];
const writer = new ZipWriter((c) => chunks.push(c));
const big = new Uint8Array(100_000);
for (let i = 0; i < big.length; i++) big[i] = (i * 31) & 0xff;
writer.begin("big.bin", Date.now());
for (let i = 0; i < big.length; i += 4096) writer.write(big.subarray(i, i + 4096));
writer.end();
writer.file("small.md", enc.encode("x"));
writer.finish();
const bytes = new Uint8Array(writer.written);
let at = 0;
for (const c of chunks) {
bytes.set(c, at);
at += c.length;
}
assert.equal(at, bytes.length, "`written` is the byte count handed to the sink");
const [a, b] = readZip(bytes);
assert.equal(a.name, "big.bin");
assert.deepEqual(a.data, big);
assert.equal(a.crc, crc32(big));
assert.ok(a.flags & (1 << 3), "data descriptor flag set");
assert.equal(b.name, "small.md");
});
it("refuses misuse loudly rather than writing a broken archive", () => {
const writer = new ZipWriter(() => undefined);
assert.throws(() => writer.write(new Uint8Array(1)), /no entry is open/);
assert.throws(() => writer.end(), /no entry is open/);
writer.begin("a");
assert.throws(() => writer.begin("b"), /still open/);
assert.throws(() => writer.finish(), /still open/);
writer.end();
writer.finish();
assert.throws(() => writer.begin("c"), /already finished/);
assert.throws(() => writer.finish(), /already finished/);
});
it("an empty archive is the 22-byte end record", () => {
const bytes = zipSync([]);
assert.equal(bytes.length, 22);
assert.deepEqual(readZip(bytes), []);
});
});
describe("a stored entry's local header", () => {
it("carries the size when the writer knows it, so a sequential reader can find the end", async () => {
const { ZipWriter } = await import("../shared/zip.ts");
const chunks: Uint8Array[] = [];
const w = new ZipWriter((c) => chunks.push(c));
w.file("a.txt", new TextEncoder().encode("hello"), 0);
w.finish();
const bytes = Buffer.concat(chunks.map((c) => Buffer.from(c)));
// local header: sig(4) ver(2) flag(2) method(2) time(2) date(2) crc(4) csize(4) usize(4)
assert.equal(bytes.readUInt32LE(18), 5);
assert.equal(bytes.readUInt32LE(22), 5);
});
});