Artifact bb180e237fd70c289a57ac19f2c923c7b3dd2ca7a28c2b9bd14c954b5e484861:
- File
internal/cone/horizon.go
— part of check-in
[5f637a3b62]
at
2026-10-21 22:15:03
on branch feat/horizon-cones
— feat(cone): serialize the horizon member as cone version 3
A horizon names one check-in by full hash, a held-set rule and a depth. Version 3 carries it between the count and the flagged members, a distinct version so a reader predating horizons refuses the cone instead of reading the lead-in as path members. Forms without a horizon are unchanged, so their digests hold. A horizon-only cone has no path members and is path-dense. Parse re-serializes and compares, so an upper-case hash or any other non-canonical input is refused.
HZ-3.3-1 and HZ-3.3-2 are now covered. (user: [email protected], size: 4815) [annotate] [blame] [check-ins using] [more...]
package cone import ( "errors" "fmt" "strings" ) // serialVersionHorizon is the version byte of the form for cones carrying a // horizon member (draft-fossil-horizon-00 §3.3). A distinct version, so a // reader predating horizons refuses the cone rather than reads its lead-in // as path members. const serialVersionHorizon = 3 // ErrInvalidHorizon: a horizon hash is not 40 or 64 hex digits, or a rule // or depth is not one of the defined values. var ErrInvalidHorizon = errors.New("cone: invalid horizon") // HorizonRule is the held-set rule a horizon names (§3.1). The values are // the serialized bytes; never renumber. type HorizonRule uint8 const ( // RuleDescendants holds C and every descendant of C: the default. RuleDescendants HorizonRule = 0 // RuleLeaves holds the descendants plus every open leaf's ancestry back // to its nearest common ancestor with C. RuleLeaves HorizonRule = 1 ) // HorizonDepth is which cut a horizon takes (§1.1). The values are the // serialized bytes; never renumber. type HorizonDepth uint8 const ( // DepthContent elides only blobs below the horizon (§4). DepthContent HorizonDepth = 0 // DepthStructural elides manifests and nodes below it too (§5). DepthStructural HorizonDepth = 1 ) func (r HorizonRule) valid() bool { return r == RuleDescendants || r == RuleLeaves } func (d HorizonDepth) valid() bool { return d == DepthContent || d == DepthStructural } // String names the rule as the operator spells it. func (r HorizonRule) String() string { switch r { case RuleDescendants: return "descendants" case RuleLeaves: return "leaves" } return fmt.Sprintf("rule(%d)", uint8(r)) } // String names the depth as the operator spells it. func (d HorizonDepth) String() string { switch d { case DepthContent: return "content" case DepthStructural: return "structural" } return fmt.Sprintf("depth(%d)", uint8(d)) } // ParseHorizonRule reads a rule by name. func ParseHorizonRule(name string) (HorizonRule, error) { for _, r := range []HorizonRule{RuleDescendants, RuleLeaves} { if r.String() == name { return r, nil } } return 0, fmt.Errorf("%w: unknown rule %q", ErrInvalidHorizon, name) } // ParseHorizonDepth reads a depth by name. func ParseHorizonDepth(name string) (HorizonDepth, error) { for _, d := range []HorizonDepth{DepthContent, DepthStructural} { if d.String() == name { return d, nil } } return 0, fmt.Errorf("%w: unknown depth %q", ErrInvalidHorizon, name) } // Horizon is the temporal member of a cone (§3.1): a check-in C by full // artifact hash, the rule deriving the held set from it, and the depth of // the cut. It is a comparable value; build one with NewHorizon. type Horizon struct { hash string // 40 or 64 lower-case hex digits rule HorizonRule depth HorizonDepth } // NewHorizon validates and builds a horizon. The hash is 40 (SHA-1) or 64 // (SHA-3) hex digits; case is not an error, the stored form is lower-case. func NewHorizon(hash string, rule HorizonRule, depth HorizonDepth) (Horizon, error) { if len(hash) != 40 && len(hash) != 64 { return Horizon{}, fmt.Errorf("%w: hash length %d", ErrInvalidHorizon, len(hash)) } for i := 0; i < len(hash); i++ { if !isHexDigit(hash[i]) { return Horizon{}, fmt.Errorf("%w: hash is not hex", ErrInvalidHorizon) } } if !rule.valid() || !depth.valid() { return Horizon{}, fmt.Errorf("%w: %v, %v", ErrInvalidHorizon, rule, depth) } return Horizon{hash: strings.ToLower(hash), rule: rule, depth: depth}, nil } func isHexDigit(b byte) bool { return b >= '0' && b <= '9' || b >= 'a' && b <= 'f' || b >= 'A' && b <= 'F' } // Hash is C's full artifact hash, lower-case. func (h Horizon) Hash() string { return h.hash } // Rule is the held-set rule. func (h Horizon) Rule() HorizonRule { return h.rule } // Depth is the depth of the cut. func (h Horizon) Depth() HorizonDepth { return h.depth } // String renders the horizon for diagnostics. func (h Horizon) String() string { return fmt.Sprintf("horizon %s %s %s", h.hash, h.rule, h.depth) } // NewWithHorizon is New for a cone that also carries a horizon. With no // inclusions the cone is horizon-only: path-dense, covering every path. func NewWithHorizon(include, exclude []string, h Horizon) (*Cone, error) { if len(h.hash) != 40 && len(h.hash) != 64 { return nil, fmt.Errorf("%w: horizon not built by NewHorizon", ErrInvalidHorizon) } return build(include, exclude, &h) } // Horizon returns the cone's horizon member, if it has one. func (c *Cone) Horizon() (Horizon, bool) { if c.horizon == nil { return Horizon{}, false } return *c.horizon, true } // pathDense reports a horizon-only cone: no path members, so every path is // covered and only the horizon narrows what is held. func (c *Cone) pathDense() bool { return len(c.include) == 0 }