zip rewritten to memory

This commit is contained in:
Radek Davidek 2026-04-29 19:30:39 +02:00
parent d51b53bb4b
commit 07434a9fd6
2 changed files with 174 additions and 25 deletions

View File

@ -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,35 +1300,74 @@ public class FileOperations {
throw new IOException("Source directory for rewrite does not exist");
}
if (targetArchive.exists()) {
targetArchive.delete();
List<File> allEntries = listArchiveEntries(sourceDir);
long total = allEntries.size();
long current = 0;
byte[] buffer = new byte[65536];
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)) {
for (File entry : allEntries) {
if (callback != null && callback.isCancelled()) {
break;
}
try (ZipFile zipFile = new ZipFile(targetArchive)) {
if (password != null && !password.isEmpty()) {
zipFile.setPassword(password.toCharArray());
String relativePath = sourceDir.toPath().relativize(entry.toPath()).toString().replace(File.separatorChar, '/');
if (relativePath.isEmpty()) {
continue;
}
net.lingala.zip4j.model.ZipParameters params = new net.lingala.zip4j.model.ZipParameters();
if (password != null && !password.isEmpty()) {
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(net.lingala.zip4j.model.enums.EncryptionMethod.AES);
params.setAesKeyStrength(net.lingala.zip4j.model.enums.AesKeyStrength.KEY_STRENGTH_256);
params.setEncryptionMethod(EncryptionMethod.AES);
params.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256);
}
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);
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<File> listArchiveEntries(File sourceDir) throws IOException {
List<File> entries = new ArrayList<>();
try (Stream<Path> 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 {
YES, YES_TO_ALL, NO, NO_TO_ALL, CANCEL

View File

@ -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;
}
}