package bundle import ( "context" "fmt" "gitea.maximumdirect.net/eric/distributor/internal/storage" ) func ValidateSourcePath(path string) error { if err := storage.ValidatePath(path); err != nil { return err } switch path { case ManifestName, storage.StateFileName: return fmt.Errorf("%q is reserved", path) } return nil } func ValidateManifest(manifest Manifest) error { if manifest.SchemaVersion != 1 { return fmt.Errorf("schema_version must be 1") } if manifest.ID == "" { return fmt.Errorf("id is required") } if err := ValidateDigest(manifest.Digest); err != nil { return fmt.Errorf("digest: %w", err) } if manifest.Created.IsZero() { return fmt.Errorf("created is required") } if len(manifest.Files) == 0 { return fmt.Errorf("files is required") } seen := make(map[string]struct{}, len(manifest.Files)) for index, file := range manifest.Files { if err := ValidateSourcePath(file.Path); err != nil { return fmt.Errorf("files[%d].path: %w", index, err) } if err := ValidateDigest(file.SHA256); err != nil { return fmt.Errorf("files[%d].sha256: %w", index, err) } if file.Size < 0 { return fmt.Errorf("files[%d].size must be non-negative", index) } if _, exists := seen[file.Path]; exists { return fmt.Errorf("files[%d].path duplicates %q", index, file.Path) } seen[file.Path] = struct{}{} } if actual := BundleDigest(manifest.Files); actual != manifest.Digest { return fmt.Errorf("digest mismatch: got %s want %s", actual, manifest.Digest) } return nil } func Validate(ctx context.Context, backend storage.Backend, bundleRoot string) (Bundle, error) { return validateAt(ctx, backend, bundleRoot, bundleRoot) } func validateAt(ctx context.Context, backend storage.Backend, bundleRoot, relativeRoot string) (Bundle, error) { if err := storage.ValidatePrefix(bundleRoot); err != nil { return Bundle{}, err } manifestPath, err := storage.Join(bundleRoot, ManifestName) if err != nil { return Bundle{}, err } manifestData, err := backend.ReadFile(ctx, manifestPath) if err != nil { return Bundle{}, fmt.Errorf("read manifest %q: %w", manifestPath, err) } manifest, err := ParseManifest(manifestData) if err != nil { return Bundle{}, fmt.Errorf("bundle %q: %w", storage.DisplayPath(relativeRoot), err) } for index, manifestFile := range manifest.Files { filePath, err := storage.Join(bundleRoot, manifestFile.Path) if err != nil { return Bundle{}, fmt.Errorf("bundle %q file %q: %w", storage.DisplayPath(relativeRoot), manifestFile.Path, err) } entry, err := backend.Stat(ctx, filePath) if err != nil { return Bundle{}, fmt.Errorf("bundle %q file %q stat: %w", storage.DisplayPath(relativeRoot), manifestFile.Path, err) } if entry.Type != storage.EntryTypeFile { return Bundle{}, fmt.Errorf("bundle %q file %q must be a regular file", storage.DisplayPath(relativeRoot), manifestFile.Path) } if entry.Size != manifestFile.Size { return Bundle{}, fmt.Errorf("bundle %q file %q size mismatch: got %d want %d", storage.DisplayPath(relativeRoot), manifestFile.Path, entry.Size, manifestFile.Size) } data, err := backend.ReadFile(ctx, filePath) if err != nil { return Bundle{}, fmt.Errorf("bundle %q file %q read: %w", storage.DisplayPath(relativeRoot), manifestFile.Path, err) } actualDigest := FileDigest(data) if actualDigest != manifestFile.SHA256 { return Bundle{}, fmt.Errorf("bundle %q file %q sha256 mismatch: got %s want %s", storage.DisplayPath(relativeRoot), manifestFile.Path, actualDigest, manifestFile.SHA256) } manifest.Files[index].SHA256 = actualDigest manifest.Files[index].Size = int64(len(data)) } actualBundleDigest := BundleDigest(manifest.Files) if actualBundleDigest != manifest.Digest { return Bundle{}, fmt.Errorf("bundle %q digest mismatch: got %s want %s", storage.DisplayPath(relativeRoot), actualBundleDigest, manifest.Digest) } return Bundle{ RootRelativePath: relativeRoot, Manifest: manifest, }, nil }