Files
Multi-Debrid-Downloader/tests/package-output-scope.test.ts
Sucukdeluxe 6724b3605c fix: close Windows extraction namespace gaps
Reject alternate data streams, reserved Win32 device names, and trailing-dot or trailing-space aliases before internal ZIP, JVM, or native extraction writes. Preflight native archive entry lists, reconcile opened JVM outputs to partial or removed state on abnormal results, and replace hardlink-based owner markers with portable exclusive reservation plus atomic replacement while allowing direct scoped extraction to continue when marker persistence is unavailable.
2026-08-22 15:40:59 +02:00

189 lines
6.0 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { PackageOutputScope } from "../src/main/package-output-scope";
const tempDirs: string[] = [];
function createRoot(): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-output-scope-"));
tempDirs.push(root);
return root;
}
afterEach(() => {
for (const directory of tempDirs.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});
describe("PackageOutputScope", () => {
it("normalizes and deduplicates complete Windows paths case-insensitively", () => {
const root = createRoot();
const outputPath = path.join(root, "Season", "Episode.mkv");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, "video");
const scope = new PackageOutputScope([root]);
scope.add({
version: 1,
archivePath: path.join(root, "archive-a.rar"),
entryPath: "Season/Episode.mkv",
outputPath,
state: "complete",
disposition: "written"
});
scope.add({
version: 1,
archivePath: path.join(root, "archive-b.rar"),
entryPath: "season/episode.mkv",
outputPath: outputPath.toUpperCase(),
state: "complete",
disposition: "overwritten"
});
expect(scope.completeFiles()).toEqual([path.resolve(outputPath)]);
expect(scope.partialFiles()).toEqual([]);
expect(scope.records()).toHaveLength(1);
});
it.each([
"../foreign.mkv",
"folder/../../foreign.mkv",
"/absolute.mkv",
"C:\\absolute.mkv",
"file.mkv:stream",
"CON",
"aux.txt",
"folder/LPT1.mkv",
"name.",
"name "
])("rejects unsafe archive entry path %s", (entryPath) => {
const root = createRoot();
const outputPath = path.join(root, "safe.mkv");
fs.writeFileSync(outputPath, "video");
const scope = new PackageOutputScope([root]);
expect(() => scope.add({
version: 1,
archivePath: path.join(root, "archive.rar"),
entryPath,
outputPath,
state: "complete",
disposition: "written"
})).toThrow(/Ausgabepfad|entry/i);
});
it("rejects outputs outside authorized roots and unknown event versions", () => {
const root = createRoot();
const foreignRoot = createRoot();
const foreignPath = path.join(foreignRoot, "foreign.mkv");
fs.writeFileSync(foreignPath, "foreign");
const scope = new PackageOutputScope([root]);
const event = {
version: 1 as const,
archivePath: path.join(root, "archive.rar"),
entryPath: "foreign.mkv",
outputPath: foreignPath,
state: "complete" as const,
disposition: "written" as const
};
expect(() => scope.add(event)).toThrow(/autorisiert/i);
expect(() => scope.add({ ...event, outputPath: path.join(root, "safe.mkv"), version: 2 as 1 })).toThrow(/Version/i);
});
it("rejects symlinked parent chains and keeps complete and partial outputs separate", () => {
const root = createRoot();
const foreignRoot = createRoot();
const completePath = path.join(root, "complete.mkv");
const partialPath = path.join(root, "partial.mkv");
fs.writeFileSync(completePath, "complete");
fs.writeFileSync(partialPath, "partial");
const scope = new PackageOutputScope([root]);
scope.add({
version: 1,
archivePath: path.join(root, "archive.rar"),
entryPath: "complete.mkv",
outputPath: completePath,
state: "complete",
disposition: "written"
});
scope.add({
version: 1,
archivePath: path.join(root, "archive.rar"),
entryPath: "partial.mkv",
outputPath: partialPath,
state: "partial",
disposition: "written"
});
expect(scope.completeFiles()).toEqual([path.resolve(completePath)]);
expect(scope.partialFiles()).toEqual([path.resolve(partialPath)]);
const link = path.join(root, "linked");
try {
fs.symlinkSync(foreignRoot, link, "junction");
} catch {
return;
}
const linkedOutput = path.join(link, "linked.mkv");
fs.writeFileSync(path.join(foreignRoot, "linked.mkv"), "foreign");
expect(() => scope.add({
version: 1,
archivePath: path.join(root, "archive.rar"),
entryPath: "linked/linked.mkv",
outputPath: linkedOutput,
state: "complete",
disposition: "written"
})).toThrow(/symbol|reparse/i);
});
it("updates ownership after rename and removal without enumerating the root", () => {
const root = createRoot();
const sourcePath = path.join(root, "source.mkv");
const targetPath = path.join(root, "renamed.mkv");
fs.writeFileSync(sourcePath, "video");
const scope = new PackageOutputScope([root]);
scope.add({
version: 1,
archivePath: path.join(root, "archive.rar"),
entryPath: "source.mkv",
outputPath: sourcePath,
state: "complete",
disposition: "written"
});
fs.renameSync(sourcePath, targetPath);
scope.replacePath(sourcePath, targetPath);
expect(scope.completeFiles()).toEqual([path.resolve(targetPath)]);
expect(scope.removePath(targetPath)).toBe(true);
expect(scope.completeFiles()).toEqual([]);
});
it("validates opened targets before creation and removes discarded partial ownership", () => {
const root = createRoot();
const outputPath = path.join(root, "episode.mkv");
const scope = new PackageOutputScope([root]);
const opened = {
version: 1 as const,
archivePath: path.join(root, "archive.rar"),
entryPath: "episode.mkv",
outputPath,
state: "opened" as const,
disposition: "written" as const
};
expect(scope.add(opened)).toBe(false);
expect(scope.records()).toEqual([]);
fs.writeFileSync(outputPath, "partial");
scope.add({ ...opened, state: "partial" });
expect(scope.partialFiles()).toEqual([outputPath]);
fs.rmSync(outputPath, { force: true });
expect(scope.add({ ...opened, state: "removed" })).toBe(true);
expect(scope.records()).toEqual([]);
});
});