Replace extraction backend with SevenZipJBinding + Zip4j JVM sidecar
Build and Release / build (push) Has been cancelled
Build and Release / build (push) Has been cancelled
- New JVM sidecar (resources/extractor-jvm/) using SevenZipJBinding for RAR/7z/TAR and Zip4j for ZIP multipart, matching JDownloader 2 stack - Auto/JVM/Legacy backend modes via RD_EXTRACT_BACKEND env variable - Fallback to legacy UnRAR/7z when JVM runtime unavailable - Fix isJvmRuntimeMissingError false positives on valid extraction errors - Cache JVM layout resolution to avoid repeated filesystem checks - Route nested ZIP extraction through JVM backend consistently Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
3ee3af03cf
commit
de369f3bcd
@@ -0,0 +1,22 @@
|
||||
# JVM extractor runtime
|
||||
|
||||
This directory contains the Java sidecar runtime used by `src/main/extractor.ts`.
|
||||
|
||||
## Included backends
|
||||
|
||||
- `sevenzipjbinding` for the primary extraction path (RAR/7z/ZIP and others)
|
||||
- `zip4j` for ZIP multipart handling (JD-style split ZIP behavior)
|
||||
|
||||
## Layout
|
||||
|
||||
- `classes/` compiled `JBindExtractorMain` classes
|
||||
- `lib/` runtime jars required by the sidecar
|
||||
- `src/` Java source for the sidecar
|
||||
|
||||
## Rebuild notes
|
||||
|
||||
The checked-in classes are Java 8 compatible and built from:
|
||||
|
||||
`resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java`
|
||||
|
||||
If you need to rebuild, compile against the jars in `lib/` with a Java 8-compatible compiler.
|
||||
@@ -0,0 +1,12 @@
|
||||
Bundled JVM extractor dependencies:
|
||||
|
||||
1) sevenzipjbinding (16.02-2.01)
|
||||
- Maven artifact: net.sf.sevenzipjbinding:sevenzipjbinding
|
||||
- Maven artifact: net.sf.sevenzipjbinding:sevenzipjbinding-all-platforms
|
||||
- Upstream: https://sevenzipjbind.sourceforge.net/
|
||||
|
||||
2) zip4j (2.11.5)
|
||||
- Maven artifact: net.lingala.zip4j:zip4j
|
||||
- Upstream: https://github.com/srikanth-lingala/zip4j
|
||||
|
||||
Please review upstream licenses and notices before redistribution.
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,856 @@
|
||||
package com.sucukdeluxe.extractor;
|
||||
|
||||
import net.lingala.zip4j.ZipFile;
|
||||
import net.lingala.zip4j.exception.ZipException;
|
||||
import net.lingala.zip4j.model.FileHeader;
|
||||
import net.sf.sevenzipjbinding.ExtractOperationResult;
|
||||
import net.sf.sevenzipjbinding.IArchiveOpenCallback;
|
||||
import net.sf.sevenzipjbinding.IArchiveOpenVolumeCallback;
|
||||
import net.sf.sevenzipjbinding.IInArchive;
|
||||
import net.sf.sevenzipjbinding.IInStream;
|
||||
import net.sf.sevenzipjbinding.ISequentialOutStream;
|
||||
import net.sf.sevenzipjbinding.ICryptoGetTextPassword;
|
||||
import net.sf.sevenzipjbinding.PropID;
|
||||
import net.sf.sevenzipjbinding.SevenZip;
|
||||
import net.sf.sevenzipjbinding.SevenZipException;
|
||||
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
|
||||
import net.sf.sevenzipjbinding.impl.VolumedArchiveInStream;
|
||||
import net.sf.sevenzipjbinding.simple.ISimpleInArchive;
|
||||
import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class JBindExtractorMain {
|
||||
private static final int BUFFER_SIZE = 64 * 1024;
|
||||
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 JBindExtractorMain() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int exit = 1;
|
||||
try {
|
||||
ExtractionRequest request = parseArgs(args);
|
||||
exit = runExtraction(request);
|
||||
} catch (IllegalArgumentException error) {
|
||||
emitError("Argumentfehler: " + safeMessage(error));
|
||||
exit = 2;
|
||||
} catch (Throwable error) {
|
||||
emitError(safeMessage(error));
|
||||
exit = 1;
|
||||
}
|
||||
System.exit(exit);
|
||||
}
|
||||
|
||||
private static int runExtraction(ExtractionRequest request) throws Exception {
|
||||
List<String> passwords = normalizePasswords(request.passwords);
|
||||
Exception lastError = null;
|
||||
boolean hadWrongPassword = false;
|
||||
for (String password : passwords) {
|
||||
try {
|
||||
extractSingle(request, password);
|
||||
emitPassword(password);
|
||||
emitDone();
|
||||
return 0;
|
||||
} catch (WrongPasswordException wrongPassword) {
|
||||
hadWrongPassword = true;
|
||||
lastError = wrongPassword;
|
||||
} catch (Exception error) {
|
||||
lastError = error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hadWrongPassword && (lastError instanceof WrongPasswordException)) {
|
||||
emitError("Falsches Archiv-Passwort");
|
||||
return 1;
|
||||
}
|
||||
if (lastError != null) {
|
||||
throw lastError;
|
||||
}
|
||||
emitError("Entpacken fehlgeschlagen");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void extractSingle(ExtractionRequest request, String password) throws Exception {
|
||||
Backend backend = request.backend;
|
||||
if (backend == Backend.AUTO) {
|
||||
backend = shouldUseZip4j(request.archiveFile) ? Backend.ZIP4J : Backend.SEVENZIPJBIND;
|
||||
}
|
||||
emitBackend(backend);
|
||||
|
||||
if (backend == Backend.ZIP4J) {
|
||||
extractWithZip4j(request, password);
|
||||
return;
|
||||
}
|
||||
extractWithSevenZip(request, password);
|
||||
}
|
||||
|
||||
private static void extractWithZip4j(ExtractionRequest request, String password) throws Exception {
|
||||
ZipFile zipFile = new ZipFile(request.archiveFile);
|
||||
try {
|
||||
if (password != null && password.length() > 0) {
|
||||
zipFile.setPassword(password.toCharArray());
|
||||
}
|
||||
|
||||
List<FileHeader> fileHeaders = zipFile.getFileHeaders();
|
||||
if (fileHeaders == null) {
|
||||
fileHeaders = new ArrayList<FileHeader>();
|
||||
}
|
||||
|
||||
long totalUnits = 0;
|
||||
boolean encrypted = false;
|
||||
for (FileHeader header : fileHeaders) {
|
||||
if (header == null || header.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
encrypted = encrypted || header.isEncrypted();
|
||||
totalUnits += safeSize(header.getUncompressedSize());
|
||||
}
|
||||
ProgressTracker progress = new ProgressTracker(totalUnits);
|
||||
progress.emitStart();
|
||||
|
||||
Set<String> reserved = new HashSet<String>();
|
||||
for (FileHeader header : fileHeaders) {
|
||||
if (header == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String entryName = normalizeEntryName(header.getFileName(), "file");
|
||||
if (header.isDirectory()) {
|
||||
File dir = resolveDirectory(request.targetDir, entryName);
|
||||
ensureDirectory(dir);
|
||||
reserved.add(pathKey(dir));
|
||||
continue;
|
||||
}
|
||||
|
||||
long itemUnits = safeSize(header.getUncompressedSize());
|
||||
File output = resolveOutputFile(request.targetDir, entryName, request.conflictMode, reserved);
|
||||
if (output == null) {
|
||||
progress.advance(itemUnits);
|
||||
continue;
|
||||
}
|
||||
|
||||
ensureDirectory(output.getParentFile());
|
||||
long[] remaining = new long[] { itemUnits };
|
||||
try {
|
||||
InputStream in = zipFile.getInputStream(header);
|
||||
OutputStream out = new FileOutputStream(output);
|
||||
try {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
while (true) {
|
||||
int read = in.read(buffer);
|
||||
if (read < 0) {
|
||||
break;
|
||||
}
|
||||
if (read == 0) {
|
||||
continue;
|
||||
}
|
||||
out.write(buffer, 0, read);
|
||||
long accounted = Math.min(remaining[0], (long) read);
|
||||
remaining[0] -= accounted;
|
||||
progress.advance(accounted);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
out.close();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
in.close();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (remaining[0] > 0) {
|
||||
progress.advance(remaining[0]);
|
||||
}
|
||||
long modified = header.getLastModifiedTimeEpoch();
|
||||
if (modified > 0) {
|
||||
output.setLastModified(modified);
|
||||
}
|
||||
} catch (ZipException error) {
|
||||
if (isWrongPassword(error, encrypted)) {
|
||||
throw new WrongPasswordException(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
progress.emitDone();
|
||||
} finally {
|
||||
try {
|
||||
zipFile.close();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractWithSevenZip(ExtractionRequest request, String password) throws Exception {
|
||||
SevenZipArchiveContext context = null;
|
||||
try {
|
||||
context = openSevenZipArchive(request.archiveFile, password);
|
||||
IInArchive archive = context.archive;
|
||||
ISimpleInArchive simple = archive.getSimpleInterface();
|
||||
ISimpleInArchiveItem[] items = simple.getArchiveItems();
|
||||
|
||||
long totalUnits = 0;
|
||||
boolean encrypted = false;
|
||||
for (ISimpleInArchiveItem item : items) {
|
||||
if (item == null || item.isFolder()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
encrypted = encrypted || item.isEncrypted();
|
||||
} catch (Throwable ignored) {
|
||||
// ignore encrypted flag read issues
|
||||
}
|
||||
totalUnits += safeSize(item.getSize());
|
||||
}
|
||||
ProgressTracker progress = new ProgressTracker(totalUnits);
|
||||
progress.emitStart();
|
||||
|
||||
Set<String> reserved = new HashSet<String>();
|
||||
for (ISimpleInArchiveItem item : items) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String entryName = normalizeEntryName(item.getPath(), "item-" + item.getItemIndex());
|
||||
if (item.isFolder()) {
|
||||
File dir = resolveDirectory(request.targetDir, entryName);
|
||||
ensureDirectory(dir);
|
||||
reserved.add(pathKey(dir));
|
||||
continue;
|
||||
}
|
||||
|
||||
long itemUnits = safeSize(item.getSize());
|
||||
File output = resolveOutputFile(request.targetDir, entryName, request.conflictMode, reserved);
|
||||
if (output == null) {
|
||||
progress.advance(itemUnits);
|
||||
continue;
|
||||
}
|
||||
|
||||
ensureDirectory(output.getParentFile());
|
||||
final FileOutputStream out = new FileOutputStream(output);
|
||||
final long[] remaining = new long[] { itemUnits };
|
||||
try {
|
||||
ExtractOperationResult result = item.extractSlow(new ISequentialOutStream() {
|
||||
@Override
|
||||
public int write(byte[] data) throws SevenZipException {
|
||||
if (data == null || data.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
out.write(data);
|
||||
} catch (IOException error) {
|
||||
throw new SevenZipException("Fehler beim Schreiben: " + error.getMessage(), error);
|
||||
}
|
||||
long accounted = Math.min(remaining[0], (long) data.length);
|
||||
remaining[0] -= accounted;
|
||||
progress.advance(accounted);
|
||||
return data.length;
|
||||
}
|
||||
});
|
||||
|
||||
if (remaining[0] > 0) {
|
||||
progress.advance(remaining[0]);
|
||||
}
|
||||
|
||||
if (result != ExtractOperationResult.OK) {
|
||||
if (isPasswordFailure(result, encrypted)) {
|
||||
throw new WrongPasswordException(new IOException("Falsches Passwort"));
|
||||
}
|
||||
throw new IOException("7z-Fehler: " + result.name());
|
||||
}
|
||||
} catch (SevenZipException error) {
|
||||
if (looksLikeWrongPassword(error, encrypted)) {
|
||||
throw new WrongPasswordException(error);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
try {
|
||||
out.close();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
java.util.Date modified = item.getLastWriteTime();
|
||||
if (modified != null) {
|
||||
output.setLastModified(modified.getTime());
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// best effort
|
||||
}
|
||||
}
|
||||
|
||||
progress.emitDone();
|
||||
} finally {
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SevenZipArchiveContext openSevenZipArchive(File archiveFile, String password) throws Exception {
|
||||
String nameLower = archiveFile.getName().toLowerCase(Locale.ROOT);
|
||||
String effectivePassword = password == null ? "" : password;
|
||||
SevenZipVolumeCallback callback = new SevenZipVolumeCallback(archiveFile, effectivePassword);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 isWrongPassword(ZipException error, boolean encrypted) {
|
||||
if (error == null) {
|
||||
return false;
|
||||
}
|
||||
if (error.getType() == ZipException.Type.WRONG_PASSWORD) {
|
||||
return true;
|
||||
}
|
||||
String text = safeMessage(error).toLowerCase(Locale.ROOT);
|
||||
if (text.contains("wrong password") || text.contains("falsches passwort")) {
|
||||
return true;
|
||||
}
|
||||
return encrypted && (text.contains("checksum") || text.contains("crc") || text.contains("password"));
|
||||
}
|
||||
|
||||
private static boolean isPasswordFailure(ExtractOperationResult result, boolean encrypted) {
|
||||
if (!encrypted || result == null) {
|
||||
return false;
|
||||
}
|
||||
return result == ExtractOperationResult.CRCERROR || result == ExtractOperationResult.DATAERROR;
|
||||
}
|
||||
|
||||
private static boolean looksLikeWrongPassword(Throwable error, boolean encrypted) {
|
||||
if (error == null) {
|
||||
return false;
|
||||
}
|
||||
String text = safeMessage(error).toLowerCase(Locale.ROOT);
|
||||
if (text.contains("wrong password") || text.contains("falsches passwort")) {
|
||||
return true;
|
||||
}
|
||||
return encrypted && (text.contains("crc") || text.contains("data error") || text.contains("checksum"));
|
||||
}
|
||||
|
||||
private static boolean shouldUseZip4j(File archiveFile) {
|
||||
String name = archiveFile.getName().toLowerCase(Locale.ROOT);
|
||||
if (NUMBERED_ZIP_SPLIT_RE.matcher(name).matches()) {
|
||||
return true;
|
||||
}
|
||||
if (OLD_ZIP_SPLIT_RE.matcher(name).matches()) {
|
||||
return true;
|
||||
}
|
||||
if (name.endsWith(".zip")) {
|
||||
File parent = archiveFile.getParentFile();
|
||||
if (parent == null || !parent.exists()) {
|
||||
return false;
|
||||
}
|
||||
String stem = archiveFile.getName().substring(0, archiveFile.getName().length() - 4);
|
||||
File[] siblings = parent.listFiles();
|
||||
if (siblings == null) {
|
||||
return false;
|
||||
}
|
||||
String prefix = (stem + ".z").toLowerCase(Locale.ROOT);
|
||||
for (File sibling : siblings) {
|
||||
String siblingName = sibling.getName().toLowerCase(Locale.ROOT);
|
||||
if (!sibling.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (siblingName.startsWith(prefix) && siblingName.length() >= prefix.length() + 2) {
|
||||
String suffix = siblingName.substring(prefix.length());
|
||||
if (suffix.matches("\\d{2,3}")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static File resolveDirectory(File targetDir, String entryName) throws IOException {
|
||||
File directory = secureResolve(targetDir, entryName);
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static File resolveOutputFile(File targetDir, String entryName, ConflictMode conflictMode, Set<String> reserved) throws IOException {
|
||||
File base = secureResolve(targetDir, entryName);
|
||||
String key = pathKey(base);
|
||||
boolean exists = base.exists() || reserved.contains(key);
|
||||
|
||||
if (!exists) {
|
||||
reserved.add(key);
|
||||
return base;
|
||||
}
|
||||
|
||||
if (conflictMode == ConflictMode.SKIP) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (conflictMode == ConflictMode.OVERWRITE) {
|
||||
if (base.exists()) {
|
||||
deleteRecursively(base);
|
||||
}
|
||||
reserved.add(key);
|
||||
return base;
|
||||
}
|
||||
|
||||
File parent = base.getParentFile();
|
||||
String fileName = base.getName();
|
||||
int dot = fileName.lastIndexOf('.');
|
||||
String stem = dot > 0 ? fileName.substring(0, dot) : fileName;
|
||||
String ext = dot > 0 ? fileName.substring(dot) : "";
|
||||
|
||||
int counter = 1;
|
||||
while (counter <= 10000) {
|
||||
String candidateName = stem + " (" + counter + ")" + ext;
|
||||
File candidate = new File(parent, candidateName);
|
||||
String candidateKey = pathKey(candidate);
|
||||
if (!candidate.exists() && !reserved.contains(candidateKey)) {
|
||||
reserved.add(candidateKey);
|
||||
return candidate;
|
||||
}
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
throw new IOException("Rename-Limit erreicht fur " + entryName);
|
||||
}
|
||||
|
||||
private static void deleteRecursively(File file) throws IOException {
|
||||
if (file == null || !file.exists()) {
|
||||
return;
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
File[] children = file.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteRecursively(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new IOException("Konnte Datei nicht uberschreiben: " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private static File secureResolve(File targetDir, String entryName) throws IOException {
|
||||
String normalized = normalizeEntryName(entryName, "file");
|
||||
while (normalized.startsWith("/")) {
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
while (normalized.startsWith("\\")) {
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
if (normalized.matches("^[a-zA-Z]:.*")) {
|
||||
normalized = normalized.substring(2);
|
||||
}
|
||||
File targetCanonical = targetDir.getCanonicalFile();
|
||||
File output = new File(targetCanonical, normalized);
|
||||
File outputCanonical = output.getCanonicalFile();
|
||||
String targetPath = targetCanonical.getPath();
|
||||
String outputPath = outputCanonical.getPath();
|
||||
String targetPathNorm = isWindows() ? targetPath.toLowerCase(Locale.ROOT) : targetPath;
|
||||
String outputPathNorm = isWindows() ? outputPath.toLowerCase(Locale.ROOT) : outputPath;
|
||||
if (!outputPathNorm.equals(targetPathNorm) && !outputPathNorm.startsWith(targetPathNorm + File.separator)) {
|
||||
throw new IOException("Path Traversal blockiert: " + entryName);
|
||||
}
|
||||
return outputCanonical;
|
||||
}
|
||||
|
||||
private static String normalizeEntryName(String value, String fallback) {
|
||||
String entry = value == null ? "" : value.trim();
|
||||
if (entry.length() == 0) {
|
||||
return fallback;
|
||||
}
|
||||
entry = entry.replace('\\', '/');
|
||||
while (entry.startsWith("./")) {
|
||||
entry = entry.substring(2);
|
||||
}
|
||||
if (entry.length() == 0) {
|
||||
return fallback;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static long safeSize(Long value) {
|
||||
if (value == null) {
|
||||
return 1;
|
||||
}
|
||||
long size = value.longValue();
|
||||
if (size <= 0) {
|
||||
return 1;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
private static void ensureDirectory(File dir) throws IOException {
|
||||
if (dir == null) {
|
||||
return;
|
||||
}
|
||||
if (dir.exists()) {
|
||||
if (!dir.isDirectory()) {
|
||||
throw new IOException("Pfad ist keine Directory: " + dir.getAbsolutePath());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!dir.mkdirs() && !dir.isDirectory()) {
|
||||
throw new IOException("Verzeichnis konnte nicht erstellt werden: " + dir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private static String pathKey(File file) {
|
||||
String value = file.getAbsolutePath();
|
||||
if (isWindows()) {
|
||||
value = value.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean isWindows() {
|
||||
String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
|
||||
return osName.contains("win");
|
||||
}
|
||||
|
||||
private static List<String> normalizePasswords(List<String> input) {
|
||||
LinkedHashSet<String> deduped = new LinkedHashSet<String>();
|
||||
deduped.add("");
|
||||
if (input != null) {
|
||||
for (String value : input) {
|
||||
deduped.add(value == null ? "" : value);
|
||||
}
|
||||
}
|
||||
return new ArrayList<String>(deduped);
|
||||
}
|
||||
|
||||
private static ExtractionRequest parseArgs(String[] args) {
|
||||
ExtractionRequest request = new ExtractionRequest();
|
||||
int index = 0;
|
||||
while (index < args.length) {
|
||||
String key = args[index];
|
||||
if ("--archive".equals(key)) {
|
||||
request.archiveFile = new File(readNext(args, ++index, key));
|
||||
} else if ("--target".equals(key)) {
|
||||
request.targetDir = new File(readNext(args, ++index, key));
|
||||
} else if ("--conflict".equals(key)) {
|
||||
request.conflictMode = ConflictMode.fromValue(readNext(args, ++index, key));
|
||||
} else if ("--backend".equals(key)) {
|
||||
request.backend = Backend.fromValue(readNext(args, ++index, key));
|
||||
} else if ("--password".equals(key)) {
|
||||
request.passwords.add(readNext(args, ++index, key));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unbekanntes Argument: " + key);
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (request.archiveFile == null) {
|
||||
throw new IllegalArgumentException("--archive fehlt");
|
||||
}
|
||||
if (request.targetDir == null) {
|
||||
throw new IllegalArgumentException("--target fehlt");
|
||||
}
|
||||
if (!request.archiveFile.exists() || !request.archiveFile.isFile()) {
|
||||
throw new IllegalArgumentException("Archiv nicht gefunden: " + request.archiveFile.getAbsolutePath());
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private static String readNext(String[] args, int index, String key) {
|
||||
if (index >= args.length) {
|
||||
throw new IllegalArgumentException("Wert fehlt fur " + key);
|
||||
}
|
||||
return args[index];
|
||||
}
|
||||
|
||||
private static String safeMessage(Throwable error) {
|
||||
if (error == null) {
|
||||
return "Unbekannter Fehler";
|
||||
}
|
||||
String message = error.getMessage();
|
||||
if (message == null || message.trim().length() == 0) {
|
||||
message = error.toString();
|
||||
}
|
||||
return message.replace('\n', ' ').replace('\r', ' ').trim();
|
||||
}
|
||||
|
||||
private static void emitBackend(Backend backend) {
|
||||
System.out.println("RD_BACKEND " + backend.value);
|
||||
}
|
||||
|
||||
private static void emitPassword(String password) {
|
||||
String encoded = Base64.getEncoder().encodeToString((password == null ? "" : password).getBytes(StandardCharsets.UTF_8));
|
||||
System.out.println("RD_PASSWORD " + encoded);
|
||||
}
|
||||
|
||||
private static void emitDone() {
|
||||
System.out.println("RD_DONE");
|
||||
}
|
||||
|
||||
private static void emitError(String message) {
|
||||
System.err.println("RD_ERROR " + message);
|
||||
}
|
||||
|
||||
private enum Backend {
|
||||
AUTO("auto"),
|
||||
SEVENZIPJBIND("7zjbinding"),
|
||||
ZIP4J("zip4j");
|
||||
|
||||
private final String value;
|
||||
|
||||
Backend(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static Backend fromValue(String raw) {
|
||||
String value = raw == null ? "" : raw.trim().toLowerCase(Locale.ROOT);
|
||||
if ("auto".equals(value)) {
|
||||
return AUTO;
|
||||
}
|
||||
if ("7zjb".equals(value) || "7zjbinding".equals(value) || "sevenzipjbinding".equals(value)) {
|
||||
return SEVENZIPJBIND;
|
||||
}
|
||||
if ("zip4j".equals(value)) {
|
||||
return ZIP4J;
|
||||
}
|
||||
throw new IllegalArgumentException("Ungueltiger Backend-Wert: " + raw);
|
||||
}
|
||||
}
|
||||
|
||||
private enum ConflictMode {
|
||||
OVERWRITE,
|
||||
SKIP,
|
||||
RENAME;
|
||||
|
||||
static ConflictMode fromValue(String raw) {
|
||||
String value = raw == null ? "" : raw.trim().toLowerCase(Locale.ROOT);
|
||||
if ("overwrite".equals(value)) {
|
||||
return OVERWRITE;
|
||||
}
|
||||
if ("skip".equals(value) || "ask".equals(value)) {
|
||||
return SKIP;
|
||||
}
|
||||
if ("rename".equals(value)) {
|
||||
return RENAME;
|
||||
}
|
||||
throw new IllegalArgumentException("Ungueltiger Conflict-Wert: " + raw);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ExtractionRequest {
|
||||
private File archiveFile;
|
||||
private File targetDir;
|
||||
private ConflictMode conflictMode = ConflictMode.SKIP;
|
||||
private Backend backend = Backend.AUTO;
|
||||
private final List<String> passwords = new ArrayList<String>();
|
||||
}
|
||||
|
||||
private static final class WrongPasswordException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
WrongPasswordException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ProgressTracker {
|
||||
private final long total;
|
||||
private long completed;
|
||||
private int lastPercent = -1;
|
||||
|
||||
ProgressTracker(long totalUnits) {
|
||||
this.total = Math.max(1L, totalUnits);
|
||||
this.completed = 0L;
|
||||
}
|
||||
|
||||
synchronized void emitStart() {
|
||||
emitPercent(0);
|
||||
}
|
||||
|
||||
synchronized void advance(long units) {
|
||||
if (units <= 0) {
|
||||
return;
|
||||
}
|
||||
completed += units;
|
||||
if (completed > total) {
|
||||
completed = total;
|
||||
}
|
||||
int percent = (int) Math.min(100L, Math.max(0L, (completed * 100L) / total));
|
||||
emitPercent(percent);
|
||||
}
|
||||
|
||||
synchronized void emitDone() {
|
||||
completed = total;
|
||||
emitPercent(100);
|
||||
}
|
||||
|
||||
private void emitPercent(int percent) {
|
||||
int bounded = Math.max(0, Math.min(100, percent));
|
||||
if (bounded == lastPercent) {
|
||||
return;
|
||||
}
|
||||
lastPercent = bounded;
|
||||
System.out.println("RD_PROGRESS " + bounded + "%");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SevenZipArchiveContext implements Closeable {
|
||||
private final IInArchive archive;
|
||||
private final IInStream rootStream;
|
||||
private final VolumedArchiveInStream volumedArchiveInStream;
|
||||
private final SevenZipVolumeCallback callback;
|
||||
|
||||
SevenZipArchiveContext(IInArchive archive, IInStream rootStream, VolumedArchiveInStream volumedArchiveInStream, SevenZipVolumeCallback callback) {
|
||||
this.archive = archive;
|
||||
this.rootStream = rootStream;
|
||||
this.volumedArchiveInStream = volumedArchiveInStream;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (archive != null) {
|
||||
try {
|
||||
archive.close();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (rootStream != null) {
|
||||
try {
|
||||
rootStream.close();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (volumedArchiveInStream != null) {
|
||||
try {
|
||||
volumedArchiveInStream.close();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (callback != null) {
|
||||
callback.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SevenZipVolumeCallback implements IArchiveOpenCallback, IArchiveOpenVolumeCallback, ICryptoGetTextPassword, Closeable {
|
||||
private final File archiveDir;
|
||||
private final String firstFileName;
|
||||
private final String password;
|
||||
private final Map<String, RandomAccessFile> openRafs = new HashMap<String, RandomAccessFile>();
|
||||
|
||||
SevenZipVolumeCallback(File archiveFile, String password) {
|
||||
this.archiveDir = archiveFile.getAbsoluteFile().getParentFile();
|
||||
this.firstFileName = archiveFile.getName();
|
||||
this.password = password == null ? "" : password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(PropID propID) {
|
||||
if (propID == PropID.NAME) {
|
||||
return firstFileName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IInStream getStream(String filename) throws SevenZipException {
|
||||
File file = resolveVolumeFile(filename);
|
||||
if (file == null || !file.exists() || !file.isFile()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String key = pathKey(file);
|
||||
RandomAccessFile raf = openRafs.get(key);
|
||||
if (raf == null) {
|
||||
raf = new RandomAccessFile(file, "r");
|
||||
openRafs.put(key, raf);
|
||||
}
|
||||
raf.seek(0L);
|
||||
return new RandomAccessFileInStream(raf);
|
||||
} catch (IOException error) {
|
||||
throw new SevenZipException("Volume konnte nicht geoffnet werden: " + filename, error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTotal(Long files, Long bytes) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCompleted(Long files, Long bytes) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String cryptoGetTextPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
private File resolveVolumeFile(String filename) {
|
||||
if (filename == null || filename.trim().length() == 0) {
|
||||
return null;
|
||||
}
|
||||
File direct = new File(filename);
|
||||
if (direct.isAbsolute() && direct.exists()) {
|
||||
return direct;
|
||||
}
|
||||
if (archiveDir != null) {
|
||||
File relative = new File(archiveDir, filename);
|
||||
if (relative.exists()) {
|
||||
return relative;
|
||||
}
|
||||
File[] siblings = archiveDir.listFiles();
|
||||
if (siblings != null) {
|
||||
for (File sibling : siblings) {
|
||||
if (!sibling.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (sibling.getName().equalsIgnoreCase(filename)) {
|
||||
return sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return direct.exists() ? direct : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
for (RandomAccessFile raf : openRafs.values()) {
|
||||
try {
|
||||
raf.close();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
openRafs.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user