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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

validate.gno

6.20 Kb · 211 lines
  1package forge
  2
  3import "strings"
  4
  5// Size caps. Every string the chain stores is bounded: an unbounded field is an
  6// unbounded storage deposit, and on gno.land the deposit is paid per byte by
  7// whoever writes it (100ugnot/byte at the time of writing).
  8const (
  9	MaxRepoPartLen = 39   // per side of "<namespace>/<name>"
 10	MaxRefNameLen  = 255  // git's own limit for a single ref name
 11	MaxTitleLen    = 200  // issue / change title
 12	MaxBodyLen     = 8192 // issue / change body
 13	MaxCommentLen  = 4096
 14	MaxDescLen     = 512
 15	MaxNoteLen     = 140 // single-line note attached to a log entry
 16	MaxMirrorLen   = 512
 17	MaxMirrors     = 8
 18	MaxLabels      = 10
 19	MaxLabelLen    = 32
 20)
 21
 22// ValidOID reports whether s is a git object id: 40 (SHA-1) or 64 (SHA-256)
 23// lowercase hex characters. Case is fixed so the same object always has the
 24// same on-chain key and the same digest-chain input.
 25func ValidOID(s string) bool {
 26	if len(s) != 40 && len(s) != 64 {
 27		return false
 28	}
 29	for i := 0; i < len(s); i++ {
 30		c := s[i]
 31		if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
 32			return false
 33		}
 34	}
 35	return true
 36}
 37
 38// ValidRefName reports whether s is a fully-qualified ref name this forge
 39// accepts: a "refs/"-rooted subset of git-check-ref-format(1).
 40//
 41// Deliberately stricter than git: the name must be fully qualified, so there is
 42// never an ambiguity between "main" the branch and "main" the tag, and a client
 43// can map an on-chain name onto a local ref without a lookup table.
 44func ValidRefName(s string) bool {
 45	if s == "" || len(s) > MaxRefNameLen {
 46		return false
 47	}
 48	if !strings.HasPrefix(s, "refs/") {
 49		return false
 50	}
 51	if strings.Contains(s, "..") || strings.Contains(s, "@{") {
 52		return false
 53	}
 54	if strings.HasSuffix(s, "/") || strings.HasSuffix(s, ".") {
 55		return false
 56	}
 57	parts := strings.Split(s, "/")
 58	if len(parts) < 2 {
 59		return false
 60	}
 61	for _, p := range parts {
 62		if p == "" || p == "@" {
 63			return false
 64		}
 65		if strings.HasPrefix(p, ".") || strings.HasSuffix(p, ".lock") {
 66			return false
 67		}
 68		for i := 0; i < len(p); i++ {
 69			if !refByteOK(p[i]) {
 70				return false
 71			}
 72		}
 73	}
 74	return true
 75}
 76
 77// refByteOK rejects the bytes git itself rejects in a ref component: ASCII
 78// control characters, space, DEL, and the pathspec/revision metacharacters.
 79func refByteOK(c byte) bool {
 80	if c <= 0x20 || c == 0x7f {
 81		return false
 82	}
 83	switch c {
 84	case '~', '^', ':', '?', '*', '[', '\\':
 85		return false
 86	}
 87	return true
 88}
 89
 90// ValidRepoID reports whether s is "<namespace>/<name>". The name is always a
 91// lowercase slug; the namespace is either a slug (a claimed user name) or a
 92// bech32 address (the caller's own). The two shapes cannot collide: an address
 93// is 40 characters and a slug caps at MaxRepoPartLen, which is 39.
 94//
 95// This layer validates the SHAPE only. Whether the caller may claim a given
 96// namespace is an ownership question that needs a chain, so it belongs to the
 97// realm (see the realm's README).
 98func ValidRepoID(s string) bool {
 99	ns, name, ok := SplitRepoID(s)
100	if !ok {
101		return false
102	}
103	return (validSlug(ns) || AddressNamespace(ns)) && validSlug(name)
104}
105
106// SplitRepoID splits "<namespace>/<name>" into its two halves. It does not
107// validate either half; ok is false only when the id is not two slash-separated
108// non-empty parts.
109func SplitRepoID(s string) (ns, name string, ok bool) {
110	i := strings.Index(s, "/")
111	if i <= 0 || i == len(s)-1 || strings.Count(s, "/") != 1 {
112		return "", "", false
113	}
114	return s[:i], s[i+1:], true
115}
116
117// AddressNamespace reports whether ns is shaped like a gno bech32 address, the
118// namespace every account owns without registering anything. The realm still
119// checks that it is the CALLER's address; this only says which of the two
120// ownership rules applies.
121func AddressNamespace(ns string) bool {
122	if len(ns) != 40 || !strings.HasPrefix(ns, "g1") {
123		return false
124	}
125	for i := 2; i < len(ns); i++ {
126		if !strings.ContainsRune(bech32Charset, rune(ns[i])) {
127			return false
128		}
129	}
130	return true
131}
132
133// bech32Charset is bech32's data alphabet: lowercase alphanumerics minus the
134// four characters it drops to avoid transcription errors (1, b, i, o).
135const bech32Charset = "023456789acdefghjklmnpqrstuvwxyz"
136
137// validSlug: 1..MaxRepoPartLen chars, [a-z0-9] at both ends, [a-z0-9-_.] inside,
138// no "..", no "--". Mixed case is rejected rather than folded: two ids that
139// differ only in case would be two avl keys and one human-visible name.
140func validSlug(s string) bool {
141	if s == "" || len(s) > MaxRepoPartLen {
142		return false
143	}
144	if !alnum(s[0]) || !alnum(s[len(s)-1]) {
145		return false
146	}
147	if strings.Contains(s, "..") || strings.Contains(s, "--") {
148		return false
149	}
150	for i := 0; i < len(s); i++ {
151		c := s[i]
152		if alnum(c) || c == '-' || c == '_' || c == '.' {
153			continue
154		}
155		return false
156	}
157	return true
158}
159
160func alnum(c byte) bool {
161	return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
162}
163
164// ValidText reports whether s fits in max bytes and carries no control
165// characters other than newline and tab. Render output is markdown served by
166// gnoweb, so a stray control byte is a rendering bug for every reader forever.
167func ValidText(s string, max int) bool {
168	if len(s) > max {
169		return false
170	}
171	for i := 0; i < len(s); i++ {
172		c := s[i]
173		if c == '\n' || c == '\t' {
174			continue
175		}
176		if c < 0x20 || c == 0x7f {
177			return false
178		}
179	}
180	return true
181}
182
183// ValidLine reports whether s is single-line text within max bytes. Used for
184// titles and for log-entry notes, which are fields of the digest chain: a
185// newline there would let one note impersonate two.
186func ValidLine(s string, max int) bool {
187	if strings.ContainsAny(s, "\n\t") {
188		return false
189	}
190	return ValidText(s, max)
191}
192
193// ValidMirror reports whether s looks like a fetch locator. The chain does not
194// resolve it: it only records where the maintainers say the objects are: so
195// the check is a shape check, not a promise that anything is reachable.
196func ValidMirror(s string) bool {
197	if !ValidLine(s, MaxMirrorLen) || s == "" {
198		return false
199	}
200	for _, p := range []string{"https://", "http://", "git://", "ssh://", "ipfs://", "ipns://", "ar://", "rad://", "git@"} {
201		if strings.HasPrefix(s, p) {
202			return true
203		}
204	}
205	return false
206}
207
208// ValidLabel reports whether s is an issue label.
209func ValidLabel(s string) bool {
210	return s != "" && ValidLine(s, MaxLabelLen) && !strings.Contains(s, ",")
211}