diff --git a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java index f45e3bf..b925698 100644 --- a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java +++ b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java @@ -12,9 +12,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.ByteArrayInputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.regex.Pattern; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -25,6 +30,12 @@ import org.apache.commons.compress.archivers.sevenz.SevenZFile; import cz.kamma.kfmanager.model.FileItem; import cz.kamma.kfmanager.model.FtpProfile; import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.io.outputstream.ZipOutputStream; +import net.lingala.zip4j.model.ZipParameters; +import net.lingala.zip4j.model.enums.AesKeyStrength; +import net.lingala.zip4j.model.enums.CompressionLevel; +import net.lingala.zip4j.model.enums.CompressionMethod; +import net.lingala.zip4j.model.enums.EncryptionMethod; /** * Service for file operations - copy, move, delete, etc. @@ -1055,6 +1066,67 @@ public class FileOperations { } } + /** + * Extract ZIP/JAR/WAR by first loading archive bytes to memory and then + * reading entries from ZipInputStream. + */ + public static void extractZipArchiveFromMemory(File archive, File targetDir, ProgressCallback callback) throws IOException { + if (!targetDir.exists() && !targetDir.mkdirs()) { + throw new IOException("Failed to create target directory: " + targetDir); + } + + String name = archive.getName().toLowerCase(); + if (!(name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".war"))) { + throw new IOException("Unsupported archive format for in-memory zip extraction: " + archive.getName()); + } + + // Encrypted archives need zip4j handling with password prompt. + try (ZipFile zipFile = new ZipFile(archive)) { + if (zipFile.isEncrypted()) { + extractZip(archive, targetDir, callback); + return; + } + } + + byte[] zipBytes = Files.readAllBytes(archive.toPath()); + long total = countZipEntries(zipBytes); + long current = 0; + byte[] buffer = new byte[65536]; + + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (callback != null && callback.isCancelled()) { + break; + } + + File targetFile = secureZipTarget(targetDir, entry.getName()); + if (entry.isDirectory()) { + targetFile.mkdirs(); + } else { + File parent = targetFile.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try (OutputStream out = new BufferedOutputStream(new FileOutputStream(targetFile), 65536)) { + int len; + while ((len = zis.read(buffer)) > 0) { + if (callback != null && callback.isCancelled()) { + break; + } + out.write(buffer, 0, len); + } + } + } + + current++; + if (callback != null) { + callback.onProgress(current, total, entry.getName()); + } + } + } + } + private static void extractZip(File archive, File targetDir, ProgressCallback callback) throws IOException { String password = null; try (ZipFile zipFile = new ZipFile(archive)) { @@ -1077,6 +1149,26 @@ public class FileOperations { } } + private static long countZipEntries(byte[] zipBytes) throws IOException { + long total = 0; + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { + while (zis.getNextEntry() != null) { + total++; + } + } + return total; + } + + private static File secureZipTarget(File targetDir, String entryName) throws IOException { + File targetFile = new File(targetDir, entryName); + String targetDirPath = targetDir.getCanonicalPath() + File.separator; + String targetFilePath = targetFile.getCanonicalPath(); + if (!targetFilePath.startsWith(targetDirPath) && !targetFilePath.equals(targetDir.getCanonicalPath())) { + throw new IOException("Blocked unsafe ZIP entry path: " + entryName); + } + return targetFile; + } + private static void extract7z(File archive, File targetDir, ProgressCallback callback) throws IOException { String password = null; SevenZFile szf = null; @@ -1208,34 +1300,73 @@ public class FileOperations { throw new IOException("Source directory for rewrite does not exist"); } - if (targetArchive.exists()) { - targetArchive.delete(); - } + List allEntries = listArchiveEntries(sourceDir); + long total = allEntries.size(); + long current = 0; + byte[] buffer = new byte[65536]; - try (ZipFile zipFile = new ZipFile(targetArchive)) { - if (password != null && !password.isEmpty()) { - zipFile.setPassword(password.toCharArray()); - } + ByteArrayOutputStream archiveBuffer = new ByteArrayOutputStream(); + char[] passwordChars = (password != null && !password.isEmpty()) ? password.toCharArray() : null; + try (ZipOutputStream zipStream = (passwordChars != null) + ? new ZipOutputStream(archiveBuffer, passwordChars) + : new ZipOutputStream(archiveBuffer)) { - net.lingala.zip4j.model.ZipParameters params = new net.lingala.zip4j.model.ZipParameters(); - if (password != null && !password.isEmpty()) { - params.setEncryptFiles(true); - params.setEncryptionMethod(net.lingala.zip4j.model.enums.EncryptionMethod.AES); - params.setAesKeyStrength(net.lingala.zip4j.model.enums.AesKeyStrength.KEY_STRENGTH_256); - } + for (File entry : allEntries) { + if (callback != null && callback.isCancelled()) { + break; + } - File[] children = sourceDir.listFiles(); - if (children != null) { - for (File child : children) { - if (callback != null && callback.isCancelled()) break; - if (child.isDirectory()) { - zipFile.addFolder(child, params); - } else { - zipFile.addFile(child, params); + String relativePath = sourceDir.toPath().relativize(entry.toPath()).toString().replace(File.separatorChar, '/'); + if (relativePath.isEmpty()) { + continue; + } + + ZipParameters params = new ZipParameters(); + params.setCompressionMethod(CompressionMethod.DEFLATE); + params.setCompressionLevel(CompressionLevel.NORMAL); + params.setFileNameInZip(entry.isDirectory() ? relativePath + "/" : relativePath); + + if (passwordChars != null && !entry.isDirectory()) { + params.setEncryptFiles(true); + params.setEncryptionMethod(EncryptionMethod.AES); + params.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256); + } + + zipStream.putNextEntry(params); + if (!entry.isDirectory()) { + try (InputStream in = new BufferedInputStream(new FileInputStream(entry), 65536)) { + int len; + while ((len = in.read(buffer)) > 0) { + if (callback != null && callback.isCancelled()) { + break; + } + zipStream.write(buffer, 0, len); + } } } + zipStream.closeEntry(); + + current++; + if (callback != null) { + callback.onProgress(current, total, relativePath); + } } } + + try (OutputStream out = new BufferedOutputStream(new FileOutputStream(targetArchive), 65536)) { + archiveBuffer.writeTo(out); + } + } + + private static List listArchiveEntries(File sourceDir) throws IOException { + List entries = new ArrayList<>(); + try (Stream stream = Files.walk(sourceDir.toPath())) { + stream + .filter(path -> !path.equals(sourceDir.toPath())) + .sorted(Comparator.comparingInt(path -> path.getNameCount())) + .forEach(path -> entries.add(path.toFile())); + } + return entries; } public enum OverwriteResponse { diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java index 2b8eb87..c78a4ba 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java @@ -38,10 +38,12 @@ import java.util.Map; import java.util.Locale; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermissions; import java.nio.file.attribute.DosFileAttributes; +import java.util.UUID; /** * Single tab in a panel - displays the contents of one directory @@ -4681,10 +4683,11 @@ public class FilePanelTab extends JPanel { Thread extractionThread = new Thread(() -> { try { - Path tempDir = java.nio.file.Files.createTempDirectory("kfmanager-archive-"); + Path tempDir = createArchiveWorkspaceDir(); final String[] usedPassword = new String[1]; - - FileOperations.extractArchive(archive, tempDir.toFile(), new FileOperations.ProgressCallback() { + + String archiveLower = archive.getName().toLowerCase(); + FileOperations.ProgressCallback callback = new FileOperations.ProgressCallback() { @Override public void onProgress(long current, long total, String currentFile) { SwingUtilities.invokeLater(() -> { @@ -4763,7 +4766,13 @@ public class FilePanelTab extends JPanel { public FileOperations.SymlinkResponse confirmSymlink(File file) { return FileOperations.SymlinkResponse.FOLLOW; } - }); + }; + + if (archiveLower.endsWith(".zip") || archiveLower.endsWith(".jar") || archiveLower.endsWith(".war")) { + FileOperations.extractZipArchiveFromMemory(archive, tempDir.toFile(), callback); + } else { + FileOperations.extractArchive(archive, tempDir.toFile(), callback); + } currentArchivePassword = usedPassword[0]; @@ -4785,4 +4794,13 @@ public class FilePanelTab extends JPanel { extractionThread.setDaemon(false); extractionThread.start(); } + + private Path createArchiveWorkspaceDir() throws IOException { + String home = System.getProperty("user.home"); + Path base = Paths.get(home, ".kfmanager", "archive-work"); + Files.createDirectories(base); + Path dir = base.resolve("session-" + UUID.randomUUID()); + Files.createDirectories(dir); + return dir; + } }