package hostbox import ( "context" "encoding/hex" "reflect" "time" ) // Parking selectors carry identities only. Root derives all paths, receipts, // sizes and deadlines independently. type ServingArtifactIdentity struct { Host, Box, Region, Incarnation, Location, Generation string Stamp, Session uint64 } type ServingArtifactDirectory struct { Device, Inode, Mount uint64 } type ServingLegacyPredecessor struct { Session uint64 Generation string Empty bool } type ServingParkRequest struct { Successor ServingArtifactIdentity Predecessor ServingArtifactIdentity Legacy ServingLegacyPredecessor Retirement string } type ServingParkResult struct { TagsFrom uint64 ID string Legacy bool Predecessor ServingArtifactIdentity OriginalSuccessor ServingArtifactIdentity Successor ServingArtifactIdentity Directory ServingArtifactDirectory } // Keep this tuple frozen when the public wire structs grow. func journalServingParkV4(p *ServingParkRequest) any { if p == nil { return nil } identity := func(s ServingArtifactIdentity) any { return []any{s.Host, s.Box, s.Region, s.Incarnation, s.Location, s.Generation, s.Stamp, s.Session} } return []any{identity(p.Successor), identity(p.Predecessor), p.Legacy.Session, p.Legacy.Generation, p.Legacy.Empty, p.Retirement} } func ServingParkOperation(op string) bool { return op == "serving-park" || op == "serving-park-legacy" || op == "serving-park-complete" || op == "serving-park-pressure" } func validateServingParkRequest(q JournalRequest) error { if q.V != 4 || q.ServingPark == nil || !ServingParkOperation(q.Op) { return ErrTypedJournal } // Reject every field belonging to a different journal operation, including // fields omitted from v4's hash. Only these selectors are remotely writable. canonical := JournalRequest{V: q.V, ID: q.ID, Op: q.Op, Nonce: q.Nonce, IssuedAt: q.IssuedAt, ServingPark: q.ServingPark} if !reflect.DeepEqual(q, canonical) { return ErrTypedJournal } nonce, err := hex.DecodeString(q.Nonce) if err != nil || len(nonce) != 16 || q.IssuedAt <= 0 { return ErrTypedJournal } if q.ID != journalRequestID(q) { return ErrChanged } p := *q.ServingPark s := p.Successor if s.Host == "" || s.Box == "" || s.Region == "" || s.Incarnation == "" || s.Location == "" || s.Generation == "" || s.Stamp == 0 || s.Session != s.Stamp { return ErrTypedJournal } switch q.Op { case "serving-park": if p.Legacy != (ServingLegacyPredecessor{}) || p.Retirement != "" { return ErrTypedJournal } case "serving-park-legacy": if p.Predecessor != (ServingArtifactIdentity{}) || p.Retirement != "" || (p.Legacy.Session == 0) != (p.Legacy.Generation == "") || p.Legacy.Session >= s.Session || (p.Legacy.Empty && p.Legacy.Session != 0) { return ErrTypedJournal } case "serving-park-complete": id, err := hex.DecodeString(p.Retirement) if err != nil || len(id) != 32 || p.Predecessor != (ServingArtifactIdentity{}) || p.Legacy != (ServingLegacyPredecessor{}) { return ErrTypedJournal } case "serving-park-pressure": // Pressure also needs a preadmission observed-claim selector. It remains // unavailable until the independent sizing and relief mechanism exists. return ErrUnsupported } return nil } type ServingParkClient struct{ Call JournalCall } func (c ServingParkClient) request(ctx context.Context, op string, p ServingParkRequest) (ServingParkResult, error) { if c.Call == nil { return ServingParkResult{}, ErrUnsupported } q, err := journalPrepare(JournalRequest{Op: op, ServingPark: &p}, time.Now()) if err != nil { return ServingParkResult{}, err } out, err := journalSend(ctx, c.Call, q, false) if err != nil { return ServingParkResult{}, err } if err = servingParkResponse(q, out); err != nil { return ServingParkResult{}, err } return *out.ServingPark, nil } func (c ServingParkClient) Park(ctx context.Context, successor, predecessor ServingArtifactIdentity) (ServingParkResult, error) { return c.request(ctx, "serving-park", ServingParkRequest{Successor: successor, Predecessor: predecessor}) } func (c ServingParkClient) ParkLegacy(ctx context.Context, successor ServingArtifactIdentity, predecessor ServingLegacyPredecessor) (ServingParkResult, error) { return c.request(ctx, "serving-park-legacy", ServingParkRequest{Successor: successor, Legacy: predecessor}) } func (c ServingParkClient) Complete(ctx context.Context, id string, successor ServingArtifactIdentity) error { _, err := c.request(ctx, "serving-park-complete", ServingParkRequest{Successor: successor, Retirement: id}) return err } func (c ServingParkClient) ReplayUncertain(ctx context.Context, uncertain *JournalUncertainError) (JournalResponse, error) { if c.Call == nil || uncertain == nil || validateServingParkRequest(uncertain.Request) != nil { return JournalResponse{}, ErrUnsupported } out, err := journalSend(ctx, c.Call, uncertain.Request, true) if err == nil { err = servingParkResponse(uncertain.Request, out) } return out, err } func servingParkResponse(q JournalRequest, out JournalResponse) error { p := q.ServingPark if p == nil || out.ServingPark == nil || out.ServingPark.Successor != p.Successor || out.ServingPark.ID == "" || (p.Retirement != "" && out.ServingPark.ID != p.Retirement) { return &JournalUncertainError{Request: q, Cause: ErrJournalUncertain} } return nil }