From 4202640904c566e61d1d74530ff5274923432bf3 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sun, 23 Aug 2026 00:22:03 +0200 Subject: [PATCH] fix(extraction): classify RAR directories without losing collision safety Use WinRAR's directory-attribute listing as a counted type source so bare folder entries remain valid while file-directory aliases are still rejected. Emit only removed for successfully deleted JVM partial outputs so asynchronous consumers preserve the original archive error instead of reporting a stale missing file. --- .../JBindExtractorMain$Backend.class | Bin 2310 -> 2310 bytes ...dExtractorMain$BulkExtractCallback$1.class | Bin 1996 -> 1996 bytes ...indExtractorMain$BulkExtractCallback.class | Bin 7032 -> 7083 bytes .../JBindExtractorMain$ConflictMode.class | Bin 2059 -> 2059 bytes ...JBindExtractorMain$ExtractionRequest.class | Bin 2539 -> 2539 bytes .../JBindExtractorMain$OutputTarget.class | Bin 885 -> 885 bytes .../JBindExtractorMain$ProgressTracker.class | Bin 1387 -> 1387 bytes ...indExtractorMain$RawArchivePlanEntry.class | Bin 1106 -> 1106 bytes ...xtractorMain$RawArchivePlanInvariant.class | Bin 3780 -> 3780 bytes ...BindExtractorMain$RawArchivePlanNode.class | Bin 1786 -> 1786 bytes ...ExtractorMain$SevenZipArchiveContext.class | Bin 1664 -> 1664 bytes ...ExtractorMain$SevenZipVolumeCallback.class | Bin 3667 -> 3667 bytes ...indExtractorMain$TargetPlanInvariant.class | Bin 2491 -> 2491 bytes .../JBindExtractorMain$TargetPlanNode.class | Bin 1284 -> 1284 bytes ...ExtractorMain$WrongPasswordException.class | Bin 458 -> 458 bytes .../extractor/JBindExtractorMain.class | Bin 28862 -> 28842 bytes .../extractor/JBindExtractorMain.java | 26 ++++++---- src/main/extractor.ts | 45 ++++++++++++++---- tests/extractor-jvm.test.ts | 34 +++++++++++-- tests/extractor.test.ts | 32 +++++++++++++ 20 files changed, 113 insertions(+), 24 deletions(-) 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 750b665b1e6a9eb14535962c33a947a89a8f8aff..c20c66de3ec53663f7a06c8c9fc7cbe92a0771eb 100644 GIT binary patch delta 96 zcmZn@Y7^RUl#Nk!@-a4jAo-2WhEs=ujYXG%n?-N3HTw!#Lk1BRBL-O(V+KVQQwAFr wGX@713kFXXO9o#S>q#6AKuwMuHbAnDBa&B*fuBX4L5@X(L6=2!@)Hgv0Kxte9{>OV delta 96 zcmZn@Y7^RUl#Nkk@-a4jAo-2WhEtn?jYWrnn?-lBHTw!#0|pTmLk3wEBL+nl69yX= wQw9eXa|TZq3kF{nt4SOVKuwMuHbAnDBa&B@fuBW@`;4Se;ejz^oR>{WY~!{_5b8Ytm8F0J0HIHeor>AeLw0)s}_xS zgMFStQP!c4+=qt`w0S2?c4_l&Y-JC&vsaru!a)X6#So6LAGI7n0|(K>CurqU4D%V@ za0sJ(jv2nh9ADr&hlPhDsyv}Uj0W!|Y!*fD;o=9ZFkv@#IU=gVY0SnlK(5gLL}Q(dL9%osOU7c0jDscPQ6&@5CVgm^Cho{p=#Yu% pmaD@bI$>LcB^IzT+96ybO06|x)jMB|s8qAQ6pg0&7k8Q&e*hBppwa*U delta 570 zcmXwzOGs349EHC-bFR)fVP)QYOc59f8WtL*kU><7M6{KHMTjzESz00@1X4b-cWK>4 z7_^Cq-en`BMGs4A6&18BsGt-dnT3^wLjR05hx2=I&UY<$YR&Cefh-@R7O`~|^$pb( zHF1RaJcn_HEn-69ns7td$Ar1phimsXhC`8=z^JdN8L0)oT8UR13v`eawX;NrSf;}) z*AcRHlw2KSosP3nCn(TKw&)Zk>R_8XsnBV5=nS=T-PKNxb=KYKq+93R4x<+$QK!%< zyJSi(Lwd>%Y<}OsXJ6F!fr)rP%ka&qfPhe)B^_eke7PITlMor z1ANnCCiTQ}^wiy&RA9Y{TujQdirx{>dy@6R(0?RdpUBdnp&v5T!<1=+T^ePN#%R(6 z2Q*Hzz7o|W_w?PA{NROtGNfNtoMzmNl+8BCw%o>)!b;({V@R-lICd-xZ9j|cfF;0( zwcU&&iGB<75Q#?>Z0I? z8M1?SDfiIN{ytj&KaO>|+f!HOcbjMrGe*-)F?PHGoJ*QLj|_W0S#|=Yb|O`_Lx-I_ di|5BPO{w*pJa;NJ)2ecp2LEdDtnLfu`~f7-kx&2t 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 40c01819d549211d6d3361eab4ab30579cebc3a2..0317fda16a330f76625ce820265cf01eaf170bd6 100644 GIT binary patch delta 83 zcmeAc=oZ)@%Eo9vS&U5|NV>6^1IaqJ*|M$-A}nqUvMlZliY%TC#w=b8<}5x84lKS5 jE-e0&&Dev0>N?qjc^w#dSsWRpSezKtS?njjWLE+JyRr}z delta 83 zcmeAc=oZ)@%Eo9nS&U5|NV>6^1IaqJ*|IJSA}p>9vMg>4iYy)s#w?x;<}BU}4lF(l jE-ZeN&Dev0>N?qjdF>f^SsWOoSR5JDS?ngiWLE+JxkwNM 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 7797e1e504d7910acd08efede6a2494ced74e98d..aa364ebac050a12f6377ae4d26f9f2bc17436eef 100644 GIT binary patch delta 126 zcmaDY{91U!H+J4&1{Rhu25y#c1|gP+$;=#DjKP!jICOwyG=~m*umA^x5rgsML=Mr( zi#Y_rd=my!F#iIFAW+pKunt~MEmn|PtI2|#qC)%|7#J8B8FYap8<1vT37(wH2{O13 JOzz|~0sztq8TkMJ delta 126 zcmaDY{91U!H+J421{Ri325y!x1|gR4$;=#Dj6sw2ICOwyG=~m*kN^jR5rgsML=Mr( zi#Y_rd=my!F#iIFAW+pKunt~MEmn|PtI2|#qC)%|7#J8B8FYap8<1vT37VYD2{O13 JOzz|~0szi28S4N5 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 680b937a01fdbf81b54d14ad7d48c0f4aa253a6f..991cc7c49b744648ca17a3896d2889dbd1da604d 100644 GIT binary patch delta 47 xcmey$_LXhJNhZMr1{Rh?22PeF20oT#24R+z$+wua7^5e%GJ{A1Fd5A(1pp(z3m53~o{BBEwz|LZKnjmNim*~2>xQpMP zAX&(fD`Ke;E2R|n3kuePjfL)+Z56UpkG&EO$|yCblIN&~+Fs5_UVTm)a<;E~OdAcj QXiTdKSIxMo>sJ`Cbj?o delta 156 zcmV~$KMMf?90uU`*UNM<*rc0mOsrN5Wm2cm$vHxI{v{;W-EO;Elv!D>l#I5UE%yO@ z244Wr6RDAUyDCWy3AXN`5{uhLh8^VCMS(g>?4iOwsx;8x08N^(IfTa%&K%>y2|`Zs zpoJ%Gyf{P5Io@30!zI34NtSC-X-b|ODRL_{?xfDWSUiYBM^1Dlq=zE_J@}=3(E>kR F{sW(LCX@gG 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 197684c2e0c01901a2ccfc3df8712d7f655e528c..ec8056a39675f74edb0bd3360f7e379ce202c403 100644 GIT binary patch delta 55 zcmeyx`-^wOOIF@a1{UTv27cx)1`+1&$=_JD8CxbRv1tKGUocq#CKt2m07WjaX#oJ* C(-4aQ delta 55 zcmeyx`-^wOOIF?v1{UU427cyF1`+12$=_JD8Jj07v1tKGUocq#CKt2m07WjaX#oJ) C+7N;O 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 ed167b38c50d45f3d8bafbb00243382651b6dd42..d90666a3d590668332ef6b534214dcd2c4ec2bba 100644 GIT binary patch delta 101 zcmZqRZQ$MD&MF$gz``2Iz{wiLz{eWRAj}%WAjuj#Ifk{(GLeCuHJL$xH5DkD!XUz$ z&LGX2$)LiT&7jVj#h}5O%b?Gi&tS${$Y9A@z+lB%%;3mc!r;MLHd&g@i7{w$9GemV DW?vD1 delta 101 zcmZqRZQ$MD&MF$jz``2Lz{wiIz{eWOAj}%ZAjuj&Ifk{(GMa&%HI_kuH6AD$#~{L* z$RN#{%%H-W%An4g!l1#L&Y;hl$zaBs&0xuz#bCvn%izeG$Kb(QFj<<-iP2|r9GemV DOy3au 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 0762d88d6bafd69cddf7afc61607447eb6a5b6ab..33816fd4f44e66aaa7b2380551a26c8ebe7e7a07 100644 GIT binary patch delta 211 zcmV;^04)F09Mc@IT?!R{00aen01pL#02&2>03ro~05AoElX41V35Ng&1&9C)1&Wj3 z3a>4X00sq+02l?402&3903QXF04@cV05b)b06zto089m%08#~;09*x}09^&0le-IB z0i=@`3?>1olRgY80j-m145&S^01gGR01ySW02~Fj03!vr05Ao(05=7>067J_073=4 z07nJA0AU5b0BHrk0Bi-p0C)w$0DA?!0D%R^lTHn*8O#7813Iax8}vGqhXD zibkkvlA7n8YKFQ}G?eB{OPp(s3vFXd@YL}i0ysPA~G9PArygAkP4nGy+TXDXW;73V* LHP4?31(yE-X)-mz 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 e8d77579c9aefaf9dd191b0600fe8c9cd9d80c7d..ba1fb3807a5075b5459c8395ed7f122b2e203714 100644 GIT binary patch delta 118 zcmV-+0Ez#*6T1_zSO*1~00aY@vt0*F0a=~^37Wb^rhX 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 4f2b161a60a1c78cd5c6b5450fad87aad51081c5..59e99dc18644ebb5fc214db48acd9b9775581ab8 100644 GIT binary patch delta 61 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 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);