From 87bfa6576c15662e3180bd522ee6e4c78c3ba1dd Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 22 Aug 2026 16:22:43 +0200 Subject: [PATCH] fix: validate raw archive targets before conflicts --- .../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 -> 885 bytes .../JBindExtractorMain$ProgressTracker.class | Bin 1387 -> 1387 bytes ...indExtractorMain$RawArchivePlanEntry.class | Bin 0 -> 1106 bytes ...xtractorMain$RawArchivePlanInvariant.class | Bin 0 -> 3780 bytes ...BindExtractorMain$RawArchivePlanNode.class | Bin 0 -> 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 27769 -> 28862 bytes .../extractor/JBindExtractorMain.java | 187 ++++++++++++++---- src/main/extractor.ts | 75 ++++++- tests/extractor-jvm.test.ts | 93 +++++++-- tests/extractor.test.ts | 49 ++++- 20 files changed, 338 insertions(+), 66 deletions(-) create mode 100644 resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanEntry.class create mode 100644 resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanInvariant.class create mode 100644 resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanNode.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 775b7f454113c504507afe929adb012e71ec7fd9..750b665b1e6a9eb14535962c33a947a89a8f8aff 100644 GIT binary patch delta 111 zcmZn@Y7^Sv$;xD8y4jnxl97o;W%4mLJrMPc&6n>82n0R}eagAClvhbCLIFPCFJ${@mg zj6s(9ID;bdNd_C{Qw$EwXBa%0&ocNjpP$5G57OtzVGW|{I3oC%cQNoY?`Du=-ou~^ J6nes;1OT?_9*zJ2 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 e51f9c4bfb6da1c08f40ba15e52d73ff1ba8d456..041ec5514f6731d05fa7d3ee175e0d1f3503b815 100644 GIT binary patch delta 104 zcmX@Ze};d92_u`OfdL~!{N#%)E}QEZS2MC38-N7dKmwCXn4Q>-4MF@SumVPwT}+He zC+}gKr+R{cljS4>H_I6Y36}E=(ky2gWLVBIShHMU@MgKl5W#YZA(`d!-j6nP*umVPwT}+Ho zllQRAQ;lWdWQk+oW=Uj_U`b(+W=Ud@VM%7NW=Uo6W=Ug+U`c04X33ab&+ZEVn7A1e 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 87f5ffdebfa76128c2fe8622ce3e660a025b8947..1276d6214e55595088dc10d98118cd06ac0ad894 100644 GIT binary patch delta 482 zcmW-bOKePW7{$+ja=tq=Z8G$7J2Rb>M55_LMy7X|j))BjX=$uR(ps@-i|9(MB>M8G zVuo+gm5qoX-ZvTIS#4~wk_ZV6+IWPLq87q`Y|d}<%Q5I;)zu=#-Ptzma(GqGsALYjnJ>FOTY1#5W~ z>v_$V+`vxW#4ZNd!wUBC76y15L%fGF?_-h=@RSenisSgoNqpxc%yGhae5_sZHuEw# z7cZEO*Qn(iBsgW?ze5w>Bg1L?e#XB3fKGnIZhk@^KjQ#ragblo&u^%34tMy&j{L+E z{=y7@n;QPnbaIm;0LAG3WP803QE}i(1fGOZFHtl~tv*jqERmSC7GRkyL|W>Q5f|HJ bF}lS=NfKIVD1_LpvkmDgQmOygnDYMtT^D`w delta 482 zcmW-dOGs347{$;3dgl5Fgl^oqbLTc%meUMs%)ON}Ar@(9K8ENqn&YY#JxN-aP)TZ5 zU`9$G714trOALbG!YpbNwXaq|&?bUf8qq>%VgG6Koz3B#-*;A8S6W}qL<~PGvyoz@ zpUY$v3NjzD<$I+i_ae1g9m$!!hQBksY9?46Y%6=Vr~2ia8I#p2C;U?L!&Di0yLQ9~ ze92gasvszPuB7CXwKaTWwHSFDWE9nOP|FzB(S^%6>gl18>(RsneAXe$dbD!`I=K-& zY`}hQLV=qx%tjn#5+}F?XPJ^6cA%Q&q@7fO%;}pAOLm_cYSSg{u(?Zz?}o=cI=lmI z>_i9m>X0rRV>eE5AI`83qwL2N^O)uV%<~}L^AJArFut;YWgfwA4&V<5mCd3QogQ^A zTySzKKZ-Rxj~K@^{RJd>5iPu=>BlwoWejlwr#XocPGOwaFu`d|@;XYK!E4^oCA0Xz zoA}9Fs)n~E?RJ~Iqq*lu|rrRQ#1+@pM@{@4s|;!9RhUY~%m{ 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 e162c6dda44f8de207e2f4df2d956c5a27a346a9..40c01819d549211d6d3361eab4ab30579cebc3a2 100644 GIT binary patch delta 89 zcmeAc=oZ)@!p6j6H(88L4@9}KnSrP}wpns4E({_pt_-p)ZVZYn9t_4To($$J-V6>b kJ`64_ev{4E13@Y~*@O64>=}4j92lfn92wMsLND2s0Oq_98~^|S delta 89 zcmeAc=oZ)@!p6jWd9oOr9*A;dGXqg|Y_sH;Z!(B5-(rwuzRjS>e3!wP`5uEg^8*G4 m=7$U}%#SCVu?K=wcCrWYF<)WeWxmQF#e9uH9VqmYT?qhO))`y? 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 01a4c0860bac73f0a66a6406e7f5ed90adb1d1ad..7797e1e504d7910acd08efede6a2494ced74e98d 100644 GIT binary patch delta 148 zcmaDY{91U!S9U&@AO;qePzG+6Fa{x(@X5>^noKM~ll3^XK~yw{HU~?P00)B+gYo1< z4p9m24Gatnj10O!k_|{RfF(^BOhJ;9FK`HeG(CbC!ON-129~y(EXXM;#E+qKaxy2_ Lu09C0lhX(Q(assg delta 148 zcmaDY{91U!S9U(;=L{^&uNb(QUo!|XznRR;p~=Mje6k*gHi(Mm(B@!%F2KQH#9%x* zkwa91djkUl10#bjkYoeW3}8tU22+sa`EAWe@TM(}cKvVo41m=DY_-DMRFfI}@WQG7~b?n6L*6uo!p$Ml8Y(lugVgJWn0!@cu+G zGdU8qm@6jLkcIuC)Uy<<%)94=wLBX&*s4pWF}bGfRIpdc!CvkdDAeSn4rlwi$AD68 RF6vRK&s8IC%DG!b@&{@Q5$6B^ delta 118 zcmV~$PYMBH9LM2r&1YV`N*N*xvt!``R&3-J4&X3$zl+RX{*);bLJdN=z+A%f6u$6D z8bquFOI27aVxu-&djtc=P9do|<}rIYGL<-}&QV=X>T%YXizZxc!N`-V%uO}!mNRSd URF#(+yw&8R4qpxV*);6`0ej3J#{d8T diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanEntry.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanEntry.class new file mode 100644 index 0000000000000000000000000000000000000000..4d195dcbc79360d957a2ce0d4f40e0ccec8b9021 GIT binary patch literal 1106 zcmb_b+iuf95Iq|wbuK0iPAS(w3jq>0Y7(guNU0D|1qm81YEVFV+c>MT#aSaeZVUgw z3%9S}0VJYIJn#X06k-+yS}A!Tf)~%u?3puj#lZk2HYL`LG|8ksrt>|?tLy*af|QW3fyOMS3L5#YN}!Il3{LLDmlEt zP%5={_%5%~)poT#45Zp_Oh&rpPKtU+T1X+ykaxNAm2|oHgv$AQ;ie!?dooWaSr(4z zBwP8xfP3QBJQhrx(25rsa)&)W?ClVY!6=nG z4Dk)WFBs-qQi%uApeKUIyysCQ!(CT|VX?ket1&c7t$&?aquf2hdQG!-{s-&i(2kV3 zuFE|AyI(Y$!g@oGFWvT|z!i6-9@)vsYw1k literal 0 HcmV?d00001 diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanInvariant.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanInvariant.class new file mode 100644 index 0000000000000000000000000000000000000000..d9d0e5e7479027bc5520db4a414c20c23eae7cd7 GIT binary patch literal 3780 zcmb_f+fx%~6#sqs0-J=jVif2qT2xd5jRB=<6YCAoMiHzi6tuQWvcl@J8+SK=wqDxW z`@Qxebfyo^bbPWic52izwSB5HedtU7fc^n}>U*d5d`l<+!UIExVRygVx%|%Wob%<^ ze|~rXpc&&K_`oIjWt5m)axFV{aG!3b+opzxRo$#T zs*dcnQ-k`jcEnK4q&ckGx@x)%r8{*~cXu)Hx=42j0ihzuP<}=oR%5hvAlBJ`MoYO8 z${2PpLT87S)&f|jH5+0YKVC4)%ChcU9Y%OD`DUG4BuB9b1*OLIM8CXn6S|j5L)G!1z zGwt-~?jS=&5#J+yV)Hr~>rqR=F*I{PY^C5N#ll7zn^4CPbh7=9hnCQtOcv2Cp`Ky6 z2d-|#lAUd%DJ|pbmKng43>(Pd=p-Y^1v7fcG7R0J)p(7eX}0Fn^y!)rLIj&dq+;_} zON^;RlZ-9c%248D44t5_E8^D}N2by(>*_|V-AbtjC7J(dXXi17@FbB6mb_Fw zDx(XuT;^Ks)`(`es1BXkn76lZcM;{n&~@U`BOHF7p}MX}K2N(9LZ6HiI9U*{r$oGt zE-GGL*~wey8GKmHB+76KFG_exhKhcMC0@Ertx{ze&?x1m+$<>Ai9$CgYq%#XEkna; zI+)4Q!5xM1%-1C_cJ#Fg)`O55BJSyY-42yR(HVV4DEaMeir2eEkZ9^IN zyw^VnmdjM3=2tnl3TV1X>Qs{q&`hr(Od|EFXbG<|JYPtjykiCA${3>`=dEA5;59nd54j z3T@9kZ7cP zV}Neg)KtXfk-j+1(Dj;T-l9>AzUjh><@I+V)lVSQ8M?|)JetE=e>#T^9XB8S zDF)+MGJ!~MxZy6MKhZ{C4vlgD1e$yAVq3hV{(CHomn#0aq(~381^BjLMX(}p13xQL zMQ}?zq?F#n_FoY3-uI9g#~L9ne?Gi7hZfS(lEVQZUkQDOLpeMbFH=f#IQ}*0Ellk0 zQA(6D+SeoG^eScd@q!OOBNUev`61Q{D3p+Zf+i++DDr)zIC^fPL6PraKndk=c9wQ) zmUcN_dNs&yKH8nPx^>FxF;rELVR?Rdjx0Vq2LJ6sPqHK{zdkbUQ1v*e7Y5zO1%_Gb zFU?Y4I)Rm|CdgZClKPhb^4iF$qtP5eb9xQbTZkHb8L zE}q5^*KmcO##ek0-*6qb_!->c27cs2_>G(Rom=>WXYdz4hkv-u6z;OsJk8egVb;J$ zSdx#jgZu(B_$%xpzsRoh%j_1vg7?UVQ+O1DHX2a3wnP^^C%BcQ@w^Uto#=eSV8sx literal 0 HcmV?d00001 diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanNode.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$RawArchivePlanNode.class new file mode 100644 index 0000000000000000000000000000000000000000..197684c2e0c01901a2ccfc3df8712d7f655e528c GIT binary patch literal 1786 zcmb_cU2hUW6g|TN3lxP?uoZ3V2Pz+x1yK{TP1QCSHME+dB>J{26S{R-n*FHt*)&mq zjV2NjO?)-+M;Y%@6e_$Bc-WmgGiT1ZckZ40>-V=G0B)lcga?8MZx}xK8Sbm?mSj1q z^QOUd=ZH)E$Tk(#Zky86P0eVO|4z3R%}DJkZy%cKo_5He>x!|{Zg7T(#hWc|*wq8B z>zdJIh*kELLq#G)Q>xk~t;w!xAdDbF3;`n08zwjC>c={10IX35H-*YZ{8}m~=ah(-E$ZYEBcLn*n)4X7FyE!e>E6Xj%zMLAPrm{U}hTUM%ADij!w)01(G){UH*hT$ZwLtfo+z1p3HSA(Rw z1vw;f*OhoVGl}8XF{Dx|++z4Sy~^~rJCg*3s%^(q`6JD(#l_K%l6SA0A^gNJxVfb( zmc=a*YYc0XJbG$5qZKAXVt6nK&42B!S7Zzer>T3ZA}CAwiMWd>P#20*6BZD2doNir zw;_Vz0@)s1q>nlu*?zL9S<%ii&F;|&4~>az7Yo_%SbEh(vI8-j>wtZs4Tcr^$KWOI z5AX`dhzK7M7d}NnG^>4@6(SJ`=g;Q4hz!p8X&=J-XaJXRxsP3@kyhc$e#PA9e-Z=e z6H;{))WQ})^rAa)=UO7)@%k#)+`1q)?5Zh);tCe)`H2> NY>rH&jSH4v(N)}0Tu-300ac+00{)?01gD|022i302u`AlUxObIt1_l z2L$o}5Crr969n@B6a@AF8wB_OBn0{ZCj|KbCai{Rv9W(|f*3YEQOTT4TM zftK`zEiIC!lRv<}(p?V+&gr^cx7xDp*nF*R7k#5?^^wvr$C@CcDNZ!UsY&_|2eR^U zCWoBq`760K7g;%UWb91kw5ake_eN{`19v}#O6FEWJoB%qLQp=mTjeMKmI&5PfTfyGIkvQ zDl9RnEDKtWyc}7rvZXaj3OLj{4duB}$elLmXwwd-+fK=*P2lX>0~0!Xwr4)~kML9@ zsS|vilG9n#s#DQ9Rb6nbOX_OS)fGK8c~Og3UGo;Pcir0N%plW_4m$*TAg_BuJyKAY KvYu?ptNjD<93=k$ 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 8da7bd210dc9f97876920ca1e23510060b9700ef..e8d77579c9aefaf9dd191b0600fe8c9cd9d80c7d 100644 GIT binary patch delta 191 zcmdljyjys~97ZN1!^!g)Z!sAeZLVT!V`MinHZWjh@S9xEWIx%BWfzx`iGcwJLl`52 z#Uy6&$%?F#*nuKOj0}!I6?T(G-BXkHf9iEHerxr zHf1njHe;}1HfOMBwq$T;wqi(SwqYn^wq>Yhwqs~zwr7~a?8qDGYK%V?qOhI-aGjYvli??v delta 122 zcmZqSYT??jm66@h*ua31!C^8lll|o1jND9yCX+dsVnq#2LBiaL$;qk3#VSCa5hH^m zP?6o_9ZbAThQ^zZGYK%V1~RZP2Ti`iti>8Az`-CpIgv$lvI2`BW8h>X7A+td3nn{Q Nbb#!wVDb@*5&+g>9Mb>* 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 e130b7d83eaa8e632c06cf4dac864d13fdde5c0f..a7ec7a52b0c4ab189f2e799aa807bd9425536e85 100644 GIT binary patch delta 18 ZcmX@be2RI)Dn>R|c?MQig~=Njr2sT|1gHQ2 delta 18 acmX@be2RI)Dn>SzEexzITPJT|lmY-ejs?&F diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class index 9b637d00bfc7ae7c69dc3ed6019db6ec0b02ef57..e5e2ec34609f9376a6ec6e1fe12cb6787743e2ed 100644 GIT binary patch literal 28862 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 Y*rq~a3dGR}^);@&;2uDO5X6Z810Z$`W&i*H delta 13819 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 fileHeaders = zipFile.getFileHeaders(); - if (fileHeaders == null) { - fileHeaders = new ArrayList(); - } - - long totalUnits = 0; - boolean encrypted = false; + if (fileHeaders == null) { + fileHeaders = new ArrayList(); + } + + RawArchivePlanInvariant rawPlan = new RawArchivePlanInvariant(); + for (FileHeader header : fileHeaders) { + if (header == null) { + continue; + } + String entryName = normalizeEntryName(header.getFileName(), "file"); + if (header.isDirectory()) { + resolveDirectory(request.targetDir, entryName); + } else { + secureResolve(request.targetDir, entryName); + } + rawPlan.add(entryName, header.isDirectory()); + } + + long totalUnits = 0; + boolean encrypted = false; for (FileHeader header : fileHeaders) { if (header == null || header.isDirectory()) { continue; @@ -259,43 +274,47 @@ public final class JBindExtractorMain { encrypted = encrypted || header.isEncrypted(); totalUnits += safeSize(header.getUncompressedSize()); } - ProgressTracker progress = new ProgressTracker(totalUnits); - progress.emitStart(); - - Set preflightReserved = new HashSet(); + Set reserved = new HashSet(); TargetPlanInvariant targetPlan = new TargetPlanInvariant(); + Map plannedEntryNames = new IdentityHashMap(); + Map plannedDirectories = new IdentityHashMap(); + Map plannedOutputs = new IdentityHashMap(); for (FileHeader header : fileHeaders) { if (header == null) { continue; } String entryName = normalizeEntryName(header.getFileName(), "file"); + plannedEntryNames.put(header, entryName); if (header.isDirectory()) { File dir = resolveDirectory(request.targetDir, entryName); targetPlan.add(dir, true); - preflightReserved.add(pathKey(dir)); + reserved.add(pathKey(dir)); + plannedDirectories.put(header, dir); } else { - OutputTarget outputTarget = resolveOutputFile(request.targetDir, entryName, request.conflictMode, preflightReserved); + OutputTarget outputTarget = resolveOutputFile(request.targetDir, entryName, request.conflictMode, reserved); targetPlan.add(outputTarget.reportedFile, false); + plannedOutputs.put(header, outputTarget); } } - Set reserved = new HashSet(); - for (FileHeader header : fileHeaders) { - if (header == null) { - continue; - } - - String entryName = normalizeEntryName(header.getFileName(), "file"); + ProgressTracker progress = new ProgressTracker(totalUnits); + progress.emitStart(); + + for (FileHeader header : fileHeaders) { + if (header == null) { + continue; + } + + String entryName = plannedEntryNames.get(header); if (header.isDirectory()) { - File dir = resolveDirectory(request.targetDir, entryName); + File dir = plannedDirectories.get(header); ensureDirectory(dir); rejectLinkedPath(request.targetDir, dir); - reserved.add(pathKey(dir)); - continue; - } - - long itemUnits = safeSize(header.getUncompressedSize()); - OutputTarget outputTarget = resolveOutputFile(request.targetDir, entryName, request.conflictMode, reserved); + continue; + } + + long itemUnits = safeSize(header.getUncompressedSize()); + OutputTarget outputTarget = plannedOutputs.get(header); File output = outputTarget.file; if (output == null) { emitOutput(request.archiveFile, entryName, outputTarget.reportedFile, "complete", outputTarget.disposition); @@ -387,26 +406,43 @@ public final class JBindExtractorMain { try { context = openSevenZipArchive(request.archiveFile, password); IInArchive archive = context.archive; - int itemCount = archive.getNumberOfItems(); + int itemCount = archive.getNumberOfItems(); if (itemCount <= 0) { throw new IOException("Archiv enthalt keine Eintrage oder konnte nicht gelesen werden: " + request.archiveFile.getAbsolutePath()); } - long totalUnits = 0; - boolean encrypted = false; + List rawEntryNames = new ArrayList(); + List rawEntryDirectories = new ArrayList(); + RawArchivePlanInvariant rawPlan = new RawArchivePlanInvariant(); + for (int i = 0; i < itemCount; i++) { + Boolean isFolder = (Boolean) archive.getProperty(i, PropID.IS_FOLDER); + String entryPath = (String) archive.getProperty(i, PropID.PATH); + String entryName = normalizeEntryName(entryPath, "item-" + i); + if (Boolean.TRUE.equals(isFolder)) { + resolveDirectory(request.targetDir, entryName); + } else { + secureResolve(request.targetDir, entryName); + } + rawPlan.add(entryName, Boolean.TRUE.equals(isFolder)); + rawEntryNames.add(entryName); + rawEntryDirectories.add(Boolean.valueOf(Boolean.TRUE.equals(isFolder))); + } + + long totalUnits = 0; + boolean encrypted = false; List fileIndices = new ArrayList(); List outputFiles = new ArrayList(); + List reportedFiles = new ArrayList(); List fileSizes = new ArrayList(); List entryNames = new ArrayList(); List dispositions = new ArrayList(); List outputDirectories = new ArrayList(); Set reserved = new HashSet(); TargetPlanInvariant targetPlan = new TargetPlanInvariant(); - - for (int i = 0; i < itemCount; i++) { - Boolean isFolder = (Boolean) archive.getProperty(i, PropID.IS_FOLDER); - String entryPath = (String) archive.getProperty(i, PropID.PATH); - String entryName = normalizeEntryName(entryPath, "item-" + i); + + for (int i = 0; i < itemCount; i++) { + Boolean isFolder = rawEntryDirectories.get(i); + String entryName = rawEntryNames.get(i); if (Boolean.TRUE.equals(isFolder)) { File dir = resolveDirectory(request.targetDir, entryName); @@ -430,16 +466,20 @@ public final class JBindExtractorMain { 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); - } fileIndices.add(i); outputFiles.add(output); + reportedFiles.add(outputTarget.reportedFile); fileSizes.add(itemSize); entryNames.add(entryName); dispositions.add(outputTarget.disposition); } + for (int i = 0; i < outputFiles.size(); i++) { + if (outputFiles.get(i) == null) { + emitOutput(request.archiveFile, entryNames.get(i), reportedFiles.get(i), "complete", dispositions.get(i)); + } + } + for (File directory : outputDirectories) { ensureDirectory(directory); rejectLinkedPath(request.targetDir, directory); @@ -811,6 +851,73 @@ public final class JBindExtractorMain { return value; } + private static final class RawArchivePlanInvariant { + private final RawArchivePlanNode root = new RawArchivePlanNode(""); + + void add(String relativeTarget, boolean directory) throws IOException { + String normalizedSpelling = relativeTarget == null ? "" : relativeTarget.replace('\\', '/'); + while (normalizedSpelling.endsWith("/")) { + normalizedSpelling = normalizedSpelling.substring(0, normalizedSpelling.length() - 1); + } + if (normalizedSpelling.length() == 0) { + throw new IOException("Raw-Archivplan-Kollision: leeres Ziel"); + } + String[] segments = normalizedSpelling.split("/", -1); + StringBuilder canonicalKey = new StringBuilder(); + RawArchivePlanNode node = root; + for (String segment : segments) { + if (node.entry != null && !node.entry.directory) { + throw new IOException("Raw-Archivplan-Kollision: Datei ist Vorfahr von " + normalizedSpelling); + } + String canonicalSegment = segment.toLowerCase(Locale.ROOT); + if (canonicalKey.length() > 0) { + canonicalKey.append('/'); + } + canonicalKey.append(canonicalSegment); + RawArchivePlanNode child = node.children.get(canonicalSegment); + if (child == null) { + child = new RawArchivePlanNode(segment); + node.children.put(canonicalSegment, child); + } else if (!child.segmentSpelling.equals(segment)) { + throw new IOException("Raw-Archivplan-Kollision: Windows-Case-Alias " + normalizedSpelling); + } + node = child; + } + if (node.entry != null) { + if (node.entry.directory && directory && node.entry.normalizedSpelling.equals(normalizedSpelling)) { + return; + } + throw new IOException("Raw-Archivplan-Kollision: mehrfaches oder typwidriges Ziel " + node.entry.canonicalWindowsKey); + } + if (!directory && !node.children.isEmpty()) { + throw new IOException("Raw-Archivplan-Kollision: Datei ist Vorfahr eines anderen Ziels " + normalizedSpelling); + } + node.entry = new RawArchivePlanEntry(canonicalKey.toString(), normalizedSpelling, directory); + } + } + + private static final class RawArchivePlanNode { + private final String segmentSpelling; + private final Map children = new HashMap(); + private RawArchivePlanEntry entry; + + private RawArchivePlanNode(String segmentSpelling) { + this.segmentSpelling = segmentSpelling; + } + } + + private static final class RawArchivePlanEntry { + private final String canonicalWindowsKey; + private final String normalizedSpelling; + private final boolean directory; + + private RawArchivePlanEntry(String canonicalWindowsKey, String normalizedSpelling, boolean directory) { + this.canonicalWindowsKey = canonicalWindowsKey; + this.normalizedSpelling = normalizedSpelling; + this.directory = directory; + } + } + private static final class TargetPlanInvariant { private final TargetPlanNode root = new TargetPlanNode(); diff --git a/src/main/extractor.ts b/src/main/extractor.ts index 91c68fe..3b601ea 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -297,6 +297,58 @@ export function pathSetKey(filePath: string): string { type TargetPlanKind = "file" | "directory"; +type RawArchivePlanEntry = { + canonicalWindowsKey: string; + normalizedSpelling: string; + kind: TargetPlanKind; +}; + +type RawArchivePlanNode = { + segmentSpelling: string; + entry?: RawArchivePlanEntry; + children: Map; +}; + +class RawArchivePlanInvariant { + private readonly root: RawArchivePlanNode = { segmentSpelling: "", children: new Map() }; + + public add(relativeTarget: string, kind: TargetPlanKind): void { + const normalizedSpelling = String(relativeTarget || "").replace(/\\/g, "/").replace(/\/+$/, ""); + const segments = normalizedSpelling.split("/").filter(Boolean); + if (segments.length === 0) { + throw new Error("Raw-Archivplan-Kollision: leeres Ziel"); + } + let node = this.root; + const canonicalSegments: string[] = []; + for (const segment of segments) { + if (node.entry?.kind === "file") { + throw new Error(`Raw-Archivplan-Kollision: Datei ist Vorfahr von ${normalizedSpelling}`); + } + const canonicalSegment = segment.toLowerCase(); + canonicalSegments.push(canonicalSegment); + let child = node.children.get(canonicalSegment); + if (!child) { + child = { segmentSpelling: segment, children: new Map() }; + node.children.set(canonicalSegment, child); + } else if (child.segmentSpelling !== segment) { + throw new Error(`Raw-Archivplan-Kollision: Windows-Case-Alias ${normalizedSpelling}`); + } + node = child; + } + const canonicalWindowsKey = canonicalSegments.join("/"); + if (node.entry) { + if (node.entry.kind === "directory" && kind === "directory" && node.entry.normalizedSpelling === normalizedSpelling) { + return; + } + throw new Error(`Raw-Archivplan-Kollision: mehrfaches oder typwidriges Ziel ${node.entry.canonicalWindowsKey}`); + } + if (kind === "file" && node.children.size > 0) { + throw new Error(`Raw-Archivplan-Kollision: Datei ist Vorfahr eines anderen Ziels ${normalizedSpelling}`); + } + node.entry = { canonicalWindowsKey, normalizedSpelling, kind }; + } +} + type TargetPlanNode = { kind?: TargetPlanKind; children: Map; @@ -2348,7 +2400,7 @@ export function validateNativeArchiveEntryCandidates(entries: readonly string[], function validateNativeArchiveTargetPlan(entries: readonly NativeArchiveEntryCandidate[], targetDir: string): void { const scope = new PackageOutputScope([targetDir]); - const targetPlan = new TargetPlanInvariant(); + const rawPlan = new RawArchivePlanInvariant(); for (const candidate of entries) { const entryPath = String(candidate.entryPath || "").replace(/\\/g, "/").replace(/\/$/, ""); if (!entryPath) { @@ -2356,7 +2408,7 @@ function validateNativeArchiveTargetPlan(entries: readonly NativeArchiveEntryCan } const outputPath = path.resolve(targetDir, ...entryPath.split("/")); scope.validateTarget(entryPath, outputPath); - targetPlan.add(outputPath, candidate.isDirectory ? "directory" : "file"); + rawPlan.add(entryPath, candidate.isDirectory ? "directory" : "file"); } } @@ -3058,8 +3110,23 @@ async function extractZipArchive( const mode = effectiveConflictMode(conflictMode); const memoryLimitBytes = zipEntryMemoryLimitBytes(); const zip = new AdmZip(archivePath); - const entries = zip.getEntries(); - const resolvedTarget = path.resolve(targetDir); + const entries = zip.getEntries(); + const resolvedTarget = path.resolve(targetDir); + const rawPlan = new RawArchivePlanInvariant(); + + for (const entry of entries) { + if (signal?.aborted) { + throw new Error("aborted:extract"); + } + const baseOutputPath = path.resolve(targetDir, entry.entryName); + if (!baseOutputPath.startsWith(resolvedTarget + path.sep) && baseOutputPath !== resolvedTarget) { + throw new Error(`ZIP-Eintrag Path Traversal blockiert: ${entry.entryName}`); + } + const entryPath = entry.entryName.replace(/\\/g, "/").replace(/\/$/, ""); + validateTarget?.(entryPath || "directory", baseOutputPath); + rawPlan.add(entryPath, entry.isDirectory ? "directory" : "file"); + } + const plannedOutputs = new Set(); const targetPlan = new TargetPlanInvariant(); const renameCounters = new Map(); diff --git a/tests/extractor-jvm.test.ts b/tests/extractor-jvm.test.ts index cc620d0..89751d8 100644 --- a/tests/extractor-jvm.test.ts +++ b/tests/extractor-jvm.test.ts @@ -13,6 +13,27 @@ const require = createRequire(import.meta.url); type ZipFixtureEntry = { name: string; directory?: boolean; content?: string }; +type TargetTreeEntry = { path: string; type: "directory" | "file"; bytes?: string }; + +function readTargetTree(root: string): TargetTreeEntry[] { + const entries: TargetTreeEntry[] = []; + const visit = (directory: string): void => { + for (const name of fs.readdirSync(directory).sort((left, right) => left.localeCompare(right))) { + const absolutePath = path.join(directory, name); + const relativePath = path.relative(root, absolutePath).replace(/\\/g, "/"); + const stat = fs.lstatSync(absolutePath); + if (stat.isDirectory()) { + entries.push({ path: relativePath, type: "directory" }); + visit(absolutePath); + } else { + entries.push({ path: relativePath, type: "file", bytes: fs.readFileSync(absolutePath).toString("base64") }); + } + } + }; + visit(root); + return entries; +} + function writeZipFixture(filePath: string, entries: readonly ZipFixtureEntry[]): void { const ZipFile = require("adm-zip/zipFile") as new (input: null, options: Record) => { setEntry: (entry: unknown) => void; @@ -47,9 +68,15 @@ const jvmTargetCollisionCases = [ ["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" }]] + ["upper-case then lower-case file aliases", [{ name: "Name", content: "first" }, { name: "name", content: "second" }]], + ["lower-case then upper-case file aliases", [{ name: "name", content: "first" }, { name: "Name", content: "second" }]], + ["upper-case then lower-case directory aliases", [{ name: "Folder", directory: true }, { name: "folder", directory: true }]], + ["lower-case then upper-case directory aliases", [{ name: "folder", directory: true }, { name: "Folder", directory: true }]], + ["duplicate file targets in forward order", [{ name: "same", content: "first" }, { name: "same", content: "second" }]], + ["duplicate file targets in reverse order", [{ name: "same", content: "second" }, { name: "same", content: "first" }]] ] as const satisfies ReadonlyArray; + +const rawPlanConflictModes = ["rename", "skip", "overwrite"] as const; function hasJavaRuntime(): boolean { const result = spawnSync("java", ["-version"], { stdio: "ignore" }); @@ -444,15 +471,16 @@ describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm b 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) => { + it.each(["7zjbinding", "zip4j"].flatMap((backend) => rawPlanConflictModes.flatMap((conflictMode) => jvmTargetCollisionCases.map(([label, entries]) => [backend, conflictMode, label, entries] as const))))( + "rejects %s %s %s before any target mutation", + (backend, conflictMode, _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 before = readTargetTree(targetDir); const zipPath = path.join(root, "collision.zip"); writeZipFixture(zipPath, entries); const runtimeRoot = path.join(process.cwd(), "resources", "extractor-jvm"); @@ -472,18 +500,17 @@ describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm b "--target", targetDir, "--conflict", - "overwrite", + conflictMode, "--backend", backend ], { encoding: "utf8" }); expect(run.status).not.toBe(0); - expect(fs.readdirSync(targetDir)).toEqual(["sentinel.txt"]); - expect(fs.readFileSync(sentinelPath, "utf8")).toBe("foreign"); + expect(readTargetTree(targetDir)).toEqual(before); } ); - it.each(["7zjbinding", "zip4j"])("allows safely identical duplicate directory targets in %s", (backend) => { + it.each(["7zjbinding", "zip4j"].flatMap((backend) => rawPlanConflictModes.map((conflictMode) => [backend, conflictMode] as const)))("allows safely identical duplicate directory targets in %s with %s", (backend, conflictMode) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), `rd-jvm-identical-directory-${backend}-`)); tempDirs.push(root); const targetDir = path.join(root, "out"); @@ -509,7 +536,7 @@ describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm b "--target", targetDir, "--conflict", - "overwrite", + conflictMode, "--backend", backend ], { encoding: "utf8" }); @@ -518,8 +545,50 @@ describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm b 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 () => { + + it.each(["7zjbinding", "zip4j"].flatMap((backend) => rawPlanConflictModes.map((conflictMode) => [backend, conflictMode] as const)))( + "preserves existing-target %s behavior for %s", + (backend, conflictMode) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), `rd-jvm-existing-target-${backend}-${conflictMode}-`)); + tempDirs.push(root); + const targetDir = path.join(root, "out"); + fs.mkdirSync(targetDir, { recursive: true }); + fs.writeFileSync(path.join(targetDir, "same.txt"), "old"); + const zipPath = path.join(root, "existing-target.zip"); + writeZipFixture(zipPath, [{ name: "same.txt", content: "new" }]); + 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", + conflictMode, + "--backend", + backend + ], { encoding: "utf8" }); + + expect(run.status).toBe(0); + expect(fs.readFileSync(path.join(targetDir, "same.txt"), "utf8")).toBe(conflictMode === "overwrite" ? "new" : "old"); + if (conflictMode === "rename") { + expect(fs.readFileSync(path.join(targetDir, "same (1).txt"), "utf8")).toBe("new"); + } else { + expect(fs.existsSync(path.join(targetDir, "same (1).txt"))).toBe(false); + } + } + ); + + it("emits progress callbacks with archiveName and percent", async () => { process.env.RD_EXTRACT_BACKEND = "jvm"; const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-jvm-progress-")); diff --git a/tests/extractor.test.ts b/tests/extractor.test.ts index d32dd7d..e34b3ce 100644 --- a/tests/extractor.test.ts +++ b/tests/extractor.test.ts @@ -34,6 +34,27 @@ const require = createRequire(import.meta.url); type ZipFixtureEntry = { name: string; directory?: boolean; content?: string }; +type TargetTreeEntry = { path: string; type: "directory" | "file"; bytes?: string }; + +function readTargetTree(root: string): TargetTreeEntry[] { + const entries: TargetTreeEntry[] = []; + const visit = (directory: string): void => { + for (const name of fs.readdirSync(directory).sort((left, right) => left.localeCompare(right))) { + const absolutePath = path.join(directory, name); + const relativePath = path.relative(root, absolutePath).replace(/\\/g, "/"); + const stat = fs.lstatSync(absolutePath); + if (stat.isDirectory()) { + entries.push({ path: relativePath, type: "directory" }); + visit(absolutePath); + } else { + entries.push({ path: relativePath, type: "file", bytes: fs.readFileSync(absolutePath).toString("base64") }); + } + } + }; + visit(root); + return entries; +} + function writeZipFixture(filePath: string, entries: readonly ZipFixtureEntry[]): void { const ZipFile = require("adm-zip/zipFile") as new (input: null, options: Record) => { setEntry: (entry: unknown) => void; @@ -68,9 +89,15 @@ const archiveTargetCollisionCases = [ ["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" }]] + ["upper-case then lower-case file aliases", [{ name: "Name", content: "first" }, { name: "name", content: "second" }]], + ["lower-case then upper-case file aliases", [{ name: "name", content: "first" }, { name: "Name", content: "second" }]], + ["upper-case then lower-case directory aliases", [{ name: "Folder", directory: true }, { name: "folder", directory: true }]], + ["lower-case then upper-case directory aliases", [{ name: "folder", directory: true }, { name: "Folder", directory: true }]], + ["duplicate file targets in forward order", [{ name: "same", content: "first" }, { name: "same", content: "second" }]], + ["duplicate file targets in reverse order", [{ name: "same", content: "second" }, { name: "same", content: "first" }]] ] as const satisfies ReadonlyArray; + +const rawPlanConflictModes = ["rename", "skip", "overwrite"] as const; beforeEach(() => { process.env.RD_EXTRACT_BACKEND = "legacy"; @@ -1741,7 +1768,7 @@ describe("extractor", () => { expect(fs.readFileSync(aliasPath, "utf8")).toBe("foreign-alias"); }); - it.each(archiveTargetCollisionCases)("rejects internal ZIP %s before any target mutation", async (_label, entries) => { + it.each(rawPlanConflictModes.flatMap((conflictMode) => archiveTargetCollisionCases.map(([label, entries]) => [conflictMode, label, entries] as const)))("rejects internal ZIP %s %s before any target mutation", async (conflictMode, _label, entries) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-zip-target-plan-")); tempDirs.push(root); const packageDir = path.join(root, "pkg"); @@ -1750,34 +1777,36 @@ describe("extractor", () => { fs.mkdirSync(targetDir, { recursive: true }); const sentinelPath = path.join(targetDir, "sentinel.txt"); fs.writeFileSync(sentinelPath, "foreign"); + const before = readTargetTree(targetDir); writeZipFixture(path.join(packageDir, "collision.zip"), entries); const result = await extractPackageArchives({ packageDir, targetDir, cleanupMode: "none", - conflictMode: "overwrite", + conflictMode, 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"); + expect(readTargetTree(targetDir)).toEqual(before); }); - it.each(archiveTargetCollisionCases)("rejects native preflight %s", (_label, entries) => { + it.each(rawPlanConflictModes.flatMap((conflictMode) => archiveTargetCollisionCases.map(([label, entries]) => [conflictMode, label, entries] as const)))("rejects native preflight before %s conflict handling for %s", (_conflictMode, _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 }); + fs.writeFileSync(path.join(targetDir, "sentinel.bin"), Buffer.from([0, 1, 2, 255])); + const before = readTargetTree(targetDir); 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([]); + expect(readTargetTree(targetDir)).toEqual(before); }); - it("allows safely identical duplicate directory targets", async () => { + it.each(rawPlanConflictModes)("allows safely identical duplicate directory targets before %s conflict handling", async (conflictMode) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-identical-directory-plan-")); tempDirs.push(root); const packageDir = path.join(root, "pkg"); @@ -1793,7 +1822,7 @@ describe("extractor", () => { packageDir, targetDir, cleanupMode: "none", - conflictMode: "overwrite", + conflictMode, removeLinks: false, removeSamples: false });