Skip to content

Commit a32bedb

Browse files
authored
Fix rare git treehashing bug (#2881)
If we have a symlink that contains a name that is a prefix of another file within a directory, the ordering of treehashing changed between v1.6.X and v1.7.0. This is a problem, as it makes certain (rare) cases hash to invalid treehashes. This is because when `tree_hash()` was refactored in `v1.7`, I simplified `git_mode(f) == mode_dir` to `isdir(f)`, but it turns out that `isdir(f)` can return true if a symlink points to a directory. This in turn prevents `/` from being appended to the filenames when they are sorted, which is what causes the potential ordering change. This only happens when the symlink has a name such as `5.28` which is a prefix of another file, say `5.28.1`, and the next character sorts higher than `/`, as `.` does. Therefore, on `v1.6.4`, we would get: ```julia julia> sort(["5.28", "5.28.1/"]) 2-element Vector{String}: "5.28" "5.28.1/" ``` Whereas on `v1.7.0`, because `5.28` is a symlink pointing to a directory, we get: ```julia julia> sort(["5.28/", "5.28.1/"]) 2-element Vector{String}: "5.28.1/" "5.28/" ``` Which changes the ordering. Consulting my ASCII table, the number of valid pathname characters that can cause this confusion is small, but enough that most "rootfs image" style artifacts will run into it, thanks to versioning schemes with symlinks such as the one that I found that triggered this whole issue.
1 parent 4e8406b commit a32bedb

File tree

2 files changed

+13
-1
lines changed

2 files changed

+13
-1
lines changed

src/GitTools.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ Calculate the git tree hash of a given path.
274274
"""
275275
function tree_hash(::Type{HashType}, root::AbstractString; debug_out::Union{IO,Nothing} = nothing, indent::Int=0) where HashType
276276
entries = Tuple{String, Vector{UInt8}, GitMode}[]
277-
for f in sort(readdir(root; join=true); by = f -> isdir(f) ? f*"/" : f)
277+
for f in sort(readdir(root; join=true); by = f -> gitmode(f) == mode_dir ? f*"/" : f)
278278
# Skip `.git` directories
279279
if basename(f) == ".git"
280280
continue

test/new.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2598,6 +2598,18 @@ tree_hash(root::AbstractString; kwargs...) = bytes2hex(@inferred Pkg.GitTools.tr
25982598
tree_hash(joinpath(dir, "FooGit")) ==
25992599
"2f42e2c1c1afd4ef8c66a2aaba5d5e1baddcab33"
26002600
end
2601+
2602+
# Test for symlinks that are a prefix of another directory, causing sorting issues
2603+
if !Sys.iswindows()
2604+
mktempdir() do dir
2605+
mkdir(joinpath(dir, "5.28.1"))
2606+
write(joinpath(dir, "5.28.1", "foo"), "")
2607+
chmod(joinpath(dir, "5.28.1", "foo"), 0o644)
2608+
symlink("5.28.1", joinpath(dir, "5.28"))
2609+
2610+
@test tree_hash(dir) == "5e50a4254773a7c689bebca79e2954630cab9c04"
2611+
end
2612+
end
26012613
end
26022614

26032615
@testset "multiple registries overlapping version ranges for different versions" begin

0 commit comments

Comments
 (0)