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

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}