Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ repositories {
}

ext {
rlibVersion = "10.0.alpha11"
rlibVersion = "10.0.alpha12"
}

dependencies {
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
rootProject.version = "10.0.alpha11"
rootProject.version = "10.0.alpha12"
group = 'javasabr.rlib'

allprojects {
Expand Down
15 changes: 7 additions & 8 deletions rlib-io/src/main/java/javasabr/rlib/io/util/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -400,36 +400,35 @@ public static String getFirstFreeName(Path directory, Path file) {
*
* @param destination the destination folder.
* @param zipFile the zip file.
*
* @return the count of unpacked files
*/
public static void unzip(Path destination, Path zipFile) {

public static int unzip(Path destination, Path zipFile) {
Comment thread
JavaSaBr marked this conversation as resolved.
if (!Files.exists(destination)) {
Comment thread
JavaSaBr marked this conversation as resolved.
throw new IllegalArgumentException("The folder " + destination + " doesn't exist.");
}

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unzip() only checks Files.exists(destination), but if destination exists and is a file (not a directory) extraction will fail later with an IOException. Consider validating Files.isDirectory(destination) up front and throwing an IllegalArgumentException with a clear message.

Suggested change
}
}
if (!Files.isDirectory(destination)) {
throw new IllegalArgumentException("The path " + destination + " is not a directory.");
}

Copilot uses AI. Check for mistakes.

int count = 0;
try (var zin = new ZipInputStream(Files.newInputStream(zipFile))) {
for (var entry = zin.getNextEntry(); entry != null; entry = zin.getNextEntry()) {

String entryName = entry.getName();
Path targetFile = destination
.resolve(entryName)
.toRealPath(LinkOption.NOFOLLOW_LINKS);

.normalize();
if (!targetFile.startsWith(destination)) {
Comment thread
JavaSaBr marked this conversation as resolved.
Outdated
LOGGER.warning(entryName, "Unexpected entry name:[%s] which is outside"::formatted);
continue;
}

if (entry.isDirectory()) {
Files.createDirectories(targetFile);
} else {
Comment thread
JavaSaBr marked this conversation as resolved.
Files.copy(zin, targetFile, StandardCopyOption.REPLACE_EXISTING);
count++;
Comment on lines +410 to +427

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zip-slip protection based on Path.normalize()+startsWith() does not protect against traversal via pre-existing symlinks inside the destination (e.g., destination contains a symlinked subdir pointing outside). To make this a robust security fix, validate against destination.toRealPath(NOFOLLOW_LINKS) and ensure the resolved target’s real parent path stays within the destination before writing.

Copilot uses AI. Check for mistakes.
}
}

} catch (IOException e) {
throw new UncheckedIOException(e);
}
return count;
}

/**
Expand Down
45 changes: 45 additions & 0 deletions rlib-io/src/test/java/javasabr/rlib/io/FileUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javasabr.rlib.io.util.FileUtils;
import org.assertj.core.api.Assertions;
Comment thread
JavaSaBr marked this conversation as resolved.
Outdated
import org.junit.jupiter.api.Test;

/**
Expand Down Expand Up @@ -74,4 +82,41 @@ void shouldCheckExistingExtension() {
assertThat(FileUtils.hasExtension(path6)).isFalse();
assertThat(FileUtils.hasExtension(path7)).isFalse();
}

@Test
void shouldUnzipFileCorrectly() throws IOException {
// given:
Path zipFile = Files.createTempFile("test-archive", ".zip");

try (var zout = new ZipOutputStream(Files.newOutputStream(zipFile, StandardOpenOption.CREATE))) {
zout.putNextEntry(new ZipEntry("fileA.txt"));
zout.write("test text".getBytes(StandardCharsets.UTF_8));

zout.putNextEntry(new ZipEntry("../fileB.txt"));
zout.write("test text 2".getBytes(StandardCharsets.UTF_8));

ZipEntry dirAEntry = new ZipEntry("dir_a/");
dirAEntry.setMethod(ZipEntry.STORED);
dirAEntry.setSize(0);
dirAEntry.setCrc(0);
zout.putNextEntry(dirAEntry);

zout.putNextEntry(new ZipEntry("dir_a/fileC.txt"));
zout.write("test text 3".getBytes(StandardCharsets.UTF_8));

zout.putNextEntry(new ZipEntry("dir_a/../fileD.txt"));
zout.write("test text 4".getBytes(StandardCharsets.UTF_8));

zout.putNextEntry(new ZipEntry("dir_a/../../../fileE.txt"));
zout.write("test text 5".getBytes(StandardCharsets.UTF_8));
}

Path outputDir = Files.createTempDirectory("test-unzip");

// when:
int unpackedFiles = FileUtils.unzip(outputDir, zipFile);

// then:
assertThat(unpackedFiles).isEqualTo(3);
Comment thread
JavaSaBr marked this conversation as resolved.
}
}
Loading