package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" ) // manifestVersion is bumped when the manifest's shape changes in a way // that older readers cannot handle. const manifestVersion = 1 // manifestTrack records what a fixture is supposed to be, so a test can // assert against the spec rather than against whatever happens to be on // disk. type manifestTrack struct { Path string `json:"path"` Case string `json:"case"` Format string `json:"format"` DurationMS int64 `json:"durationMs"` FreqHz float64 `json:"freqHz"` Cover string `json:"cover,omitempty"` CoverSHA string `json:"coverSha,omitempty"` Tags map[string]any `json:"tags"` } // manifest describes a generated fixture library. // // Hash covers the *logical* spec — paths, formats, durations, tags, // cover identity — and deliberately not the encoded bytes: ffmpeg // stamps its own encoder strings, so byte hashes differ between ffmpeg // builds while the fixtures they describe are identical. type manifest struct { Version int `json:"version"` Generator string `json:"generator"` Hash string `json:"hash"` LibraryRoot string `json:"libraryRoot"` BrokenRoot string `json:"brokenRoot"` // TrackCount is written by the bulk library, which has tens of // thousands of tracks and no reason to describe each one: nothing // selects a bulk track by name, only the total matters. TrackCount int `json:"trackCount,omitempty"` Cases map[string][]string `json:"cases"` Tracks []manifestTrack `json:"tracks"` Extras []string `json:"extras"` Broken []string `json:"broken"` } // buildManifest derives the manifest from the spec alone. It runs // before any file is written, which is what lets generation be skipped // when the on-disk manifest already matches. func buildManifest(libraryRoot, brokenRoot string) (*manifest, error) { m := &manifest{ Version: manifestVersion, Generator: "gentestdata", LibraryRoot: filepath.ToSlash(libraryRoot), BrokenRoot: filepath.ToSlash(brokenRoot), Cases: map[string][]string{}, Tracks: make([]manifestTrack, 0, len(libraryFixtures)), } for _, f := range libraryFixtures { track := manifestTrack{ Path: f.Rel, Case: f.Case, Format: string(f.Format), DurationMS: f.Duration.Milliseconds(), FreqHz: f.FreqHz, Cover: f.Cover, Tags: f.Tags.changes(), } if f.Cover != "" { img, err := coverJPEG(f.Cover) if err != nil { return nil, err } sum := sha256.Sum256(img) track.CoverSHA = hex.EncodeToString(sum[:]) } m.Tracks = append(m.Tracks, track) m.Cases[f.Case] = append(m.Cases[f.Case], f.Rel) } for _, e := range libraryExtras { m.Extras = append(m.Extras, e.Rel) } for _, b := range brokenFiles { m.Broken = append(m.Broken, b.Rel) m.Cases[caseBroken] = append(m.Cases[caseBroken], b.Rel) } hash, err := hashManifest(m) if err != nil { return nil, err } m.Hash = hash return m, nil } // hashManifest hashes everything except the hash field itself. func hashManifest(m *manifest) (string, error) { clone := *m clone.Hash = "" raw, err := json.Marshal(clone) if err != nil { return "", fmt.Errorf("marshal manifest for hashing: %w", err) } sum := sha256.Sum256(raw) return hex.EncodeToString(sum[:]), nil } // writeManifest persists the manifest next to (not inside) the library // root, so the scanner never sees it as a stray file. func writeManifest(path string, m *manifest) error { raw, err := json.MarshalIndent(m, "", " ") if err != nil { return fmt.Errorf("marshal manifest: %w", err) } if err := os.MkdirAll(filepath.Dir(path), dirPerm); err != nil { return fmt.Errorf("mkdir %s: %w", filepath.Dir(path), err) } if err := os.WriteFile(path, append(raw, '\n'), filePerm); err != nil { return fmt.Errorf("write manifest %s: %w", path, err) } return nil } // readManifestHash returns the hash recorded in an existing manifest, // or "" when there is no readable manifest at path. func readManifestHash(path string) string { raw, err := os.ReadFile(path) if err != nil { return "" } var m manifest if err := json.Unmarshal(raw, &m); err != nil { return "" } return m.Hash }