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.
189 lines
6.0 KiB
TypeScript
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([]);
|
|
});
|
|
});
|