Limit how much fetch and check read for a manifest or a file (closes #168)
check / check (push) Waiting to run
check / check (push) Waiting to run
NewManifestFromReader reads at most one byte past MaxManifestSize, a new constant of 258 MiB: the 256 MiB decompressed limit grown by zstd's worst case of 1/256, plus 1 MiB for the signature, the signing key and the other outer fields. It refuses a larger manifest. fetch, and check given a URL, stop downloading a manifest at the same point, so the refusal comes from the library. fetch stops reading a file one byte past its listed size, so a longer body ends in the size mismatch at once instead of filling the disk. docs/FORMAT.md states the limit and gives the decompressed limit as 256 MiB, the size the code uses. Model: opus-5-5
This commit is contained in:
@@ -441,6 +441,80 @@ func TestFetchSizeMismatch(t *testing.T) {
|
||||
"temp file should be cleaned up on size mismatch")
|
||||
}
|
||||
|
||||
// TestFetchStopsReadingFilePastListedSize serves 1 MiB for a file the
|
||||
// manifest lists at 16 bytes. fetch must stop reading one byte past the
|
||||
// listed size, report the size mismatch and remove its temp file.
|
||||
//
|
||||
//nolint:paralleltest // changes the process-global working directory
|
||||
func TestFetchStopsReadingFilePastListedSize(t *testing.T) {
|
||||
content := []byte("sixteen bytes...")
|
||||
|
||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(
|
||||
manifestOf(t, map[string][]byte{testFileTxt: content})))
|
||||
require.NoError(t, err)
|
||||
|
||||
server := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write(make([]byte, 1<<20))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
chdirTemp(t)
|
||||
|
||||
err = downloadFile(context.Background(), testClient(),
|
||||
server.URL+"/"+testFileTxt, ".", testFileTxt, manifest.Files()[0], nil)
|
||||
require.ErrorIs(t, err, errSizeMismatch)
|
||||
assert.EqualError(t, err, "size mismatch: expected 16 bytes, got 17")
|
||||
assert.NoFileExists(t, tempPathFor(testFileTxt))
|
||||
}
|
||||
|
||||
// zeros is an io.Reader of zero bytes without end.
|
||||
type zeros struct{}
|
||||
|
||||
func (zeros) Read(p []byte) (int, error) {
|
||||
clear(p)
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// TestManifestDownloadStopsPastLimit serves a manifest 32 MiB larger than
|
||||
// mfer.MaxManifestSize to fetch and to check. Each must stop reading it
|
||||
// once it passes the maximum, fail with an error naming the maximum and
|
||||
// leave no temp file. The connection's buffers hold less than 32 MiB, so a
|
||||
// client that stops reading leaves the server unable to send all of it.
|
||||
//
|
||||
//nolint:paralleltest // sets TMPDIR, where check writes the manifest
|
||||
func TestManifestDownloadStopsPastLimit(t *testing.T) {
|
||||
size := mfer.MaxManifestSize + 32<<20
|
||||
limit := strconv.FormatInt(mfer.MaxManifestSize, 10)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("TMPDIR", tmpDir)
|
||||
|
||||
for _, command := range []string{cmdFetch, cmdCheck} {
|
||||
var sent atomic.Int64
|
||||
|
||||
server := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
n, _ := io.CopyN(w, zeros{}, size)
|
||||
sent.Store(n)
|
||||
}))
|
||||
|
||||
opts := testOpts([]string{testApp, command, "-q", server.URL + "/index.mf"},
|
||||
afero.NewOsFs())
|
||||
assert.Equal(t, 1, runCLI(opts), command)
|
||||
assert.Contains(t, testStderr(t, opts),
|
||||
"maximum allowed size of "+limit+" bytes", command)
|
||||
|
||||
server.Close()
|
||||
assert.Less(t, sent.Load(), size, "%s read the whole manifest", command)
|
||||
}
|
||||
|
||||
leftover, err := os.ReadDir(tmpDir)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, leftover)
|
||||
}
|
||||
|
||||
//nolint:paralleltest // changes the process-global working directory
|
||||
func TestFetchProgress(t *testing.T) {
|
||||
// Create source filesystem with a larger test file
|
||||
|
||||
Reference in New Issue
Block a user