package catalogue import ( "bytes" "encoding/json" "fmt" "math" "regexp" "sort" "strings" "text/template" ) // A kind a seat receives as data, rendered by its holder (novox/hq ADR 0255). // // ADR 0212 made a contribution text in the tool's own grammar, which the controller places and never // reads. That is right where the tool is the seat's for good — a hotkey daemon's trigger lines — and // wrong where the seat's whole point is that its tool can be replaced: a module adding a battery to the // bar would write i3status-rust's TOML, and a second bar would have to read it. So a seat may define a // kind as data instead. The seat says the data's shape; a contributor gives data in that shape; the // holder gives a template that renders one piece of it into its tool's grammar, as a module's facts // template renders the roster (`facts`): the data is the mesh's, the format is the holder's. // // What the holder places is narrowed by the shape's placing fields — the bar's `bar` and `place` — in // the placeholder, `${contribution:::=,…}`, and every combination of them is // placed by the holder exactly once, so nothing a module contributes is dropped by a holder that forgot // a corner of the shape. // Shape is what a kind received as data looks like. type Shape struct { // Places are the fields a holder narrows its placeholders by, each with every value it takes. Places []PlaceField // Order is the whole-number field that orders the pieces within one placeholder (lowest first, // then module order); absent in a piece means OrderDefault. Order string // Check is what else is wrong with one piece's data, beyond its placing fields and its order. Check func(data map[string]any) []string `json:"-"` // Examples are pieces every holder's template must render, one for each variant the shape has, // so a holder that cannot render one is refused at registration and not found on a machine. Examples []map[string]any } // PlaceField is one field a holder places by, and the values it takes. type PlaceField struct { Field string Values []string } // OrderDefault is a piece's order when it gives none: the middle of 0–99. const OrderDefault = 50 // capabilityName is the shape of a capability's name, as a node's profile reports it. var capabilityName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // KnownCapabilities is every capability the node-engine detects (mesh-host internal/profile), the // names an `if-capability` may give (novox/hq ADR 0255). A name nothing detects would leave its // contribution out on every machine, silently, so it is refused. A detector added there is added here. var KnownCapabilities = []string{ "battery", "container-runtime", "firewall", "graphical-session", "overlay", "package-manager", "power-meter", "privileged", "seat", "service-manager", "uplink-dhcpcd", "uplink-networkmanager", "uplink-systemd-networkd", "virtualisation", } // renderFuncs are the functions a holder's template may call. `quote` writes a value as a JSON // string with DEL escaped as well — JSON leaves it bare, and TOML refuses it — which makes it a valid // basic string in TOML and in most tools' grammars, so a contributed command with quotes or control // characters in it cannot break out of the holder's line. var renderFuncs = template.FuncMap{"quote": quote} func quote(v any) (string, error) { b, err := json.Marshal(fmt.Sprint(v)) return strings.ReplaceAll(string(b), "\x7f", `\u007f`), err } // shapedProblems is what is wrong with one contribution of a kind received as data. func shapedProblems(module string, i int, r Receivable, c SeatContribution) []string { var problems []string at := fmt.Sprintf("%s's contribution %d to %s (%s)", module, i+1, c.Seat, c.Kind) if strings.TrimSpace(c.Content) != "" { problems = append(problems, at+" has content; this kind is data, given as `data` (novox/hq ADR 0255)") } if len(c.Data) == 0 { return append(problems, at+" has no data") } for _, p := range r.Shape.Places { v, _ := c.Data[p.Field].(string) if !oneOf(p.Values, v) { problems = append(problems, fmt.Sprintf("%s says %s %q; it is one of %s", at, p.Field, fmt.Sprint(c.Data[p.Field]), strings.Join(p.Values, ", "))) } } if r.Shape.Order != "" { if v, has := c.Data[r.Shape.Order]; has { if _, ok := wholeIn(v, 0, 99); !ok { problems = append(problems, fmt.Sprintf("%s says %s %v; it is a whole number from 0 to 99", at, r.Shape.Order, v)) } } } if r.Shape.Check != nil { for _, p := range r.Shape.Check(c.Data) { problems = append(problems, at+": "+p) } } return problems } // wholeIn is v as a whole number within [lo, hi]; JSON gives every number as a float. func wholeIn(v any, lo, hi int) (int, bool) { f, ok := v.(float64) if !ok { if i, isInt := v.(int); isInt { f, ok = float64(i), true } } if !ok || f != math.Trunc(f) || f < float64(lo) || f > float64(hi) { return 0, false } return int(f), true } // placeholderWhere parses the narrowing part of a placeholder, `bar=bottom,place=status`, against the // kind's shape. Empty narrows nothing. func placeholderWhere(r Receivable, where string) (map[string]string, error) { out := map[string]string{} if where == "" { return out, nil } if r.Shape == nil { return nil, fmt.Errorf("%s is text, which a placeholder does not narrow", r.Kind) } for _, pair := range strings.Split(where, ",") { field, value, ok := strings.Cut(pair, "=") var p *PlaceField for i := range r.Shape.Places { if r.Shape.Places[i].Field == field { p = &r.Shape.Places[i] } } switch { case !ok || p == nil: return nil, fmt.Errorf("%q narrows by %s, and %s is placed by %s", where, field, r.Kind, placeFields(r)) case !oneOf(p.Values, value): return nil, fmt.Errorf("%q says %s=%s; %s is one of %s", where, field, value, field, strings.Join(p.Values, ", ")) case out[field] != "": return nil, fmt.Errorf("%q names %s twice", where, field) } out[field] = value } return out, nil } func placeFields(r Receivable) string { var names []string for _, p := range r.Shape.Places { names = append(names, p.Field) } return strings.Join(names, ", ") } // holderTemplate is the holder's template for a kind, parsed. func holderTemplate(holder Manifest, seat Seat, kind string) (*template.Template, error) { for _, c := range holder.Claims { if s, known := SeatNamed(c.Name); !known || s.Name != seat.Name { continue } text, has := c.Renders[kind] if !has || strings.TrimSpace(text) == "" { break } t, err := template.New(seat.Name + ":" + kind).Funcs(renderFuncs).Parse(text) if err != nil { return nil, fmt.Errorf("%s's template for %s:%s does not parse: %v", holder.Module, seat.Name, kind, err) } return t, nil } return nil, fmt.Errorf("%s places %s:%s, which is data, and its claim gives no template for it in `renders` "+ "(novox/hq ADR 0255)", holder.Module, seat.Name, kind) } // renderPiece is one piece of data in the holder's grammar. The template sees the piece's fields and // `module`, the contributor. A value the piece does not have reads as nothing in a `with` or an `if`; // printed bare it would write `` into the tool's file, which is refused here instead. func renderPiece(t *template.Template, module string, data map[string]any) (string, error) { view := map[string]any{"module": module} for k, v := range data { view[k] = v } var b bytes.Buffer if err := t.Execute(&b, view); err != nil { return "", fmt.Errorf("rendering %s's piece: %v", module, err) } out := b.String() if strings.Contains(out, "") { return "", fmt.Errorf("rendering %s's piece printed a value it does not have: %q", module, out) } return out, nil } // placedPiece is one contributed piece on its way into a holder's file. type placedPiece struct { module string data map[string]any order int } // shapedContributions is every module's data of one kind to one seat, narrowed by where and by the // machine's capabilities, ordered, and rendered through the holder's template — each piece under a // comment line naming its module. // // A piece the template renders to nothing — a `shows` the holder does not know yet — or fails on is // left out and answered as unplaced, naming its module: the other pieces and the rest of the machine's // declaration go on (novox/hq ADR 0255). func shapedContributions(modules []Manifest, holder Manifest, s Seat, r Receivable, where map[string]string, caps map[string]bool) (string, []string, error) { var pieces []placedPiece for _, m := range inModuleOrder(modules) { for _, c := range m.allContributions() { if c.Kind != r.Kind || !capable(c, caps) { continue } if cs, known := SeatNamed(c.Seat); !known || cs.Name != s.Name { continue } match := true for field, value := range where { if v, _ := c.Data[field].(string); v != value { match = false } } if !match { continue } order := OrderDefault if v, ok := wholeIn(c.Data[r.Shape.Order], 0, 99); ok { order = v } pieces = append(pieces, placedPiece{module: m.Module, data: c.Data, order: order}) } } if len(pieces) == 0 { return "", nil, nil } t, err := holderTemplate(holder, s, r.Kind) if err != nil { return "", nil, err } sort.SliceStable(pieces, func(a, b int) bool { return pieces[a].order < pieces[b].order }) var b strings.Builder var unplaced []string for _, p := range pieces { out, err := renderPiece(t, p.module, p.data) if err == nil && strings.TrimSpace(out) == "" { err = fmt.Errorf("%s's template renders nothing for it", holder.Module) } if err != nil { unplaced = append(unplaced, fmt.Sprintf("%s's %s:%s %s is not placed by %s: %v", p.module, s.Name, r.Kind, describePiece(p.data), holder.Module, err)) continue } fmt.Fprintf(&b, "%s %s\n", r.Comment, p.module) b.WriteString(out) if !strings.HasSuffix(out, "\n") { b.WriteString("\n") } } return b.String(), unplaced, nil } // describePiece is a piece in one line, its fields in order, for a person reading why it was left out. func describePiece(data map[string]any) string { var parts []string for _, k := range sortedKeys(data) { if _, nested := data[k].(map[string]any); nested { continue } parts = append(parts, fmt.Sprintf("%s=%v", k, data[k])) } return "(" + strings.Join(parts, " ") + ")" } // capable is whether a contribution applies on a machine with these capabilities: always, unless it // names one with `if-capability` the machine did not report (novox/hq ADR 0255). A machine whose // capabilities are not known has none, so a conditional piece is left out rather than guessed in. func capable(c SeatContribution, caps map[string]bool) bool { return c.IfCapability == "" || caps[c.IfCapability] } // placementProblems is what is wrong with how a holder places the kinds its seats receive as data: // once it places one at all, it needs a template that renders every example of the shape, and every combination of the placing // fields placed exactly once across its files — or something contributed would be dropped, or written // twice (novox/hq ADR 0255). func (m Manifest) placementProblems() []string { var problems []string for _, c := range m.Claims { s, known := SeatNamed(c.Name) if !known { continue } for _, r := range s.Receives { // A holder that places none of the kind is older than it (novox/hq ADR 0255): refusing it // would refuse every bar registered before the kind existed, and the controller that // defines the kind ships first. Its contributions wait, unplaced, until it places them. if r.Shape == nil || !placesKind(m, s, r.Kind) { continue } t, err := holderTemplate(m, s, r.Kind) if err != nil { problems = append(problems, err.Error()) continue } for _, ex := range r.Shape.Examples { if out, err := renderPiece(t, "example", ex); err != nil || strings.TrimSpace(out) == "" { problems = append(problems, fmt.Sprintf("%s's template for %s:%s renders nothing for %v: %v", m.Module, s.Name, r.Kind, ex, err)) } } placed := map[string]int{} for _, res := range m.Resources { content, _ := res["content"].(string) for _, f := range ofContribution.FindAllStringSubmatch(content, -1) { seat, rest, _ := strings.Cut(f[1], ":") kind, whereText, _ := strings.Cut(rest, ":") if cs, ok := SeatNamed(seat); !ok || cs.Name != s.Name || kind != r.Kind { continue } where, err := placeholderWhere(r, whereText) if err != nil { continue // said by seatPlaceholderProblems } for _, combo := range combinations(r.Shape.Places) { if covers(where, combo) { placed[comboKey(combo)]++ } } } } for _, combo := range combinations(r.Shape.Places) { switch n := placed[comboKey(combo)]; { case n == 0: problems = append(problems, fmt.Sprintf("%s never places %s:%s for %s; every combination of %s is "+ "placed once, or a contribution to it is lost (novox/hq ADR 0255)", m.Module, s.Name, r.Kind, comboKey(combo), placeFields(r))) case n > 1: problems = append(problems, fmt.Sprintf("%s places %s:%s for %s %d times; once, or a contribution "+ "is written twice (novox/hq ADR 0255)", m.Module, s.Name, r.Kind, comboKey(combo), n)) } } } } return problems } // placesKind is whether any of a holder's files names the kind's placeholder. func placesKind(m Manifest, s Seat, kind string) bool { for _, res := range m.Resources { content, _ := res["content"].(string) for _, f := range ofContribution.FindAllStringSubmatch(content, -1) { seat, rest, _ := strings.Cut(f[1], ":") k, _, _ := strings.Cut(rest, ":") if cs, ok := SeatNamed(seat); ok && cs.Name == s.Name && k == kind { return true } } } return false } // combinations is every assignment of a value to each placing field. func combinations(places []PlaceField) []map[string]string { out := []map[string]string{{}} for _, p := range places { var next []map[string]string for _, partial := range out { for _, v := range p.Values { c := map[string]string{p.Field: v} for k, w := range partial { c[k] = w } next = append(next, c) } } out = next } return out } func covers(where, combo map[string]string) bool { for field, value := range where { if combo[field] != value { return false } } return true } func comboKey(combo map[string]string) string { var parts []string for _, k := range sortedKeys(combo) { parts = append(parts, k+"="+combo[k]) } return strings.Join(parts, ",") }