From 77a448c35d6dc3c39ef123e3b15a4f3390eee5c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:38:31 +0000 Subject: [PATCH 01/25] chore(deps): bump jacksonVersion from 2.22.0 to 2.22.1 Bumps `jacksonVersion` from 2.22.0 to 2.22.1. Updates `com.fasterxml.jackson.core:jackson-databind` from 2.22.0 to 2.22.1 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.22.0 to 2.22.1 --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.22.1 dependency-type: direct:production - dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency-version: 2.22.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f3faa49a..39911df6 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ repositories { } ext { - jacksonVersion = '2.22.0' + jacksonVersion = '2.22.1' } dependencies { From eb44111fd34f69ce9b165bf648b58c193c481608 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:03:57 +0000 Subject: [PATCH 02/25] chore(deps): update actions/setup-java action to v5.6.0 --- .github/workflows/gradle.yml | 4 ++-- .github/workflows/publishRelease.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b8474831..fa69d219 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up openJDK version - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: java-version: ${{ matrix.jdk }} distribution: "zulu" @@ -49,7 +49,7 @@ jobs: - name: Checkout repo uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up OpenJDK version ... - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: "zulu" java-version: ${{ env.jdkversion }} diff --git a/.github/workflows/publishRelease.yml b/.github/workflows/publishRelease.yml index 1508010f..dd2e1b93 100644 --- a/.github/workflows/publishRelease.yml +++ b/.github/workflows/publishRelease.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up Java - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: java-version: 21 distribution: 'zulu' # openjdk From 94cb5027eae53f42c500fcdf00ea08f5180347a8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:43:43 +0000 Subject: [PATCH 03/25] fix(deps): update dependency org.junit:junit-bom to v6.1.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f3faa49a..b844e5b1 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ ext { dependencies { // JUnit setup for testing - testImplementation(platform("org.junit:junit-bom:6.1.0")) + testImplementation(platform("org.junit:junit-bom:6.1.2")) testImplementation('org.junit.jupiter:junit-jupiter') testRuntimeOnly('org.junit.platform:junit-platform-launcher') // JSON object mapping / (de-)serialization From 20052d30373e9aaf9b10dfcaa0b31e6355a99165 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 22 Jul 2026 17:21:19 +0200 Subject: [PATCH 04/25] test: write tests to detect missing paths when reading a crate --- .../ro_crate/crate/ReadAndWriteTest.java | 183 ++++++++++++++++-- 1 file changed, 167 insertions(+), 16 deletions(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java index ca742ded..80848c87 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java @@ -1,42 +1,60 @@ package edu.kit.datamanager.ro_crate.crate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import edu.kit.datamanager.ro_crate.Crate; import edu.kit.datamanager.ro_crate.HelpFunctions; import edu.kit.datamanager.ro_crate.RoCrate; +import edu.kit.datamanager.ro_crate.entities.data.DataEntity; +import edu.kit.datamanager.ro_crate.entities.data.FileEntity; import edu.kit.datamanager.ro_crate.preview.StaticPreview; import edu.kit.datamanager.ro_crate.reader.CrateReader; import edu.kit.datamanager.ro_crate.reader.Readers; - +import edu.kit.datamanager.ro_crate.special.IdentifierUtils; +import edu.kit.datamanager.ro_crate.writer.CrateWriter; import edu.kit.datamanager.ro_crate.writer.Writers; -import org.apache.commons.io.FileUtils; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - import java.io.IOException; import java.nio.charset.Charset; +import java.nio.file.Files; import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.assertEquals; - +import java.util.List; +import java.util.UUID; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class ReadAndWriteTest { @Test void testReadingAndWriting(@TempDir Path path) throws IOException { Path htmlFile = path.resolve("htmlFile.html"); - FileUtils.writeStringToFile(htmlFile.toFile(), "useful file", Charset.defaultCharset()); + FileUtils.writeStringToFile( + htmlFile.toFile(), + "useful file", + Charset.defaultCharset() + ); Path htmlDir = path.resolve("dir"); Path fileInDir = htmlDir.resolve("file.html"); - FileUtils.writeStringToFile(fileInDir.toFile(), "fileN2", Charset.defaultCharset()); + FileUtils.writeStringToFile( + fileInDir.toFile(), + "fileN2", + Charset.defaultCharset() + ); - RoCrate crate = new RoCrate.RoCrateBuilder("name", "description", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/") - .setPreview(new StaticPreview(htmlFile.toFile(), htmlDir.toFile())) - .build(); + RoCrate crate = new RoCrate.RoCrateBuilder( + "name", + "description", + "2024", + "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + ) + .setPreview(new StaticPreview(htmlFile.toFile(), htmlDir.toFile())) + .build(); Path writeDir = path.resolve("crate"); - Writers.newFolderWriter() - .save(crate, writeDir.toAbsolutePath().toString()); + Writers.newFolderWriter().save(crate, writeDir.toAbsolutePath().toString()); CrateReader reader = Readers.newFolderReader(); Crate newCrate = reader.readCrate(writeDir.toAbsolutePath().toString()); @@ -51,8 +69,141 @@ void testReadingAndWriting(@TempDir Path path) throws IOException { @Test void testReadCrateWithHasPartHierarchy() throws IOException { CrateReader reader = Readers.newFolderReader(); - RoCrate crate = reader.readCrate(ReadAndWriteTest.class.getResource("/crates/hasPartHierarchy").getPath()); + RoCrate crate = reader.readCrate( + ReadAndWriteTest.class.getResource("/crates/hasPartHierarchy").getPath() + ); assertEquals(1, crate.getAllContextualEntities().size()); assertEquals(6, crate.getAllDataEntities().size()); } + + @Test + void testEncodedIdsFindTheirPaths(@TempDir Path tempDir) throws IOException { + RoCrate.RoCrateBuilder builder = new RoCrate.RoCrateBuilder(); + { + FileEntity.FileEntityBuilder dataEntityBuilder = + new FileEntity.FileEntityBuilder(); + dataEntityBuilder.setId("id 1"); + dataEntityBuilder.addTypes(List.of("File")); + UUID uuid = UUID.randomUUID(); + Path path = tempDir.resolve(uuid.toString()); + Files.writeString(path, "File"); + dataEntityBuilder.setLocation(path); + + builder.addDataEntity(dataEntityBuilder.build()); + } + { + FileEntity.FileEntityBuilder dataEntityBuilder = + new FileEntity.FileEntityBuilder(); + dataEntityBuilder.setId("id\uD83E\uDD791"); + dataEntityBuilder.addTypes(List.of("File")); + UUID uuid = UUID.randomUUID(); + Path path = tempDir.resolve(uuid.toString()); + Files.writeString(path, "File"); + dataEntityBuilder.setLocation(path); + + builder.addDataEntity(dataEntityBuilder.build()); + } + { + FileEntity.FileEntityBuilder dataEntityBuilder = + new FileEntity.FileEntityBuilder(); + dataEntityBuilder.setId("id|1"); + dataEntityBuilder.addTypes(List.of("File")); + UUID uuid = UUID.randomUUID(); + Path path = tempDir.resolve(uuid.toString()); + Files.writeString(path, "File"); + dataEntityBuilder.setLocation(path); + + builder.addDataEntity(dataEntityBuilder.build()); + } + Path location = tempDir.resolve("out-crate-simple-file"); + { + RoCrate crate = builder.build(); + CrateWriter writer = Writers.newFolderWriter(); + writer.save(crate, location.toAbsolutePath().toString()); + } + { + CrateReader roCrateReader = Readers.newFolderReader(); + + RoCrate roCrate = roCrateReader.readCrate( + location.toAbsolutePath().toString() + ); + for (DataEntity dataEntity : roCrate.getAllDataEntities()) { + System.out.println(dataEntity.getId() + ": " + dataEntity.getPath()); + } + for (DataEntity dataEntity : roCrate.getAllDataEntities()) { + assertNotNull( + dataEntity.getPath(), + "Path of ID: " + dataEntity.getId() + ); + } + } + } + + /** + * Test we detect files which use the encoded IDs as filename, + * as well as ones which use the decoded filename. + */ + @Test + void testDetectingEncodedFileNames(@TempDir Path tempDir) throws IOException { + // This is how we add the id. But the space will be encoded + String id = "id 42"; + // This is how we get it out (the encoded id as it will exist in the crate) + String idEncoded = IdentifierUtils.encode(id).orElseThrow(); + + RoCrate.RoCrateBuilder builder = new RoCrate.RoCrateBuilder(); + + { + // Add file entity without a file with id + FileEntity.FileEntityBuilder fileEntityBuilder = + new FileEntity.FileEntityBuilder(); + fileEntityBuilder.setId(id); + fileEntityBuilder.addTypes(List.of("File")); + + builder.addDataEntity(fileEntityBuilder.build()); + } + + Path cratepath1 = tempDir.resolve("test1"); + { + Writers.newFolderWriter().save(builder.build(), cratepath1.toString()); + // add file manually (decoded id) + Path filepath = cratepath1.resolve(id); + Files.writeString(filepath, "File"); + } + + Path cratepath2 = tempDir.resolve("test2"); + { + Writers.newFolderWriter().save(builder.build(), cratepath2.toString()); + // add file manually (encoded id) + Path filepath = cratepath2.resolve(idEncoded); + Files.writeString(filepath, "File"); + } + + { + RoCrate crate = Readers.newFolderReader().readCrate( + cratepath1.toString() + ); + + DataEntity entity = crate.getDataEntityById(idEncoded); + assertEquals(idEncoded, entity.getId()); + + Path filepath = entity.getPath(); + assertNotNull(filepath); + assertTrue(Files.exists(filepath)); + assertEquals(id, filepath.getFileName().toString()); + } + + { + RoCrate crate = Readers.newFolderReader().readCrate( + cratepath2.toString() + ); + + DataEntity entity = crate.getDataEntityById(idEncoded); + assertEquals(idEncoded, entity.getId()); + + Path filepath = entity.getPath(); + assertNotNull(filepath); + assertTrue(Files.exists(filepath)); + assertEquals(idEncoded, filepath.getFileName().toString()); + } + } } From 2a1f0e1c1eeecb0cc03f606dc2dc25bb9678089e Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 22 Jul 2026 17:49:27 +0200 Subject: [PATCH 05/25] docs: add javadoc to DataEntity.getPath --- .../datamanager/ro_crate/entities/data/DataEntity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java index 864d3044..829ee9c2 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java @@ -48,6 +48,16 @@ public void addAuthorId(String id) { this.addIdProperty("author", id); } + /** + * Gets the path of this entity. + *

+ * When reading a crate, this will point to the entity's file within the crate. + * When creating a new entity or crate, it might point to a file outside the crate, + * as set by {@link DataEntityBuilder#setLocation(Path)}. + * Such a file will be copied into the crate when writing. + * + * @return the path to the file this entity represents. + */ @JsonIgnore public Path getPath() { return path; From ffe5b910f84ca23179e77a73b9237245469a4e2e Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 22 Jul 2026 17:51:33 +0200 Subject: [PATCH 06/25] fix: writing files uses decoded ID of an entity --- .../ro_crate/writer/WriteFolderStrategy.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java index 8a4844ed..a694c0ca 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java @@ -5,6 +5,7 @@ import edu.kit.datamanager.ro_crate.Crate; import edu.kit.datamanager.ro_crate.entities.data.DataEntity; import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper; +import edu.kit.datamanager.ro_crate.special.IdentifierUtils; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,16 +65,18 @@ public void save(Crate crate, String destination) throws IOException { } } for (DataEntity dataEntity : crate.getAllDataEntities()) { - savetoFile(dataEntity, file); + saveToFile(dataEntity, file); } } - private void savetoFile(DataEntity entity, File file) throws IOException { + private void saveToFile(DataEntity entity, File file) throws IOException { if (entity.getPath() != null) { + String id = entity.getId(); + String filename = IdentifierUtils.decode(id).orElse(id); if (entity.getPath().toFile().isDirectory()) { - FileUtils.copyDirectory(entity.getPath().toFile(), file.toPath().resolve(entity.getId()).toFile()); + FileUtils.copyDirectory(entity.getPath().toFile(), file.toPath().resolve(filename).toFile()); } else { - FileUtils.copyFile(entity.getPath().toFile(), file.toPath().resolve(entity.getId()).toFile()); + FileUtils.copyFile(entity.getPath().toFile(), file.toPath().resolve(filename).toFile()); } } } From ee2a7f65e132459ad6ec3cece7764c6508aad37e Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 22 Jul 2026 17:57:05 +0200 Subject: [PATCH 07/25] fix: detect files matching the decoded or encoded id of an entity --- .../ro_crate/reader/CrateReader.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java index 15cae146..72d30e79 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java @@ -22,6 +22,7 @@ import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; /** @@ -142,6 +143,7 @@ private RoCrate rebuildCrate(ObjectNode metadataJson, File files, HashSet { + System.out.println("Found file for: " + file.toString()); usedFiles.add(file.getPath()); builder.setLocationWithExceptions(file.toPath()) .setId(file.getName()); @@ -237,15 +239,24 @@ protected String unpackId(JsonNode node) { } protected Optional checkFolderHasFile(String filepathOrId, File folder) { + System.out.println("DEBUG: " + Arrays.toString(folder.listFiles())); if (IdentifierUtils.isUrl(filepathOrId)) { return Optional.empty(); } - return IdentifierUtils.decode(filepathOrId) - .map(decoded -> folder.toPath().resolve(decoded).normalize()) + System.out.println( "DEBUG: " + + IdentifierUtils.decode(filepathOrId) + .map(decoded -> folder.toPath().resolve(decoded).normalize()) + .filter(resolved -> resolved.startsWith(folder.toPath())) + .map(Path::toFile) + .map(File::exists) + ); + return Stream.of(IdentifierUtils.decode(filepathOrId).orElse(filepathOrId), filepathOrId) + .map(filename -> folder.toPath().resolve(filename).normalize().toAbsolutePath()) // defence-in-depth: ensure we are still inside the crate folder .filter(resolved -> resolved.startsWith(folder.toPath())) .map(Path::toFile) - .filter(File::exists); + .filter(File::exists) + .findFirst(); } /** From a0564bb721d15a1220d9d0240a58e796e7fd6bd8 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 22 Jul 2026 18:21:21 +0200 Subject: [PATCH 08/25] fix(test): avoid invalid paths on windows in a test --- .../kit/datamanager/ro_crate/crate/ReadAndWriteTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java index 80848c87..ad41b1e2 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java @@ -115,17 +115,17 @@ void testEncodedIdsFindTheirPaths(@TempDir Path tempDir) throws IOException { builder.addDataEntity(dataEntityBuilder.build()); } - Path location = tempDir.resolve("out-crate-simple-file"); + Path location = tempDir.resolve("out"); { RoCrate crate = builder.build(); CrateWriter writer = Writers.newFolderWriter(); - writer.save(crate, location.toAbsolutePath().toString()); + writer.save(crate, location.toString()); } { CrateReader roCrateReader = Readers.newFolderReader(); RoCrate roCrate = roCrateReader.readCrate( - location.toAbsolutePath().toString() + location.toString() ); for (DataEntity dataEntity : roCrate.getAllDataEntities()) { System.out.println(dataEntity.getId() + ": " + dataEntity.getPath()); From 14d9f103d10826a4a6ad4c9df2067663a85b4d68 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 22 Jul 2026 18:39:16 +0200 Subject: [PATCH 09/25] test: loading and saving "heals" filename in crates --- .../ro_crate/crate/ReadAndWriteTest.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java index ad41b1e2..7688e09f 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java @@ -1,6 +1,7 @@ package edu.kit.datamanager.ro_crate.crate; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -206,4 +207,78 @@ void testDetectingEncodedFileNames(@TempDir Path tempDir) throws IOException { assertEquals(idEncoded, filepath.getFileName().toString()); } } + + @Test + void testFilenamesAreSelfHealing(@TempDir Path tempDir) throws IOException { + // This is how we add the id. But the space will be encoded + String id = "id 42"; + // This is how we get it out (the encoded id as it will exist in the crate) + String idEncoded = IdentifierUtils.encode(id).orElseThrow(); + + RoCrate.RoCrateBuilder builder = new RoCrate.RoCrateBuilder(); + + // add dummy file + Path filepath_outside = tempDir.resolve("someFile.txt"); + Files.writeString(filepath_outside, "File"); + + { + // Add file entity without a file with id + FileEntity.FileEntityBuilder fileEntityBuilder = + new FileEntity.FileEntityBuilder(); + fileEntityBuilder.setId(id); + fileEntityBuilder.addTypes(List.of("File")); + fileEntityBuilder.setLocation(filepath_outside); + + builder.addDataEntity(fileEntityBuilder.build()); + } + + Path cratepath = tempDir.resolve("test1"); + { + Writers.newFolderWriter().save(builder.build(), cratepath.toString()); + Path currentFilePath = cratepath.resolve(id); + assertTrue(currentFilePath.toFile().exists()); + // swap file names + Path newFilePath = cratepath.resolve(idEncoded); + Files.move(currentFilePath, newFilePath); + assertFalse(currentFilePath.toFile().exists()); + assertTrue(newFilePath.toFile().exists()); + } + + { + RoCrate crate = Readers.newFolderReader().readCrate( + cratepath.toString() + ); + + DataEntity entity = crate.getDataEntityById(idEncoded); + assertEquals(idEncoded, entity.getId()); + + // Even if a file's name is encoded, the path will work as expected + Path filepath = entity.getPath(); + assertNotNull(filepath); + assertTrue(Files.exists(filepath)); + assertEquals(idEncoded, filepath.getFileName().toString()); + } + + // When saving the crate again, the file will be renamed to the decoded id + Path cratepath2 = tempDir.resolve("test2"); + { + Writers.newFolderWriter().save(builder.build(), cratepath2.toString()); + } + + { + RoCrate crate = Readers.newFolderReader().readCrate( + cratepath2.toString() + ); + + DataEntity entity = crate.getDataEntityById(idEncoded); + assertEquals(idEncoded, entity.getId()); + + // The filename is now decoded to the original id + Path filepath = entity.getPath(); + assertNotNull(filepath); + assertTrue(Files.exists(filepath)); + assertEquals(id, filepath.getFileName().toString()); + } + + } } From 3f51510e11dc9db0438802ea1659f0f1bba1fe48 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 22 Jul 2026 23:35:00 +0200 Subject: [PATCH 10/25] test: all writers write decoded ids as filenames --- .../ro_crate/writer/CommonWriterTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java index 0eae96c2..3f67f4f2 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java @@ -4,6 +4,7 @@ import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity; +import edu.kit.datamanager.ro_crate.entities.data.FileEntity; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -34,6 +35,7 @@ default void testFilesBeingAdjusted(@TempDir Path tempDir) throws IOException { Path writtenCrate = tempDir.resolve("written-crate"); Path extractionPath = tempDir.resolve("checkMe"); + String id = "id will be encoded"; { RoCrate builtCrate = getCrateWithFileAndDir(pathToFile, pathToDir) .addDataEntity(new DataSetEntity.DataSetBuilder() @@ -43,6 +45,11 @@ default void testFilesBeingAdjusted(@TempDir Path tempDir) throws IOException { .setId("lots_of_little_files/subdir-renamed/") .build() ) + .addDataEntity(new FileEntity.FileEntityBuilder() + .setId(id) + .setLocation(pathToFile) + .build() + ) .build(); this.saveCrate(builtCrate, writtenCrate); ensureCrateIsExtractedIn(writtenCrate, extractionPath); @@ -51,6 +58,12 @@ default void testFilesBeingAdjusted(@TempDir Path tempDir) throws IOException { HelpFunctions.printFileTree(correctCrate); HelpFunctions.printFileTree(extractionPath); + // Ensure the file uses the id, not the encoded id as a file name + assertTrue( + Files.exists(extractionPath.resolve(id)), + "The file '%s' should exist, because this is the ID of the entity".formatted(id) + ); + // The actual file name should **not** appear in the crate String fileName = pathToFile.getFileName().toString(); assertFalse( From 43fe41f99a01c7a60ee08ea50115e613d7b73b84 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 22 Jul 2026 23:36:50 +0200 Subject: [PATCH 11/25] fix: zip stream writer writes decoded filenames --- .../ro_crate/writer/WriteZipStreamStrategy.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java index 33572db2..5216ccb5 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java @@ -15,6 +15,7 @@ import java.util.UUID; import edu.kit.datamanager.ro_crate.preview.CratePreview; +import edu.kit.datamanager.ro_crate.special.IdentifierUtils; import edu.kit.datamanager.ro_crate.util.FileSystemUtil; import edu.kit.datamanager.ro_crate.util.ZipStreamUtil; import net.lingala.zip4j.io.outputstream.ZipOutputStream; @@ -154,16 +155,18 @@ private void saveToStream(DataEntity entity, ZipOutputStream zipStream, String p } boolean isDirectory = entity.getPath().toFile().isDirectory(); + String id = entity.getId(); + String filename = IdentifierUtils.decode(id).orElse(id); if (isDirectory) { ZipStreamUtil.addFolderToZipStream( zipStream, entity.getPath().toFile(), - prefix + entity.getId()); + prefix + filename); } else { ZipStreamUtil.addFileToZipStream( zipStream, entity.getPath().toFile(), - prefix + entity.getId()); + prefix + filename); } } } From 3898149c0cf171196ad3c53a5d0c20579c29bf85 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:13:12 +0000 Subject: [PATCH 12/25] chore(deps): update actions/checkout action to v7.0.1 --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/gradle.yml | 4 ++-- .github/workflows/publishRelease.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3a28ed5c..174f510d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b8474831..7c21e3f1 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -22,7 +22,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up openJDK version uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: @@ -47,7 +47,7 @@ jobs: jdkversion: 25 steps: - name: Checkout repo - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up OpenJDK version ... uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: diff --git a/.github/workflows/publishRelease.yml b/.github/workflows/publishRelease.yml index 1508010f..515afe41 100644 --- a/.github/workflows/publishRelease.yml +++ b/.github/workflows/publishRelease.yml @@ -8,7 +8,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Java uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: From 668309e6bd63c30cc41f24f2c845dc15f5d1f1da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 06:01:50 +0000 Subject: [PATCH 13/25] chore(deps): update github/codeql-action action to v4.37.3 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3a28ed5c..c98b086a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/autobuild@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 # Command-line programs to run using the OS shell. # See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 From e75c9b23516df62442373b5cb4f81c08be3c3918 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Mon, 27 Jul 2026 10:54:43 +0200 Subject: [PATCH 14/25] test: avoid emoji in filenames because it causes issues on windows --- .../edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java index 7688e09f..cc31a5a7 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java @@ -95,7 +95,7 @@ void testEncodedIdsFindTheirPaths(@TempDir Path tempDir) throws IOException { { FileEntity.FileEntityBuilder dataEntityBuilder = new FileEntity.FileEntityBuilder(); - dataEntityBuilder.setId("id\uD83E\uDD791"); + dataEntityBuilder.setId("id1"); dataEntityBuilder.addTypes(List.of("File")); UUID uuid = UUID.randomUUID(); Path path = tempDir.resolve(uuid.toString()); From d596dcc5112dc8eb39d48b2679d381e11287c814 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Mon, 27 Jul 2026 11:02:30 +0200 Subject: [PATCH 15/25] test: avoid pipe in filenames because it might cause issues on windows --- .../edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java index cc31a5a7..8d6163e6 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java @@ -107,7 +107,7 @@ void testEncodedIdsFindTheirPaths(@TempDir Path tempDir) throws IOException { { FileEntity.FileEntityBuilder dataEntityBuilder = new FileEntity.FileEntityBuilder(); - dataEntityBuilder.setId("id|1"); + dataEntityBuilder.setId("id面试1"); dataEntityBuilder.addTypes(List.of("File")); UUID uuid = UUID.randomUUID(); Path path = tempDir.resolve(uuid.toString()); From e370c45892da4f245be9e076fb8f2d466e654e62 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Mon, 27 Jul 2026 11:08:11 +0200 Subject: [PATCH 16/25] Revert "test: avoid emoji in filenames because it causes issues on windows" This reverts commit e75c9b23516df62442373b5cb4f81c08be3c3918. --- .../edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java index 8d6163e6..3475d185 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java @@ -95,7 +95,7 @@ void testEncodedIdsFindTheirPaths(@TempDir Path tempDir) throws IOException { { FileEntity.FileEntityBuilder dataEntityBuilder = new FileEntity.FileEntityBuilder(); - dataEntityBuilder.setId("id1"); + dataEntityBuilder.setId("id\uD83E\uDD791"); dataEntityBuilder.addTypes(List.of("File")); UUID uuid = UUID.randomUUID(); Path path = tempDir.resolve(uuid.toString()); From 3d7f6cd15632db12f4c7b54b6c1e9e63fc86015c Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Wed, 12 Aug 2026 16:24:06 +0200 Subject: [PATCH 17/25] cleanup: remove debug println calls --- .../edu/kit/datamanager/ro_crate/reader/CrateReader.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java b/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java index 72d30e79..c63ba21a 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java @@ -143,7 +143,6 @@ private RoCrate rebuildCrate(ObjectNode metadataJson, File files, HashSet { - System.out.println("Found file for: " + file.toString()); usedFiles.add(file.getPath()); builder.setLocationWithExceptions(file.toPath()) .setId(file.getName()); @@ -239,17 +238,9 @@ protected String unpackId(JsonNode node) { } protected Optional checkFolderHasFile(String filepathOrId, File folder) { - System.out.println("DEBUG: " + Arrays.toString(folder.listFiles())); if (IdentifierUtils.isUrl(filepathOrId)) { return Optional.empty(); } - System.out.println( "DEBUG: " + - IdentifierUtils.decode(filepathOrId) - .map(decoded -> folder.toPath().resolve(decoded).normalize()) - .filter(resolved -> resolved.startsWith(folder.toPath())) - .map(Path::toFile) - .map(File::exists) - ); return Stream.of(IdentifierUtils.decode(filepathOrId).orElse(filepathOrId), filepathOrId) .map(filename -> folder.toPath().resolve(filename).normalize().toAbsolutePath()) // defence-in-depth: ensure we are still inside the crate folder From 359edde92ced01761a05820c5c1a204706bbce27 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 13 Aug 2026 18:25:40 +0200 Subject: [PATCH 18/25] fix: add checks of the filenames when writing files This avoids attacks when unzipping a crate later, and writing outside the crate when writing in general. --- .../ro_crate/writer/WriteFolderStrategy.java | 12 +++++-- .../writer/WriteZipStreamStrategy.java | 34 ++++++++++++++++-- .../ro_crate/writer/CommonWriterTest.java | 35 +++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java index a694c0ca..a3e29250 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; /** * A class for writing a crate to a folder. @@ -73,10 +74,17 @@ private void saveToFile(DataEntity entity, File file) throws IOException { if (entity.getPath() != null) { String id = entity.getId(); String filename = IdentifierUtils.decode(id).orElse(id); + Path baseFolder = file.toPath().toAbsolutePath(); + Path destination = baseFolder.resolve(filename).normalize(); + // defence-in-depth: ensure the resolved path remains inside the crate folder + if (!destination.startsWith(baseFolder)) { + logger.warn("Skipping entity '{}': resolved path '{}' escapes destination folder '{}'", id, destination, file); + return; + } if (entity.getPath().toFile().isDirectory()) { - FileUtils.copyDirectory(entity.getPath().toFile(), file.toPath().resolve(filename).toFile()); + FileUtils.copyDirectory(entity.getPath().toFile(), destination.toFile()); } else { - FileUtils.copyFile(entity.getPath().toFile(), file.toPath().resolve(filename).toFile()); + FileUtils.copyFile(entity.getPath().toFile(), destination.toFile()); } } } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java index 5216ccb5..cf621062 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java @@ -157,16 +157,46 @@ private void saveToStream(DataEntity entity, ZipOutputStream zipStream, String p boolean isDirectory = entity.getPath().toFile().isDirectory(); String id = entity.getId(); String filename = IdentifierUtils.decode(id).orElse(id); + String safeName = sanitizeZipEntryName(filename); + if (safeName.isEmpty()) { + logger.warn("Skipping entity '{}': decoded name contains only traversal or absolute segments", id); + return; + } + String entryName = prefix + safeName; if (isDirectory) { ZipStreamUtil.addFolderToZipStream( zipStream, entity.getPath().toFile(), - prefix + filename); + entryName); } else { ZipStreamUtil.addFileToZipStream( zipStream, entity.getPath().toFile(), - prefix + filename); + entryName); + } + } + + /** + * Strips absolute markers and path-traversal segments from a name so it + * cannot escape the crate root when the zip is extracted. + * + * @param name the raw decoded entry name + * @return a safe relative entry name with no leading slashes or ".." segments + */ + private static String sanitizeZipEntryName(String name) { + // zip entries always use forward slashes as separators + String normalized = name.replace('\\', '/'); + String[] segments = normalized.split("/"); + StringBuilder safe = new StringBuilder(); + for (String segment : segments) { + if (segment.isEmpty() || segment.equals(".") || segment.equals("..")) { + continue; + } + if (safe.length() > 0) { + safe.append("/"); + } + safe.append(segment); } + return safe.toString(); } } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java index 3f67f4f2..587f4ae6 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java @@ -185,4 +185,39 @@ default void testWritingOnlyConsidersAddedFiles(@TempDir Path tempDir) throws IO roCrate, "/json/crate/fileAndDir.json"); } + + /** + * Tests that data entity ids containing path traversal segments (e.g. "../") + * cannot cause files to be written outside the crate destination. + * + * @param tempDir the temporary directory given by junit for our test + * @throws IOException if an error occurs while writing the crate + */ + @Test + default void testPathTraversalIsBlocked(@TempDir Path tempDir) throws IOException { + Path sourceFile = tempDir.resolve("source.txt"); + FileUtils.writeStringToFile(sourceFile.toFile(), "content", Charset.defaultCharset()); + + RoCrate crate = new RoCrate.RoCrateBuilder( + "Traversal Test", + "Crate with a traversal id", + "2024", + "https://creativecommons.org/licenses/by/4.0/") + .setPreview(new edu.kit.datamanager.ro_crate.preview.AutomaticPreview()) + .addDataEntity(new FileEntity.FileEntityBuilder() + .setLocationWithExceptions(sourceFile) + .setId("../escape.txt") + .build()) + .build(); + + Path crateDestination = tempDir.resolve("my-crate"); + this.saveCrate(crate, crateDestination); + + Path extractionPath = tempDir.resolve("extracted"); + ensureCrateIsExtractedIn(crateDestination, extractionPath); + + Path escapeTarget = tempDir.resolve("escape.txt"); + assertFalse(Files.exists(escapeTarget), + "Path traversal must be blocked: '%s' must not exist outside the crate".formatted(escapeTarget)); + } } From 23cf2c74f11f804e0be4a01bd12fa4165c50be2c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:32:53 +0000 Subject: [PATCH 19/25] fix(deps): update dependency org.junit:junit-bom to v6.1.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6109669a..6caebd61 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ ext { dependencies { // JUnit setup for testing - testImplementation(platform("org.junit:junit-bom:6.1.2")) + testImplementation(platform("org.junit:junit-bom:6.1.3")) testImplementation('org.junit.jupiter:junit-jupiter') testRuntimeOnly('org.junit.platform:junit-platform-launcher') // JSON object mapping / (de-)serialization From 555c88fa7f3296f43333776563f9c5504f600db4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:34:48 +0000 Subject: [PATCH 20/25] fix(deps): update dependency commons-validator:commons-validator to v1.11.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6109669a..421bb0af 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ dependencies { // compare json documents in tests implementation 'com.github.fslev:json-compare:8.1' // url validator - implementation group: 'commons-validator', name: 'commons-validator', version: '1.10.1' + implementation group: 'commons-validator', name: 'commons-validator', version: '1.11.0' // logging implementation group: 'org.slf4j', name: 'slf4j-jdk14', version: '2.0.18' // JSON-LD, Zenodo mapping From d5e5569e97e180fc29911e70e84cc355d3a6b6a3 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Thu, 13 Aug 2026 19:13:58 +0200 Subject: [PATCH 21/25] fix: Normalize base folder in writer containment check Without normalization, valid destinations containing parent segments (..) were falsely rejected. --- .../ro_crate/writer/WriteFolderStrategy.java | 2 +- .../ro_crate/writer/CommonWriterTest.java | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java index a3e29250..0816cd59 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteFolderStrategy.java @@ -74,7 +74,7 @@ private void saveToFile(DataEntity entity, File file) throws IOException { if (entity.getPath() != null) { String id = entity.getId(); String filename = IdentifierUtils.decode(id).orElse(id); - Path baseFolder = file.toPath().toAbsolutePath(); + Path baseFolder = file.toPath().toAbsolutePath().normalize(); Path destination = baseFolder.resolve(filename).normalize(); // defence-in-depth: ensure the resolved path remains inside the crate folder if (!destination.startsWith(baseFolder)) { diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java index 587f4ae6..3125143b 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java @@ -220,4 +220,43 @@ default void testPathTraversalIsBlocked(@TempDir Path tempDir) throws IOExceptio assertFalse(Files.exists(escapeTarget), "Path traversal must be blocked: '%s' must not exist outside the crate".formatted(escapeTarget)); } + + /** + * Tests that valid files are accepted even when the destination path itself + * contains parent segments (e.g. "../"). The containment check must compare + * normalized paths so equivalent base folders are not falsely rejected. + * + * @param tempDir the temporary directory given by junit for our test + * @throws IOException if an error occurs while writing the crate + */ + @Test + default void testValidFileAcceptedWithParentSegmentInDestination(@TempDir Path tempDir) throws IOException { + // Create a subdirectory so the ".." in the destination path resolves correctly + Files.createDirectories(tempDir.resolve("outer")); + + Path sourceFile = tempDir.resolve("source.txt"); + FileUtils.writeStringToFile(sourceFile.toFile(), "content", Charset.defaultCharset()); + + RoCrate crate = new RoCrate.RoCrateBuilder( + "Parent Segment Test", + "Crate written to a destination with a parent segment", + "2024", + "https://creativecommons.org/licenses/by/4.0/") + .setPreview(new edu.kit.datamanager.ro_crate.preview.AutomaticPreview()) + .addDataEntity(new FileEntity.FileEntityBuilder() + .setLocationWithExceptions(sourceFile) + .setId("valid.txt") + .build()) + .build(); + + // Destination contains a parent segment (..) that resolves inside tempDir + Path crateDestination = tempDir.resolve("outer").resolve("../my-crate"); + this.saveCrate(crate, crateDestination); + + Path extractionPath = tempDir.resolve("extracted"); + ensureCrateIsExtractedIn(crateDestination, extractionPath); + + assertTrue(Files.isRegularFile(extractionPath.resolve("valid.txt")), + "Valid file should be written even when destination path contains parent segments"); + } } From 2ce6ea680e2922f7dd9c2db1939c6f7e70dec628 Mon Sep 17 00:00:00 2001 From: Andreas Pfeil Date: Fri, 14 Aug 2026 17:20:16 +0200 Subject: [PATCH 22/25] fix: ensure zip path sanitization normalizes properly Previously, ".." segments were stripped instead of resolved, so "subdir/../valid.txt" became "subdir/valid.txt" rather than "valid.txt". This aligns the zip writer with WriteFolderStrategy and explicitly rejects absolute and Windows drive-qualified paths. --- .../writer/WriteZipStreamStrategy.java | 41 ++++++++++++++----- .../ro_crate/writer/CommonWriterTest.java | 38 +++++++++++++++++ 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java index cf621062..8dfb6adf 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/writer/WriteZipStreamStrategy.java @@ -10,6 +10,8 @@ import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -159,7 +161,7 @@ private void saveToStream(DataEntity entity, ZipOutputStream zipStream, String p String filename = IdentifierUtils.decode(id).orElse(id); String safeName = sanitizeZipEntryName(filename); if (safeName.isEmpty()) { - logger.warn("Skipping entity '{}': decoded name contains only traversal or absolute segments", id); + logger.warn("Skipping entity '{}': decoded name resolves outside the crate root", id); return; } String entryName = prefix + safeName; @@ -177,26 +179,45 @@ private void saveToStream(DataEntity entity, ZipOutputStream zipStream, String p } /** - * Strips absolute markers and path-traversal segments from a name so it - * cannot escape the crate root when the zip is extracted. + * Normalizes a decoded entry name into a safe relative path, consistent + * with the containment check in {@link WriteFolderStrategy#saveToFile}. + *

