fetch refuses a manifest that lists another file's temp name (closes #151)
check / check (push) Failing after 1s

fetch downloads each file to a temp name beside it and first removes
whatever is there. A manifest listing both a.txt and .a.txt.tmp had
fetch delete the second while fetching the first, then exit 0 with a
tree check rejects.

The refusal of a manifest that lists the saved manifest's own name or
temp name now covers this too: a listed file, or a directory a listed
file is in, may not sit at any name fetch writes besides the listed
files themselves. Temp names come from tempPathFor, names are compared
ignoring case as before, and the refusal still happens before the
destination is created or any file requested.

Model: opus-5-5
This commit is contained in:
2026-10-04 16:37:58 +00:00
parent a3e37d1ab8
commit 7122b56a51
3 changed files with 121 additions and 39 deletions
+79 -18
View File
@@ -1166,33 +1166,94 @@ func TestFetchRequireSignature(t *testing.T) {
func TestFetchRefusesListedManifestName(t *testing.T) {
t.Parallel()
for _, listed := range []string{
defaultManifestName,
tempPathFor(defaultManifestName),
defaultManifestName + "/" + testFileTxt,
"INDEX.MF",
"./" + defaultManifestName,
for listed, writtenThere := range map[string]string{
defaultManifestName: "the saved manifest",
tempPathFor(defaultManifestName): "the saved manifest's temp file",
defaultManifestName + "/" + testFileTxt: "the saved manifest",
"INDEX.MF": "the saved manifest",
"./" + defaultManifestName: "the saved manifest",
} {
t.Run(listed, func(t *testing.T) {
t.Parallel()
// Built directly rather than scanned, since a scan lists no
// hidden files and never a path starting with "./".
content := []byte("listed")
builder := mfer.NewBuilder()
_, err := builder.AddFile(mfer.RelFilePath(listed), mfer.FileSize(len(content)),
mfer.ModTime(time.Now()), bytes.NewReader(content), nil)
require.NoError(t, err)
files := map[string][]byte{listed: []byte("listed")}
var manifest bytes.Buffer
require.NoError(t, builder.Build(context.Background(), &manifest))
assertFetchRefused(t, manifest.Bytes(), map[string][]byte{listed: content},
"manifest lists a file where fetch saves the manifest: "+listed)
assertFetchRefused(t, builtManifest(t, files), files,
"manifest lists a file where fetch writes another file: "+
listed+" ("+writtenThere+")")
})
}
}
// TestFetchRefusesListedTempName fetches manifests that list a.txt and
// .a.txt.tmp, the temp file fetch downloads a.txt to, at the top of the
// tree and in a directory. Downloading a.txt would remove .a.txt.tmp, so
// fetch must refuse the manifest before it creates the destination or
// requests any file. A manifest that lists only one of the two is fetched
// in full.
func TestFetchRefusesListedTempName(t *testing.T) {
t.Parallel()
for _, dir := range []string{"", "sub/"} {
t.Run(dir+"a.txt and "+dir+".a.txt.tmp", func(t *testing.T) {
t.Parallel()
files := map[string][]byte{
dir + "a.txt": []byte("a file"),
dir + ".a.txt.tmp": []byte("a file at its temp name"),
}
assertFetchRefused(t, builtManifest(t, files), files,
"manifest lists a file where fetch writes another file: "+
dir+".a.txt.tmp (the temp file for "+dir+"a.txt)")
})
}
for _, listed := range []string{"a.txt", ".a.txt.tmp"} {
t.Run("only "+listed, func(t *testing.T) {
t.Parallel()
files := map[string][]byte{listed: []byte("listed")}
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 "./".
func builtManifest(t *testing.T, files map[string][]byte) []byte {
t.Helper()
builder := mfer.NewBuilder()
for p, content := range files {
_, err := builder.AddFile(mfer.RelFilePath(p), mfer.FileSize(len(content)),
mfer.ModTime(time.Now()), bytes.NewReader(content), nil)
require.NoError(t, err)
}
var manifest bytes.Buffer
require.NoError(t, builder.Build(context.Background(), &manifest))
return manifest.Bytes()
}
// assertFetchRefused serves manifest, a manifest of files, and fetches it
// with flags into a directory that does not exist yet. fetch must fail
// with message after requesting only the manifest, and must not create