Skip to content

Commit 76efe5c

Browse files
fix: parsing of archives with directories that have type=FILE name=.../ (#115)
* Fix parsing of archives with directories that have type=FILE name=.../ <https://ftp.okass.net/pub/mirror/minnie.tuhs.org/Distributions/Research/Dennis_v5/v5root.tar.gz> has directory entries with typeflag of "" and names that end in / GNU tar and libarchive understand this This patch mirrors libarchive Ref: https://github.com/libarchive/libarchive/blob/8d1c9b95f1fbc68f3882ef859d6caf7b701fde7c/libarchive/archive_read_support_format_tar.c#L583 * fix(unpacker): move directory switch --------- Co-authored-by: наб <nabijaczleweli@nabijaczleweli.xyz>
1 parent 0aae183 commit 76efe5c

File tree

3 files changed

+110
-16
lines changed

3 files changed

+110
-16
lines changed

src/tar/unpacker.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createChunkQueue } from "./chunk-queue";
2-
import { BLOCK_SIZE, BLOCK_SIZE_MASK } from "./constants";
2+
import { BLOCK_SIZE, BLOCK_SIZE_MASK, DIRECTORY, FILE } from "./constants";
33
import {
44
applyOverrides,
55
getMetaParser,
@@ -155,6 +155,11 @@ export function createUnpacker(options: DecoderOptions = {}) {
155155

156156
applyOverrides(header, paxGlobals);
157157
applyOverrides(header, nextEntryOverrides);
158+
159+
if (header.name.endsWith("/") && header.type === FILE) {
160+
header.type = DIRECTORY;
161+
}
162+
158163
nextEntryOverrides = {}; // Reset for the next entry.
159164

160165
// Set up state for body processing.

tests/fs/security.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2075,7 +2075,7 @@ describe("security", () => {
20752075
},
20762076
{
20772077
header: {
2078-
name: "folder//", // Double slash should normalize to same path
2078+
name: "folder", // No trailing slash should still normalize to same path but remain a file
20792079
size: 4,
20802080
type: "file",
20812081
mode: 0o644,

tests/fs/unpack.test.ts

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -682,11 +682,8 @@ describe("extract", () => {
682682
const createdPath = path.join(destDir, "my-file.txt");
683683

684684
const stats = await fs.stat(createdPath);
685-
expect(stats.isFile()).toBe(true);
686-
expect(stats.isDirectory()).toBe(false);
687-
688-
const content = await fs.readFile(createdPath, "utf-8");
689-
expect(content).toBe("content");
685+
expect(stats.isDirectory()).toBe(true);
686+
expect(stats.isFile()).toBe(false);
690687
});
691688

692689
it("should handle multiple trailing slashes on files", async () => {
@@ -709,13 +706,10 @@ describe("extract", () => {
709706

710707
await pipeline(Readable.from([tarBuffer]), unpackStream);
711708

712-
// Should create file without trailing slashes
709+
// Should create directory without trailing slashes
713710
const filePath = path.join(destDir, "document.pdf");
714711
const stats = await fs.stat(filePath);
715-
expect(stats.isFile()).toBe(true);
716-
717-
const content = await fs.readFile(filePath, "utf-8");
718-
expect(content).toBe("PDF content\n");
712+
expect(stats.isDirectory()).toBe(true);
719713
});
720714

721715
it("should handle trailing slashes on directories (which is valid)", async () => {
@@ -764,13 +758,108 @@ describe("extract", () => {
764758

765759
await pipeline(Readable.from([tarBuffer]), unpackStream);
766760

767-
// Should create the nested file structure correctly
761+
// Should create the nested directory structure correctly
768762
const filePath = path.join(destDir, "nested", "path", "file.txt");
769763
const stats = await fs.stat(filePath);
770-
expect(stats.isFile()).toBe(true);
764+
expect(stats.isDirectory()).toBe(true);
765+
});
766+
767+
it("converts to directory when PAX header overrides name with trailing slash", async () => {
768+
const destDir = path.join(tmpDir, "pax-override");
769+
await fs.mkdir(destDir, { recursive: true });
770+
771+
const entries = [
772+
{
773+
header: {
774+
name: "original-name", // No slash in standard header
775+
type: "file" as const,
776+
size: 0,
777+
pax: {
778+
path: "overridden-name/", // Slash in PAX override
779+
},
780+
},
781+
},
782+
];
783+
784+
const tarBuffer = await packTarWeb(entries);
785+
const unpackStream = unpackTar(destDir);
786+
await pipeline(Readable.from([tarBuffer]), unpackStream);
787+
788+
const createdPath = path.join(destDir, "overridden-name");
789+
const stats = await fs.stat(createdPath);
790+
expect(stats.isDirectory()).toBe(true);
791+
});
792+
793+
it("does NOT convert symlinks to directories even with trailing slash", async () => {
794+
const destDir = path.join(tmpDir, "symlink-slash");
795+
await fs.mkdir(destDir, { recursive: true });
796+
797+
// Create a target for the symlink
798+
await fs.writeFile(path.join(destDir, "target"), "target content");
799+
800+
const entries = [
801+
{
802+
header: {
803+
name: "mylink/", // Trailing slash
804+
type: "symlink" as const, // But type is SYMLINK
805+
linkname: "target",
806+
size: 0,
807+
},
808+
},
809+
];
810+
811+
const tarBuffer = await packTarWeb(entries);
812+
const unpackStream = unpackTar(destDir);
813+
await pipeline(Readable.from([tarBuffer]), unpackStream);
814+
815+
const createdPath = path.join(destDir, "mylink");
816+
const stats = await fs.lstat(createdPath);
817+
818+
expect(stats.isSymbolicLink()).toBe(true);
819+
expect(stats.isDirectory()).toBe(false);
820+
});
821+
822+
it("discards body content when a FILE is converted to a DIRECTORY", async () => {
823+
const destDir = path.join(tmpDir, "content-discard");
824+
await fs.mkdir(destDir, { recursive: true });
825+
826+
const bodyContent = "this content should be discarded";
827+
const entries = [
828+
{
829+
header: {
830+
name: "weird-dir/",
831+
type: "file" as const,
832+
size: bodyContent.length, // Header claims it has size
833+
},
834+
body: bodyContent,
835+
},
836+
{
837+
header: {
838+
name: "next-file.txt",
839+
type: "file" as const,
840+
size: 5,
841+
},
842+
body: "hello",
843+
},
844+
];
845+
846+
const tarBuffer = await packTarWeb(entries);
847+
const unpackStream = unpackTar(destDir);
848+
await pipeline(Readable.from([tarBuffer]), unpackStream);
849+
850+
// Check weird-dir is a directory
851+
const dirPath = path.join(destDir, "weird-dir");
852+
const dirStats = await fs.stat(dirPath);
853+
expect(dirStats.isDirectory()).toBe(true);
854+
855+
// Verify no file was created inside (content was discarded, not treated as file-in-dir)
856+
const dirContents = await fs.readdir(dirPath);
857+
expect(dirContents).toHaveLength(0);
771858

772-
const readContent = await fs.readFile(filePath, "utf-8");
773-
expect(readContent).toBe(content);
859+
// Verify stream continued correctly to next file
860+
const nextFilePath = path.join(destDir, "next-file.txt");
861+
const nextFileContent = await fs.readFile(nextFilePath, "utf-8");
862+
expect(nextFileContent).toBe("hello");
774863
});
775864

776865
it("map filters out empty directory names", async () => {

0 commit comments

Comments
 (0)