Artifact 6a62eef3dfbb9dbfe8a2c83d6e2b0c0912dae33ba367ef8a2f1bf74102ef047d:
- File
internal/artifact/artifact.go
— part of check-in
[3129a9afe0]
at
2026-08-15 10:40:01
on branch trunk
— artifact: structural-artifact parser per the artifact-format RFC
Framing gates, escape decoding, card grammar/ordering, type classification, Z verification; corpus testdata from the fixture. (user: dmestas, size: 8825) [annotate] [blame] [check-ins using]
// Package artifact parses Fossil structural artifacts per the published // artifact-format RFC (draft-fossil-artifact-format-00; §-cites below // refer to it). This is quarry's half of the 0001 §2 contract: zit // transports whole artifacts out of its private container, quarry // interprets them against the public spec. // // Parse returns (nil, nil) for content that fails the framing gates — // an ordinary blob, not an error. It returns a non-nil error only for // input that passed framing (so was plausibly intended as structure) // but violates the card grammar; ingest may log those, but stores treat // them as content too. package artifact import ( "bytes" "crypto/md5" "encoding/hex" "fmt" "strings" ) // Type is the artifact classification (§5.1). type Type int const ( Manifest Type = iota + 1 // "checkin" Cluster // "cluster" Control // "tag" Wiki // "wiki" Ticket // "ticket" Attachment // "attachment" Technote // "technote" Forum // "forumpost" ) func (t Type) String() string { names := [...]string{"", "checkin", "cluster", "tag", "wiki", "ticket", "attachment", "technote", "forumpost"} if int(t) < len(names) { return names[t] } return "unknown" } // File is one F card (§4.7.6). An empty Hash in a delta manifest marks // deletion. type File struct { Name string Hash string Perm string OldName string } // Tag is one T card (§4.7.16). Name keeps its sign prefix (+, -, *); // Target is a hash or "*" (self). type Tag struct { Name string Target string Value string } // Field is one J card (§4.7.8). type Field struct { Name string Value string } // Attach is the A card (§4.7.2). type Attach struct { Name string Target string Source string } // Cherrypick is one Q card (§4.7.14). Sign is '+' or '-'. type Cherrypick struct { Sign byte Hash string } // Artifact is one parsed structural artifact. type Artifact struct { Type Type Comment string // C Date string // D, raw canonical token User string // U ("anonymous" when empty per §4.7.17) Mimetype string // N Baseline string // B Files []File // F Parents []string // P Tags []Tag // T Cherrypicks []Cherrypick // Q RCard string // R (structurally validated only, §6.3.1) TicketID string // K Fields []Field // J WikiTitle string // L Body []byte // W EventTime string // E token 1 EventID string // E token 2 ThreadRoot string // G InReplyTo string // I ThreadTitle string // H Attachment *Attach // A Members []string // M } // Parse classifies and parses raw stored bytes. (nil, nil) means the // input is an opaque content blob. func Parse(raw []byte) (*Artifact, error) { body, ok := frame(raw) if !ok { return nil, nil } p := &parser{rest: body} a, err := p.parse() if err != nil { return nil, err } return a, nil } // frame runs the §4.1 gates: LF-terminated, signature wrapper stripped, // line-1 shape, Z self-checksum. A failed gate means "content". func frame(raw []byte) ([]byte, bool) { if len(raw) == 0 || raw[len(raw)-1] != '\n' { // gate 0, raw bytes return nil, false } body := stripSignature(raw) // Gate 2 (§4.1 step 3): stripped content ≥10 bytes, first byte A-Z, // second byte exactly one space (argument-less first card rejected). if len(body) < 10 || body[0] < 'A' || body[0] > 'Z' || body[1] != ' ' { return nil, false } // Gate 3: Z self-checksum — verify when present, pass when absent // (absence is caught by the per-type required-card rule, §5.2). if n := len(body); n >= 35 && body[n-35] == 'Z' && body[n-34] == ' ' { sum := md5.Sum(body[:n-35]) if hex.EncodeToString(sum[:]) != string(body[n-33:n-1]) { return nil, false } } return body, true } // stripSignature removes a PGP/SSH clearsign wrapper (§4.1.1). func stripSignature(raw []byte) []byte { if !bytes.HasPrefix(raw, []byte("-----BEGIN PGP SIGNED MESSAGE-----")) && !bytes.HasPrefix(raw, []byte("-----BEGIN SSH SIGNED MESSAGE-----")) { return raw } start := raw if i := bytes.Index(raw, []byte("\n\n")); i >= 0 { start = raw[i+2:] } else if i := bytes.Index(raw, []byte("\n\r\n")); i >= 0 { start = raw[i+3:] } else { return raw // no blank line: wholly unstripped (§4.1.1) } // Backward truncate at the signature trailer, keeping its lead LF. for _, marker := range []string{"\n-----BEGIN PGP SIGNATURE----", "\n-----BEGIN SSH SIGNATURE----"} { if i := bytes.LastIndex(start, []byte(marker)); i >= 0 { return start[:i+1] } } return start } // singleOccurrence lists the cards a second copy of which rejects (§4.5.1). const singleOccurrence = "ABCDEGHIKLNRUW" // typeOf maps type-setting card letters (§5.3). Zero = not type-setting. var typeOf = map[byte]Type{ 'A': Attachment, 'B': Manifest, 'F': Manifest, 'Q': Manifest, 'R': Manifest, 'E': Technote, 'G': Forum, 'H': Forum, 'I': Forum, 'J': Ticket, 'K': Ticket, 'L': Wiki, 'M': Cluster, } // legality is the §5.2 decision table: allowed and required card sets. var legality = map[Type]struct{ allowed, required string }{ Manifest: {"BCDFNPQRTUZ", "DZ"}, Cluster: {"MZ", "MZ"}, Control: {"DTUZ", "DTUZ"}, Wiki: {"CDLNPUWZ", "DLUWZ"}, Ticket: {"DJKUZ", "DJKUZ"}, Attachment: {"ACDNUZ", "ADZ"}, Technote: {"CDENPTUWZ", "DEWZ"}, Forum: {"DGHINPUWZ", "DUWZ"}, } type parser struct { rest []byte a Artifact seen [26]bool // per-letter presence prev byte // last card letter, for §4.4 ordering // last raw sort keys for the repeatable-card intra-run checks (§4.5.2) lastFile, lastField, lastMember string lastTagName, lastTagTarget string } func (p *parser) parse() (*Artifact, error) { for len(p.rest) > 0 { line, rest, _ := bytes.Cut(p.rest, []byte{'\n'}) p.rest = rest if err := p.card(line); err != nil { return nil, err } } return p.finish() } func (p *parser) card(line []byte) error { if len(line) == 0 || line[0] < 'A' || line[0] > 'Z' { return fmt.Errorf("artifact: bad card line %q", line) } letter := line[0] if strings.IndexByte("ABCDEFGHIJKLMNPQRTUWZ", letter) < 0 { return fmt.Errorf("artifact: undefined card %c (§4.2.2)", letter) } var tokens []string switch { case len(line) == 1: // argument-less case line[1] != ' ': return fmt.Errorf("artifact: card %c: byte after letter must be space", letter) default: for _, tok := range bytes.Split(line[2:], []byte{' '}) { if len(tok) == 0 { return fmt.Errorf("artifact: card %c: empty token", letter) } tokens = append(tokens, string(tok)) } } // Ordering (§4.4): non-decreasing, with the technote N-after-P // exception. if letter < p.prev && !(letter == 'N' && p.prev == 'P' && p.seen['E'-'A']) { return fmt.Errorf("artifact: card %c out of order after %c", letter, p.prev) } p.prev = letter // Occurrence class (§4.5.1). if strings.IndexByte(singleOccurrence, letter) >= 0 && p.seen[letter-'A'] { return fmt.Errorf("artifact: duplicate %c card", letter) } p.seen[letter-'A'] = true return p.apply(letter, tokens) } // finish resolves type and runs the post-classification checks. func (p *parser) finish() (*Artifact, error) { // Type: last-processed type-setting card wins (§5.3); the ordering // rule makes that the alphabetically latest one, so a forward scan // that overwrites is exactly right — apply() did that. Fallback: if p.a.Type == 0 { if p.seen['C'-'A'] || len(p.a.Files) > 0 || p.seen['P'-'A'] { p.a.Type = Manifest } else { p.a.Type = Control } } rule := legality[p.a.Type] for l := byte('A'); l <= 'Z'; l++ { if p.seen[l-'A'] && strings.IndexByte(rule.allowed, l) < 0 { return nil, fmt.Errorf("artifact: card %c not allowed in %s", l, p.a.Type) } } for i := 0; i < len(rule.required); i++ { if !p.seen[rule.required[i]-'A'] { return nil, fmt.Errorf("artifact: %s missing required %c card", p.a.Type, rule.required[i]) } } // Per-type semantic checks (§5.4). switch p.a.Type { case Control: for _, t := range p.a.Tags { if t.Target == "*" { return nil, fmt.Errorf("artifact: control artifact with self-referential tag") } } case Technote: for _, t := range p.a.Tags { if t.Target != "*" || !strings.HasPrefix(t.Name, "+") { return nil, fmt.Errorf("artifact: technote tag must be self-referential +tag") } } case Forum: if p.seen['H'-'A'] && p.seen['I'-'A'] { return nil, fmt.Errorf("artifact: forum post with both H and I cards") } if len(p.a.Parents) > 1 { return nil, fmt.Errorf("artifact: forum post with merge parents") } } if p.a.User == "" { p.a.User = "anonymous" // §4.7.17 } return &p.a, nil }