Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jakarta-mail = "2.1.3"
# https://mvnrepository.com/artifact/org.eclipse.angus/angus-mail
angus-mail = "2.0.4"
# https://mvnrepository.com/artifact/org.testcontainers/testcontainers
testcontainers = "1.21.3"
testcontainers = "2.0.3"
# https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine
junit-jupiter = "6.0.1"
# https://mvnrepository.com/artifact/org.projectlombok/lombok
Expand Down Expand Up @@ -39,4 +39,4 @@ mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj-core" }

[bundles]
mail = ["jakarta-mail-api", "angus-mail"]
mail = ["jakarta-mail-api", "angus-mail"]

This file was deleted.

This file was deleted.

19 changes: 10 additions & 9 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,37 @@ 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) {
if (!Files.exists(destination)) {
throw new IllegalArgumentException("The folder " + destination + " doesn't exist.");
}
Copy link

Copilot AI Feb 6, 2026

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.

Path normalizedDestination = destination.normalize();
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);

if (!targetFile.startsWith(destination)) {
.normalize();
if (!targetFile.startsWith(normalizedDestination)) {
LOGGER.warning(entryName, "Unexpected entry name:[%s] which is outside"::formatted);
continue;
}

if (entry.isDirectory()) {
Files.createDirectories(targetFile);
} else {
Files.createDirectories(targetFile.getParent());
Files.copy(zin, targetFile, StandardCopyOption.REPLACE_EXISTING);
count++;
Comment on lines +410 to +427
Copy link

Copilot AI Feb 6, 2026

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
63 changes: 63 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,6 +2,13 @@

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.junit.jupiter.api.Test;

Expand Down Expand Up @@ -74,4 +81,60 @@ 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 tempDirectory = Files.createTempDirectory("test-unzip");
Path outputDir = tempDirectory
.resolve("output")
.resolve("folder");

Files.createDirectories(outputDir);

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

// then:
assertThat(unpackedFiles).isEqualTo(3);
assertThat(outputDir
.resolve("fileA.txt"))
.exists();
assertThat(outputDir
.resolve("dir_a")
.resolve("fileC.txt"))
.exists();
Comment on lines +124 to +131
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

This test expects 3 unpacked files, which implies the entry "dir_a/../fileD.txt" is accepted and extracted, but it never asserts that fileD actually exists. Adding an assertion for outputDir.resolve("fileD.txt") (and/or ensuring the directory entry behavior) would make the test fully validate the intended unzip behavior.

Copilot uses AI. Check for mistakes.
assertThat(tempDirectory
.resolve("output")
.resolve("fileB.txt"))
.doesNotExist();
assertThat(tempDirectory
.resolve("fileE.txt"))
.doesNotExist();
}
}
Loading