Release Archive Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// @vitest-environment node
|
||||
import { File } from "node:buffer";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { gzipDeterministic, gunzipBounded } from "../../src/archive/gzip";
|
||||
import { createTar, parseTar } from "../../src/archive/tar";
|
||||
|
||||
function input(path: string, text: string) {
|
||||
return {
|
||||
path,
|
||||
file: new File(
|
||||
[text],
|
||||
path.split("/").at(-1)!,
|
||||
) as unknown as globalThis.File,
|
||||
};
|
||||
}
|
||||
|
||||
describe("TAR, USTAR, PAX and gzip", () => {
|
||||
it("creates deterministic TAR and parses regular entries", async () => {
|
||||
const inputs = [input("b.txt", "second"), input("folder/a.txt", "first")];
|
||||
const first = await createTar(inputs);
|
||||
const second = await createTar(inputs);
|
||||
expect(first).toEqual(second);
|
||||
const parsed = parseTar(first);
|
||||
expect(parsed.entries.map((entry) => entry.path)).toEqual([
|
||||
"b.txt",
|
||||
"folder/a.txt",
|
||||
]);
|
||||
expect(parsed.entries.every((entry) => entry.extractable)).toBe(true);
|
||||
expect(parsed.entries[1]?.crc32).toBe("9271ee57");
|
||||
});
|
||||
|
||||
it("round-trips a long path through a bounded PAX path header", async () => {
|
||||
const longPath = `${"long-".repeat(24)}name.txt`;
|
||||
const tar = await createTar([input(longPath, "PAX")]);
|
||||
expect(parseTar(tar).entries[0]?.path).toBe(longPath);
|
||||
});
|
||||
|
||||
it("lists but blocks symbolic links", async () => {
|
||||
const tar = await createTar([input("link", "target")]);
|
||||
tar[156] = 0x32;
|
||||
rewriteChecksum(tar.subarray(0, 512));
|
||||
const entry = parseTar(tar).entries[0]!;
|
||||
expect(entry.kind).toBe("symlink");
|
||||
expect(entry.extractable).toBe(false);
|
||||
expect(entry.issues.map((issue) => issue.code)).toContain("SPECIAL_ENTRY");
|
||||
});
|
||||
|
||||
it("rejects a damaged TAR header checksum", async () => {
|
||||
const tar = await createTar([input("file.txt", "data")]);
|
||||
tar[0] = tar[0]! ^ 1;
|
||||
expect(() => parseTar(tar)).toThrow(/checksum/iu);
|
||||
});
|
||||
|
||||
it("rejects ambiguous non-zero data after the TAR end marker", async () => {
|
||||
const tar = await createTar([input("file.txt", "data")]);
|
||||
const appended = new Uint8Array(tar.length + 1);
|
||||
appended.set(tar);
|
||||
appended[tar.length] = 1;
|
||||
expect(() => parseTar(appended)).toThrow(/follows the TAR end/iu);
|
||||
});
|
||||
|
||||
it("creates deterministic gzip and validates CRC and ISIZE", () => {
|
||||
const source = new TextEncoder().encode("local archive payload");
|
||||
const first = gzipDeterministic(source);
|
||||
expect(first).toEqual(gzipDeterministic(source));
|
||||
expect(gunzipBounded(first, 1024)).toEqual(source);
|
||||
first[first.length - 8] = first[first.length - 8]! ^ 1;
|
||||
expect(() => gunzipBounded(first, 1024)).toThrow(/CRC-32|footer/iu);
|
||||
});
|
||||
|
||||
it("stops gzip expansion at the configured operation limit", () => {
|
||||
const compressed = gzipDeterministic(new Uint8Array(32_768));
|
||||
expect(() => gunzipBounded(compressed, 1024)).toThrow(/expanded-byte/iu);
|
||||
});
|
||||
|
||||
it("explicitly rejects concatenated gzip members", () => {
|
||||
const left = gzipDeterministic(new TextEncoder().encode("left"));
|
||||
const right = gzipDeterministic(new TextEncoder().encode("right"));
|
||||
const joined = new Uint8Array(left.length + right.length);
|
||||
joined.set(left);
|
||||
joined.set(right, left.length);
|
||||
expect(() => gunzipBounded(joined, 1024)).toThrow(/multi-member/iu);
|
||||
});
|
||||
});
|
||||
|
||||
function rewriteChecksum(header: Uint8Array) {
|
||||
header.fill(0x20, 148, 156);
|
||||
const checksum = header.reduce((sum, byte) => sum + byte, 0);
|
||||
const text = checksum.toString(8).padStart(6, "0");
|
||||
header.set(new TextEncoder().encode(text), 148);
|
||||
header[154] = 0;
|
||||
header[155] = 0x20;
|
||||
}
|
||||
Reference in New Issue
Block a user