Skip to content

Commit a20fa3c

Browse files
authored
fix(pack): accept parsed tar entry with data (#93)
1 parent 6ec3563 commit a20fa3c

File tree

3 files changed

+110
-16
lines changed

3 files changed

+110
-16
lines changed

src/web/helpers.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,19 @@ import { createTarDecoder } from "./unpack";
4040
* });
4141
* ```
4242
*/
43-
export async function packTar(entries: TarEntry[]): Promise<Uint8Array> {
43+
export async function packTar(
44+
entries: (TarEntry | ParsedTarEntryWithData)[],
45+
): Promise<Uint8Array> {
4446
const { readable, controller } = createTarPacker();
4547

4648
// This promise runs the packing process in the background.
4749
const packingPromise = (async () => {
4850
for (const entry of entries) {
4951
const entryStream = controller.add(entry.header);
50-
const { body } = entry;
52+
53+
// Handle both TarEntry and ParsedTarEntryWithData formats.
54+
const body =
55+
"body" in entry ? entry.body : (entry as ParsedTarEntryWithData).data;
5156

5257
if (!body) {
5358
await entryStream.close();

tests/fs/repack.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { readFile, writeFile } from "node:fs/promises";
2+
import { tmpdir } from "node:os";
3+
import { join } from "node:path";
4+
import { describe, expect, it } from "vitest";
5+
import { decoder } from "../../src/tar/utils";
6+
import { packTar, unpackTar } from "../../src/web/helpers";
7+
8+
describe("repack", () => {
9+
it("successfully repacks unpacked entries", async () => {
10+
const originalEntries = [
11+
{
12+
header: { name: "example.txt", size: 11, type: "file" as const },
13+
body: "hello world",
14+
},
15+
{
16+
header: { name: "empty.txt", size: 0, type: "file" as const },
17+
body: "",
18+
},
19+
{
20+
header: { name: "dir/", type: "directory" as const, size: 0 },
21+
},
22+
];
23+
24+
const archive = await packTar(originalEntries);
25+
const tempPath = join(tmpdir(), "test-repack.tar");
26+
await writeFile(tempPath, archive);
27+
28+
// Original workflow that was failing
29+
const data = await readFile(tempPath);
30+
const entries = await unpackTar(data);
31+
32+
// This used to throw "Size mismatch" but now works
33+
const repackedArchive = await packTar(entries);
34+
35+
// Verify integrity
36+
const verifyEntries = await unpackTar(repackedArchive);
37+
expect(verifyEntries).toHaveLength(3);
38+
39+
const textFile = verifyEntries.find((e) => e.header.name === "example.txt");
40+
expect(decoder.decode(textFile?.data)).toBe("hello world");
41+
});
42+
43+
it("supports adding new entries to existing archive", async () => {
44+
const original = await packTar([
45+
{
46+
header: { name: "existing.txt", size: 8, type: "file" as const },
47+
body: "existing",
48+
},
49+
]);
50+
51+
// Unpack and add new entries
52+
const entries = await unpackTar(original);
53+
const newEntry = {
54+
header: { name: "new.txt", size: 3, type: "file" as const },
55+
body: "new",
56+
};
57+
58+
// Combine and repack
59+
const updatedArchive = await packTar([...entries, newEntry]);
60+
const finalEntries = await unpackTar(updatedArchive);
61+
62+
expect(finalEntries).toHaveLength(2);
63+
expect(finalEntries.map((e) => e.header.name)).toEqual([
64+
"existing.txt",
65+
"new.txt",
66+
]);
67+
});
68+
69+
it("handles mixed entry formats", async () => {
70+
// Fresh entry with body
71+
const freshEntry = {
72+
header: { name: "fresh.txt", size: 5, type: "file" as const },
73+
body: "fresh",
74+
};
75+
76+
// Unpacked entry with data
77+
const archive = await packTar([freshEntry]);
78+
const [unpackedEntry] = await unpackTar(archive);
79+
80+
// Mix both formats in one call
81+
const mixedEntries = [
82+
unpackedEntry, // Has 'data' property
83+
{
84+
header: { name: "another.txt", size: 7, type: "file" as const },
85+
body: "another", // Has 'body' property
86+
},
87+
];
88+
89+
const mixedArchive = await packTar(mixedEntries);
90+
const verifyEntries = await unpackTar(mixedArchive);
91+
92+
expect(verifyEntries).toHaveLength(2);
93+
expect(decoder.decode(verifyEntries[0].data)).toBe("fresh");
94+
expect(decoder.decode(verifyEntries[1].data)).toBe("another");
95+
});
96+
});

tests/web/repack.ts renamed to tests/web/repack.test.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/** biome-ignore-all lint/style/noNonNullAssertion: Tests */
22
import { describe, expect, it } from "vitest";
3-
import { isBodyless } from "../../src/tar/utils";
3+
import { decoder, isBodyless } from "../../src/tar/utils";
44
import { packTar, unpackTar } from "../../src/web/helpers";
55
import type { TarEntry } from "../../src/web/types";
66

7-
describe("round-trip pack/unpack", () => {
7+
describe("repack", () => {
88
it("handles unpack then repack correctly", async () => {
99
// Create a test archive with various entry types
1010
const originalEntries: TarEntry[] = [
@@ -45,9 +45,7 @@ describe("round-trip pack/unpack", () => {
4545
expect(unpackedEntries[0].header.size).toBe(11);
4646
expect(unpackedEntries[0].data).toBeDefined();
4747
expect(unpackedEntries[0].data!.length).toBe(11);
48-
expect(new TextDecoder().decode(unpackedEntries[0].data!)).toBe(
49-
"hello world",
50-
);
48+
expect(decoder.decode(unpackedEntries[0].data!)).toBe("hello world");
5149

5250
expect(unpackedEntries[1].header.name).toBe("empty.txt");
5351
expect(unpackedEntries[1].header.size).toBe(0);
@@ -63,20 +61,15 @@ describe("round-trip pack/unpack", () => {
6361
expect(unpackedEntries[3].header.size).toBe(12);
6462
expect(unpackedEntries[3].data).toBeDefined();
6563
expect(unpackedEntries[3].data!.length).toBe(12);
66-
expect(new TextDecoder().decode(unpackedEntries[3].data!)).toBe(
67-
"nested file!",
68-
);
64+
expect(decoder.decode(unpackedEntries[3].data!)).toBe("nested file!");
6965

7066
expect(unpackedEntries[4].header.name).toBe("link");
7167
expect(unpackedEntries[4].header.type).toBe("symlink");
7268
expect(unpackedEntries[4].header.size).toBe(0);
7369
expect(unpackedEntries[4].data).toBeUndefined();
7470

75-
// Convert unpacked entries back to TarEntry format for repacking
76-
const entriesForRepack: TarEntry[] = unpackedEntries.map((entry) => ({
77-
header: { ...entry.header },
78-
body: entry.data,
79-
}));
71+
// With unified API, unpacked entries can be repacked directly
72+
const entriesForRepack: TarEntry[] = unpackedEntries;
8073

8174
// This should not throw an error (this was the original bug)
8275
const repackedArchive = await packTar(entriesForRepack);
@@ -101,7 +94,7 @@ describe("round-trip pack/unpack", () => {
10194
expect(final.data).toBeDefined();
10295
expect(final.data!.length).toBe(original.header.size);
10396
if (original.body && typeof original.body === "string") {
104-
expect(new TextDecoder().decode(final.data!)).toBe(original.body);
97+
expect(decoder.decode(final.data!)).toBe(original.body);
10598
}
10699
}
107100
}

0 commit comments

Comments
 (0)