Artifact 88789bfa593f17514357e6a5c6577678088323f60a098f7459ad72fad47628b8:
- File
internal/xfer/container.go
— part of check-in
[dc058fa551]
at
2026-08-24 19:16:15
on branch clone-counts
— Count clones and fetches
A clone reads and writes nothing, so it is the one kind of forge activity that leaves no trace in the DAG at all.
The sync edge classifies the protocol's cards: clone SEQNO 1 opens a new clone, later rounds do not, so a big store cloned in forty rounds still counts once. Two sinks: the vocabulary event for anything reacting, and a JetStream counter for how many, which converges across nodes instead of losing increments to a read-modify-write. (user: dmestas, size: 6297) [annotate] [blame] [check-ins using] [more...]
// container.go — the sync protocol's compressed message container and // the card peeks quarry's edge needs (sync RFC §4.1, §6): a 4-byte // big-endian pre-compression length followed by a zlib stream. Quarry // inspects requests only enough to gate pushes; payload semantics stay // zit's business. package xfer import ( "bytes" "compress/zlib" "crypto/sha1" "encoding/binary" "encoding/hex" "fmt" "io" "net/http" "net/url" "strings" ) // decodeContainer returns the decompressed body of an // application/x-fossil message. Malformed containers return the bytes // as-is (the RFC: the receiver retains the original buffer and applies // ordinary card processing to it). func decodeContainer(raw []byte) []byte { if len(raw) < 6 { return raw } capacity := binary.BigEndian.Uint32(raw[:4]) zr, err := zlib.NewReader(bytes.NewReader(raw[4:])) if err != nil { return raw } defer zr.Close() out, err := io.ReadAll(io.LimitReader(zr, int64(capacity)+1)) if err != nil || uint64(len(out)) > uint64(capacity) { return raw } return out } // encodeContainer wraps a plaintext message for the wire. func encodeContainer(body []byte) []byte { var buf bytes.Buffer var lenb [4]byte binary.BigEndian.PutUint32(lenb[:], uint32(len(body))) buf.Write(lenb[:]) zw := zlib.NewWriter(&buf) zw.Write(body) zw.Close() return buf.Bytes() } // pushRequest holds what the gate needs from a request: whether it // mutates, and the login card's parts with the nonce-covered region. type pushRequest struct { hasPush bool // newClone is a clone card opening a NEW clone, not a later round of // one. The protocol says "first sequence is 1" (sync draft §8), so // SEQNO 1 is the only round that means somebody started cloning — // counting every clone card would count ROUNDS, and a big store // takes many. newClone bool // hasPull is a pull card. Unlike newClone this is per ROUND, because // the protocol gives a fetch no opening marker: "push, pull, and // sync requests carry push, pull, or both each round". hasPull bool projectCode string // from the push card user string nonce string sig string covered []byte // bytes after the login line's LF, through end } // isFossilWS matches the sync protocol's card-token separators (§5.2: // SP, HTAB, VT, FF, CR — LF terminates the line, never separates). The // gate MUST tokenize exactly as zit's tokenizeCard does: a card the // backend honors but the gate misses is a full auth bypass. In // particular leading whitespace is skippable, so a card keyword is not // anchored at byte 0. func isFossilWS(r rune) bool { switch r { case ' ', '\t', 0x0b, 0x0c, '\r': return true } return false } // cardTokens tokenizes one card line the way zit does — runs of Fossil // whitespace (including a leading run) separate tokens. func cardTokens(line []byte) []string { return strings.FieldsFunc(string(line), isFossilWS) } // scanRequest reads the decompressed card stream. The RFC places the // login card first; zit emits it after the client-version pragma, so // the scan accepts a login card in any position (the nonce covers the // bytes after that card's LF either way). Push cards may appear // anywhere among the operation cards. Classification is by first token // after Fossil-whitespace skipping — never a raw byte-prefix, which an // indented card would defeat while the backend still honored it. func scanRequest(body []byte) pushRequest { var pr pushRequest rest := body for { line, tail, found := bytes.Cut(rest, []byte("\n")) f := cardTokens(line) if len(f) > 0 && f[0] == "login" { if len(f) == 4 { pr.user, pr.nonce, pr.sig = defossilize(f[1]), f[2], f[3] pr.covered = tail } break } if !found { break } rest = tail } for _, line := range bytes.Split(body, []byte("\n")) { f := cardTokens(line) if len(f) == 0 { continue } switch f[0] { case "push": pr.hasPush = true if len(f) == 3 { pr.projectCode = f[2] } case "pull": pr.hasPull = true case "clone": // clone VERSION SEQNO. Arity is checked because a malformed // card must not be counted as a clone, and SEQNO is compared // as a string: the spec's own rule is digit-only recognition, // so "01" is not sequence one and neither is " 1". if len(f) == 3 && f[2] == "1" { pr.newClone = true } } } return pr } // cookieLogin parses the x-f-l-c cookie (RFC §6.5): the body-login // card with spaces as + and the USER percent-encoded. Its nonce covers // the entire decompressed body. func cookieLogin(req *http.Request, body []byte) (pushRequest, bool) { c, err := req.Cookie("x-f-l-c") if err != nil { return pushRequest{}, false } f := strings.Split(c.Value, "+") if len(f) != 4 || f[0] != "login" { return pushRequest{}, false } user, err := url.QueryUnescape(f[1]) if err != nil { return pushRequest{}, false } return pushRequest{user: defossilize(user), nonce: f[2], sig: f[3], covered: body}, true } // verifyLogin checks the RFC §6 chain: NONCE is SHA-1 over the covered // bytes; SIGNATURE is SHA-1 over nonce-hex || shared-secret-hex. func verifyLogin(pr pushRequest, secret string) bool { if pr.user == "" || len(pr.nonce) != 40 || len(pr.sig) != 40 { return false } nonce := sha1.Sum(pr.covered) if hex.EncodeToString(nonce[:]) != pr.nonce { return false } sig := sha1.Sum([]byte(pr.nonce + secret)) return hex.EncodeToString(sig[:]) == pr.sig } // SharedSecret derives the stored credential (RFC §6.3): SHA-1 of // "projectcode/user/password". Plaintext never persists. func SharedSecret(projectCode, user, password string) string { sum := sha1.Sum([]byte(projectCode + "/" + user + "/" + password)) return hex.EncodeToString(sum[:]) } // errorReply builds a complete application/x-fossil reply holding one // error card (message spaces fossilized as \s). func errorReply(msg string) []byte { return encodeContainer([]byte("error " + strings.ReplaceAll(msg, " ", `\s`) + "\n")) } // defossilize undoes \s escaping in a card token. func defossilize(s string) string { return strings.ReplaceAll(s, `\s`, " ") } // ErrNoProjectCode reports a handshake that produced no push card. var ErrNoProjectCode = fmt.Errorf("handshake reply carried no project code")