diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class index 321cb68..a6fa276 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 64e4dca..7ed46ec 100644 --- a/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java +++ b/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java @@ -11,6 +11,7 @@ import net.sf.sevenzipjbinding.IInStream; import net.sf.sevenzipjbinding.ISequentialOutStream; import net.sf.sevenzipjbinding.ICryptoGetTextPassword; import net.sf.sevenzipjbinding.PropID; +import net.sf.sevenzipjbinding.ArchiveFormat; import net.sf.sevenzipjbinding.SevenZip; import net.sf.sevenzipjbinding.SevenZipException; import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; @@ -42,6 +43,8 @@ public final class JBindExtractorMain { private static final Pattern NUMBERED_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.zip\\.\\d{3}$"); private static final Pattern OLD_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.z\\d{2,3}$"); private static final Pattern SEVEN_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.7z\\.001$"); + private static final Pattern RAR_MULTIPART_RE = Pattern.compile("(?i).*\\.part\\d+\\.rar$"); + private static final Pattern RAR_OLDSPLIT_RE = Pattern.compile("(?i).*\\.r\\d{2,3}$"); private static volatile boolean sevenZipInitialized = false; private JBindExtractorMain() { @@ -327,21 +330,78 @@ public final class JBindExtractorMain { SevenZipVolumeCallback callback = new SevenZipVolumeCallback(archiveFile, effectivePassword); // VolumedArchiveInStream is ONLY for .7z.001 split archives. + // It internally checks for the ".7z.001" suffix and rejects everything else. if (SEVEN_ZIP_SPLIT_RE.matcher(nameLower).matches()) { VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback); IInArchive archive = SevenZip.openInArchive(null, volumed, callback); return new SevenZipArchiveContext(archive, null, volumed, callback); } - // All other archives (including multi-part RAR): use RandomAccessFileInStream - // with auto-detection. The IArchiveOpenVolumeCallback handles additional - // volumes when 7z-JBinding requests them. + // Multi-part RAR (.part1.rar, .part2.rar or old-style .rar/.r01/.r02): + // The first stream MUST be obtained via the callback so the volume name + // tracker is properly initialized. 7z-JBinding uses getProperty(NAME) + // to compute subsequent volume filenames. + boolean isMultiPartRar = RAR_MULTIPART_RE.matcher(nameLower).matches() + || hasOldStyleRarSplits(archiveFile); + + if (isMultiPartRar) { + IInStream inStream = callback.getStream(archiveFile.getAbsolutePath()); + if (inStream == null) { + throw new IOException("Archiv konnte nicht geoeffnet werden: " + archiveFile.getAbsolutePath()); + } + // Try RAR5 first (modern), then RAR4, then auto-detect + Exception lastError = null; + ArchiveFormat[] rarFormats = { ArchiveFormat.RAR5, ArchiveFormat.RAR, null }; + for (ArchiveFormat fmt : rarFormats) { + try { + inStream.seek(0L, 0); + IInArchive archive = SevenZip.openInArchive(fmt, inStream, callback); + return new SevenZipArchiveContext(archive, null, null, callback); + } catch (Exception e) { + lastError = e; + } + } + callback.close(); + throw lastError != null ? lastError : new IOException("Archiv konnte nicht geoeffnet werden"); + } + + // Single-file archives: open directly with auto-detection RandomAccessFile raf = new RandomAccessFile(archiveFile, "r"); RandomAccessFileInStream stream = new RandomAccessFileInStream(raf); IInArchive archive = SevenZip.openInArchive(null, stream, callback); return new SevenZipArchiveContext(archive, stream, null, callback); } + private static boolean hasOldStyleRarSplits(File archiveFile) { + // Old-style RAR splits: main.rar + main.r01, main.r02, ... + String name = archiveFile.getName(); + if (!name.toLowerCase(Locale.ROOT).endsWith(".rar")) { + return false; + } + File parent = archiveFile.getParentFile(); + if (parent == null || !parent.exists()) { + return false; + } + File[] siblings = parent.listFiles(); + if (siblings == null) { + return false; + } + String stem = name.substring(0, name.length() - 4); + for (File sibling : siblings) { + if (!sibling.isFile()) { + continue; + } + String sibName = sibling.getName(); + if (sibName.length() > stem.length() + 1 && sibName.substring(0, stem.length()).equalsIgnoreCase(stem)) { + String suffix = sibName.substring(stem.length()); + if (RAR_OLDSPLIT_RE.matcher(suffix).matches() || suffix.toLowerCase(Locale.ROOT).matches("\\.r\\d{2,3}")) { + return true; + } + } + } + return false; + } + private static boolean isWrongPassword(ZipException error, boolean encrypted) { if (error == null) { return false;