@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user