package provision import ( "bytes" "encoding/json" "errors" "io" "strings" "github.com/carlosframework/platform/internal/maintenanceguard" ) var ErrMaintenanceProjection = errors.New("provision: invalid maintenance projection or protected fields") // MaintenanceProjection retains a present projection, including invalid input. // Its value receiver validates on marshal, so an ignored decode error cannot // turn a present invalid projection into an absent one. An empty slice is absent. type MaintenanceProjection []byte func (p *MaintenanceProjection) UnmarshalJSON(raw []byte) error { *p = append((*p)[:0], raw...) _, err := maintenanceguard.DecodeProjection(raw) if err != nil { return ErrMaintenanceProjection } return nil } func (p MaintenanceProjection) MarshalJSON() ([]byte, error) { if _, err := maintenanceguard.DecodeProjection(p); err != nil { return nil, ErrMaintenanceProjection } return append([]byte(nil), p...), nil } // DecodeRecord is the bucket-record parser. API response views may use ordinary // JSON decoding; they do not authorize writes. Keep this off Record's method // set so embedding Record cannot swallow a wrapper's fields or Gate shadow. func DecodeRecord(raw []byte) (Record, error) { invalid := Record{Maintenance: MaintenanceProjection("null")} dec := json.NewDecoder(bytes.NewReader(raw)) first, e := dec.Token() if e != nil || first != json.Delim('{') { return invalid, ErrMaintenanceProjection } seen := false for dec.More() { key, e := dec.Token() if e != nil { return invalid, ErrMaintenanceProjection } name, ok := key.(string) if !ok { return invalid, ErrMaintenanceProjection } if strings.EqualFold(name, "maintenance") { if name != "maintenance" || seen { return invalid, ErrMaintenanceProjection } seen = true } var value json.RawMessage if dec.Decode(&value) != nil { return invalid, ErrMaintenanceProjection } } if end, e := dec.Token(); e != nil || end != json.Delim('}') { return invalid, ErrMaintenanceProjection } if _, e = dec.Token(); e != io.EOF { return invalid, ErrMaintenanceProjection } var rec Record if json.Unmarshal(raw, &rec) != nil { return invalid, ErrMaintenanceProjection } return rec, nil } // Equality preserves the projection, not permission to change a fenced record. // Durable guard and lifecycle checks belong to the maintenance integration. func protectedFieldsEqual(before, after Record) bool { if before.RecordSeq != after.RecordSeq || before.Account != after.Account || before.App != after.App || before.Host != after.Host || !before.CreatedAt.Equal(after.CreatedAt) { return false } if len(before.Maintenance) == 0 || len(after.Maintenance) == 0 { return len(before.Maintenance) == len(after.Maintenance) } a, e := maintenanceguard.DecodeProjection(before.Maintenance) if e != nil { return false } b, e := maintenanceguard.DecodeProjection(after.Maintenance) return e == nil && a == b }