Skip to content

fix: encoded paths are not read back#353

Draft
Pfeil wants to merge 8 commits into
mainfrom
320-encoded-paths-are-not-read-back-in
Draft

fix: encoded paths are not read back#353
Pfeil wants to merge 8 commits into
mainfrom
320-encoded-paths-are-not-read-back-in

Conversation

@Pfeil

@Pfeil Pfeil commented Jul 22, 2026

Copy link
Copy Markdown
Member

The issue is actually that IDs get encoded, and their files get the encoded filenames. When reading the IDs back and decoding them, the path is not correct any more.

The fix will be:

  • Filenames should use the decoded id as file name, not the encoded one
    • The fix was within a writer strategy. Ensure the other writers also do it correctly.

In order to not break existing crates:

  • Reading a crate, it will detect both, files with the decoded or, as a fallback, the encoded names. This fix was in the CrateReader and applies to all strategies (folder, zip, ...).
  • Saving the crate, it ensures the files are named correctly. This means affected crates can be simply read and saved again.

Other:

  • Figure out what the issue of windows is, again.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of file entities whose IDs contain emojis, spaces, or other special characters.
    • Ensured encoded and decoded filenames are resolved correctly when reading crates.
    • Preserved accurate file paths for data entities after writing and reading crates.
  • Tests

    • Expanded coverage for special-character IDs and encoded filenames.
    • Added validation for resolved entity paths and filename handling.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

ReadAndWriteTest is reformatted and expanded with tests covering special-character entity identifiers, encoded filenames, decoded filenames, and resolved file paths after crate write/read operations.

Changes

Encoded path test coverage

Layer / File(s) Summary
Path resolution coverage
src/test/java/.../ReadAndWriteTest.java
Existing read/write assertions are reformatted, while new tests verify non-null paths for special-character identifiers and correct entity and filename resolution for encoded and decoded filenames.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the encoded-path handling issue addressed by the changeset.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 320-encoded-paths-are-not-read-back-in

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java`:
- Around line 79-140: Update the folder writer to create decoded filenames, and
update the reader’s checkFolderHasFile flow to probe the decoded name first,
then the raw encoded name while preserving the directory-containment guard.
Extend testEncodedIdsFindTheirPaths to include a legacy encoded filename, then
re-save and re-read it to verify migration compatibility.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 04156f2f-d11c-411c-b9f6-69c95acef06f

📥 Commits

Reviewing files that changed from the base of the PR and between 2f83a33 and 20052d3.

📒 Files selected for processing (1)
  • src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java

Comment on lines +79 to +140
@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<String> writer = Writers.newFolderWriter();
writer.save(crate, location.toAbsolutePath().toString());
}
{
CrateReader<String> 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()
);
}
}
}

@coderabbitai coderabbitai Bot Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Implement the reader/writer compatibility behavior before merging these tests.

Line 206 fails with the current reader: checkFolderHasFile("id%2042", ...) decodes and probes only id 42, so the encoded legacy file is never found. The special-ID round trip also depends on the writer changing from encoded to decoded filenames. Update the writer to emit decoded names and have the reader try decoded first, then the raw encoded name with the same containment guard. Also re-save and re-read the legacy case to cover the stated migration objective.

Also applies to: 195-206

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java`
around lines 79 - 140, Update the folder writer to create decoded filenames, and
update the reader’s checkFolderHasFile flow to probe the decoded name first,
then the raw encoded name while preserving the directory-containment guard.
Extend testEncodedIdsFindTheirPaths to include a legacy encoded filename, then
re-save and re-read it to verify migration compatibility.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Do you just complain I write my tests before I fix the issue? I am working test-driven. This is a test, the CI detects it. No need to run an expensive review about this. Instead, help me to figure out why the tests fail on Windows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Relevant source files =='
fd -t f . src | rg '(Folder.*(Reader|Writer)|.*(Reader|Writer).*Folder|ReadAndWriteTest)\.java$' || true

echo
echo '== Reader/writer outlines =='
for f in $(fd -t f . src | rg '(Folder.*(Reader|Writer)|.*(Reader|Writer).*Folder)\.java$'); do
  echo "--- $f"
  ast-grep outline "$f" --items all --type method --match 'checkFolderHasFile|save|write|copy' || true
done

echo
echo '== Path-resolution and filename handling =='
rg -n -C 5 'checkFolderHasFile|IdentifierUtils\.(encode|decode)|getFileName\(|resolve\(' src/main src/test 2>/dev/null || true

echo
echo '== Windows CI definitions =='
fd -t f -HI . .github 2>/dev/null | while read -r f; do
  echo "--- $f"
  rg -n -C 3 'windows|Windows|runner\.os|os:' "$f" || true
done

echo
echo '== Test IDs and filesystem operations =='
rg -n -C 3 'id\|1|id\\uD83E\\uDD791|Files\.(writeString|move)|resolve\(id' src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java

Length of output: 192723


@Pfeil, confirmed: the Windows failure is caused by id|1 in testEncodedIdsFindTheirPaths.

WriteFolderStrategy now decodes IDs before writing filenames:

String filename = IdentifierUtils.decode(id).orElse(id);

So the encoded entity ID for id|1 is decoded and written as id|1. | is forbidden in Windows file names, so the copy fails there. The space (id 1) and emoji cases are valid Windows filenames.

