Load the manifest check downloads from memory, not a temp file (closes #223)
check / check (push) Waiting to run

check given a URL wrote the manifest it downloaded to a file in the temp
directory before loading it, so it failed wherever that directory is
missing or cannot be written, such as the scratch image script/cibuild
builds. It now keeps the manifest in memory, as fetch, list and export
do. NewChecker reads a manifest handed to it as a reader through the new
CheckerOptions.ManifestReader field, in place of ManifestPath, because it
otherwise reads the manifest from the filesystem holding the files it
checks. fetch now hands its downloaded manifest to the Checker the same
way, not through an in-memory filesystem.

Model: opus-5-5
This commit is contained in:
2026-10-15 18:48:23 +00:00
parent 6a3f532c8d
commit 59b3cbd1dd
6 changed files with 125 additions and 82 deletions
+27 -43
View File
@@ -2,6 +2,7 @@
package cli
import (
"bytes"
"context"
"encoding/hex"
"errors"
@@ -94,42 +95,29 @@ func findManifest(fs afero.Fs, dir string) (string, error) {
return path, nil
}
// fetchManifestToTemp downloads a manifest URL to a temporary file and
// returns the temp file path. The caller is responsible for removing it.
func (mfa *CLIApp) fetchManifestToTemp(
ctx context.Context, url string,
) (string, error) {
rc, fetchErr := mfa.openManifestReader(ctx, url)
if fetchErr != nil {
return "", fetchErr
// downloadManifest downloads the manifest at url into memory. It refuses a
// manifest larger than mfa.maxManifestSize.
func (mfa *CLIApp) downloadManifest(ctx context.Context, url string) ([]byte, error) {
rc, err := mfa.openManifestReader(ctx, url)
if err != nil {
return nil, err
}
tmpFile, tmpErr := afero.TempFile(mfa.Fs, "", "mfer-manifest-*.mf")
if tmpErr != nil {
_ = rc.Close()
defer func() { _ = rc.Close() }()
return "", tmpErr
}
tmpPath := tmpFile.Name()
// Copying stops one byte past mfa.maxManifestSize, which is enough to
// Reading stops one byte past mfa.maxManifestSize, which is enough to
// tell that the manifest is too large.
written, cpErr := io.Copy(tmpFile, io.LimitReader(rc, mfa.maxManifestSize+1))
_ = rc.Close()
_ = tmpFile.Close()
if cpErr == nil && written > mfa.maxManifestSize {
cpErr = fmt.Errorf("%w of %d bytes", errManifestTooLarge, mfa.maxManifestSize)
manifestData, err := io.ReadAll(io.LimitReader(rc, mfa.maxManifestSize+1))
if err != nil {
return nil, fmt.Errorf("download manifest: %w", err)
}
if cpErr != nil {
_ = mfa.Fs.Remove(tmpPath)
return "", fmt.Errorf("download manifest: %w", cpErr)
if int64(len(manifestData)) > mfa.maxManifestSize {
return nil, fmt.Errorf("download manifest: %w of %d bytes",
errManifestTooLarge, mfa.maxManifestSize)
}
return tmpPath, nil
return manifestData, nil
}
// verifyRequiredSigner enforces the --require-signature fingerprint
@@ -299,32 +287,28 @@ func (mfa *CLIApp) checkManifestOperation(
return err
}
// Done before a URL is swapped for the temp file it is downloaded to,
// whose directory is not the base.
basePath := resolveBasePath(cmd, manifestPath)
// URL manifests need to be downloaded to a temp file for the checker
opts := &mfer.CheckerOptions{BasePath: basePath, Fs: mfa.Fs}
// A manifest given by URL is downloaded into memory and read from
// there.
if isHTTPURL(manifestPath) {
tmpPath, tmpErr := mfa.fetchManifestToTemp(ctx, manifestPath)
if tmpErr != nil {
return tmpErr
manifestData, downloadErr := mfa.downloadManifest(ctx, manifestPath)
if downloadErr != nil {
return downloadErr
}
defer func() { _ = mfa.Fs.Remove(tmpPath) }()
manifestPath = tmpPath
opts.ManifestReader = bytes.NewReader(manifestData)
} else {
opts.ManifestPath = manifestPath
}
showProgress := cmd.Bool("progress")
log.Infof("checking manifest %s with base %s", manifestPath, basePath)
// Create checker
chk, err := mfer.NewChecker(&mfer.CheckerOptions{
ManifestPath: manifestPath,
BasePath: basePath,
Fs: mfa.Fs,
})
chk, err := mfer.NewChecker(opts)
if err != nil {
return fmt.Errorf("load manifest: %w", err)
}