package nwsync import ( "crypto/sha1" "encoding/json" "fmt" "path" ) // AssembleOptions describes one merged manifest. type AssembleOptions struct { // ArtifactKeys are the depot keys of the artifacts to merge, in // Mod_HakList order — highest priority first. Each one's index is read // from the key beside it. ArtifactKeys []string TLKKey string // the TLK's key, if the manifest carries one OutDir string // write locally instead of uploading — the conformance path GroupID int // 1 = current, 2 = testing; 0 means absent ModuleName string Description string Sink sink // test seam; nil means OutDir or the zone } // AssembleResult reports what one assemble run produced. type AssembleResult struct { SHA1 string ManifestPath string Entries int } // Assemble merges the per-artifact NSYM manifests named by Order into one // manifest. It reads no bulk data at all — only the small index files. // // Merge rule is resref shadowing, not concatenation: a resref present in more // than one artifact resolves to the earliest artifact in Order, which is how // the game resolves it (upstream's resman adds haks in reverse and lets the // last one win). Get this backwards and the wrong texture ships silently. func Assemble(options AssembleOptions) (AssembleResult, error) { if len(options.ArtifactKeys) == 0 { return AssembleResult{}, fmt.Errorf("assemble: no artifact keys given") } target, err := openSink(options.OutDir, options.Sink) if err != nil { return AssembleResult{}, err } // The TLK carries no precedence — it is not a hak and shadows nothing — // so it merges last, after every hak has had its say. keys := append([]string{}, options.ArtifactKeys...) if options.TLKKey != "" { keys = append(keys, options.TLKKey) } merged := make([]Entry, 0, 1024) winner := make(map[Identity]bool, 1024) var onDiskBytes int64 for _, artifactKey := range keys { key, err := resolveIndexKey(artifactKey, options.OutDir) if err != nil { return AssembleResult{}, err } data, sidecarBody, err := target.getIndex(key) if err != nil { // An artifact with no index is an artifact whose emit never // finished. Publishing a manifest without it would ship a release // missing a hak, so this fails closed. return AssembleResult{}, fmt.Errorf("assemble: no index for %s: %w", artifactKey, err) } entries, err := readManifest(data) if err != nil { return AssembleResult{}, fmt.Errorf("%s: %w", target.describe(key), err) } sidecar, err := parseSidecar(target.describe(key), sidecarBody) if err != nil { return AssembleResult{}, err } // Two producers of blobs means a skewed emitter can write blobs the // merged manifest quietly disagrees with. Refuse to merge across // mismatched emitter versions. if sidecar.EmitterVersion != emitterVersion { return AssembleResult{}, fmt.Errorf( "assemble: emitter version mismatch: %s was emitted by emitter %q, this is emitter %q", artifactKey, sidecar.EmitterVersion, emitterVersion) } // on_disk_bytes overcounts by the handful of cross-artifact // duplicates. It is a display statistic; no dedupe pass for it. onDiskBytes += sidecar.OnDiskBytes for _, entry := range entries { identity := entry.identity() if winner[identity] { continue } winner[identity] = true merged = append(merged, entry) } } if len(merged) == 0 { return AssembleResult{}, fmt.Errorf("assemble: merged manifest is empty") } data, err := writeManifest(merged) if err != nil { return AssembleResult{}, err } sha1Hex := fmt.Sprintf("%x", sha1.Sum(data)) manifestKey := path.Join("manifests", sha1Hex) sidecar := Sidecar{ ModuleName: options.ModuleName, Description: options.Description, GroupID: options.GroupID, } if err := putManifestPair(target, manifestKey, data, merged, onDiskBytes, sidecar); err != nil { return AssembleResult{}, err } return AssembleResult{SHA1: sha1Hex, ManifestPath: target.describe(manifestKey), Entries: len(merged)}, nil } func parseSidecar(where string, data []byte) (Sidecar, error) { var sidecar Sidecar if err := json.Unmarshal(data, &sidecar); err != nil { return Sidecar{}, fmt.Errorf("%s: %w", where, err) } return sidecar, nil }