From b5f57e7a292f439702e546adc5533cb79dcffe6f Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 22 Aug 2026 16:07:35 +0200 Subject: [PATCH] fix: reject colliding archive targets before extraction Build a typed normalized target plan for internal ZIP, native list preflight, Zip4j, and SevenZipJBinding before any output directory or file is mutated. Reject file-directory aliases, file ancestors, case-insensitive aliases, and duplicate file targets while allowing identical directory declarations. Preserve linear archive planning and ship the rebuilt Java runtime classes. --- .../JBindExtractorMain$Backend.class | Bin 2310 -> 2310 bytes ...dExtractorMain$BulkExtractCallback$1.class | Bin 1996 -> 1996 bytes ...indExtractorMain$BulkExtractCallback.class | Bin 7032 -> 7032 bytes .../JBindExtractorMain$ConflictMode.class | Bin 2059 -> 2059 bytes ...JBindExtractorMain$ExtractionRequest.class | Bin 2539 -> 2539 bytes .../JBindExtractorMain$OutputTarget.class | Bin 884 -> 884 bytes .../JBindExtractorMain$ProgressTracker.class | Bin 1387 -> 1387 bytes ...ExtractorMain$SevenZipArchiveContext.class | Bin 1663 -> 1664 bytes ...ExtractorMain$SevenZipVolumeCallback.class | Bin 3667 -> 3667 bytes ...indExtractorMain$TargetPlanInvariant.class | Bin 0 -> 2491 bytes .../JBindExtractorMain$TargetPlanNode.class | Bin 0 -> 1284 bytes ...ExtractorMain$WrongPasswordException.class | Bin 458 -> 458 bytes .../extractor/JBindExtractorMain.class | Bin 27379 -> 27769 bytes .../extractor/JBindExtractorMain.java | 65 ++++++++-- src/main/extractor.ts | 97 ++++++++++++-- tests/extractor-jvm.test.ts | 121 +++++++++++++++++- tests/extractor.test.ts | 116 ++++++++++++++++- 17 files changed, 376 insertions(+), 23 deletions(-) create mode 100644 resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanInvariant.class create mode 100644 resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanNode.class 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 eac6eac06e484fa07eb5cd3b4d400b24094c6337..775b7f454113c504507afe929adb012e71ec7fd9 100644 GIT binary patch delta 103 zcmZn@Y7^Sv#mZ>5*_*YBk#XnbV{H0B@*A5C=K%&b=7S8}%!ejhv#*dn${@mgj6s(9 zID;bdNd_C{Qw$EwXBa%0&ocNjpP$6x0Mz8jVFM)VI3js>G4M0*W{_jv!=TH&bMg}o FB>*=99!LNH delta 103 zcmZn@Y7^Sv#mZ=~*_*YBk+Ey?F*bc5`Hjtnb0Py9^CSjt=E;+-*;mL;V-R7U&LGP? zgF%sb7K07*Yz7DBxeT7n^B8=Y7fj-C0BUmNumO^F9Fe@;4E)SJ406o947$u+lb>)X F0RWY|8pQwr 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 6e2d9e5fe9f5fb3b8e51eceb0c948975bcf74efb..e51f9c4bfb6da1c08f40ba15e52d73ff1ba8d456 100644 GIT binary patch delta 105 zcmX@Ze};d9F(bQ?fq?-dL;U26EH0bt7*_)Y3_${JAc4sx%ueh^KoyJ(O<)C#EW4N( zqbBcRo2MGfz{wKFz|E4#AiIsvgk8#vluZ*u$VGPvluhTu$VAdvzRe>vzRkPuvjo8vsg~9XZHmFel{0L 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 67f8beca129d3701697a9df81af144910be68b93..87f5ffdebfa76128c2fe8622ce3e660a025b8947 100644 GIT binary patch delta 476 zcmWlVOGs347{$+jIQOOoLN~5=?%XEJavIZO=K7csu}H&|(W1v_>Z%r+isC|rQhcNa zX{7W~5j_a9q#y_`%)&N-`)U;gZ6cy&5-kK5^`Fi8HivV5-`TF)u6ws+sFW-ly=FR* zP{_)vVatzQmOM0y^>rj|W>kH!U`AQ!SLZD?@pk!chUH&@6MQZDX@>lSeAqJ*@{^LV zvIV}MH{FcOXX|+I-l__nr46zKg>+ENF!s`g%Lq#8p`80r$taTCj}%MM$OCBRLA0?9 z=XeNN9!4+AafLDT^9Y6+ms&d+VwGI8W6GBmt*IbqXH<8CZfS(g6FU7QJf70&O=w^< zns{2LwBRbwV1TU{Vg_Su#~eE_&+}O21#I#nKJgO1F^k{4j6dweUv?>*J<{W}snKA~ zNvn=A?B+PaoY45!5#tRU8gorYBU+SX+4xb>7&=9ipk+A`m_bcdN&JN^4+f)TI1s=!t^`(&4rn6Qd z?B%M`-L!sX?eL#jO`$%S2W0^f`GBZ=M6G;+E1yv>UyziqXq0d8WD)JMggx>dowAH> z`GI~}K|y|^D8F!8eq&TB7?(e~&Gtg_SD&?0O7v8~uwSBiiu@{}}#~=@5j7L!7QQY7FZt)my^Ek>J!c!K5k`s8z zlUU#>Rl{MOaSs|J!R%2z>E`V*M0f!fOTme8Y~)2GcuBu@XA%>Ea~TbsM21&_#;X|M bHH`2&rg&4A>vpLZdZ})Ez<9jMu6WCTAE9xP 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 c5ca777ea1acdac2af04a40e25131f3745994362..e162c6dda44f8de207e2f4df2d956c5a27a346a9 100644 GIT binary patch delta 83 zcmeAc=oZ)@%EowkvKX5_kaS}+2aGGB1_lNOMh0CV$p)kun4eEh<^&ns K2PSuN8UX+bOd<^c delta 126 zcmaDY{91U!H+J3~3@psM8MvAEFbFa4oy^Rk#kga#9)}K)jONf`-yy)kV8mcNIgvwj z@?s7_FyDm16wJTCAqZ6U2&{vbQ;QX()@rgKr>GGB1_lNOMh0CV$p)kun0HK0<^&ns K2PSuN8UX-RW*>V1 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 4494fee342cf1a2e43808b497528ea72297a6db1..b682b5fd2b0b29d81027dbf490e66f59bf97e8dd 100644 GIT binary patch delta 143 zcmeyu_JwUi6BA3?#2y7Mo}$!({GyW76t{`%IyuY@3>X=_HGC!;FzHOrW>n|1010p< zCMTyB7pquKUch*Rlaqmqfq_AYfp>Bvla3C%md;iNZY|xd42&Cr!i)@JKvIl>m4Siz m3j+)DR|ZbzZw!3Q-x-9Ne@wo~q{aAtG7B?^)CZGM%u)cbvLqn@ delta 143 zcmeyu_JwUi6BkcWYC(QcNotDQ#1;i6mb8iMIyuY?3>X=_HGC!;FzHOrW>n`h2MKT` zCMTyB7pquIUch*Rlb3;ufq_AYfpc;rla3Czmd;iNb}ikl42&Cr!i)@JKvIl>m4Siz mC<6=gF$PZN;|zSvCm4j8Pfotcq{Vn(G7B?^)CZGM%u)c510tRP 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 f146309b51eb7deee307e1a596dd5a74c0ecf70b..037d8a7023b14922e6b52a41e9bfcbaa24d90dc2 100644 GIT binary patch delta 113 zcmV-%0FM9b3hN56#RCz&00acP00{)X01^bh02c(nlhy+(1;YRZ1jLgD1Zf?|00soe z00#uh01*Vs038I(03`&@06qlI07C@OlZ*sD3DW=w1k?Zy1l5z%1YH%_01E`!022h- T02Bn=02u_`05=5QlSKt4Fv}ma delta 113 zcmV-%0FM9b3hN56#RCzJ00ab#00{(-01^a{02c(2lhy+(1(W~<1eKEp1Zf?Z00sn^ z00#t{01*V7038IK03`&U06qku07C?!lZ*sD38VlB1f>8D1g4YI1YH%W01E`F022hO T02BnR02u_X05=4$lSKt4KYkla 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 0e095f664cc668315b43e209f7801383bbca9bbd..654938aa0c8393c5c013d3ad11d5b4cdf7bcad8b 100644 GIT binary patch delta 117 zcmV-*0E++r41f)=#sdlqVPj)ub8{pyFtg7CcL5dW00ac+00{)?01gD|022i302u`A zlUoIbIq(1n1o8k71oQwC1oHqC1oi+M1o!|X1o{9c1o;3c1pELm1pWXv1poj!1pfd! X1p)v;1p@#`1qG8D20{VhlU)WP4kRDD delta 116 zcmV-)0E_>C4gUXY1&j22k=#z?cu;VQ-A7z|Hho%$zSX}p2pK0MhOyHA*pqiR3>Gg-H}ULaWXPw?aciu zv8s7;T4pUW^UATI5}T?J=!7jbD5y=S9^2}(V}BO9PQ_+S;OyID6FP@>Y(9=oUdB;5c2 delta 221 zcmWN{ODjZi9LM3$nZ7@hELXVN1Vk>(5lXxpKUy7kOSX`IMnDwZ&2 RlT>Yr37cldYA%;u{s){aF)aW9 diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanInvariant.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$TargetPlanInvariant.class new file mode 100644 index 0000000000000000000000000000000000000000..8da7bd210dc9f97876920ca1e23510060b9700ef GIT binary patch literal 2491 zcmbtW%~Kpj6#q57yR!^K*boR=1l>eJSd!i4JIV492#`nu1O-7hF*@vaV8Sr7&deey zuX^(8A#m{EB{!>5r9ezowDe$=|Al{op0p}ue7(EK5;h0SWxD%yzhA$P-|IKO{qw^! z06lP3RDdh+Y49V!Fr2p65^gS?Tg?bFw;>W@!*lesXFG`-R}CvOzPFsx4Xb5Ncb0`W zW9rs~wXQpcZg~uW0mCx9AqL(SomUZ*@Rbaq`}(?`prgx)TTAyv+EY-)(EmU0o3=AT zg^Frv_c4a) z`U#nyb`9rop~Sat>D%xTll4-VqbUV(h7)`7nYcB+krr9cuq_1y->}cLLLjc@3^OAf z6$$Vlx*1{x#F%7m?51s+hD*xH$e8X4BVxE-WZrh}=_^iT-L@i9uUA7KE|Q0OHY=35B|^}% z3%;p{;i@cxQHJ_*Hc<(x7}szO*GuxyBl9r*nt3R=oAV4aF{Nkwt1y8Z3T|qc#1uny z-pRdP;sklg&{}F^;UPG{?V?Cx-O@0FcS$juBgIQ)6ied$YAEUdtcJM*JnZf02x6X4 zU%`Ymu|fn$uLw6nmF+~l2iZ@JjAJa5AY}+MA`SdN!y-~tW`;Yymh~Rwd7ld64m~8V zz&*?`VOWHzTXc)C^4KmhPQ_gfAK~MYl=XIWF^s%k%2Lsk#9q>nK7idVv3;ZDDzR~r zESk=(EeU5%Uy}UfdWINZTTH;-iAl z76tY<9BwReNgXPa3Cj}BsHwZIAgvSo??6erIkqpAkFWxVA%2vXx1|#u)Cq#r56MZ{ z4rscq<2io_jd7M`g^p$-SJPsdE(CQwRX-%r^4^pk7 z@!a`coCw_sCAJZsj@7=kjnheA`*)~G`oq3tARKtsr|`aDeXw5n23|N&AM8q2hAX#m zb_;<=xD)p8BD#swPZ3*uip1g$I%~VO(Q|$qmuT3=&<;j^l&8kRl~3?a1%85>RKx18 zXxcNFmWBPW`UKM)3t=_s|2)VZz4$T@zlAC}zPE|KrcKlp=UQ>k_qg=#_Yd9Ow1v1l zI`#w$3|j|Hy?y9-*x$5^4;N_Ig5rBbf!l??AOl(f8PL1bM8UhD@ov=d9yIb^^zc3m z@Qb*@FXK8-Lhu2s^Fe&Zhwufzg0J`p9@4q5`6#~SWB8em;}?D%zw-(F$#1X_pJWYu zina1-tk4~E&|aVwl?o~pTxJRy6&MOs`ZCJ*bDSd4+2@Fp>~HYD;IFA`j?d8Q1!)If zR+A_y!z%4bzQjV1KPN>FHPc8GfuA_U_IV>ICV5iy(s*>isV{{9od8t$bLgQg>HAb})7z{fiyA< zDQ8c*9bb41+4?^J$gL=puC>KK*h*#?IH$tL-$_?wXd`RD#2iC%U3xOyV9*N1-3;cD z)p4G|>?0^{*LvCBr;s`>Fw70W&v>v$@C+7^rM#N5>+vx1DaW^?;n)AVeN;2_rPsCV zQ`ErTu;~_?{c2?E(i4s7pe_6z-gYT=tuPt8Y8Q7IX1L>sAjns1l?ubd2^pjR$S$^q zfNKiam=ZWzPrCkTz7G_B$h*(*Xu4pnk@Ra7B4(Ps$aln3Y8-~evrW3Hnu5W2?s>xB zba@bnKu3|GJjrinm>n&ojxxjQB#3+!73%DHnmY|SH5OpFNL~z==%N1auW1a)>a;@x zK#I%cmFWZ{YnG0YEB(O2n`0~Dw{eGTj#e=gP@;O?B_C$$4>Nu+a{vGU literal 0 HcmV?d00001 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 6d0f56e54db7cfe2c6a19adf91c19f5ce57e4348..e130b7d83eaa8e632c06cf4dac864d13fdde5c0f 100644 GIT binary patch delta 17 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 delta 12413 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 JP5c3Z{|_h*U^V~% diff --git a/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java b/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java index 6982c05..cafb013 100644 --- a/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java +++ b/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java @@ -263,6 +263,7 @@ public final class JBindExtractorMain { progress.emitStart(); Set preflightReserved = new HashSet(); + TargetPlanInvariant targetPlan = new TargetPlanInvariant(); for (FileHeader header : fileHeaders) { if (header == null) { continue; @@ -270,9 +271,11 @@ public final class JBindExtractorMain { String entryName = normalizeEntryName(header.getFileName(), "file"); if (header.isDirectory()) { File dir = resolveDirectory(request.targetDir, entryName); + targetPlan.add(dir, true); preflightReserved.add(pathKey(dir)); } else { - resolveOutputFile(request.targetDir, entryName, request.conflictMode, preflightReserved); + OutputTarget outputTarget = resolveOutputFile(request.targetDir, entryName, request.conflictMode, preflightReserved); + targetPlan.add(outputTarget.reportedFile, false); } } @@ -397,7 +400,8 @@ public final class JBindExtractorMain { List entryNames = new ArrayList(); List dispositions = new ArrayList(); List outputDirectories = new ArrayList(); - Set reserved = new HashSet(); + Set reserved = new HashSet(); + TargetPlanInvariant targetPlan = new TargetPlanInvariant(); for (int i = 0; i < itemCount; i++) { Boolean isFolder = (Boolean) archive.getProperty(i, PropID.IS_FOLDER); @@ -406,6 +410,7 @@ public final class JBindExtractorMain { if (Boolean.TRUE.equals(isFolder)) { File dir = resolveDirectory(request.targetDir, entryName); + targetPlan.add(dir, true); outputDirectories.add(dir); reserved.add(pathKey(dir)); continue; @@ -423,6 +428,7 @@ public final class JBindExtractorMain { totalUnits += itemSize; OutputTarget outputTarget = resolveOutputFile(request.targetDir, entryName, request.conflictMode, reserved); + targetPlan.add(outputTarget.reportedFile, false); File output = outputTarget.file; if (output == null) { emitOutput(request.archiveFile, entryName, outputTarget.reportedFile, "complete", outputTarget.disposition); @@ -797,13 +803,54 @@ public final class JBindExtractorMain { } } - private static String pathKey(File file) { - String value = file.getAbsolutePath(); - if (isWindows()) { - value = value.toLowerCase(Locale.ROOT); - } - return value; - } + private static String pathKey(File file) { + String value = file.toPath().toAbsolutePath().normalize().toString(); + if (isWindows()) { + value = value.toLowerCase(Locale.ROOT); + } + return value; + } + + private static final class TargetPlanInvariant { + private final TargetPlanNode root = new TargetPlanNode(); + + void add(File file, boolean directory) throws IOException { + String key = pathKey(file).replace('\\', '/'); + String[] segments = key.split("/"); + TargetPlanNode node = root; + for (String segment : segments) { + if (segment.length() == 0) { + continue; + } + if (node.file) { + throw new IOException("Target-Plan-Kollision: Datei ist Vorfahr von " + file.getAbsolutePath()); + } + TargetPlanNode child = node.children.get(segment); + if (child == null) { + child = new TargetPlanNode(); + node.children.put(segment, child); + } + node = child; + } + if (node.file || node.directory) { + if (directory && node.directory && !node.file) { + return; + } + throw new IOException("Target-Plan-Kollision: mehrfaches oder typwidriges Ziel " + file.getAbsolutePath()); + } + if (!directory && !node.children.isEmpty()) { + throw new IOException("Target-Plan-Kollision: Datei ist Vorfahr eines anderen Ziels " + file.getAbsolutePath()); + } + node.directory = directory; + node.file = !directory; + } + } + + private static final class TargetPlanNode { + private final Map children = new HashMap(); + private boolean file; + private boolean directory; + } private static boolean isWindows() { String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); diff --git a/src/main/extractor.ts b/src/main/extractor.ts index c0b3a52..91c68fe 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -291,9 +291,47 @@ export async function detectArchiveSignature(filePath: string): Promise; +}; + +class TargetPlanInvariant { + private readonly root: TargetPlanNode = { children: new Map() }; + + public add(outputPath: string, kind: TargetPlanKind): void { + const key = pathSetKey(path.resolve(outputPath)).replace(/\\/g, "/"); + const segments = key.split("/").filter(Boolean); + let node = this.root; + for (const segment of segments) { + if (node.kind === "file") { + throw new Error(`Target-Plan-Kollision: Datei ist Vorfahr von ${outputPath}`); + } + let child = node.children.get(segment); + if (!child) { + child = { children: new Map() }; + node.children.set(segment, child); + } + node = child; + } + if (node.kind) { + if (node.kind === "directory" && kind === "directory") { + return; + } + throw new Error(`Target-Plan-Kollision: mehrfaches oder typwidriges Ziel ${outputPath}`); + } + if (kind === "file" && node.children.size > 0) { + throw new Error(`Target-Plan-Kollision: Datei ist Vorfahr eines anderen Ziels ${outputPath}`); + } + node.kind = kind; + } +} function stripDuplicateSuffixBeforeExtension(fileName: string): string { return String(fileName || "").replace(/ \(\d+\)(?=\.[^.]+$)/, ""); @@ -2255,12 +2293,30 @@ export function buildExternalListArgs(command: string, archivePath: string, pass } export function parseNativeArchiveEntryList(command: string, output: string): string[] { + return parseNativeArchiveEntryCandidates(command, output).map((entry) => ( + entry.isDirectory && !/[\\/]$/.test(entry.entryPath) ? `${entry.entryPath}/` : entry.entryPath + )); +} + +type NativeArchiveEntryCandidate = { entryPath: string; isDirectory: boolean }; + +function parseNativeArchiveEntryCandidates(command: string, output: string): NativeArchiveEntryCandidate[] { const lines = String(output || "").split(/\r?\n/); if (isRarNativeCommand(command)) { - return lines.filter((line) => line.length > 0); + return lines.filter((line) => line.length > 0).map((entryPath) => ({ + entryPath, + isDirectory: /[\\/]$/.test(entryPath) + })); } - const entries: string[] = []; + const entries: NativeArchiveEntryCandidate[] = []; let inEntries = false; + let current: NativeArchiveEntryCandidate | null = null; + const commitCurrent = (): void => { + if (current) { + entries.push(current); + current = null; + } + }; for (const line of lines) { if (/^-{8,}\s*$/.test(line.trim())) { inEntries = true; @@ -2271,21 +2327,36 @@ export function parseNativeArchiveEntryList(command: string, output: string): st } const match = line.match(/^Path = (.*)$/); if (match?.[1]) { - entries.push(match[1]); + commitCurrent(); + current = { entryPath: match[1], isDirectory: /[\\/]$/.test(match[1]) }; + continue; + } + if (current && /^Folder = \+\s*$/.test(line)) { + current.isDirectory = true; } } + commitCurrent(); return entries; } export function validateNativeArchiveEntryCandidates(entries: readonly string[], targetDir: string): void { + validateNativeArchiveTargetPlan(entries.map((entryPath) => ({ + entryPath, + isDirectory: /[\\/]$/.test(entryPath) + })), targetDir); +} + +function validateNativeArchiveTargetPlan(entries: readonly NativeArchiveEntryCandidate[], targetDir: string): void { const scope = new PackageOutputScope([targetDir]); - for (const rawEntry of entries) { - const entryPath = String(rawEntry || "").replace(/\\/g, "/").replace(/\/$/, ""); + const targetPlan = new TargetPlanInvariant(); + for (const candidate of entries) { + const entryPath = String(candidate.entryPath || "").replace(/\\/g, "/").replace(/\/$/, ""); if (!entryPath) { continue; } const outputPath = path.resolve(targetDir, ...entryPath.split("/")); scope.validateTarget(entryPath, outputPath); + targetPlan.add(outputPath, candidate.isDirectory ? "directory" : "file"); } } @@ -2359,11 +2430,11 @@ async function runNativeEntryPreflight( return result; } try { - const entries = parseNativeArchiveEntryList(command, chunks.join("")); + const entries = parseNativeArchiveEntryCandidates(command, chunks.join("")); if (entries.length === 0) { throw new Error("Native Archivliste enthält keine validierbaren Einträge"); } - validateNativeArchiveEntryCandidates(entries, targetDir); + validateNativeArchiveTargetPlan(entries, targetDir); return result; } catch (error) { return { @@ -2943,6 +3014,7 @@ function isZipSafetyGuardError(error: unknown): boolean { || text.includes("zip-eintrag verdaechtig gross") || text.includes("symbolischer link") || text.includes("reparse point") + || text.includes("target-plan-kollision") || text.includes("extract_output_callback_failed"); } @@ -2989,6 +3061,7 @@ async function extractZipArchive( const entries = zip.getEntries(); const resolvedTarget = path.resolve(targetDir); const plannedOutputs = new Set(); + const targetPlan = new TargetPlanInvariant(); const renameCounters = new Map(); const directoryPlans: Array<{ entryPath: string; outputPath: string }> = []; const filePlans: Array<{ @@ -3012,6 +3085,8 @@ async function extractZipArchive( if (entry.isDirectory) { const entryPath = entry.entryName.replace(/\\/g, "/").replace(/\/$/, "") || "directory"; validateTarget?.(entryPath, baseOutputPath); + targetPlan.add(baseOutputPath, "directory"); + plannedOutputs.add(pathSetKey(baseOutputPath)); directoryPlans.push({ entryPath, outputPath: baseOutputPath }); continue; } @@ -3058,6 +3133,7 @@ async function extractZipArchive( if (mode === "skip") { const entryPath = entry.entryName.replace(/\\/g, "/"); validateTarget?.(entryPath, baseOutputPath); + targetPlan.add(baseOutputPath, "file"); filePlans.push({ entry, entryPath, @@ -3100,6 +3176,7 @@ async function extractZipArchive( const normalizedEntryPath = entry.entryName.replace(/\\/g, "/"); validateTarget?.(normalizedEntryPath, outputPath); + targetPlan.add(outputPath, "file"); plannedOutputs.add(outputKey); filePlans.push({ entry, diff --git a/tests/extractor-jvm.test.ts b/tests/extractor-jvm.test.ts index 5b93d88..cc620d0 100644 --- a/tests/extractor-jvm.test.ts +++ b/tests/extractor-jvm.test.ts @@ -1,4 +1,5 @@ -import fs from "node:fs"; +import fs from "node:fs"; +import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; @@ -7,7 +8,48 @@ import { afterEach, describe, expect, it } from "vitest"; import { extractPackageArchives } from "../src/main/extractor"; const tempDirs: string[] = []; -const originalBackend = process.env.RD_EXTRACT_BACKEND; +const originalBackend = process.env.RD_EXTRACT_BACKEND; +const require = createRequire(import.meta.url); + +type ZipFixtureEntry = { name: string; directory?: boolean; content?: string }; + +function writeZipFixture(filePath: string, entries: readonly ZipFixtureEntry[]): void { + const ZipFile = require("adm-zip/zipFile") as new (input: null, options: Record) => { + setEntry: (entry: unknown) => void; + compressToBuffer: () => Buffer; + }; + const ZipEntry = require("adm-zip/zipEntry") as new (options: Record) => { + entryName: string; + setData: (data: Buffer) => void; + }; + const utils = require("adm-zip/util") as { + Constants: { NONE: number }; + decoder: unknown; + }; + const options = { + noSort: true, + readEntries: false, + method: utils.Constants.NONE, + decoder: utils.decoder + }; + const zip = new ZipFile(null, options); + for (const fixture of entries) { + const entry = new ZipEntry(options); + entry.entryName = fixture.directory && !fixture.name.endsWith("/") ? `${fixture.name}/` : fixture.name; + entry.setData(Buffer.from(fixture.content || "")); + zip.setEntry(entry); + } + fs.writeFileSync(filePath, zip.compressToBuffer()); +} + +const jvmTargetCollisionCases = [ + ["directory then same-name file", [{ name: "same", directory: true }, { name: "same", content: "file" }]], + ["file then same-name directory", [{ name: "same", content: "file" }, { name: "same", directory: true }]], + ["parent file then child file", [{ name: "same", content: "parent" }, { name: "same/child", content: "child" }]], + ["child file then parent file", [{ name: "same/child", content: "child" }, { name: "same", content: "parent" }]], + ["case-insensitive file aliases", [{ name: "Name", content: "first" }, { name: "name", content: "second" }]], + ["duplicate file targets", [{ name: "same", content: "first" }, { name: "same", content: "second" }]] +] as const satisfies ReadonlyArray; function hasJavaRuntime(): boolean { const result = spawnSync("java", ["-version"], { stdio: "ignore" }); @@ -401,6 +443,81 @@ describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm b expect(fs.readFileSync(safePath, "utf8")).toBe("foreign-safe"); expect(fs.readFileSync(aliasPath, "utf8")).toBe("foreign-alias"); }); + + it.each(["7zjbinding", "zip4j"].flatMap((backend) => jvmTargetCollisionCases.map(([label, entries]) => [backend, label, entries] as const)))( + "rejects %s %s before any target mutation", + (backend, _label, entries) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), `rd-jvm-target-plan-${backend}-`)); + tempDirs.push(root); + const targetDir = path.join(root, "out"); + fs.mkdirSync(targetDir, { recursive: true }); + const sentinelPath = path.join(targetDir, "sentinel.txt"); + fs.writeFileSync(sentinelPath, "foreign"); + const zipPath = path.join(root, "collision.zip"); + writeZipFixture(zipPath, entries); + 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.readdirSync(targetDir)).toEqual(["sentinel.txt"]); + expect(fs.readFileSync(sentinelPath, "utf8")).toBe("foreign"); + } + ); + + it.each(["7zjbinding", "zip4j"])("allows safely identical duplicate directory targets in %s", (backend) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), `rd-jvm-identical-directory-${backend}-`)); + tempDirs.push(root); + const targetDir = path.join(root, "out"); + const zipPath = path.join(root, "directories.zip"); + writeZipFixture(zipPath, [ + { name: "same", directory: true }, + { name: "same", directory: true } + ]); + 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).toBe(0); + expect(fs.readdirSync(targetDir)).toEqual(["same"]); + expect(fs.statSync(path.join(targetDir, "same")).isDirectory()).toBe(true); + }); it("emits progress callbacks with archiveName and percent", async () => { process.env.RD_EXTRACT_BACKEND = "jvm"; diff --git a/tests/extractor.test.ts b/tests/extractor.test.ts index 06dd718..d32dd7d 100644 --- a/tests/extractor.test.ts +++ b/tests/extractor.test.ts @@ -1,5 +1,6 @@ -import fs from "node:fs"; -import os from "node:os"; +import fs from "node:fs"; +import { createRequire } from "node:module"; +import os from "node:os"; import path from "node:path"; import AdmZip from "adm-zip"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -29,6 +30,47 @@ import { const tempDirs: string[] = []; const originalExtractBackend = process.env.RD_EXTRACT_BACKEND; const originalStatfs = fs.promises.statfs; +const require = createRequire(import.meta.url); + +type ZipFixtureEntry = { name: string; directory?: boolean; content?: string }; + +function writeZipFixture(filePath: string, entries: readonly ZipFixtureEntry[]): void { + const ZipFile = require("adm-zip/zipFile") as new (input: null, options: Record) => { + setEntry: (entry: unknown) => void; + compressToBuffer: () => Buffer; + }; + const ZipEntry = require("adm-zip/zipEntry") as new (options: Record) => { + entryName: string; + setData: (data: Buffer) => void; + }; + const utils = require("adm-zip/util") as { + Constants: { NONE: number }; + decoder: unknown; + }; + const options = { + noSort: true, + readEntries: false, + method: utils.Constants.NONE, + decoder: utils.decoder + }; + const zip = new ZipFile(null, options); + for (const fixture of entries) { + const entry = new ZipEntry(options); + entry.entryName = fixture.directory && !fixture.name.endsWith("/") ? `${fixture.name}/` : fixture.name; + entry.setData(Buffer.from(fixture.content || "")); + zip.setEntry(entry); + } + fs.writeFileSync(filePath, zip.compressToBuffer()); +} + +const archiveTargetCollisionCases = [ + ["directory then same-name file", [{ name: "same", directory: true }, { name: "same", content: "file" }]], + ["file then same-name directory", [{ name: "same", content: "file" }, { name: "same", directory: true }]], + ["parent file then child file", [{ name: "same", content: "parent" }, { name: "same/child", content: "child" }]], + ["child file then parent file", [{ name: "same/child", content: "child" }, { name: "same", content: "parent" }]], + ["case-insensitive file aliases", [{ name: "Name", content: "first" }, { name: "name", content: "second" }]], + ["duplicate file targets", [{ name: "same", content: "first" }, { name: "same", content: "second" }]] +] as const satisfies ReadonlyArray; beforeEach(() => { process.env.RD_EXTRACT_BACKEND = "legacy"; @@ -1659,6 +1701,13 @@ describe("extractor", () => { "folder/episode.mkv", "subtitle.srt" ]); + expect(parseNativeArchiveEntryList("7z.exe", [ + "----------", + "Path = folder", + "Folder = +", + "Path = folder/episode.mkv", + "Folder = -" + ].join("\n"))).toEqual(["folder/", "folder/episode.mkv"]); }); it("preflights every internal ZIP entry before overwriting an earlier safe target", async () => { @@ -1692,6 +1741,69 @@ describe("extractor", () => { expect(fs.readFileSync(aliasPath, "utf8")).toBe("foreign-alias"); }); + it.each(archiveTargetCollisionCases)("rejects internal ZIP %s before any target mutation", async (_label, entries) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-zip-target-plan-")); + 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 sentinelPath = path.join(targetDir, "sentinel.txt"); + fs.writeFileSync(sentinelPath, "foreign"); + writeZipFixture(path.join(packageDir, "collision.zip"), entries); + + const result = await extractPackageArchives({ + packageDir, + targetDir, + cleanupMode: "none", + conflictMode: "overwrite", + removeLinks: false, + removeSamples: false + }); + + expect(result).toEqual(expect.objectContaining({ extracted: 0, failed: 1 })); + expect(fs.readdirSync(targetDir)).toEqual(["sentinel.txt"]); + expect(fs.readFileSync(sentinelPath, "utf8")).toBe("foreign"); + }); + + it.each(archiveTargetCollisionCases)("rejects native preflight %s", (_label, entries) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-target-plan-")); + tempDirs.push(root); + const targetDir = path.join(root, "out"); + fs.mkdirSync(targetDir, { recursive: true }); + const candidates = entries.map((entry) => "directory" in entry && entry.directory ? `${entry.name}/` : entry.name); + + expect(() => validateNativeArchiveEntryCandidates(candidates, targetDir)).toThrow(/target|ziel|kollision/i); + expect(fs.readdirSync(targetDir)).toEqual([]); + }); + + it("allows safely identical duplicate directory targets", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-identical-directory-plan-")); + tempDirs.push(root); + const packageDir = path.join(root, "pkg"); + const targetDir = path.join(root, "out"); + fs.mkdirSync(packageDir, { recursive: true }); + writeZipFixture(path.join(packageDir, "directories.zip"), [ + { name: "same", directory: true }, + { name: "same", directory: true }, + { name: "same/child.txt", content: "child" } + ]); + + const result = await extractPackageArchives({ + packageDir, + targetDir, + cleanupMode: "none", + conflictMode: "overwrite", + removeLinks: false, + removeSamples: false + }); + + expect(result).toEqual(expect.objectContaining({ extracted: 1, failed: 0 })); + expect(fs.statSync(path.join(targetDir, "same")).isDirectory()).toBe(true); + expect(fs.readFileSync(path.join(targetDir, "same", "child.txt"), "utf8")).toBe("child"); + expect(() => validateNativeArchiveEntryCandidates(["same/", "same/"], targetDir)).not.toThrow(); + }); + 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."]);