diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$Backend.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$Backend.class index 750b665..c20c66d 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$Backend.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$Backend.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$BulkExtractCallback$1.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$BulkExtractCallback$1.class index 041ec55..1a901bc 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$BulkExtractCallback$1.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$BulkExtractCallback$1.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$BulkExtractCallback.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$BulkExtractCallback.class index 1276d62..acdb6fd 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$BulkExtractCallback.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$BulkExtractCallback.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ConflictMode.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ConflictMode.class index 40c0181..0317fda 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ConflictMode.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ConflictMode.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ExtractionRequest.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ExtractionRequest.class index 7797e1e..aa364eb 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ExtractionRequest.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ExtractionRequest.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$OutputTarget.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$OutputTarget.class index 680b937..991cc7c 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$OutputTarget.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$OutputTarget.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ProgressTracker.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ProgressTracker.class index fd3441f..8c8a0c7 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ProgressTracker.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ProgressTracker.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanEntry.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanEntry.class index 4d195dc..3630119 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanEntry.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanEntry.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanInvariant.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanInvariant.class index d9d0e5e..be482e3 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanInvariant.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanInvariant.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanNode.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanNode.class index 197684c..ec8056a 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanNode.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanNode.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipArchiveContext.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipArchiveContext.class index ed167b3..d90666a 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipArchiveContext.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipArchiveContext.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipVolumeCallback.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipVolumeCallback.class index 0762d88..33816fd 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipVolumeCallback.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipVolumeCallback.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanInvariant.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanInvariant.class index e8d7757..ba1fb38 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanInvariant.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanInvariant.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanNode.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanNode.class index 4f2b161..59e99dc 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanNode.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanNode.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$WrongPasswordException.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$WrongPasswordException.class index a7ec7a5..4303c1d 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$WrongPasswordException.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$WrongPasswordException.class differ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class index e5e2ec3..c443794 100644 Binary files a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class and b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class differ diff --git a/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java b/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java index f30094c..60c2826 100644 --- a/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java +++ b/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java @@ -375,12 +375,13 @@ public final class JBindExtractorMain { throw error; } finally { if (!extractionSuccess && output.exists()) { - emitOutput(request.archiveFile, entryName, output, "partial", outputTarget.disposition); + if (output.delete()) { + emitOutput(request.archiveFile, entryName, output, "removed", outputTarget.disposition); + } else { + emitOutput(request.archiveFile, entryName, output, "partial", outputTarget.disposition); + } } - if (!extractionSuccess && output.exists() && output.delete()) { - emitOutput(request.archiveFile, entryName, output, "removed", outputTarget.disposition); - } - } + } } progress.emitDone(); @@ -1294,14 +1295,19 @@ public final class JBindExtractorMain { closeCurrentStreamOnly(); if (!currentSuccess[0] && currentOutput[0] != null && currentOutput[0].exists()) { int pos = currentPos[0]; - if (pos >= 0) { + if (currentOutput[0].delete()) { + if (pos >= 0) { + emitOutput(archiveFile, entryNames.get(pos), currentOutput[0], "removed", dispositions.get(pos)); + } + } else if (pos >= 0) { emitOutput(archiveFile, entryNames.get(pos), currentOutput[0], "partial", dispositions.get(pos)); } - if (currentOutput[0].delete() && pos >= 0) { - emitOutput(archiveFile, entryNames.get(pos), currentOutput[0], "removed", dispositions.get(pos)); - } } - } + currentOutput[0] = null; + currentPos[0] = -1; + currentSuccess[0] = false; + currentRemaining[0] = 0; + } } private static final class WrongPasswordException extends Exception { diff --git a/src/main/extractor.ts b/src/main/extractor.ts index 49671bd..7b060f4 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -2396,17 +2396,17 @@ export function buildExternalExtractArgs( return ["x", "-y", "-bb1", "-sccUTF-8", overwrite, pass, archivePath, `-o${targetDir}`]; } -export function buildExternalListArgs(command: string, archivePath: string, password = ""): string[] { +export function buildExternalListArgs(command: string, archivePath: string, password = "", directoriesOnly = false): string[] { if (isRarNativeCommand(command)) { const pass = password ? `-p${password}` : "-p-"; - return ["lb", pass, "-y", archivePath]; + return directoriesOnly ? ["lb", "-e+d", pass, "-y", archivePath] : ["lb", pass, "-y", archivePath]; } const pass = password ? `-p${password}` : "-p"; return ["l", "-slt", "-sccUTF-8", pass, archivePath]; } -export function parseNativeArchiveEntryList(command: string, output: string): string[] { - return parseNativeArchiveEntryCandidates(command, output).map((entry) => ( +export function parseNativeArchiveEntryList(command: string, output: string, directoryOutput = ""): string[] { + return parseNativeArchiveEntryCandidates(command, output, directoryOutput).map((entry) => ( entry.isDirectory && !/[\\/]$/.test(entry.entryPath) ? `${entry.entryPath}/` : entry.entryPath )); } @@ -2415,13 +2415,25 @@ type NativeArchiveEntryCandidate = { entryPath: string; isDirectory: boolean }; type NativeEntryPreflightResult = ExtractSpawnResult & { entries: NativeArchiveEntryCandidate[] }; -function parseNativeArchiveEntryCandidates(command: string, output: string): NativeArchiveEntryCandidate[] { +function parseNativeArchiveEntryCandidates(command: string, output: string, directoryOutput = ""): NativeArchiveEntryCandidate[] { const lines = String(output || "").split(/\r?\n/); if (isRarNativeCommand(command)) { - return lines.filter((line) => line.length > 0).map((entryPath) => ({ - entryPath, - isDirectory: /[\\/]$/.test(entryPath) - })); + const directoryCounts = new Map(); + for (const entryPath of String(directoryOutput || "").split(/\r?\n/).filter((line) => line.length > 0)) { + const key = entryPath.replace(/\\/g, "/").replace(/\/+$/, "").toLocaleLowerCase("en-US"); + directoryCounts.set(key, (directoryCounts.get(key) || 0) + 1); + } + return lines.filter((line) => line.length > 0).map((entryPath) => { + const key = entryPath.replace(/\\/g, "/").replace(/\/+$/, "").toLocaleLowerCase("en-US"); + const directoryCount = directoryCounts.get(key) || 0; + if (directoryCount > 0) { + directoryCounts.set(key, directoryCount - 1); + } + return { + entryPath, + isDirectory: /[\\/]$/.test(entryPath) || directoryCount > 0 + }; + }); } const entries: NativeArchiveEntryCandidate[] = []; let inEntries = false; @@ -2573,8 +2585,21 @@ async function runNativeEntryPreflight( if (!result.ok) { return { ...result, entries: [] }; } + const directoryChunks: string[] = []; + if (isRarNativeCommand(command)) { + const directoryResult = await runExtractCommand( + command, + buildExternalListArgs(command, archivePath, password, true), + (chunk) => directoryChunks.push(chunk), + signal, + timeoutMs + ); + if (!directoryResult.ok) { + return { ...directoryResult, entries: [] }; + } + } try { - const entries = parseNativeArchiveEntryCandidates(command, chunks.join("")); + const entries = parseNativeArchiveEntryCandidates(command, chunks.join(""), directoryChunks.join("")); if (entries.length === 0) { throw new Error("Native Archivliste enthält keine validierbaren Einträge"); } diff --git a/tests/extractor-jvm.test.ts b/tests/extractor-jvm.test.ts index 89751d8..81bd76e 100644 --- a/tests/extractor-jvm.test.ts +++ b/tests/extractor-jvm.test.ts @@ -297,7 +297,7 @@ describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm b expect(second).toEqual(expect.objectContaining({ extracted: 1, failed: 0 })); }, 10000); - it.each(["7zjbinding", "zip4j"])("reports %s partial output before removing a failed file", (backend) => { + it.each(["7zjbinding", "zip4j"])("reports only removed after deleting a failed %s output", (backend) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), `rd-jvm-partial-${backend}-`)); tempDirs.push(root); const targetDir = path.join(root, "out"); @@ -333,9 +333,35 @@ describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm b .map((line) => line.split(" ")[2]); expect(run.status).not.toBe(0); - expect(states[0]).toBe("opened"); - expect(states).toContain("partial"); - expect(states[states.length - 1]).toBe("removed"); + expect(states).toEqual(["opened", "removed"]); + expect(fs.existsSync(path.join(targetDir, "episode.bin"))).toBe(false); + }); + + it("preserves the real JVM archive error when a failed output is removed before Node consumes the event", async () => { + process.env.RD_EXTRACT_BACKEND = "jvm"; + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-jvm-removed-output-error-")); + tempDirs.push(root); + const packageDir = path.join(root, "pkg"); + const targetDir = path.join(root, "out"); + const zipPath = path.join(packageDir, "corrupt.zip"); + fs.mkdirSync(packageDir, { recursive: true }); + const zip = new AdmZip(); + zip.addFile("episode.bin", Buffer.from("payload-".repeat(20_000))); + zip.writeZip(zipPath); + corruptFirstZipPayload(zipPath); + + const result = await extractPackageArchives({ + packageDir, + targetDir, + cleanupMode: "none", + conflictMode: "overwrite", + removeLinks: false, + removeSamples: false + }); + + expect(result).toEqual(expect.objectContaining({ extracted: 0, failed: 1 })); + expect(result.lastError).not.toContain("Gemeldete Extract-Ausgabe existiert nicht"); + expect(result.lastError).toMatch(/crc|data|checksum|zip|archive/i); expect(fs.existsSync(path.join(targetDir, "episode.bin"))).toBe(false); }); diff --git a/tests/extractor.test.ts b/tests/extractor.test.ts index 5208a06..3335377 100644 --- a/tests/extractor.test.ts +++ b/tests/extractor.test.ts @@ -1930,6 +1930,38 @@ describe("extractor", () => { ].join("\n"))).toEqual(["folder/", "folder/episode.mkv"]); }); + it("classifies bare RAR directory names through the attribute-filtered directory listing", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-rar-directory-list-")); + tempDirs.push(root); + const allEntries = [ + "Doona.S01E02.German.DL.720p.WEB.x264-WvF\\doona.s01e02.german.dl.720p.web.x264-wvf.mkv", + "Doona.S01E02.German.DL.720p.WEB.x264-WvF" + ].join("\r\n"); + const directoryEntries = "Doona.S01E02.German.DL.720p.WEB.x264-WvF\r\n"; + + expect(buildExternalListArgs("Rar.exe", "archive.rar", "", true)).toEqual([ + "lb", "-e+d", "-p-", "-y", "archive.rar" + ]); + const parsed = parseNativeArchiveEntryList("Rar.exe", allEntries, directoryEntries); + expect(parsed).toEqual([ + "Doona.S01E02.German.DL.720p.WEB.x264-WvF\\doona.s01e02.german.dl.720p.web.x264-wvf.mkv", + "Doona.S01E02.German.DL.720p.WEB.x264-WvF/" + ]); + expect(() => validateNativeArchiveEntryCandidates(parsed, root)).not.toThrow(); + expect(() => validateNativeArchiveEntryCandidates( + parseNativeArchiveEntryList("Rar.exe", allEntries), + root + )).toThrow(/Datei ist Vorfahr/i); + expect(() => validateNativeArchiveEntryCandidates( + parseNativeArchiveEntryList("Rar.exe", "same\r\nsame\r\n", "same\r\n"), + root + )).toThrow(/mehrfaches|typwidriges/i); + expect(() => validateNativeArchiveEntryCandidates( + parseNativeArchiveEntryList("Rar.exe", "same\r\nsame\r\n", "same\r\nsame\r\n"), + root + )).not.toThrow(); + }); + 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);