package bundle import ( "bytes" "encoding/json" "fmt" "io" "os" "path/filepath" "time" ) type rawManifest struct { SchemaVersion *int `json:"schema_version"` ID *string `json:"id"` Digest *string `json:"digest"` Created *string `json:"created"` Files []rawManifestFile `json:"files"` } type rawManifestFile struct { Path *string `json:"path"` SHA256 *string `json:"sha256"` Size *int64 `json:"size"` } func ParseManifest(data []byte) (Manifest, error) { decoder := json.NewDecoder(bytes.NewReader(data)) var raw rawManifest if err := decoder.Decode(&raw); err != nil { return Manifest{}, fmt.Errorf("parse manifest: %w", err) } var extra any if err := decoder.Decode(&extra); err != io.EOF { return Manifest{}, fmt.Errorf("parse manifest: trailing data") } var manifest Manifest if raw.SchemaVersion == nil { return Manifest{}, fmt.Errorf("manifest schema_version is required") } manifest.SchemaVersion = *raw.SchemaVersion if raw.ID == nil || *raw.ID == "" { return Manifest{}, fmt.Errorf("manifest id is required") } manifest.ID = *raw.ID if raw.Digest == nil || *raw.Digest == "" { return Manifest{}, fmt.Errorf("manifest digest is required") } manifest.Digest = *raw.Digest if raw.Created == nil || *raw.Created == "" { return Manifest{}, fmt.Errorf("manifest created is required") } created, err := time.Parse(time.RFC3339, *raw.Created) if err != nil { return Manifest{}, fmt.Errorf("manifest created must be RFC3339: %w", err) } manifest.Created = created if len(raw.Files) == 0 { return Manifest{}, fmt.Errorf("manifest files is required") } for index, rawFile := range raw.Files { file, err := parseManifestFile(index, rawFile) if err != nil { return Manifest{}, err } manifest.Files = append(manifest.Files, file) } if err := ValidateManifest(manifest); err != nil { return Manifest{}, fmt.Errorf("manifest %w", err) } return manifest, nil } func parseManifestFile(index int, raw rawManifestFile) (ManifestFile, error) { if raw.Path == nil || *raw.Path == "" { return ManifestFile{}, fmt.Errorf("manifest files[%d].path is required", index) } if raw.SHA256 == nil || *raw.SHA256 == "" { return ManifestFile{}, fmt.Errorf("manifest files[%d].sha256 is required", index) } if raw.Size == nil { return ManifestFile{}, fmt.Errorf("manifest files[%d].size is required", index) } return ManifestFile{ Path: *raw.Path, SHA256: *raw.SHA256, Size: *raw.Size, }, nil } func MarshalManifest(manifest Manifest) ([]byte, error) { if err := ValidateManifest(manifest); err != nil { return nil, err } data, err := json.MarshalIndent(manifest, "", " ") if err != nil { return nil, err } return append(data, '\n'), nil } func LoadManifest(root string) (Manifest, error) { data, err := os.ReadFile(filepath.Join(root, ManifestName)) if err != nil { return Manifest{}, fmt.Errorf("read manifest: %w", err) } return ParseManifest(data) } func WriteManifest(root string, manifest Manifest, opts WriteManifestOptions) error { data, err := MarshalManifest(manifest) if err != nil { return err } manifestPath := filepath.Join(root, ManifestName) if !opts.Overwrite { file, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666) if err != nil { return fmt.Errorf("write manifest: %w", err) } if _, err := file.Write(data); err != nil { _ = file.Close() return fmt.Errorf("write manifest: %w", err) } if err := file.Close(); err != nil { return fmt.Errorf("write manifest: %w", err) } return nil } dir := root if dir == "" { dir = "." } tmp, err := os.CreateTemp(dir, ".manifest-*.tmp") if err != nil { return fmt.Errorf("write manifest temp: %w", err) } tmpPath := tmp.Name() cleanup := true defer func() { if cleanup { _ = os.Remove(tmpPath) } }() if _, err := tmp.Write(data); err != nil { _ = tmp.Close() return fmt.Errorf("write manifest temp: %w", err) } if err := tmp.Close(); err != nil { return fmt.Errorf("write manifest temp: %w", err) } if err := os.Rename(tmpPath, manifestPath); err != nil { return fmt.Errorf("replace manifest: %w", err) } cleanup = false return nil }