package release import ( "encoding/json" "os" "slices" "strings" ) func replacementReconstruction(g HistoryReconstruction, prior *reconstructionIntent) (HistoryReconstruction, error) { raw, err := json.Marshal(prior.Grant) if err != nil || reconstructionDigest(raw) != prior.Digest || g.Replaces != prior.Digest || g.Identity != prior.Grant.Identity || g.Revision != prior.Grant.Revision || g.Account != prior.Grant.Account || g.App != prior.Grant.App || g.Operation == prior.Grant.Operation || len(prior.Images) != len(prior.Grant.Hosts)+2 { return g, os.ErrPermission } // Copy signed input before adding restrictive evidence from the old intent. g.Hosts = append([]HistoryReconstructionHost(nil), g.Hosts...) holds := map[string]HistoryReconstructionHold{} for channel, bound := range g.Holds { holds[channel] = bound } g.Holds = holds for i := range g.Hosts { g.Hosts[i].Refusals = append([]Refusal(nil), g.Hosts[i].Refusals...) } expected := map[string]bool{rootBakeKey: true, historyAppKey(g.Account, g.App): true} for _, old := range prior.Grant.Hosts { if !slices.ContainsFunc(g.Hosts, func(h HistoryReconstructionHost) bool { return h.Host == old.Host }) { return g, os.ErrPermission } expected[historyKey(HistoryBinding{Host: old.Host, Account: g.Account, App: g.App})] = true } for _, im := range prior.Images { if !expected[im.Name] { return g, os.ErrPermission } delete(expected, im.Name) switch im.Name { case historyAppKey(g.Account, g.App): var app historyApp if strictCanonicalJSON(im.After, &app) != nil || !validHistoryApp(app) || app.Account != g.Account || app.App != g.App { return g, os.ErrPermission } case rootBakeKey: var bake rootBakeRecord if strictCanonicalJSON(im.After, &bake) != nil || !validRootBake(bake) { return g, os.ErrPermission } if p := bake.Clock.Production[prodKey(g.Account, g.App)]; p != nil { g.Production = g.Production || p.WasProduction if p.NewestSetAt.After(g.ProductionSetAt) { g.ProductionSetAt = p.NewestSetAt } } for key, mem := range bake.Clock.Holds { if !strings.HasPrefix(key, holdKeyPrefix(g.Account, g.App)) { continue } channel := strings.TrimPrefix(key, holdKeyPrefix(g.Account, g.App)) b := g.Holds[channel] if mem.Window > b.Window { b.Window = mem.Window } if mem.SetAt.After(b.SetAt) { b.SetAt = mem.SetAt } g.Holds[channel] = b } default: var r historyRecord if strictCanonicalJSON(im.After, &r) != nil || !validHistory(r, r.Binding) || r.Binding.Account != g.Account || r.Binding.App != g.App || historyKey(r.Binding) != im.Name || r.Attempt != nil { return g, os.ErrPermission } for i := range g.Hosts { if g.Hosts[i].Host != r.Binding.Host { continue } for _, ref := range r.Refusals.Values { ref.Recovered = false if !slices.Contains(g.Hosts[i].Refusals, ref) { g.Hosts[i].Refusals = append(g.Hosts[i].Refusals, ref) } } } } } return g, nil }