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
84 changes: 71 additions & 13 deletions eo-maven-plugin/src/main/java/org/eolang/maven/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Comparator;
import java.util.stream.Stream;
import org.cactoos.Func;
import org.cactoos.func.UncheckedFunc;

Expand Down Expand Up @@ -78,8 +81,6 @@ public void apply(final Path source, final Path target, final Path tail) {
"Failed to perform an IO operation with cache",
ioexception
);
} catch (final NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 hashing algorithm isn't found", exception);
}
}

Expand All @@ -93,23 +94,80 @@ private Path hash(final Path tail) {
return full.getParent().resolve(String.format("%s.sha256", full.getFileName().toString()));
}
Comment on lines 94 to 95

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

hash(tail) uses this.base.resolve(tail.normalize()), and Path.resolve(...) will ignore base when tail is absolute. This allows callers to make the cache read/write outside the cache root (and similarly, .. segments can escape after normalization). Consider validating that tail is relative and that the resolved path stays within base before using it to construct cache/hash paths.

Copilot uses AI. Check for mistakes.

/**
* Calculate SHA-256 hash of a file or directory.
* @param any File or directory path
* @return Base64-encoded SHA-256 hash of the file or directory contents
*/
private static String sha(final Path any) {
final String result;
if (Files.isDirectory(any)) {
result = Cache.dirSha(any);
} else if (Files.isRegularFile(any)) {
result = Cache.fileSha(any);
} else {
throw new IllegalArgumentException(
String.format("Path '%s' is neither a regular file nor a directory", any)
);
}
Comment on lines +102 to +112

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

sha(Path) now throws IllegalArgumentException when the path doesn't exist (neither regular file nor directory). This is a behavior change compared to the rest of Cache.apply()/fileSha() which wrap filesystem problems into IllegalStateException, and it also bypasses the apply() catch that only wraps IOException. Consider treating non-existent/unreadable paths as an I/O failure (wrap into IllegalStateException with a consistent message) instead of IllegalArgumentException.

Copilot uses AI. Check for mistakes.
return result;
}

/**
* Calculate SHA-256 hash of a directory by hashing all regular files inside it.
* @param dir Directory path.
* @return Base64-encoded SHA-256 hash of the directory contents.
*/
private static String dirSha(final Path dir) {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (Stream<Path> stream = Files.walk(dir)) {
stream.filter(Files::isRegularFile)
.sorted(Comparator.comparing(Path::toString))
.map(Cache::fileSha)
.map(s -> s.getBytes(StandardCharsets.UTF_8))
.forEach(digest::update);
Comment on lines +127 to +129

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

dirSha() currently hashes only the contents of regular files (via fileSha) and ignores their relative paths. This means renaming a file (without changing its bytes) will not change the directory hash (e.g., a single-file directory), which can leave stale cache entries when directory structure changes. Consider incorporating each file’s relative path (and a separator) into the digest update along with its content hash so renames/moves inside the directory also invalidate the cache.

Suggested change
.map(Cache::fileSha)
.map(s -> s.getBytes(StandardCharsets.UTF_8))
.forEach(digest::update);
.forEach(
path -> {
digest.update(
dir.relativize(path).toString().getBytes(StandardCharsets.UTF_8)
);
digest.update((byte) 0);
digest.update(fileSha(path).getBytes(StandardCharsets.UTF_8));
digest.update((byte) 0);
}
);

Copilot uses AI. Check for mistakes.
}
return Base64.getEncoder().encodeToString(digest.digest());
} catch (final NoSuchAlgorithmException exception) {
throw new IllegalStateException(
"SHA-256 algorithm is not available for dir hashing",
exception
);
} catch (final IOException exception) {
throw new IllegalStateException(
String.format("Failed to read directory '%s' for hashing", dir),
exception
);
}
}

