Release Archive Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 09:34:59 +02:00
parent a49ec6b17a
commit fe578f46bd
47 changed files with 3337 additions and 253 deletions
+72 -3
View File
@@ -1,7 +1,11 @@
// @vitest-environment node
import { File } from "node:buffer";
import { describe, expect, it } from "vitest";
import { gzipDeterministic, gunzipBounded } from "../../src/archive/gzip";
import {
gzipDeterministic,
gunzipBounded,
gunzipBoundedDetailed,
} from "../../src/archive/gzip";
import { createTar, parseTar } from "../../src/archive/tar";
function input(path: string, text: string) {
@@ -73,16 +77,81 @@ describe("TAR, USTAR, PAX and gzip", () => {
expect(() => gunzipBounded(compressed, 1024)).toThrow(/expanded-byte/iu);
});
it("explicitly rejects concatenated gzip members", () => {
it("decodes concatenated gzip members and validates each footer", () => {
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);
expect(new TextDecoder().decode(gunzipBounded(joined, 1024))).toBe(
"leftright",
);
expect(gunzipBoundedDetailed(joined, 1024).members).toMatchObject([
{ index: 0, expandedBytes: 4 },
{ index: 1, expandedBytes: 5 },
]);
joined[left.length - 8] = joined[left.length - 8]! ^ 1;
expect(() => gunzipBounded(joined, 1024)).toThrow(/member 1/iu);
});
it("reads bounded GNU long-name and long-link metadata", async () => {
const regular = await createTar([input("placeholder", "data")]);
const longPath = `${"gnu/".repeat(35)}entry.txt`;
const named = prependGnuMetadata(regular, 0x4c, longPath);
expect(parseTar(named).entries[0]?.path).toBe(longPath);
const linkTarget = `${"target/".repeat(24)}file.txt`;
const linked = prependGnuMetadata(regular.slice(), 0x4b, linkTarget);
linked[512 + paddedPayloadSize(linkTarget) + 156] = 0x32;
rewriteChecksum(
linked.subarray(
512 + paddedPayloadSize(linkTarget),
1024 + paddedPayloadSize(linkTarget),
),
);
expect(parseTar(linked).entries[0]).toMatchObject({
kind: "symlink",
linkTarget,
extractable: false,
});
});
});
function paddedPayloadSize(value: string): number {
return (
Math.ceil(new TextEncoder().encode(`${value}\0`).byteLength / 512) * 512
);
}
function prependGnuMetadata(
tar: Uint8Array,
type: number,
value: string,
): Uint8Array {
const payload = new TextEncoder().encode(`${value}\0`);
const padded = Math.ceil(payload.byteLength / 512) * 512;
const output = new Uint8Array(512 + padded + tar.byteLength);
const header = output.subarray(0, 512);
header.set(new TextEncoder().encode("././@LongLink"), 0);
header.set(new TextEncoder().encode("0000644\0"), 100);
header.set(new TextEncoder().encode("0000000\0"), 108);
header.set(new TextEncoder().encode("0000000\0"), 116);
header.set(
new TextEncoder().encode(
`${payload.byteLength.toString(8).padStart(11, "0")}\0`,
),
124,
);
header.set(new TextEncoder().encode("00000000000\0"), 136);
header[156] = type;
header.set(new TextEncoder().encode("ustar\0"), 257);
header.set(new TextEncoder().encode("00"), 263);
rewriteChecksum(header);
output.set(payload, 512);
output.set(tar, 512 + padded);
return output;
}
function rewriteChecksum(header: Uint8Array) {
header.fill(0x20, 148, 156);
const checksum = header.reduce((sum, byte) => sum + byte, 0);