Artifact 050eb6d0fe395f70e9e2200057d44b813f91faad40726fbae26cf788e20dd96f:
- File
internal/horizon/name.go
— part of check-in
[6f12912a01]
at
2026-10-24 00:37:09
on branch main
— fix(horizon): walk back in one bounded query
ResolveName stepped back one query per check-in, up to 2^32-1 times. A recursive query now walks the primary parents at most depth steps and never more than the check-ins the repository holds, so even a corrupt cycle of primary parents ends. (user: [email protected], size: 3862) [annotate] [blame] [check-ins using] [more...]
package horizon import ( "database/sql" "errors" "fmt" "strings" "github.com/danmestas/go-libfossil/db" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" ) // ErrNameNotFound is returned when a horizon name resolves to no check-in. var ErrNameNotFound = errors.New("horizon: name resolves to no check-in") // NameTip is the name of the most recent check-in. const NameTip = "tip" // Resolved is the check-in a horizon name resolved to and how many steps // back along primary parents were taken to reach it. type Resolved struct { Hash string Steps uint32 } // ResolveName resolves a horizon name over q's check-ins as // draft-fossil-horizon-00 ยง6.2.7 does: tip is the most recent check-in, a // full hash names its check-in, and any other name, or a full hash naming no // check-in, is the most recent check-in carrying an active sym-NAME tag, // propagated or not. A tie on mtime goes to the lexicographically lowest // full hash, never a local rid, so every origin resolves a name alike. From there it steps back up // to depth times along primary parents, stopping early at a check-in with // none, in one query bounded by the check-ins the repository holds. func ResolveName(q db.Querier, name string, depth uint32) (Resolved, error) { if q == nil { panic("horizon.ResolveName: nil querier") } rid, err := namedCheckin(q, name) if err != nil { return Resolved{}, err } // One query walks back, at most depth steps and never more than there // are check-ins, so a corrupt cycle of primary parents ends too. var steps uint32 err = q.QueryRow(`WITH RECURSIVE back(rid, n) AS ( SELECT ?1, 0 UNION ALL SELECT p.pid, back.n+1 FROM back JOIN plink p ON p.cid=back.rid AND p.isprim=1 JOIN event e ON e.objid=p.pid AND e.type='ci' WHERE back.n < min(?2, (SELECT count(*) FROM event WHERE type='ci'))) SELECT rid, n FROM back ORDER BY n DESC LIMIT 1`, rid, int64(depth)).Scan(&rid, &steps) if err != nil { return Resolved{}, fmt.Errorf("horizon: primary parents: %w", err) } if steps > depth { panic("horizon.ResolveName: walked past the depth") } var uuid string if err := q.QueryRow("SELECT uuid FROM blob WHERE rid=?", rid).Scan(&uuid); err != nil { return Resolved{}, fmt.Errorf("horizon: resolved check-in: %w", err) } return Resolved{Hash: uuid, Steps: steps}, nil } // namedCheckin is the check-in name names before any step back. func namedCheckin(q db.Querier, name string) (libfossil.FslID, error) { if name == NameTip { return firstCheckin(q, `SELECT e.objid FROM event e JOIN blob b ON b.rid=e.objid WHERE e.type='ci' ORDER BY e.mtime DESC, b.uuid ASC LIMIT 1`) } if isFullHash(name) { rid, err := firstCheckin(q, `SELECT e.objid FROM blob b JOIN event e ON e.objid=b.rid AND e.type='ci' WHERE b.uuid=?`, strings.ToLower(name)) if !errors.Is(err, ErrNameNotFound) { return rid, err } } return firstCheckin(q, `SELECT e.objid FROM tagxref x JOIN tag t ON t.tagid=x.tagid JOIN event e ON e.objid=x.rid AND e.type='ci' JOIN blob b ON b.rid=e.objid WHERE t.tagname=? AND x.tagtype>0 ORDER BY e.mtime DESC, b.uuid ASC LIMIT 1`, "sym-"+name) } // firstCheckin is the rid query's first row names, ErrNameNotFound for none. func firstCheckin(q db.Querier, query string, args ...any) (libfossil.FslID, error) { var rid libfossil.FslID err := q.QueryRow(query, args...).Scan(&rid) if errors.Is(err, sql.ErrNoRows) { return 0, ErrNameNotFound } if err != nil { return 0, fmt.Errorf("horizon: resolve name: %w", err) } return rid, nil } // isFullHash reports a 40 or 64 digit hex string, of either case. func isFullHash(s string) bool { if len(s) != 40 && len(s) != 64 { return false } for i := 0; i < len(s); i++ { c := s[i] if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') { return false } } return true }