mesh-wireguard is provided by the mesh itself (mesh-sdk delivery.ProvidedByMesh): no repository builds it, so the indexer holds no fingerprint of it and the assigner never wants it by one; the controller composes it with the node's own. The grants part named it "no fingerprint of the new form: the indexer holds no value of it" on every pass, a warning nothing could clear (sweep 2026-10-30 item 6). Quick fix: drop mesh-wireguard by name from the missing list. Thorough fix, chosen as cheap and covering every module the mesh may provide later: MissingGrants passes over any module ProvidedByMesh names. The controller says the same of it in mesh-control (its grants and switch lines).
507 lines
20 KiB
Go
507 lines
20 KiB
Go
package grants
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// **The grants the part writes** (novox/hq ADR 0547 §4, §5, ADR 0561 §2, §6; issue 774). A module's grant is the record
|
|
// that its bus credential exists for the fingerprint the catalogue's trunk names for it: `<module>.<fingerprint hex>` in
|
|
// the part's state grants, naming the accounts (the module's own and each process's) the vault holds a hash for. It is the
|
|
// controller's grant as the controller composes it (cmd/mesh-controller grants.go grantsFrom), so the two compare by key.
|
|
// Until the controller's lease is handed away nobody reads the part's keys: the assigner reads the controller's
|
|
// (delivery.GrantSource), and the tool grants says every difference, so the switch is made on a measured equality.
|
|
//
|
|
// A module of which an account has no hash yet is granted the accounts that have one; a module none of whose accounts has
|
|
// one has no grant, and one it was written for is deleted (an older grant naming a node whose account is gone would let the
|
|
// assigner want it, and a grant naming no account names every node). Nothing else is ever deleted, and nothing is written
|
|
// or deleted in a pass that could not read the vault's hashes, the indexer's modules or a manifest of the module.
|
|
|
|
// GrantsWritten is what one pass did to the state grants.
|
|
type GrantsWritten struct {
|
|
Wanted int `json:"wanted"`
|
|
Written []string `json:"written,omitempty"`
|
|
Deleted []string `json:"deleted,omitempty"`
|
|
// Kept says why a pass wrote and deleted nothing, or left a module out.
|
|
Kept []string `json:"kept,omitempty"`
|
|
// Missing is each module the placement or an assignment puts on a machine that has no grant after the pass,
|
|
// `<module>: <why>`: the assigner's sixth rule wants none of it (MissingGrants). Never passed over in silence: once
|
|
// the part's grants are the ones read, it is the condition mesh-grants.grant-missing.
|
|
Missing []string `json:"missing,omitempty"`
|
|
// Waits is each grant composed and not written, `<key>: <why>`: after take-over a grant is new or changed only once the
|
|
// list the bus loaded carries it (issue 774, GrantWaits). Read with grants_grants (last_pass).
|
|
Waits []string `json:"waits,omitempty"`
|
|
}
|
|
|
|
// GrantWaits says why a grant composed now, new or changed, is not written yet, or "" when it may be (issue 774). The
|
|
// assigner moves a module's want to the fingerprint once its grant is there, so the grant is the word that the list the bus
|
|
// has loaded already carries the module's accounts as composed now (composed, from ListRecords: what runs and what the trunk
|
|
// names): every publish and subscribe allowed, the allowance of responses, and no deny the composed account lacks. A grant
|
|
// that adds no permission is written at once, whatever list is held or pending; one that adds any waits until a list that
|
|
// carries it is loaded. loaded is the loaded list by user, nil when its contents are unknown (unknown says why): then only
|
|
// a grant naming no account is written. In shadow nobody reads the part's grants and nothing waits. Pure.
|
|
func GrantWaits(tookOver bool, accounts []string, composed, loaded map[string]User, unknown string) string {
|
|
if !tookOver || len(accounts) == 0 {
|
|
return ""
|
|
}
|
|
if loaded == nil {
|
|
return "waits: the contents of the list the bus loaded are unknown (" + unknown + "), so no grant adding an account is written"
|
|
}
|
|
var adds []string
|
|
for _, user := range accounts {
|
|
c, ok := composed[user]
|
|
if !ok {
|
|
continue
|
|
}
|
|
l, ok := loaded[user]
|
|
if !ok {
|
|
adds = append(adds, user+" (not in the loaded list)")
|
|
continue
|
|
}
|
|
n := len(minus(c.Publish, l.Publish)) + len(minus(c.Subscribe, l.Subscribe))
|
|
if c.AllowResponses && !l.AllowResponses {
|
|
n++
|
|
}
|
|
if l.PublishDeny != nil {
|
|
n += len(minus(l.PublishDeny, c.PublishDeny))
|
|
}
|
|
if n > 0 {
|
|
adds = append(adds, fmt.Sprintf("%s (%d permissions)", user, n))
|
|
}
|
|
}
|
|
if len(adds) == 0 {
|
|
return ""
|
|
}
|
|
return "waits for the bus to load a list carrying what it adds: " + strings.Join(adds, ", ")
|
|
}
|
|
|
|
// **The permissions of each list the part sealed, one key per user** (issue 774, sweep 2026-10-30 item 5): the last two
|
|
// lists, by digest, each user's permissions as sealed, never a hash: what tells which grants the list the bus loaded
|
|
// carries, without the bus module's tools. One value holding every user of two lists outgrew the bucket's largest value
|
|
// (JetStream 10054, "message size exceeds maximum", 16:37 and 16:55 on 2026-10-30), so a list's permissions were never
|
|
// kept and a grant judged against them waited. Each user's permissions are a key of their own,
|
|
// `sealed-grants.<digest hex>.<user>`, which grows with that user alone; the key `sealed-lists` names the lists kept and
|
|
// their users, and is written only once every user's key of its newest list is: it never names a list whose keys are not
|
|
// all there.
|
|
|
|
// KeySealedGrants is the key that held every user of the last two lists in one value, before this release: read when the
|
|
// index names no list, and taken away once the index is written.
|
|
const KeySealedGrants = "sealed-grants"
|
|
|
|
// KeySealedLists is the key of the state user-list naming the sealed lists whose permissions are kept, newest first.
|
|
const KeySealedLists = "sealed-lists"
|
|
|
|
// sealedKept is how many sealed lists keep their permissions: the one in force and the one before.
|
|
const sealedKept = 2
|
|
|
|
// SealedGrants is the value KeySealedGrants held.
|
|
type SealedGrants struct {
|
|
Lists []SealedList `json:"lists"`
|
|
}
|
|
|
|
// SealedList is one sealed list's users and their permissions.
|
|
type SealedList struct {
|
|
Digest string `json:"digest"`
|
|
Users []User `json:"users"`
|
|
}
|
|
|
|
// SealedLists is the value of KeySealedLists: the lists kept, newest first, each by its digest and its users' names.
|
|
type SealedLists struct {
|
|
Lists []SealedNames `json:"lists"`
|
|
}
|
|
|
|
// SealedNames is one kept list: its digest and its users, sorted.
|
|
type SealedNames struct {
|
|
Digest string `json:"digest"`
|
|
Users []string `json:"users"`
|
|
}
|
|
|
|
// SealedUserKey is the key of one user's permissions in the sealed list of digest.
|
|
func SealedUserKey(digest, user string) string {
|
|
return KeySealedGrants + "." + strings.TrimPrefix(digest, "sha256:") + "." + user
|
|
}
|
|
|
|
// keepSealedGrants records the permissions of the list sealed now, one key per user, beside the list before; the lists
|
|
// older than those two lose their keys.
|
|
func (p *Part) keepSealedGrants(res *Result, digest string, list []byte, composed []User) {
|
|
in := listUsers(list)
|
|
var users []User
|
|
for _, u := range composed {
|
|
if slices.Contains(in, u.User) {
|
|
u.Hash = ""
|
|
users = append(users, u)
|
|
}
|
|
}
|
|
failed := false
|
|
names := make([]string, 0, len(users))
|
|
for _, u := range users {
|
|
names = append(names, u.User)
|
|
data, _ := json.Marshal(u)
|
|
if _, err := p.Store.Put(StateUserList, SealedUserKey(digest, u.User), data); err != nil {
|
|
failed = true
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, SealedUserKey(digest, u.User), err))
|
|
}
|
|
}
|
|
if failed {
|
|
// The index keeps naming the lists whose keys are all there; this list's permissions are not kept.
|
|
return
|
|
}
|
|
sort.Strings(names)
|
|
was := p.sealedLists()
|
|
next := SealedLists{Lists: []SealedNames{{Digest: digest, Users: names}}}
|
|
var gone []SealedNames
|
|
for _, l := range was.Lists {
|
|
switch {
|
|
case l.Digest == digest:
|
|
case len(next.Lists) < sealedKept:
|
|
next.Lists = append(next.Lists, l)
|
|
default:
|
|
gone = append(gone, l)
|
|
}
|
|
}
|
|
data, _ := json.Marshal(next)
|
|
if _, err := p.Store.Put(StateUserList, KeySealedLists, data); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeySealedLists, err))
|
|
return
|
|
}
|
|
for _, l := range gone {
|
|
for _, user := range l.Users {
|
|
if err := p.Store.Delete(StateUserList, SealedUserKey(l.Digest, user)); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be taken away: %v", StateUserList, SealedUserKey(l.Digest, user), err))
|
|
}
|
|
}
|
|
}
|
|
if _, _, found, err := p.Store.Get(StateUserList, KeySealedGrants); err == nil && found {
|
|
if err := p.Store.Delete(StateUserList, KeySealedGrants); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be taken away: %v", StateUserList, KeySealedGrants, err))
|
|
}
|
|
}
|
|
}
|
|
|
|
// sealedLists is the index of kept lists; none when it cannot be read.
|
|
func (p *Part) sealedLists() SealedLists {
|
|
var out SealedLists
|
|
if raw, _, found, err := p.Store.Get(StateUserList, KeySealedLists); err == nil && found {
|
|
_ = json.Unmarshal(raw, &out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sealedGrantsOf is the permissions of each user of the sealed list of digest, as the part kept them, and whether it kept
|
|
// every one: from the index and each user's key, else from the one value written before this release.
|
|
func (p *Part) sealedGrantsOf(digest string) (map[string]User, bool) {
|
|
for _, l := range p.sealedLists().Lists {
|
|
if l.Digest != digest {
|
|
continue
|
|
}
|
|
out := map[string]User{}
|
|
for _, user := range l.Users {
|
|
raw, _, found, err := p.Store.Get(StateUserList, SealedUserKey(digest, user))
|
|
if err != nil || !found {
|
|
return nil, false
|
|
}
|
|
var u User
|
|
if json.Unmarshal(raw, &u) != nil {
|
|
return nil, false
|
|
}
|
|
out[u.User] = u
|
|
}
|
|
return out, true
|
|
}
|
|
raw, _, found, err := p.Store.Get(StateUserList, KeySealedGrants)
|
|
if err == nil && found {
|
|
var sg SealedGrants
|
|
if json.Unmarshal(raw, &sg) == nil {
|
|
for _, sl := range sg.Lists {
|
|
if sl.Digest == digest {
|
|
out := map[string]User{}
|
|
for _, u := range sl.Users {
|
|
out[u.User] = u
|
|
}
|
|
return out, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// loadedContents is the list the bus loaded, by user: as the bus module's nats_users read it, else as the part sealed the
|
|
// list of the digest the bus node says it loaded from the part's key; nil and why when neither says.
|
|
func (p *Part) loadedContents(in Inputs, read []User, readErr error) (map[string]User, string) {
|
|
byUser := func(us []User) map[string]User {
|
|
out := map[string]User{}
|
|
for _, u := range us {
|
|
out[u.User] = u
|
|
}
|
|
return out
|
|
}
|
|
if readErr == nil {
|
|
return byUser(read), ""
|
|
}
|
|
l := in.Loaded
|
|
switch {
|
|
case l == nil:
|
|
return nil, "the bus node says no loaded list, and nats_users: " + readErr.Error()
|
|
case l.Source != delivery.GrantsUserListSource || l.Follows != delivery.FollowGrants || l.Error != "":
|
|
return nil, "the bus loaded no list of the grants part's (source " + l.Source + ", follows " + l.Follows + "), and nats_users: " + readErr.Error()
|
|
}
|
|
if kept, ok := p.sealedGrantsOf(l.Digest); ok {
|
|
return kept, ""
|
|
}
|
|
return nil, "the part keeps no permissions of the loaded list " + short(l.Digest) + " (sealed before it kept them), and nats_users: " + readErr.Error()
|
|
}
|
|
|
|
// GrantMissingCondition is the key of the warning raised while a placed module has no grant (issue 1126).
|
|
const GrantMissingCondition = "mesh-grants.grant-missing"
|
|
|
|
// MissingGrants is each module assigned or placed on a machine that want leaves without a grant, `<module>: <why>`,
|
|
// sorted, but a module the mesh provides itself, which is never wanted by fingerprint: its manifest refused (skip), no fingerprint of the new form at the indexer's trunk, or no account with a hash
|
|
// in the vault (drop). **The fingerprint is the indexer's, never a commit's** (issue 1126: link2pay, snake, f1-circuits
|
|
// and txt-game, whose source commit has not moved since a build of the old form, have a done key of the new form at it,
|
|
// and that is the fingerprint granted). Pure.
|
|
func MissingGrants(in Inputs, want map[string]delivery.ControllerGrant, drop []string, skip map[string]bool) []string {
|
|
placed := map[string]bool{}
|
|
for _, ms := range in.Assigned {
|
|
for _, m := range ms {
|
|
placed[m] = true
|
|
}
|
|
}
|
|
var out []string
|
|
for _, module := range sortedKeys(placed) {
|
|
// A module the mesh provides itself (the private network) is built by no repository, so the indexer holds no
|
|
// fingerprint of it and the assigner wants none: the controller composes it with the node's own. It needs no grant,
|
|
// and naming it missing raised a warning nothing could clear (sweep 2026-10-30 item 6).
|
|
if delivery.ProvidedByMesh(module) {
|
|
continue
|
|
}
|
|
fp, has := in.Fingerprints[module]
|
|
key, err := delivery.PartGrantKey(module, fp)
|
|
if has && err == nil {
|
|
if _, granted := want[key]; granted {
|
|
continue
|
|
}
|
|
}
|
|
why := ""
|
|
switch {
|
|
case skip[module]:
|
|
why = "its manifest at the indexer's trunk is refused"
|
|
case !has:
|
|
why = "no fingerprint of the new form: " + orSaid(in.NoFingerprint[module], "the indexer holds no value of it")
|
|
case err != nil:
|
|
why = fmt.Sprintf("no grant key for %s: %v", fp, err)
|
|
case slices.Contains(drop, key):
|
|
why = "none of its accounts at " + fp + " has a hash in the vault"
|
|
default:
|
|
why = "no grant composed at " + fp
|
|
}
|
|
out = append(out, module+": "+why)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func orSaid(s, none string) string {
|
|
if s == "" {
|
|
return none
|
|
}
|
|
return s
|
|
}
|
|
|
|
// sayMissing raises mesh-grants.grant-missing while a placed module has no grant, once the controller's lease is handed
|
|
// away (the part's grants are then the ones the assigner reads), and clears it when every one has one; in shadow the
|
|
// pass and the tool say them. Said once per set, again when it changes, and always at the first pass after a start.
|
|
func (p *Part) sayMissing(missing []string) {
|
|
if p.Say == nil {
|
|
return
|
|
}
|
|
why := ""
|
|
if on, _ := leaseHandedAway(p.Read); on && len(missing) > 0 {
|
|
why = strings.Join(missing, "; ")
|
|
}
|
|
p.holdMu.Lock()
|
|
defer p.holdMu.Unlock()
|
|
if p.missingKnown && why == p.missingSaid {
|
|
return
|
|
}
|
|
c := asks.Condition{Key: GrantMissingCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
|
|
if why == "" {
|
|
c.State = asks.StateCleared
|
|
} else {
|
|
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
|
|
c.Summary = fmt.Sprintf("%d placed module(s) have no grant, so no want can name them", len(missing))
|
|
c.Headline, c.Needs = "A placed module has no bus grant", "a look at why each has none"
|
|
c.Explanation = asks.NeedsYou + "a look. The grants part wrote no grant for these modules, so the assigner's " +
|
|
"credential rule gives them no want: " + why
|
|
c.Resolved = "every placed module has a grant at the fingerprint the indexer names"
|
|
}
|
|
if p.Say(c) == nil {
|
|
p.missingSaid, p.missingKnown = why, true
|
|
}
|
|
}
|
|
|
|
// GrantsOf is the grant of every module at the fingerprint the indexer's trunk names for it, by key, and the keys that
|
|
// have none (drop): see the comment above. A module in skip (its manifest was refused) is neither. Pure.
|
|
func GrantsOf(in Inputs, recs buscompose.Records, skip map[string]bool) (map[string]delivery.ControllerGrant, []string, error) {
|
|
principals, err := buscompose.Users(recs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
accounts, missing := map[string][]string{}, map[string]bool{}
|
|
for _, p := range principals {
|
|
if p.Module == "" || (p.Kind != buscompose.KindModule && p.Kind != buscompose.KindProcess) {
|
|
continue
|
|
}
|
|
if in.Hashes[p.Username()].Hash == "" {
|
|
missing[p.Module] = true
|
|
continue
|
|
}
|
|
accounts[p.Module] = append(accounts[p.Module], p.Username())
|
|
}
|
|
out := map[string]delivery.ControllerGrant{}
|
|
var drop []string
|
|
for module, fp := range in.Fingerprints {
|
|
if skip[module] || !strings.HasPrefix(fp, delivery.SourcePrefix2) {
|
|
continue
|
|
}
|
|
key, err := delivery.PartGrantKey(module, fp)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
names := accounts[module]
|
|
if missing[module] && len(names) == 0 {
|
|
drop = append(drop, key)
|
|
continue
|
|
}
|
|
sort.Strings(names)
|
|
out[key] = delivery.ControllerGrant{Accounts: names}
|
|
}
|
|
sort.Strings(drop)
|
|
return out, drop, nil
|
|
}
|
|
|
|
// grantsReadFailed names a read of this pass without which no grant is written: the hashes and the indexer's modules and
|
|
// manifests. A manifest refused for one module only leaves that module out (writeGrants).
|
|
func grantsReadFailed(in Inputs) string {
|
|
for _, g := range in.Gaps {
|
|
if !g.Failed {
|
|
continue
|
|
}
|
|
for _, input := range []string{ReadAccounts, ReadModules, ReadManifests} {
|
|
if strings.HasPrefix(g.Input, input) {
|
|
return g.Input + ": " + g.Why
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// writeGrants writes each grant whose key is absent or differs and says grant-written for each, and deletes the key of a
|
|
// module that has none now.
|
|
func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records, composed []User, loaded map[string]User, unknown string) {
|
|
if why := grantsReadFailed(in); why != "" {
|
|
res.Grants = &GrantsWritten{Kept: []string{"no grant is written or deleted in a pass that could not read " + why}}
|
|
return
|
|
}
|
|
skip := map[string]bool{}
|
|
for _, g := range in.Refused {
|
|
skip[strings.TrimPrefix(g.Input, "manifest of ")] = true
|
|
}
|
|
want, drop, err := GrantsOf(in, recs, skip)
|
|
if err != nil {
|
|
res.Errors = append(res.Errors, "the grants could not be composed: "+err.Error())
|
|
return
|
|
}
|
|
done := &GrantsWritten{Wanted: len(want), Missing: MissingGrants(in, want, drop, skip)}
|
|
res.Grants = done
|
|
p.sayMissing(done.Missing)
|
|
tookOver := p.SwitchState() == TookOver
|
|
byUser := map[string]User{}
|
|
for _, u := range composed {
|
|
byUser[u.User] = u
|
|
}
|
|
for _, key := range sortedKeys(want) {
|
|
g := want[key]
|
|
data, err := delivery.EncodeControllerGrant(g)
|
|
if err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("the grant %s cannot be encoded: %v", key, err))
|
|
continue
|
|
}
|
|
if raw, _, found, err := p.Store.Get(StateGrants, key); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", StateGrants, key, err))
|
|
continue
|
|
} else if found {
|
|
if was, err := delivery.DecodeControllerGrant(raw); err == nil && delivery.SameControllerGrant(was, withVersion(g)) {
|
|
continue
|
|
}
|
|
}
|
|
if waits := GrantWaits(tookOver, g.Accounts, byUser, loaded, unknown); waits != "" {
|
|
done.Waits = append(done.Waits, key+": "+waits)
|
|
continue
|
|
}
|
|
if _, err := p.Store.Put(StateGrants, key, data); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateGrants, key, err))
|
|
continue
|
|
}
|
|
done.Written = append(done.Written, key)
|
|
module, fp, _ := delivery.ParsePartGrantKey(key)
|
|
p.say(res, EventGrantWritten, map[string]any{"module": module, "fingerprint": fp, "accounts": g.Accounts})
|
|
}
|
|
for _, key := range drop {
|
|
if _, _, found, err := p.Store.Get(StateGrants, key); err != nil || !found {
|
|
continue
|
|
}
|
|
if err := p.Store.Delete(StateGrants, key); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be deleted: %v", StateGrants, key, err))
|
|
continue
|
|
}
|
|
done.Deleted = append(done.Deleted, key)
|
|
}
|
|
}
|
|
|
|
func withVersion(g delivery.ControllerGrant) delivery.ControllerGrant {
|
|
g.V = delivery.Version
|
|
return g
|
|
}
|
|
|
|
// GrantDifference is one key on which the part's grants and the controller's differ.
|
|
type GrantDifference struct {
|
|
Key string `json:"key"`
|
|
// Kind is `only the controller's`, `only the part's` or `accounts differ`.
|
|
Kind string `json:"kind"`
|
|
Controller []string `json:"controller_accounts,omitempty"`
|
|
Part []string `json:"part_accounts,omitempty"`
|
|
}
|
|
|
|
// CompareGrants lists every key the two sets differ on, in key order: a key held by one alone, or held by both with other
|
|
// accounts. Pure.
|
|
func CompareGrants(controller, part map[string]delivery.ControllerGrant) []GrantDifference {
|
|
keys := map[string]bool{}
|
|
for k := range controller {
|
|
keys[k] = true
|
|
}
|
|
for k := range part {
|
|
keys[k] = true
|
|
}
|
|
out := []GrantDifference{}
|
|
for _, k := range sortedKeys(keys) {
|
|
c, inC := controller[k]
|
|
g, inP := part[k]
|
|
switch {
|
|
case inC && !inP:
|
|
out = append(out, GrantDifference{Key: k, Kind: "only the controller's", Controller: c.Accounts})
|
|
case inP && !inC:
|
|
out = append(out, GrantDifference{Key: k, Kind: "only the part's", Part: g.Accounts})
|
|
case !delivery.SameControllerGrant(withVersion(c), withVersion(g)):
|
|
out = append(out, GrantDifference{Key: k, Kind: "accounts differ", Controller: c.Accounts, Part: g.Accounts})
|
|
}
|
|
}
|
|
return out
|
|
}
|