diff --git a/pom.xml b/pom.xml
index 0938060..895e57a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -91,11 +91,21 @@
commons-compress
1.21
+
+ org.tukaani
+ xz
+ 1.9
+
com.github.junrar
junrar
7.4.1
+
+ net.lingala.zip4j
+ zip4j
+ 2.11.5
+
com.formdev
flatlaf
diff --git a/src/main/java/cz/kamma/kfmanager/MainApp.java b/src/main/java/cz/kamma/kfmanager/MainApp.java
index 5fc4209..07252b4 100644
--- a/src/main/java/cz/kamma/kfmanager/MainApp.java
+++ b/src/main/java/cz/kamma/kfmanager/MainApp.java
@@ -15,7 +15,7 @@ import java.io.InputStreamReader;
*/
public class MainApp {
- public static final String APP_VERSION = "1.1.1";
+ public static final String APP_VERSION = "1.1.2";
public enum OS {
WINDOWS, LINUX, MACOS, UNKNOWN
diff --git a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java
index b5bb876..67c1d43 100644
--- a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java
+++ b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java
@@ -13,6 +13,9 @@ import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.*;
+import net.lingala.zip4j.ZipFile;
+import net.lingala.zip4j.model.FileHeader;
+
/**
* Service for file operations - copy, move, delete, etc.
*/
@@ -1106,49 +1109,77 @@ public class FileOperations {
}
private static void extractSevenZ(File archive, File targetDir, ProgressCallback callback) throws IOException {
- try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) {
- org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
- long current = 0;
- while ((entry = sevenZFile.getNextEntry()) != null) {
- current++;
- if (callback != null) callback.onProgress(current, -1, entry.getName());
-
- File newFile = new File(targetDir, entry.getName());
- if (entry.isDirectory()) {
- newFile.mkdirs();
- } else {
- newFile.getParentFile().mkdirs();
- try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
- byte[] buffer = new byte[8192];
- int bytesRead;
- while ((bytesRead = sevenZFile.read(buffer)) != -1) {
- os.write(buffer, 0, bytesRead);
+ char[] password = null;
+ while (true) {
+ try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile =
+ (password == null) ? new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive) :
+ new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive, password)) {
+ org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
+ long current = 0;
+ while ((entry = sevenZFile.getNextEntry()) != null) {
+ current++;
+ if (callback != null) callback.onProgress(current, -1, entry.getName());
+
+ File newFile = new File(targetDir, entry.getName());
+ if (entry.isDirectory()) {
+ newFile.mkdirs();
+ } else {
+ newFile.getParentFile().mkdirs();
+ try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = sevenZFile.read(buffer)) != -1) {
+ os.write(buffer, 0, bytesRead);
+ }
}
}
}
+ break;
+ } catch (IOException e) {
+ if (e.getMessage() != null && (e.getMessage().toLowerCase().contains("password") || e.getMessage().toLowerCase().contains("encrypted"))) {
+ String pass = (callback != null) ? callback.requestPassword(archive.getName()) : null;
+ if (pass == null) throw new IOException("Password required for " + archive.getName());
+ password = pass.toCharArray();
+ } else {
+ throw e;
+ }
}
}
}
private static void extractRar(File archiveFile, File targetDir, ProgressCallback callback) throws Exception {
- try (com.github.junrar.Archive archive = new com.github.junrar.Archive(archiveFile)) {
- com.github.junrar.rarfile.FileHeader fh = archive.nextFileHeader();
- long current = 0;
- while (fh != null) {
- current++;
- String entryName = fh.getFileName().replace('\\', '/');
- if (callback != null) callback.onProgress(current, -1, entryName);
-
- File newFile = new File(targetDir, entryName);
- if (fh.isDirectory()) {
- newFile.mkdirs();
- } else {
- newFile.getParentFile().mkdirs();
- try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
- archive.extractFile(fh, os);
- }
+ String password = null;
+ while (true) {
+ try (com.github.junrar.Archive archive = (password == null) ? new com.github.junrar.Archive(archiveFile) : new com.github.junrar.Archive(archiveFile, password)) {
+ if (archive.isEncrypted() && password == null) {
+ password = (callback != null) ? callback.requestPassword(archiveFile.getName()) : null;
+ if (password == null) throw new IOException("Password required");
+ continue;
}
- fh = archive.nextFileHeader();
+ com.github.junrar.rarfile.FileHeader fh = archive.nextFileHeader();
+ long current = 0;
+ while (fh != null) {
+ if (fh.isEncrypted() && password == null) {
+ password = (callback != null) ? callback.requestPassword(archiveFile.getName()) : null;
+ if (password == null) throw new IOException("Password required");
+ break; // exit inner loop to reopen with password
+ }
+ current++;
+ String entryName = fh.getFileName().replace('\\', '/');
+ if (callback != null) callback.onProgress(current, -1, entryName);
+
+ File newFile = new File(targetDir, entryName);
+ if (fh.isDirectory()) {
+ newFile.mkdirs();
+ } else {
+ newFile.getParentFile().mkdirs();
+ try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
+ archive.extractFile(fh, os);
+ }
+ }
+ fh = archive.nextFileHeader();
+ }
+ if (fh == null) break; // Finished successfully
}
}
}
@@ -1161,39 +1192,71 @@ public class FileOperations {
Files.createDirectories(targetDirectory.toPath());
}
- try (org.apache.commons.compress.archivers.zip.ZipFile zf = new org.apache.commons.compress.archivers.zip.ZipFile(zipFile)) {
- java.util.List entries = java.util.Collections.list(zf.getEntries());
+ ZipFile zf = null;
+ String password = null;
+
+ try {
+ zf = new ZipFile(zipFile);
+
+ // Check if archive is encrypted and request password
+ if (zf.isEncrypted()) {
+ password = (callback != null) ? callback.requestPassword(zipFile.getName()) : null;
+ if (password == null) {
+ throw new IOException("Password required for " + zipFile.getName());
+ }
+ zf.setPassword(password.toCharArray());
+ }
+
+ List entries = zf.getFileHeaders();
long totalItems = entries.size();
long currentItem = 0;
- for (org.apache.commons.compress.archivers.zip.ZipArchiveEntry entry : entries) {
+ for (FileHeader entry : entries) {
currentItem++;
- File newFile = new File(targetDirectory, entry.getName());
-
+ File newFile = new File(targetDirectory, entry.getFileName());
+
if (callback != null) {
- callback.onProgress(currentItem, totalItems, entry.getName());
+ callback.onProgress(currentItem, totalItems, entry.getFileName());
+ if (callback.isCancelled()) break;
}
- if (entry.isDirectory()) {
- if (!newFile.isDirectory() && !newFile.mkdirs()) {
- throw new IOException("Failed to create directory " + newFile);
+ try {
+ if (entry.isDirectory()) {
+ if (!newFile.isDirectory() && !newFile.mkdirs()) {
+ throw new IOException("Failed to create directory " + newFile);
+ }
+ } else {
+ File parent = newFile.getParentFile();
+ if (parent != null && !parent.exists()) {
+ parent.mkdirs();
+ }
+ zf.extractFile(entry, targetDirectory.getAbsolutePath());
}
- setPermissionsFromMode(newFile.toPath(), entry.getUnixMode());
- } else {
- // create parent directories if they don't exist
- File parent = newFile.getParentFile();
- if (parent != null && !parent.exists()) {
- parent.mkdirs();
+ } catch (Exception e) {
+ // Check if this is a password-related error on an encrypted file
+ if (entry.isEncrypted() && e.getMessage() != null &&
+ (e.getMessage().toLowerCase().contains("password") ||
+ e.getMessage().toLowerCase().contains("decrypt") ||
+ e.getMessage().toLowerCase().contains("checksum"))) {
+ throw new IOException("Incorrect password for encrypted archive", e);
}
-
- if (newFile.exists() && newFile.isDirectory()) {
- deleteDirectoryInternal(newFile.toPath());
- }
-
- try (InputStream is = zf.getInputStream(entry)) {
- Files.copy(is, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
- }
- setPermissionsFromMode(newFile.toPath(), entry.getUnixMode());
+ // For other errors, skip the file and continue
+ System.err.println("Warning: Failed to extract " + entry.getFileName() + ": " + e.getMessage());
+ }
+ }
+ } catch (Exception e) {
+ if (e instanceof IOException && e.getMessage() != null &&
+ (e.getMessage().contains("Password required") ||
+ e.getMessage().contains("Incorrect password"))) {
+ throw (IOException) e;
+ }
+ throw new IOException("Failed to unzip: " + e.getMessage(), e);
+ } finally {
+ if (zf != null) {
+ try {
+ zf.close();
+ } catch (Exception e) {
+ // Ignore errors during close
}
}
}
@@ -1223,6 +1286,7 @@ public class FileOperations {
default OverwriteResponse confirmOverwrite(File source, File destination) { return OverwriteResponse.YES; }
default SymlinkResponse confirmSymlink(File symlink) { return SymlinkResponse.IGNORE; }
default ErrorResponse onError(File file, Exception e) { return ErrorResponse.ABORT; }
+ default String requestPassword(String archiveName) { return null; }
}
/**
diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java
index 4220577..e83d0cf 100644
--- a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java
+++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java
@@ -1310,7 +1310,35 @@ public class FilePanelTab extends JPanel {
if (archive == null || !archive.isFile()) return null;
try {
Path tempDir = Files.createTempDirectory("kfmanager-archive-");
- FileOperations.extractArchive(archive, tempDir.toFile(), null);
+ FileOperations.extractArchive(archive, tempDir.toFile(), new FileOperations.ProgressCallback() {
+ @Override
+ public void onProgress(long current, long total, String currentFile) {}
+
+ @Override
+ public String requestPassword(String archiveName) {
+ final String[] result = new String[1];
+ try {
+ if (SwingUtilities.isEventDispatchThread()) {
+ JPasswordField pf = new JPasswordField();
+ int ok = JOptionPane.showConfirmDialog(FilePanelTab.this, pf, "Enter password for " + archiveName, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
+ if (ok == JOptionPane.OK_OPTION) {
+ result[0] = new String(pf.getPassword());
+ }
+ } else {
+ SwingUtilities.invokeAndWait(() -> {
+ JPasswordField pf = new JPasswordField();
+ int ok = JOptionPane.showConfirmDialog(FilePanelTab.this, pf, "Enter password for " + archiveName, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
+ if (ok == JOptionPane.OK_OPTION) {
+ result[0] = new String(pf.getPassword());
+ }
+ });
+ }
+ } catch (Exception e) {
+ result[0] = null;
+ }
+ return result[0];
+ }
+ });
return tempDir;
} catch (Exception ex) {
// extraction failed; attempt best-effort cleanup
diff --git a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java
index fe66c1e..2cf131a 100644
--- a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java
+++ b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java
@@ -2698,6 +2698,25 @@ public class MainWindow extends JFrame {
return result[0];
}
+ @Override
+ public String requestPassword(String archiveName) {
+ final String[] result = new String[1];
+ try {
+ SwingUtilities.invokeAndWait(() -> {
+ JPasswordField pf = new JPasswordField();
+ int ok = JOptionPane.showConfirmDialog(progressDialog, pf, "Enter password for " + archiveName, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
+ if (ok == JOptionPane.OK_OPTION) {
+ result[0] = new String(pf.getPassword());
+ } else {
+ result[0] = null;
+ }
+ });
+ } catch (Exception e) {
+ result[0] = null;
+ }
+ return result[0];
+ }
+
@Override
public FileOperations.SymlinkResponse confirmSymlink(File symlink) {
final FileOperations.SymlinkResponse[] result = new FileOperations.SymlinkResponse[1];