Fix multi-part RAR: get first stream via callback, track current volume name

Two bugs in SevenZipVolumeCallback caused multi-part RAR extraction to fail:

1. getProperty(NAME) always returned firstFileName instead of tracking the
   last opened volume name. 7z-JBinding needs this to compute subsequent
   volume filenames.

2. The first IInStream was created separately instead of through the
   callback's getStream() method, so the volume name tracker was not
   properly initialized.

Verified with real multi-part RAR5 test archives (3 parts, WinRAR 7.01).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-03 16:15:44 +01:00
parent 26bf675a41
commit 93d54c8f84
3 changed files with 29 additions and 24 deletions

View File

@ -337,37 +337,37 @@ public final class JBindExtractorMain {
return new SevenZipArchiveContext(archive, null, volumed, callback); return new SevenZipArchiveContext(archive, null, volumed, callback);
} }
RandomAccessFile raf = new RandomAccessFile(archiveFile, "r");
RandomAccessFileInStream stream = new RandomAccessFileInStream(raf);
// Multi-part RAR (.part1.rar, .part2.rar or old-style .rar/.r01/.r02): // Multi-part RAR (.part1.rar, .part2.rar or old-style .rar/.r01/.r02):
// Auto-detection with null format can fail for multi-volume RAR because // The first stream MUST be obtained via the callback so the volume name
// 7z-JBinding may not fully recognize the format from a single part. // tracker is properly initialized. 7z-JBinding uses getProperty(NAME)
// Specify the format explicitly (try RAR5 first, then RAR4). // to compute subsequent volume filenames.
if (RAR_MULTIPART_RE.matcher(nameLower).matches() || hasOldStyleRarSplits(archiveFile)) { boolean isMultiPartRar = RAR_MULTIPART_RE.matcher(nameLower).matches()
ArchiveFormat[] rarFormats = { ArchiveFormat.RAR5, ArchiveFormat.RAR }; || 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; Exception lastError = null;
ArchiveFormat[] rarFormats = { ArchiveFormat.RAR5, ArchiveFormat.RAR, null };
for (ArchiveFormat fmt : rarFormats) { for (ArchiveFormat fmt : rarFormats) {
try { try {
stream.seek(0L, 0); inStream.seek(0L, 0);
IInArchive archive = SevenZip.openInArchive(fmt, stream, callback); IInArchive archive = SevenZip.openInArchive(fmt, inStream, callback);
return new SevenZipArchiveContext(archive, stream, null, callback); return new SevenZipArchiveContext(archive, null, null, callback);
} catch (Exception e) { } catch (Exception e) {
lastError = e; lastError = e;
} }
} }
// Final attempt with auto-detection callback.close();
try { throw lastError != null ? lastError : new IOException("Archiv konnte nicht geoeffnet werden");
stream.seek(0L, 0);
IInArchive archive = SevenZip.openInArchive(null, stream, callback);
return new SevenZipArchiveContext(archive, stream, null, callback);
} catch (Exception e) {
// Close the RAF since we're about to throw
try { raf.close(); } catch (Throwable ignored) {}
throw lastError != null ? lastError : e;
}
} }
// 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); IInArchive archive = SevenZip.openInArchive(null, stream, callback);
return new SevenZipArchiveContext(archive, stream, null, callback); return new SevenZipArchiveContext(archive, stream, null, callback);
} }
@ -840,20 +840,22 @@ public final class JBindExtractorMain {
private static final class SevenZipVolumeCallback implements IArchiveOpenCallback, IArchiveOpenVolumeCallback, ICryptoGetTextPassword, Closeable { private static final class SevenZipVolumeCallback implements IArchiveOpenCallback, IArchiveOpenVolumeCallback, ICryptoGetTextPassword, Closeable {
private final File archiveDir; private final File archiveDir;
private final String firstFileName;
private final String password; private final String password;
private final Map<String, RandomAccessFile> openRafs = new HashMap<String, RandomAccessFile>(); private final Map<String, RandomAccessFile> openRafs = new HashMap<String, RandomAccessFile>();
// Must track the LAST opened volume name 7z-JBinding queries this via
// getProperty(NAME) to compute the next volume filename.
private volatile String currentVolumeName;
SevenZipVolumeCallback(File archiveFile, String password) { SevenZipVolumeCallback(File archiveFile, String password) {
this.archiveDir = archiveFile.getAbsoluteFile().getParentFile(); this.archiveDir = archiveFile.getAbsoluteFile().getParentFile();
this.firstFileName = archiveFile.getName(); this.currentVolumeName = archiveFile.getName();
this.password = password == null ? "" : password; this.password = password == null ? "" : password;
} }
@Override @Override
public Object getProperty(PropID propID) { public Object getProperty(PropID propID) {
if (propID == PropID.NAME) { if (propID == PropID.NAME) {
return firstFileName; return currentVolumeName;
} }
return null; return null;
} }
@ -872,6 +874,9 @@ public final class JBindExtractorMain {
openRafs.put(key, raf); openRafs.put(key, raf);
} }
raf.seek(0L); raf.seek(0L);
// Update current volume name so getProperty(NAME) returns the
// correct value when 7z-JBinding computes the next volume.
currentVolumeName = filename;
return new RandomAccessFileInStream(raf); return new RandomAccessFileInStream(raf);
} catch (IOException error) { } catch (IOException error) {
throw new SevenZipException("Volume konnte nicht geoffnet werden: " + filename, error); throw new SevenZipException("Volume konnte nicht geoffnet werden: " + filename, error);