Skip to content

Commit a3468f7

Browse files
authored
fix(options): handle invalid entries after map (#90)
* fix(options): handle invalid entries after map * test: use more consistent unicode across platforms * test: handle file join on windows * test: add log for giget core * fix(unpack): validate directory exists first before creating * test: clear cases * test: add giget core reproduction
1 parent b5e0cf4 commit a3468f7

File tree

3 files changed

+103
-6
lines changed

3 files changed

+103
-6
lines changed

src/fs/unpack.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,6 @@ function createFSHandler(directoryPath: string, options: UnpackOptionsFS) {
180180

181181
// Check if the directory exists.
182182
try {
183-
await fs.mkdir(dirPath, { mode: dmode });
184-
return "directory";
185-
} catch (err: unknown) {
186-
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
187-
188183
const stat = await fs.lstat(dirPath);
189184
if (stat.isDirectory()) return "directory";
190185

@@ -200,6 +195,12 @@ function createFSHandler(directoryPath: string, options: UnpackOptionsFS) {
200195
if (realStat.isDirectory()) return "directory";
201196
}
202197
throw new Error(`"${dirPath}" is not a valid directory component.`);
198+
} catch (err: unknown) {
199+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
200+
await fs.mkdir(dirPath, { mode: dmode });
201+
return "directory";
202+
}
203+
throw err;
203204
}
204205
})();
205206

src/tar/options.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,18 @@ export function transformHeader(
3939
return null; // Skip filtered entry
4040
}
4141

42-
return map ? map(h) : h;
42+
const result = map ? map(h) : h;
43+
44+
// Skip entries with empty names, whitespace only names, or paths that would resolve to extraction root.
45+
if (
46+
result &&
47+
(!result.name ||
48+
!result.name.trim() ||
49+
result.name === "." ||
50+
result.name === "/")
51+
) {
52+
return null;
53+
}
54+
55+
return result;
4356
}

tests/fs/unpack.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,5 +607,88 @@ describe("extract", () => {
607607
const readContent = await fs.readFile(filePath, "utf-8");
608608
expect(readContent).toBe(content);
609609
});
610+
611+
it("map filters out empty directory names", async () => {
612+
const sourceDir = path.join(tmpDir, "source");
613+
await fs.mkdir(path.join(sourceDir, "dir"), { recursive: true });
614+
615+
const packStream = packTar(sourceDir);
616+
const packData: Buffer[] = [];
617+
for await (const chunk of packStream) {
618+
packData.push(Buffer.from(chunk));
619+
}
620+
621+
const extractDir = path.join(tmpDir, "extract");
622+
const readStream = Readable.from([Buffer.concat(packData)]);
623+
624+
const unpackStream = unpackTar(extractDir, {
625+
map(entry) {
626+
if (entry.name === "dir/") entry.name = ""; // Creates empty name
627+
return entry;
628+
},
629+
});
630+
631+
// Should complete without hanging (empty entries are filtered out)
632+
await pipeline(readStream, unpackStream);
633+
634+
// Should have no files since the only directory entry was filtered out
635+
try {
636+
const files = await fs.readdir(extractDir);
637+
expect(files).toHaveLength(0);
638+
} catch (error) {
639+
// If directory doesn't exist because no entries were extracted, that's fine
640+
expect((error as NodeJS.ErrnoException).code).toBe("ENOENT");
641+
}
642+
}, 2000);
643+
644+
it("handles mapping with subdir extraction", async () => {
645+
// Create a test archive that mimics GitHub tarball structure
646+
const sourceDir = path.join(tmpDir, "source");
647+
const rootDir = path.join(sourceDir, "withastro-starlight-abc123");
648+
const examplesDir = path.join(rootDir, "examples");
649+
const basicsDir = path.join(examplesDir, "basics");
650+
const srcDir = path.join(basicsDir, "src");
651+
const pagesDir = path.join(srcDir, "pages");
652+
653+
await fs.mkdir(pagesDir, { recursive: true });
654+
await fs.writeFile(
655+
path.join(basicsDir, "package.json"),
656+
'{"name": "@example/basics", "type": "module"}',
657+
);
658+
await fs.writeFile(path.join(pagesDir, "index.mdx"), "# Welcome");
659+
660+
const packStream = packTar(sourceDir);
661+
const packData: Buffer[] = [];
662+
for await (const chunk of packStream) {
663+
packData.push(Buffer.from(chunk));
664+
}
665+
666+
// Reproduce exact giget-core extraction logic
667+
const extractDir = path.join(tmpDir, "starlight-unpack");
668+
const readStream = Readable.from([Buffer.concat(packData)]);
669+
const subdir = "examples/basics/";
670+
671+
const unpackStream = unpackTar(extractDir, {
672+
filter(entry) {
673+
const path = entry.name.split("/").slice(1).join("/");
674+
if (path === "") return false;
675+
return path.startsWith(subdir);
676+
},
677+
map(entry) {
678+
let path = entry.name.split("/").slice(1).join("/");
679+
if (subdir) path = path.slice(subdir.length);
680+
entry.name = path;
681+
return entry;
682+
},
683+
});
684+
685+
// This should now work without hanging
686+
await pipeline(readStream, unpackStream);
687+
688+
// Verify extraction worked correctly
689+
const files = await fs.readdir(extractDir, { recursive: true });
690+
expect(files).toContain("package.json");
691+
expect(files.some((f) => f.includes("pages"))).toBe(true);
692+
});
610693
});
611694
});

0 commit comments

Comments
 (0)