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];