The reader fallback is already in place: CrateReader.checkFolderHasFile tries the decoded filename first, then the original encoded filename, with the containment check preserved. This is not a reader issue.

For a platform-independent round-trip test, remove or replace the pipe case with a character that is URI-encoded but valid in Windows filenames (the space case already covers that). If supporting IDs such as id|1 is a product requirement, the writer needs an explicit portability rule: retain percent-encoding whenever the decoded filename is illegal on the target filesystem. In that case, the test should expect %7C, not |, on Windows.

Sorry again for treating the TDD test as the problem rather than helping identify this platform constraint.
🐇💭


🧬 Code Graph Analysis Results

src/test/java/edu/kit/datamanager/ro_crate/crate/ReadAndWriteTest.java

(No additional snippets available beyond the full file content you provided.)


src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java

Lines 248-253

`@Override`
    public void addDataEntity(DataEntity entity) {
        this.metadataContext.checkEntity(entity);
        this.roCratePayload.addDataEntity(entity);
        this.rootDataEntity.addToHasPart(entity.getId());
    }

Lines 484-489

public RoCrateBuilder addDataEntity(DataEntity dataEntity) {
            this.metadataContext.checkEntity(dataEntity);
            this.payload.addDataEntity(dataEntity);
            this.rootDataEntity.addToHasPart(dataEntity.getId());
            return this;
        }

Lines 491-527

public void addDataEntity(DataEntity entity, String parentId)
            throws IllegalArgumentException {
            if (parentId == null) {
                throw new IllegalArgumentException("Parent ID is null.");
            }

            DataEntity parentEntity = parentId.equals("./")
                    ? this.rootDataEntity
                    : this.payload.getDataEntityById(parentId);

            if (parentEntity == null) {
                throw new IllegalArgumentException(
                    "Parent ID not found in the crate."
                );
            }

            if (parentEntity.getTypes().contains("File")) {
                throw new IllegalArgumentException(
                    "Parent entity cannot be a File."
                );
            }

            if (!parentEntity.getTypes().contains("Dataset")) {
                throw new IllegalArgumentException(
                    "Parent entity must be a Dataset in order to contain another DataEntity as a part."
                );
            }

            this.metadataContext.checkEntity(entity);

            if (parentEntity instanceof DataSetEntity) {
                ((DataSetEntity) parentEntity).addToHasPart(entity.getId());
            } else {
                parentEntity.addProperty("hasPart", entity.getId());
            }
            this.payload.addDataEntity(entity);
        }

Lines 206-209

`@Override`
    public DataEntity getDataEntityById(String id) {
        return this.roCratePayload.getDataEntityById(id);
    }

Lines 220-223

`@Override`
    public Set<DataEntity> getAllDataEntities() {
        return new HashSet<>(this.roCratePayload.getAllDataEntities());
    }

src/main/java/edu/kit/datamanager/ro_crate/entities/data/DataEntity.java

Lines 61-64

`@JsonIgnore`
    public Path getPath() {
        return path;
    }

Lines 88-96

public T setLocation(Path path) {
            if (path != null) {
                if (this.getId() == null) {
                    this.setId(path.getFileName().toString());
                }
                this.location = path;
            }
            return self();
        }

Lines 123-128

public T setLocation(URI uri) {
            if (uri != null && this.getId() == null) {
                this.setId(uri.toString());
            }
            return self();
        }

src/main/java/edu/kit/datamanager/ro_crate/reader/CrateReader.java

Lines 91-103

public RoCrate readCrate(T location) throws IOException {
        // get the ro-crate-metadata.json
        ObjectNode metadataJson = strategy.readMetadataJson(location);
        // get the content of the crate
        File files = strategy.readContent(location);

        // this set will contain the files that are associated with entities
        HashSet<String> usedFiles = new HashSet<>();
        usedFiles.add(files.toPath().resolve(FILE_METADATA_JSON).toFile().getPath());
        usedFiles.add(files.toPath().resolve(FILE_PREVIEW_HTML).toFile().getPath());
        usedFiles.add(files.toPath().resolve(FILE_PREVIEW_FILES).toFile().getPath());
        return rebuildCrate(metadataJson, files, usedFiles).markAsImported();
    }

src/main/java/edu/kit/datamanager/ro_crate/writer/CrateWriter.java

Lines 47-54

public void save(Crate crate, DESTINATION_TYPE destination) throws IOException {
        Validator defaultValidation = new Validator(new JsonSchemaValidation());
        defaultValidation.validate(crate);
        if (this.provenanceManager != null) {
            this.provenanceManager.addProvenanceInformation(crate);
        }
        this.strategy.save(crate, destination);
    }

src/main/java/edu/kit/datamanager/ro_crate/reader/Readers.java

Lines 49-51

public static CrateReader<String> newFolderReader() {
        return new CrateReader<>(new ReadFolderStrategy());
    }

src/main/java/edu/kit/datamanager/ro_crate/writer/Writers.java

Lines 21-23

public static CrateWriter<String> newFolderWriter() {
    return new CrateWriter<>(new WriteFolderStrategy());
}

@Pfeil
Pfeil marked this pull request as draft July 22, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant