diff --git a/src/infra/archive-path.test.ts b/src/infra/archive-path.test.ts index bc900c6964c..02ed7f4ff2d 100644 --- a/src/infra/archive-path.test.ts +++ b/src/infra/archive-path.test.ts @@ -1,18 +1,71 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { + isWindowsDrivePath, + normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-path.js"; describe("archive path helpers", () => { - it("uses custom escape labels in traversal errors", () => { + it.each([ + { value: "C:\\temp\\file.txt", expected: true }, + { value: "D:/temp/file.txt", expected: true }, + { value: "tmp/file.txt", expected: false }, + { value: "/tmp/file.txt", expected: false }, + ])("detects Windows drive paths for %j", ({ value, expected }) => { + expect(isWindowsDrivePath(value)).toBe(expected); + }); + + it.each([ + { raw: "dir\\file.txt", expected: "dir/file.txt" }, + { raw: "dir/file.txt", expected: "dir/file.txt" }, + ])("normalizes archive separators for %j", ({ raw, expected }) => { + expect(normalizeArchiveEntryPath(raw)).toBe(expected); + }); + + it.each(["", ".", "./"])("accepts empty-like entry paths: %j", (entryPath) => { + expect(() => validateArchiveEntryPath(entryPath)).not.toThrow(); + }); + + it.each([ + { + name: "uses custom escape labels in traversal errors", + entryPath: "../escape.txt", + message: "archive entry escapes targetDir: ../escape.txt", + }, + { + name: "rejects Windows drive paths", + entryPath: "C:\\temp\\file.txt", + message: "archive entry uses a drive path: C:\\temp\\file.txt", + }, + { + name: "rejects absolute paths after normalization", + entryPath: "/tmp/file.txt", + message: "archive entry is absolute: /tmp/file.txt", + }, + { + name: "rejects double-slash absolute paths after normalization", + entryPath: "\\\\server\\share.txt", + message: "archive entry is absolute: \\\\server\\share.txt", + }, + ])("$name", ({ entryPath, message }) => { expect(() => - validateArchiveEntryPath("../escape.txt", { + validateArchiveEntryPath(entryPath, { escapeLabel: "targetDir", }), - ).toThrow("archive entry escapes targetDir: ../escape.txt"); + ).toThrow(message); + }); + + it.each([ + { entryPath: "a/../escape.txt", stripComponents: 1, expected: "../escape.txt" }, + { entryPath: "a//b/file.txt", stripComponents: 1, expected: "b/file.txt" }, + { entryPath: "./", stripComponents: 0, expected: null }, + { entryPath: "a", stripComponents: 3, expected: null }, + { entryPath: "dir\\sub\\file.txt", stripComponents: 1, expected: "sub/file.txt" }, + ])("strips archive paths for %j", ({ entryPath, stripComponents, expected }) => { + expect(stripArchivePath(entryPath, stripComponents)).toBe(expected); }); it("preserves strip-induced traversal for follow-up validation", () => { @@ -25,22 +78,40 @@ describe("archive path helpers", () => { ).toThrow("archive entry escapes targetDir: ../escape.txt"); }); - it("keeps resolved output paths inside the root", () => { - const rootDir = path.join(path.sep, "tmp", "archive-root"); - const safe = resolveArchiveOutputPath({ - rootDir, + it.each([ + { + name: "keeps resolved output paths inside the root", relPath: "sub/file.txt", originalPath: "sub/file.txt", - }); - expect(safe).toBe(path.resolve(rootDir, "sub/file.txt")); + expected: path.resolve(path.join(path.sep, "tmp", "archive-root"), "sub/file.txt"), + }, + { + name: "rejects output paths that escape the root", + relPath: "../escape.txt", + originalPath: "../escape.txt", + escapeLabel: "targetDir", + message: "archive entry escapes targetDir: ../escape.txt", + }, + ])("$name", ({ relPath, originalPath, escapeLabel, expected, message }) => { + const rootDir = path.join(path.sep, "tmp", "archive-root"); + if (message) { + expect(() => + resolveArchiveOutputPath({ + rootDir, + relPath, + originalPath, + escapeLabel, + }), + ).toThrow(message); + return; + } - expect(() => + expect( resolveArchiveOutputPath({ rootDir, - relPath: "../escape.txt", - originalPath: "../escape.txt", - escapeLabel: "targetDir", + relPath, + originalPath, }), - ).toThrow("archive entry escapes targetDir: ../escape.txt"); + ).toBe(expected); }); });