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
Prev Previous commit
fix: ignore . path segment
  • Loading branch information
ppkarwasz committed Apr 11, 2026
commit b30589571bdab54cbf54f8a3a433c5644d43ac5e
25 changes: 13 additions & 12 deletions src/main/java/org/apache/commons/codec/digest/GitIdentifiers.java
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,13 @@ private interface BlobIdSupplier {
byte[] get() throws IOException;
}

private static void checkPathComponent(String name) {
if (".".equals(name) || "..".equals(name)) {
private static String requireNoParentTraversal(String name) {
if ("..".equals(name)) {
throw new IllegalArgumentException("Path component not allowed: " + name);
}
return name;
}

private final Map<String, TreeIdBuilder> dirEntries = new HashMap<>();
private final Map<String, DirectoryEntry> fileEntries = new HashMap<>();
private final MessageDigest messageDigest;
Expand All @@ -209,16 +211,16 @@ private TreeIdBuilder(final MessageDigest messageDigest) {
*
* @param name The relative path of the subdirectory in normalized form (may contain {@code '/'}).
* @return The {@link TreeIdBuilder} for the subdirectory.
* @throws IllegalArgumentException If any path component is {@code "."} or {@code ".."}.
* @throws IllegalArgumentException If any path component is {@code ".."}.
*/
public TreeIdBuilder addDirectory(final String name) {
TreeIdBuilder current = this;
for (final String component : name.split("/", -1)) {
if (component.isEmpty()) {
// Noop segments
if (component.isEmpty() || ".".equals(component)) {
continue;
}
checkPathComponent(component);
current = current.dirEntries.computeIfAbsent(component, k -> new TreeIdBuilder(messageDigest));
current = current.dirEntries.computeIfAbsent(requireNoParentTraversal(component), k -> new TreeIdBuilder(messageDigest));
}
return current;
}
Expand All @@ -235,17 +237,16 @@ public TreeIdBuilder addDirectory(final String name) {
* @param dataSize The exact number of bytes in {@code data}.
* @param data The file content.
* @throws IOException If the stream cannot be read.
* @throws IllegalArgumentException If any path component is {@code "."} or {@code ".."}.
* @throws IllegalArgumentException If any path component is {@code ".."}.
*/
public void addFile(final FileMode mode, final String name, final long dataSize, final InputStream data) throws IOException {
addFile(mode, name, () -> blobId(messageDigest, dataSize, data));
}

private void addFile(final FileMode mode, final String name, final BlobIdSupplier blobId) throws IOException {
final int slash = name.indexOf('/');
final int slash = name.lastIndexOf('/');
if (slash < 0) {
checkPathComponent(name);
fileEntries.put(name, new DirectoryEntry(name, mode, blobId.get()));
fileEntries.put(name, new DirectoryEntry(requireNoParentTraversal(name), mode, blobId.get()));
} else {
addDirectory(name.substring(0, slash)).addFile(mode, name.substring(slash + 1), blobId);
}
Expand All @@ -260,7 +261,7 @@ private void addFile(final FileMode mode, final String name, final BlobIdSupplie
* @param name The relative path of the entry in normalized form(may contain {@code '/'}).
* @param data The file content.
* @throws IOException If an I/O error occurs.
* @throws IllegalArgumentException If any path component is {@code "."} or {@code ".."}.
* @throws IllegalArgumentException If any path component is {@code ".."}.
*/
public void addFile(final FileMode mode, final String name, final byte[] data) throws IOException {
addFile(mode, name, () -> blobId(messageDigest, data));
Expand All @@ -274,7 +275,7 @@ public void addFile(final FileMode mode, final String name, final byte[] data) t
* @param name The relative path of the entry in normalized form(may contain {@code '/'}).
* @param target The target of the symbolic link.
* @throws IOException If an I/O error occurs.
* @throws IllegalArgumentException If any path component is {@code "."} or {@code ".."}.
* @throws IllegalArgumentException If any path component is {@code ".."}.
*/
public void addSymbolicLink(final String name, final String target) throws IOException {
addFile(FileMode.SYMBOLIC_LINK, name, target.getBytes(StandardCharsets.UTF_8));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,46 +204,19 @@ void testTreeIdBuilderAddFileInputStream() throws Exception {
}

@Test
void testTreeIdBuilderEmptyPathSegments() throws Exception {
final MessageDigest md = DigestUtils.getSha1Digest();
final byte[] content = "hello\n".getBytes(StandardCharsets.UTF_8);

// Canonical form
final GitIdentifiers.TreeIdBuilder canonical = GitIdentifiers.treeIdBuilder(md);
canonical.addFile(GitIdentifiers.FileMode.REGULAR, "subdir/file.txt", content);
final byte[] expected = canonical.build();

// Leading slash
final GitIdentifiers.TreeIdBuilder withLeading = GitIdentifiers.treeIdBuilder(md);
withLeading.addFile(GitIdentifiers.FileMode.REGULAR, "/subdir/file.txt", content);
assertArrayEquals(expected, withLeading.build());

// Consecutive slashes
final GitIdentifiers.TreeIdBuilder withDouble = GitIdentifiers.treeIdBuilder(md);
withDouble.addFile(GitIdentifiers.FileMode.REGULAR, "subdir//file.txt", content);
assertArrayEquals(expected, withDouble.build());

// addDirectory with leading/trailing slashes
final GitIdentifiers.TreeIdBuilder viaDirectory = GitIdentifiers.treeIdBuilder(md);
viaDirectory.addDirectory("/subdir/").addFile(GitIdentifiers.FileMode.REGULAR, "file.txt", content);
assertArrayEquals(expected, viaDirectory.build());
}

@ParameterizedTest
@ValueSource(strings = {".", ".."})
void testTreeIdBuilderInvalidPathSegments(final String segment) {
void testTreeIdBuilderInvalidPathSegments() {
final MessageDigest md = DigestUtils.getSha1Digest();
final byte[] data = new byte[0];
// Sole path component
assertThrows(IllegalArgumentException.class,
() -> GitIdentifiers.treeIdBuilder(md).addFile(GitIdentifiers.FileMode.REGULAR, segment, data));
() -> GitIdentifiers.treeIdBuilder(md).addFile(GitIdentifiers.FileMode.REGULAR, "..", data));
assertThrows(IllegalArgumentException.class,
() -> GitIdentifiers.treeIdBuilder(md).addDirectory(segment));
() -> GitIdentifiers.treeIdBuilder(md).addDirectory(".."));
// Embedded in a longer path
assertThrows(IllegalArgumentException.class,
() -> GitIdentifiers.treeIdBuilder(md).addFile(GitIdentifiers.FileMode.REGULAR, "subdir/" + segment + "/file.txt", data));
() -> GitIdentifiers.treeIdBuilder(md).addFile(GitIdentifiers.FileMode.REGULAR, "subdir/../file.txt", data));
assertThrows(IllegalArgumentException.class,
() -> GitIdentifiers.treeIdBuilder(md).addDirectory("subdir/" + segment));
() -> GitIdentifiers.treeIdBuilder(md).addDirectory("subdir/.."));
}

@Test
Expand All @@ -260,6 +233,33 @@ void testTreeIdBuilderNestedFileEquivalentToDirectoryAndFile() throws Exception
assertArrayEquals(direct.build(), indirect.build());
}

@ParameterizedTest
@ValueSource(strings = {"", "."})
void testTreeIdBuilderNoopPathSegments(String segment) throws Exception {
final MessageDigest md = DigestUtils.getSha1Digest();
final byte[] content = "hello\n".getBytes(StandardCharsets.UTF_8);

// Canonical form
final GitIdentifiers.TreeIdBuilder canonical = GitIdentifiers.treeIdBuilder(md);
canonical.addFile(GitIdentifiers.FileMode.REGULAR, "subdir/file.txt", content);
final byte[] expected = canonical.build();

// Leading segment
final GitIdentifiers.TreeIdBuilder withLeading = GitIdentifiers.treeIdBuilder(md);
withLeading.addFile(GitIdentifiers.FileMode.REGULAR, segment + "/subdir/file.txt", content);
assertArrayEquals(expected, withLeading.build());

// Intermediate segment
final GitIdentifiers.TreeIdBuilder withIntermediate = GitIdentifiers.treeIdBuilder(md);
withIntermediate.addFile(GitIdentifiers.FileMode.REGULAR, "subdir/" + segment + "/file.txt", content);
assertArrayEquals(expected, withIntermediate.build());

// addDirectory with leading/trailing segments
final GitIdentifiers.TreeIdBuilder viaDirectory = GitIdentifiers.treeIdBuilder(md);
viaDirectory.addDirectory(segment + "/subdir/" + segment).addFile(GitIdentifiers.FileMode.REGULAR, "file.txt", content);
assertArrayEquals(expected, viaDirectory.build());
}

@Test
void testTreeIdPath() throws Exception {
assertArrayEquals(Hex.decodeHex("e4b21f6d78ceba6eb7c211ac15e3337ec4614e8a"),
Expand Down
Loading