Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

keys.gno

5.57 Kb · 167 lines
  1package config
  2
  3import (
  4	"strings"
  5
  6	"chain/runtime"
  7	"chain/runtime/unsafe"
  8)
  9
 10// The key grammar.
 11//
 12// A key is a NAME, optionally followed by "@" and a SCOPE:
 13//
 14//	pause                  applies to every realm that asks
 15//	pause@r/moul/home      applies to that realm only
 16//
 17// The scope is a package path with the chain domain stripped, because the
 18// domain is the same for every realm reading this one and repeating it in a
 19// few dozen keys buys nothing but storage.
 20//
 21// Two settings therefore answer every scoped question, and each reader decides
 22// how they combine: pause takes the stricter of the two (pausable.Strictest),
 23// while the notice blocks show both. That choice belongs to the reader and not
 24// here, because "stricter wins" and "show both" are both right, for different
 25// settings.
 26const (
 27	// ScopeSep separates a name from the realm it applies to. "@" and not "."
 28	// so a scope can never be mistaken for a longer name, and not ":" because
 29	// gno realm render paths already use that.
 30	ScopeSep = "@"
 31
 32	maxNameLen  = 64
 33	maxScopeLen = 128
 34)
 35
 36// realmPath is this realm's own path, including its version. Stated once so
 37// the page can link to itself and the proxy check can recognise a sibling
 38// version without either restating the string.
 39const realmPath = "gno.land/r/moul/config/v1"
 40
 41// validName reports whether name is a legal setting name: 1..maxNameLen bytes
 42// of [a-z0-9._-]. The dot is the namespace separator ("block.top"), lowercase
 43// only so a setting has exactly one name, and no whitespace so a name
 44// round-trips through Manifest's tab-separated lines.
 45func validName(name string) bool {
 46	if len(name) == 0 || len(name) > maxNameLen {
 47		return false
 48	}
 49	for i := 0; i < len(name); i++ {
 50		c := name[i]
 51		switch {
 52		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
 53		case c == '-', c == '_', c == '.':
 54		default:
 55			return false
 56		}
 57	}
 58	return true
 59}
 60
 61// validScope reports whether scope is shaped like a package path with the
 62// domain stripped: 1..maxScopeLen bytes of [a-z0-9._/-], no empty segment.
 63func validScope(scope string) bool {
 64	if len(scope) == 0 || len(scope) > maxScopeLen {
 65		return false
 66	}
 67	lastSlash := true // a leading slash would be an empty first segment
 68	for i := 0; i < len(scope); i++ {
 69		c := scope[i]
 70		switch {
 71		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
 72			lastSlash = false
 73		case c == '-', c == '_', c == '.':
 74			lastSlash = false
 75		case c == '/':
 76			if lastSlash {
 77				return false
 78			}
 79			lastSlash = true
 80		default:
 81			return false
 82		}
 83	}
 84	return !lastSlash
 85}
 86
 87// validKey reports whether key is a legal name, or a legal name and scope
 88// joined by ScopeSep.
 89func validKey(key string) bool {
 90	name, scope := SplitKey(key)
 91	if scope == "" {
 92		return !strings.Contains(key, ScopeSep) && validName(name)
 93	}
 94	return validName(name) && validScope(scope)
 95}
 96
 97// SplitKey takes a key apart. An unscoped key returns an empty scope, and so
 98// does a malformed one: callers pair this with validKey rather than trusting
 99// the split.
100func SplitKey(key string) (name, scope string) {
101	i := strings.Index(key, ScopeSep)
102	if i < 0 {
103		return key, ""
104	}
105	return key[:i], key[i+1:]
106}
107
108// Scope turns a package path into the scope half of a key, stripping the chain
109// domain: "gno.land/r/moul/home" and "r/moul/home" both give "r/moul/home".
110//
111// A path that is not shaped like one comes back empty, and every caller here
112// treats that as "no scope", falling back to the global setting rather than
113// inventing a key nobody can type.
114//
115// The chain domain is tried first and the literal "gno.land/" second, so a key
116// written on one chain still resolves on another whose domain differs. Twin of
117// mygnoscan.TrimDomain, which answers the same question for a URL and is
118// stricter about the characters, because its output lands inside a link.
119func Scope(pkgPath string) string {
120	s := strings.TrimPrefix(pkgPath, runtime.ChainDomain()+"/")
121	s = strings.TrimPrefix(s, "gno.land/")
122	s = strings.Trim(s, "/")
123	if !validScope(s) {
124		return ""
125	}
126	return s
127}
128
129// KeyFor builds the scoped key for a setting on one realm, and returns the
130// bare name when pkgPath names no realm this can scope to.
131//
132// This is what a realm should render when it wants to tell a manager which
133// setting to change, so the command in the page is the command that works.
134func KeyFor(name, pkgPath string) string {
135	scope := Scope(pkgPath)
136	if scope == "" {
137		return name
138	}
139	return name + ScopeSep + scope
140}
141
142// caller is the package path of the realm that called into this one.
143//
144// Every exported function here that uses it is a plain read with no `cur realm`
145// parameter, so it can only ever be BORROWED: gno runs it without opening a
146// realm frame, and unsafe.CurrentRealm() therefore reports the borrower. That
147// is what makes the zero-argument helpers (TopBlock, IsPaused, …) able to name
148// their caller at all.
149//
150// Measured in the test harness on 2026-09-22: a call from a code realm at
151// gno.land/r/test/caller reports CurrentRealm=gno.land/r/test/caller and
152// PreviousRealm=gno.land/r/moul/config/v1.
153//
154// The moment one of these grows a `cur realm` parameter this stops being true
155// and starts reporting this realm instead. Do not add one. Anything that needs
156// a realm frame takes the path explicitly, which is what the …For variants are.
157func caller() string { return unsafe.CurrentRealm().PkgPath() }
158
159// scopedPair reads both halves of a scoped setting: the global one and the one
160// for pkgPath, either of which may be empty.
161func scopedPair(name, pkgPath string) (global, scoped string) {
162	global = Get(name)
163	if scope := Scope(pkgPath); scope != "" {
164		scoped = Get(name + ScopeSep + scope)
165	}
166	return global, scoped
167}