/**
* Calculate SHA-256 hash of a file and return it as Base64 string.
* @param file File path
* @return Base64-encoded SHA-256 hash
* @throws NoSuchAlgorithmException If SHA-256 algorithm is not available
* @throws IOException If an I/O error occurs reading the file
* @throws IllegalStateException If SHA-256 algorithm is not available
* @throws IllegalStateException If an I/O error occurs reading the file
*/
private static String sha(final Path file) throws NoSuchAlgorithmException, IOException {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream stream = Files.newInputStream(file)) {
final byte[] buffer = new byte[8192];
int read = stream.read(buffer);
while (read != -1) {
digest.update(buffer, 0, read);
read = stream.read(buffer);
private static String fileSha(final Path file) {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream stream = Files.newInputStream(file)) {
final byte[] buffer = new byte[8192];
int read = stream.read(buffer);
while (read != -1) {
digest.update(buffer, 0, read);
read = stream.read(buffer);
}
}
return Base64.getEncoder().encodeToString(digest.digest());
} catch (final NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 algorithm is not available", exception);
} catch (final IOException exception) {
throw new IllegalStateException(
String.format("Failed to read file '%s' for hashing", file),
exception
);
}
return Base64.getEncoder().encodeToString(digest.digest());
}
}
45 changes: 39 additions & 6 deletions eo-maven-plugin/src/test/java/org/eolang/maven/CacheTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,45 @@ void generatesCorrectHashForTinyFile(
cache.resolve(String.format("%s.sha256", tail)),
StandardCharsets.UTF_8
),
Matchers.equalTo(
Base64.getEncoder().encodeToString(
MessageDigest.getInstance("SHA-256")
.digest(content.getBytes(StandardCharsets.UTF_8))
)
)
Matchers.equalTo(CacheTest.hash(content))
);
}

@Test
void generateCorrectHashForEntireFolderWithSeveralFiles(

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

Test method name generateCorrectHashForEntireFolderWithSeveralFiles is inconsistent with the existing naming pattern in this class (e.g., generatesCorrectHashForTinyFile, generatesCorrectHashForLargeFile). Consider renaming to keep a consistent verb tense (likely generates...).

Suggested change
void generateCorrectHashForEntireFolderWithSeveralFiles(
void generatesCorrectHashForEntireFolderWithSeveralFiles(

Copilot uses AI. Check for mistakes.
@Mktmp final Path temp
) throws IOException, NoSuchAlgorithmException {
final Path cache = temp.resolve("cache");
Files.createDirectories(cache);
final Path source = temp.resolve("folder");
Files.createDirectories(source);
final String first = "file1 content";
final String second = "file2 content";
final String result = String.format("%s%s", first, second);
Files.writeString(source.resolve("file1.txt"), first, StandardCharsets.UTF_8);
Files.writeString(source.resolve("file2.txt"), second, StandardCharsets.UTF_8);
new Cache(cache, p -> result).apply(
source,
temp.resolve("out.txt"),
source.resolve("folder-cache")

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

In this test, the tail argument is passed as source.resolve("folder-cache"), which is an absolute path. Cache.apply() uses base.resolve(tail), and when tail is absolute it will ignore the cache base and write outside the cache directory, so this assertion will be checking the wrong location (and the cache implementation can be misused to escape the cache root). Use a relative tail (e.g., Paths.get("folder-cache") or source.getFileName() depending on intended cache key).

Suggested change
source.resolve("folder-cache")
Path.of("folder-cache")

Copilot uses AI. Check for mistakes.
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
final MessageDigest instance = MessageDigest.getInstance("SHA-256");
instance.update(CacheTest.hash(first).getBytes(StandardCharsets.UTF_8));
instance.update(CacheTest.hash(second).getBytes(StandardCharsets.UTF_8));
MatcherAssert.assertThat(
"SHA-256 hash file has incorrect content for folder with several files",
Files.readString(
cache.resolve("folder-cache.sha256"),
StandardCharsets.UTF_8
),
Matchers.equalTo(Base64.getEncoder().encodeToString(instance.digest()))
);
}

private static String hash(final String content) throws NoSuchAlgorithmException {
return Base64.getEncoder().encodeToString(
MessageDigest.getInstance("SHA-256")
.digest(content.getBytes(StandardCharsets.UTF_8))
);
}
}
Loading