fix: preflight complete archives before mutation
Plan and validate every internal ZIP, Zip4j, and SevenZipJBinding entry and final conflict target before any archive output mutates the filesystem. Preserve raw RAR list entry whitespace for strict validation and keep any non-matching package owner marker immutable while direct scoped outputs continue without marker reissuance.
This commit is contained in:
@@ -13581,6 +13581,62 @@ describe("download manager", () => {
|
||||
expect(session.packages[packageId].outputRecords).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["stale-generation", 2, 1, "11111111-1111-4111-8111-111111111111", "11111111-1111-4111-8111-111111111111"],
|
||||
["stale-owner", 1, 1, "22222222-2222-4222-8222-222222222222", "33333333-3333-4333-8333-333333333333"]
|
||||
] as const)("keeps a %s marker unchanged while direct scoped output continues", async (_label, currentGeneration, markerGeneration, currentOwner, markerOwner) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-owner-stale-empty-"));
|
||||
tempDirs.push(root);
|
||||
const packageName = "stale-empty-package";
|
||||
const extractDir = path.join(root, "extract", packageName);
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
const packageId = "stale-empty-package-id";
|
||||
const markerPath = path.join(extractDir, ".rd-package-output-owner-v1.json");
|
||||
const markerContent = JSON.stringify({
|
||||
version: 1,
|
||||
packageId,
|
||||
generation: markerGeneration,
|
||||
ownerId: markerOwner
|
||||
});
|
||||
fs.writeFileSync(markerPath, markerContent);
|
||||
const session = emptySession();
|
||||
const pkg: PackageEntry = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir: path.join(root, "downloads", packageName),
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
outputOwnerId: currentOwner,
|
||||
outputOwnerGeneration: currentGeneration,
|
||||
resultGeneration: currentGeneration,
|
||||
createdAt: 1_000,
|
||||
updatedAt: 1_000
|
||||
};
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = pkg;
|
||||
const manager = new DownloadManager(defaultSettings(), session, createStoragePaths(path.join(root, "state")));
|
||||
|
||||
await (manager as any).runWithPackageOutputProvenance(pkg, async (targetDir: string, scope: any) => {
|
||||
const outputPath = path.join(targetDir, "owned.mkv");
|
||||
fs.writeFileSync(outputPath, "owned");
|
||||
scope.add({
|
||||
version: 1,
|
||||
archivePath: path.join(pkg.outputDir, "archive.rar"),
|
||||
entryPath: "owned.mkv",
|
||||
outputPath,
|
||||
state: "complete",
|
||||
disposition: "written"
|
||||
});
|
||||
});
|
||||
|
||||
expect(fs.readFileSync(markerPath, "utf8")).toBe(markerContent);
|
||||
expect(fs.readFileSync(path.join(extractDir, "owned.mkv"), "utf8")).toBe("owned");
|
||||
expect(pkg.outputRecords).toEqual([expect.objectContaining({ entryPath: "owned.mkv" })]);
|
||||
});
|
||||
|
||||
it("does NOT move bonus files from Extras subdirectory to flat library", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
@@ -360,6 +360,47 @@ describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm b
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}, 10000);
|
||||
|
||||
it.each(["7zjbinding", "zip4j"])("preflights every %s entry before overwriting an earlier safe target", (backend) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), `rd-jvm-full-preflight-${backend}-`));
|
||||
tempDirs.push(root);
|
||||
const targetDir = path.join(root, "out");
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
const safePath = path.join(targetDir, "00-safe.txt");
|
||||
const aliasPath = path.join(targetDir, "zz-name");
|
||||
fs.writeFileSync(safePath, "foreign-safe");
|
||||
fs.writeFileSync(aliasPath, "foreign-alias");
|
||||
const zipPath = path.join(root, "preflight.zip");
|
||||
const zip = new AdmZip();
|
||||
zip.addFile("00-safe.txt", Buffer.from("package-safe"));
|
||||
zip.addFile("zz-name.", Buffer.from("package-invalid"));
|
||||
zip.writeZip(zipPath);
|
||||
const runtimeRoot = path.join(process.cwd(), "resources", "extractor-jvm");
|
||||
const classPath = [
|
||||
path.join(runtimeRoot, "classes"),
|
||||
path.join(runtimeRoot, "lib", "sevenzipjbinding.jar"),
|
||||
path.join(runtimeRoot, "lib", "sevenzipjbinding-all-platforms.jar"),
|
||||
path.join(runtimeRoot, "lib", "zip4j.jar")
|
||||
].join(path.delimiter);
|
||||
|
||||
const run = spawnSync("java", [
|
||||
"-cp",
|
||||
classPath,
|
||||
"com.sucukdeluxe.extractor.JBindExtractorMain",
|
||||
"--archive",
|
||||
zipPath,
|
||||
"--target",
|
||||
targetDir,
|
||||
"--conflict",
|
||||
"overwrite",
|
||||
"--backend",
|
||||
backend
|
||||
], { encoding: "utf8" });
|
||||
|
||||
expect(run.status).not.toBe(0);
|
||||
expect(fs.readFileSync(safePath, "utf8")).toBe("foreign-safe");
|
||||
expect(fs.readFileSync(aliasPath, "utf8")).toBe("foreign-alias");
|
||||
});
|
||||
|
||||
it("emits progress callbacks with archiveName and percent", async () => {
|
||||
process.env.RD_EXTRACT_BACKEND = "jvm";
|
||||
|
||||
@@ -1661,5 +1661,44 @@ describe("extractor", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preflights every internal ZIP entry before overwriting an earlier safe target", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-zip-full-preflight-"));
|
||||
tempDirs.push(root);
|
||||
const packageDir = path.join(root, "pkg");
|
||||
const targetDir = path.join(root, "out");
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
const safePath = path.join(targetDir, "00-safe.txt");
|
||||
const aliasPath = path.join(targetDir, "zz-name");
|
||||
fs.writeFileSync(safePath, "foreign-safe");
|
||||
fs.writeFileSync(aliasPath, "foreign-alias");
|
||||
const zip = new AdmZip();
|
||||
zip.addFile("00-safe.txt", Buffer.from("package-safe"));
|
||||
zip.addFile("zz-name.", Buffer.from("package-invalid"));
|
||||
zip.writeZip(path.join(packageDir, "release.zip"));
|
||||
|
||||
const result = await extractPackageArchives({
|
||||
packageDir,
|
||||
targetDir,
|
||||
cleanupMode: "none",
|
||||
conflictMode: "overwrite",
|
||||
removeLinks: false,
|
||||
removeSamples: false
|
||||
});
|
||||
|
||||
expect(result.extracted).toBe(0);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(fs.readFileSync(safePath, "utf8")).toBe("foreign-safe");
|
||||
expect(fs.readFileSync(aliasPath, "utf8")).toBe("foreign-alias");
|
||||
});
|
||||
|
||||
it("preserves raw RAR list trailing whitespace and dots for validation", () => {
|
||||
const entries = parseNativeArchiveEntryList("UnRAR.exe", "safe.mkv\r\nname \r\nname.\r\n");
|
||||
expect(entries).toEqual(["safe.mkv", "name ", "name."]);
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-raw-list-"));
|
||||
tempDirs.push(root);
|
||||
expect(() => validateNativeArchiveEntryCandidates(entries, root)).toThrow(/Ausgabepfad|entry/i);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user