Restore multipart RAR open strategy for JVM extractor

Bring back callback-first RAR multipart opening with explicit RAR5/RAR fallback while keeping 7z.001 on VolumedArchiveInStream to improve 7z-JBinding compatibility on split RAR sets.
This commit is contained in:
Sucukdeluxe 2026-03-03 17:32:22 +01:00
parent 02bd4a61fb
commit 8fad61e329
2 changed files with 63 additions and 3 deletions

View File

@ -11,6 +11,7 @@ import net.sf.sevenzipjbinding.IInStream;
import net.sf.sevenzipjbinding.ISequentialOutStream; import net.sf.sevenzipjbinding.ISequentialOutStream;
import net.sf.sevenzipjbinding.ICryptoGetTextPassword; import net.sf.sevenzipjbinding.ICryptoGetTextPassword;
import net.sf.sevenzipjbinding.PropID; import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.ArchiveFormat;
import net.sf.sevenzipjbinding.SevenZip; import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException; import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; 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 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 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 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 static volatile boolean sevenZipInitialized = false;
private JBindExtractorMain() { private JBindExtractorMain() {
@ -327,21 +330,78 @@ public final class JBindExtractorMain {
SevenZipVolumeCallback callback = new SevenZipVolumeCallback(archiveFile, effectivePassword); SevenZipVolumeCallback callback = new SevenZipVolumeCallback(archiveFile, effectivePassword);
// VolumedArchiveInStream is ONLY for .7z.001 split archives. // 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()) { if (SEVEN_ZIP_SPLIT_RE.matcher(nameLower).matches()) {
VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback); VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback);
IInArchive archive = SevenZip.openInArchive(null, volumed, callback); IInArchive archive = SevenZip.openInArchive(null, volumed, callback);
return new SevenZipArchiveContext(archive, null, volumed, callback); return new SevenZipArchiveContext(archive, null, volumed, callback);
} }
// All other archives (including multi-part RAR): use RandomAccessFileInStream // Multi-part RAR (.part1.rar, .part2.rar or old-style .rar/.r01/.r02):
// with auto-detection. The IArchiveOpenVolumeCallback handles additional // The first stream MUST be obtained via the callback so the volume name
// volumes when 7z-JBinding requests them. // 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"); RandomAccessFile raf = new RandomAccessFile(archiveFile, "r");
RandomAccessFileInStream stream = new RandomAccessFileInStream(raf); RandomAccessFileInStream stream = new RandomAccessFileInStream(raf);
IInArchive archive = SevenZip.openInArchive(null, stream, callback); IInArchive archive = SevenZip.openInArchive(null, stream, callback);
return new SevenZipArchiveContext(archive, stream, null, 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) { private static boolean isWrongPassword(ZipException error, boolean encrypted) {
if (error == null) { if (error == null) {
return false; return false;