fix: reconcile native extraction outputs

Use the validated native archive plan to recognize exact files after successful WinRAR and 7-Zip runs even when progress output contains no parseable completion lines. Preserve extractor-specific rename targets across subst mappings, keep 7-Zip directory paths when RAR flat mode is active, and reject colliding RAR flat targets before extraction.
This commit is contained in:
Sucukdeluxe
2026-08-22 21:11:58 +02:00
parent bd727208bc
commit 378b5e2a9e
2 changed files with 304 additions and 16 deletions
+121 -2
View File
@@ -21,11 +21,14 @@ import {
orderExtractorCandidatesForArchive,
parseNativeExtractOutput,
parseNativeArchiveEntryList,
resolveExtractorBackendModeForArchive,
remapNativeSubstOutput,
reconcileNativeExtractOutputs,
resolveExtractorBackendModeForArchive,
resolveExtractorBackendMode,
shouldFallbackLegacyRarToJvm,
validateNativeArchiveEntryCandidates,
} from "../src/main/extractor";
validateNativeFlatArchiveEntryCandidates,
} from "../src/main/extractor";
const tempDirs: string[] = [];
const originalExtractBackend = process.env.RD_EXTRACT_BACKEND;
@@ -1731,6 +1734,56 @@ describe("extractor", () => {
]);
});
it("preserves a native renamed filename when remapping a subst output", () => {
const targetDir = "C:\\Downloads\\Extracted";
const event = remapNativeSubstOutput({
version: 1,
archivePath: "C:\\Downloads\\release.rar",
entryPath: "episode.mkv",
outputPath: "Z:\\episode(2).mkv",
state: "complete",
disposition: "renamed"
}, "Z:\\", targetDir);
expect(event.outputPath).toBe(path.resolve(targetDir, "episode(2).mkv"));
expect(event.entryPath).toBe("episode.mkv");
});
it("keeps native 7-Zip output directories when a previous RAR requested flat mode", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-seven-flat-"));
tempDirs.push(root);
const targetDir = path.join(root, "out");
const outputPath = path.join(targetDir, "folder", "episode.mkv");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, "video");
const events = reconcileNativeExtractOutputs(
"7z.exe",
[{ entryPath: "folder/episode.mkv", isDirectory: false }],
path.join(root, "release.7z"),
targetDir,
"overwrite",
new Set(),
true
);
expect(events).toEqual([
expect.objectContaining({ entryPath: "folder/episode.mkv", outputPath })
]);
});
it("rejects colliding RAR flat-mode basenames before extraction", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-rar-flat-collision-"));
tempDirs.push(root);
const targetDir = path.join(root, "out");
fs.mkdirSync(targetDir, { recursive: true });
expect(() => validateNativeFlatArchiveEntryCandidates("Rar.exe", [
"folder-a/episode.mkv",
"folder-b/episode.mkv"
], targetDir)).toThrow(/Kollision/i);
});
it.each(["Entpacke", "Extrayendo", "Extraction"])("parses verified native RAR candidates without depending on the %s locale verb", (verb) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-locale-"));
tempDirs.push(root);
@@ -1744,6 +1797,72 @@ describe("extractor", () => {
]);
});
it("reconciles verified native outputs when WinRAR emits no parseable completion line", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-reconcile-"));
tempDirs.push(root);
const targetDir = path.join(root, "out");
const archivePath = path.join(root, "release.part1.rar");
const outputPath = path.join(targetDir, "folder", "episode.mkv");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, "video");
const events = reconcileNativeExtractOutputs(
"Rar.exe",
[{ entryPath: "folder/", isDirectory: true }, { entryPath: "folder/episode.mkv", isDirectory: false }],
archivePath,
targetDir,
"overwrite",
new Set()
);
expect(events).toEqual([{
version: 1,
archivePath: path.resolve(archivePath),
entryPath: "folder/episode.mkv",
outputPath,
state: "complete",
disposition: "written"
}]);
});
it.each([
["Rar.exe", "episode(1).mkv", "episode(2).mkv"],
["7z.exe", "episode_1.mkv", "episode_2.mkv"]
])("reconciles the newly renamed %s output without claiming an older collision", (command, firstRenamedName, newRenamedName) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-rename-reconcile-"));
tempDirs.push(root);
const targetDir = path.join(root, "out");
const archivePath = path.join(root, "release.rar");
const basePath = path.join(targetDir, "episode.mkv");
const firstRenamedPath = path.join(targetDir, firstRenamedName);
const newRenamedPath = path.join(targetDir, newRenamedName);
fs.mkdirSync(targetDir, { recursive: true });
fs.writeFileSync(basePath, "foreign-base");
fs.writeFileSync(firstRenamedPath, "foreign-renamed");
const existingBefore = new Set([basePath, firstRenamedPath].map((filePath) => (
process.platform === "win32" ? path.resolve(filePath).toLowerCase() : path.resolve(filePath)
)));
fs.writeFileSync(newRenamedPath, "owned");
const events = reconcileNativeExtractOutputs(
command,
[{ entryPath: "episode.mkv", isDirectory: false }],
archivePath,
targetDir,
"rename",
existingBefore
);
expect(events).toEqual([
expect.objectContaining({
entryPath: "episode.mkv",
outputPath: newRenamedPath,
state: "complete",
disposition: "renamed"
})
]);
});
it.each([
["file.mkv:stream", "file.mkv"],
["name.", "name"],