+ * Absolute paths (leading {@code /}) and Windows drive-qualified paths + * (e.g. {@code C:/}) are rejected. Internal {@code .} and {@code ..} + * segments are resolved; if the result escapes the virtual crate root + * the name is rejected. * * @param name the raw decoded entry name - * @return a safe relative entry name with no leading slashes or ".." segments + * @return a canonical relative entry name, or an empty string if the name + * is absolute, drive-qualified, or escapes the crate root */ private static String sanitizeZipEntryName(String name) { // zip entries always use forward slashes as separators String normalized = name.replace('\\', '/'); + + // Reject absolute paths and Windows drive-qualified paths (e.g. C:/) + if (normalized.startsWith("/") + || (normalized.length() >= 2 && normalized.charAt(1) == ':' + && Character.isLetter(normalized.charAt(0)))) { + return ""; + } + + // Normalize "." and ".." segments, rejecting paths that escape the root String[] segments = normalized.split("/"); - StringBuilder safe = new StringBuilder(); + List stack = new ArrayList<>(); for (String segment : segments) { - if (segment.isEmpty() || segment.equals(".") || segment.equals("..")) { + if (segment.isEmpty() || segment.equals(".")) { continue; } - if (safe.length() > 0) { - safe.append("/"); + if (segment.equals("..")) { + if (stack.isEmpty()) { + return ""; + } + stack.remove(stack.size() - 1); + } else { + stack.add(segment); } - safe.append(segment); } - return safe.toString(); + return String.join("/", stack); } } diff --git a/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java b/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java index 3125143b..15f38df7 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/writer/CommonWriterTest.java @@ -259,4 +259,42 @@ default void testValidFileAcceptedWithParentSegmentInDestination(@TempDir Path t assertTrue(Files.isRegularFile(extractionPath.resolve("valid.txt")), "Valid file should be written even when destination path contains parent segments"); } + + /** + * Tests that internal ".." segments are normalized rather than deleted. + * An entity id like "subdir/../valid.txt" should resolve to "valid.txt" + * (consistent with WriteFolderStrategy), not "subdir/valid.txt". + * + * @param tempDir the temporary directory given by junit for our test + * @throws IOException if an error occurs while writing the crate + */ + @Test + default void testInternalTraversalSegmentsAreNormalized(@TempDir Path tempDir) throws IOException { + Path sourceFile = tempDir.resolve("source.txt"); + FileUtils.writeStringToFile(sourceFile.toFile(), "content", Charset.defaultCharset()); + + RoCrate crate = new RoCrate.RoCrateBuilder( + "Normalization Test", + "Crate with an internal traversal segment in an entity id", + "2024", + "https://creativecommons.org/licenses/by/4.0/") + .setPreview(new edu.kit.datamanager.ro_crate.preview.AutomaticPreview()) + .addDataEntity(new FileEntity.FileEntityBuilder() + .setLocationWithExceptions(sourceFile) + .setId("subdir/../valid.txt") + .build()) + .build(); + + Path crateDestination = tempDir.resolve("my-crate"); + this.saveCrate(crate, crateDestination); + + Path extractionPath = tempDir.resolve("extracted"); + ensureCrateIsExtractedIn(crateDestination, extractionPath); + + // After normalization, the file should be at the root, not inside "subdir" + assertTrue(Files.isRegularFile(extractionPath.resolve("valid.txt")), + "Internal '..' should be normalized: 'subdir/../valid.txt' must resolve to 'valid.txt'"); + assertFalse(Files.exists(extractionPath.resolve("subdir")), + "The 'subdir' segment should have been cancelled by the following '..'"); + } } From 0c2cb9ca63f8b52994e00ae4fabcab26108cb02e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:22:16 +0000 Subject: [PATCH 23/25] chore(deps): update github/codeql-action action to v4.37.6 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 57c78c92..321f671b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 # Command-line programs to run using the OS shell. # See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 From d9a51f0219aae46b7bfcb9e3a964516eac8f0d72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:22:25 +0000 Subject: [PATCH 24/25] chore(deps): update actions/setup-java action to v5.7.0 --- .github/workflows/gradle.yml | 4 ++-- .github/workflows/publishRelease.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 0a0c077c..83133ae2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up openJDK version - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: ${{ matrix.jdk }} distribution: "zulu" @@ -49,7 +49,7 @@ jobs: - name: Checkout repo uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up OpenJDK version ... - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: "zulu" java-version: ${{ env.jdkversion }} diff --git a/.github/workflows/publishRelease.yml b/.github/workflows/publishRelease.yml index d7b0edc1..2bf14be3 100644 --- a/.github/workflows/publishRelease.yml +++ b/.github/workflows/publishRelease.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Java - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: 21 distribution: 'zulu' # openjdk From b9576600a9cbcd95e2e8c997576f619af025d153 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:22:32 +0000 Subject: [PATCH 25/25] chore(deps): update gradle/actions action to v6.3.0 --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 0a0c077c..d1921396 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -31,7 +31,7 @@ jobs: - name: Install Dependencies run: npm install -g ro-crate-html-js - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - name: Build and Test with Gradle run: ./gradlew -Dprofile=release build - name: Upload (test) reports as artifact on GitHub on manual runs