package engine import ( "errors" "fmt" "os" "path/filepath" "strings" ) // seedMarkerFile names the file, in the flat dictionary directory, that records the // version the directory was first seeded as. It is dot-prefixed so OpenWithVersions' // version scan skips it (like the .staging upload area). const seedMarkerFile = ".seed_version" // checkSeedMarker reconciles the flat dictionary directory's recorded seed version // with bootVersion, guarding the seed-drift footgun. // // The contour's dictionary lives on a named volume seeded from the image once and // never re-seeded (deploy/docker-compose.yml). The flat directory's DAWG files carry // no embedded version, so their version is only the label BACKEND_DICT_VERSION gives // them. Bumping the build seed on a live volume would therefore relabel the already // seeded bytes: games that pinned the old label become unreplayable (voided) and new // games would silently use the wrong dictionary. checkSeedMarker records the seed // version on a fresh directory and, on every later boot, returns an error when // bootVersion no longer matches the recorded seed — so an operator changes a live // dictionary by uploading the new release through the admin console, or wipes the // volume to re-seed, never by bumping the build seed (docs/ARCHITECTURE.md §5). // // A directory that cannot be written makes the first record fail; that already breaks // the admin console (which writes version subdirectories here), so the error is // returned rather than swallowed, matching the package's fail-loud dictionary setup. func checkSeedMarker(dir, bootVersion string) error { path := filepath.Join(dir, seedMarkerFile) data, err := os.ReadFile(path) switch { case err == nil: if recorded := strings.TrimSpace(string(data)); recorded != bootVersion { return fmt.Errorf("engine: dictionary volume was seeded as %q but BACKEND_DICT_VERSION is %q; "+ "change a live dictionary by uploading the release through the admin console, or wipe the "+ "volume to re-seed — do not bump the build seed (docs/ARCHITECTURE.md §5)", recorded, bootVersion) } return nil case errors.Is(err, os.ErrNotExist): if err := os.WriteFile(path, []byte(bootVersion+"\n"), 0o644); err != nil { return fmt.Errorf("engine: record dictionary seed marker %s: %w", path, err) } return nil default: return fmt.Errorf("engine: read dictionary seed marker %s: %w", path, err) } }