fetch refuses a manifest whose paths differ only in letter case (closes #154)
check / check (push) Failing after 1s
check / check (push) Failing after 1s
On a case-insensitive filesystem, such as the macOS default, A.txt and a.txt are one file: fetch downloaded one, replaced it with the other and exited 0, leaving a tree check rejects. A file and a directory whose names differ only in case did the same. The existing name-clash check now also records each listed file, and each directory one is in, among the names fetch writes. A listed file at a name already taken, or a directory spelled differently from one already there, refuses the manifest on every filesystem, before the destination is created or any file requested. The message names both paths. Model: opus-5-5
This commit is contained in:
@@ -1236,6 +1236,66 @@ func TestFetchRefusesListedTempName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFetchRefusesNamesEqualIgnoringCase fetches manifests that list two
|
||||
// paths that are one name on a case-insensitive filesystem: two files,
|
||||
// at the top of the tree and in a directory, a file and a directory
|
||||
// another file is in, and two directories. Fetching such a manifest
|
||||
// there would replace one with the other, so fetch must refuse it on
|
||||
// every filesystem before it creates the destination or requests any
|
||||
// file. A file and a directory with the same name, and a file listed
|
||||
// twice, are refused the same way. A manifest whose names differ in more
|
||||
// than letter case is fetched in full.
|
||||
func TestFetchRefusesNamesEqualIgnoringCase(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct{ first, second, message string }{
|
||||
{"X.txt", "x.txt", "x.txt (the file X.txt)"},
|
||||
{"sub/X.txt", "sub/x.txt", "sub/x.txt (the file sub/X.txt)"},
|
||||
{"Dir", "dir/x", "dir/x (the file Dir)"},
|
||||
{"Dir/a.txt", "dir/b.txt", "dir/b.txt (the directory Dir)"},
|
||||
{"dir", "dir/x", "dir/x (the file dir)"},
|
||||
{"./x.txt", "x.txt", "x.txt (the file ./x.txt)"},
|
||||
} {
|
||||
t.Run(tc.first+" and "+tc.second, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := map[string][]byte{
|
||||
tc.first: []byte("the first file"),
|
||||
tc.second: []byte("the second file"),
|
||||
}
|
||||
|
||||
assertFetchRefused(t, builtManifest(t, files), files,
|
||||
"manifest lists a file where fetch writes another file: "+tc.message)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("names that differ in more than letter case", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := map[string][]byte{
|
||||
"A.txt": []byte("at the top"),
|
||||
"B.txt": []byte("also at the top"),
|
||||
"dir/a.txt": []byte("in a directory"),
|
||||
"dir/b.txt": []byte("in the same directory"),
|
||||
}
|
||||
manifest := builtManifest(t, files)
|
||||
|
||||
server := httptest.NewServer(fetchTestHandler(manifest, files))
|
||||
defer server.Close()
|
||||
|
||||
dest := t.TempDir()
|
||||
|
||||
opts := testOpts([]string{
|
||||
testApp, cmdFetch, "-q", "--" + flagDest, dest, server.URL,
|
||||
}, afero.NewOsFs())
|
||||
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
||||
|
||||
want := maps.Clone(files)
|
||||
want[defaultManifestName] = manifest
|
||||
assert.Equal(t, want, filesUnder(t, dest))
|
||||
})
|
||||
}
|
||||
|
||||
// builtManifest returns a manifest of files, built directly rather than
|
||||
// scanned, since a scan lists no hidden files and never a path starting
|
||||
// with "./".
|
||||
|
||||
Reference in New Issue
Block a user