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

5.54 Kb · 193 lines
  1package gnopm
  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.
  8const (
  9	MaxPkgPathLen = 256
 10	MaxRepoLen    = 512
 11	MaxDirLen     = 256
 12	MaxRefLen     = 255 // git's own limit for a single ref name
 13
 14	// MaxClaimants bounds how many addresses may claim one package path.
 15	// Unbounded, it is a spam surface: one path with ten thousand claims is a
 16	// Render that never returns and a listing nobody can read.
 17	MaxClaimants = 16
 18)
 19
 20// ValidCommit reports whether s is a git object id: 40 (SHA-1) or 64 (SHA-256)
 21// lowercase hex characters. Case is fixed so the same object always produces
 22// the same stored bytes, and so a verifier comparing two claims compares two
 23// comparable strings.
 24func ValidCommit(s string) bool {
 25	if len(s) != 40 && len(s) != 64 {
 26		return false
 27	}
 28	for i := 0; i < len(s); i++ {
 29		c := s[i]
 30		if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
 31			return false
 32		}
 33	}
 34	return true
 35}
 36
 37// ValidRepo reports whether s is a repository URL this registry accepts.
 38//
 39// Deliberately narrow: "https://" only, a host, and a path. Not a taste
 40// judgement about git transports, a safety one. Whatever is stored here is
 41// eventually rendered as a link by this realm, by gnoweb and by every explorer
 42// that reads the registry, and the set of schemes that are safe to hand a
 43// browser is exactly one. "javascript:", "data:" and "file:" are the attack;
 44// "git://" and "ssh://" are merely unreachable from a web page, and a claimant
 45// who needs one can point at the https mirror every forge already serves.
 46func ValidRepo(s string) bool {
 47	if s == "" || len(s) > MaxRepoLen {
 48		return false
 49	}
 50	const scheme = "https://"
 51	if !strings.HasPrefix(s, scheme) {
 52		return false
 53	}
 54	rest := s[len(scheme):]
 55	if rest == "" || strings.HasPrefix(rest, "/") {
 56		return false
 57	}
 58	// A host must be present and must not be a userinfo trick
 59	// ("https://[email protected]/x" fetches from evil.example while
 60	// reading as GitHub).
 61	slash := strings.IndexByte(rest, '/')
 62	host := rest
 63	if slash >= 0 {
 64		host = rest[:slash]
 65	}
 66	if host == "" || strings.ContainsAny(host, "@:") {
 67		return false
 68	}
 69	if !strings.Contains(host, ".") {
 70		return false
 71	}
 72	for i := 0; i < len(s); i++ {
 73		if !urlByteOK(s[i]) {
 74			return false
 75		}
 76	}
 77	return !strings.Contains(s, "..")
 78}
 79
 80// urlByteOK is an allowlist, not a denylist: the printable ASCII that appears
 81// in a real repository URL. Everything else, control characters and the bidi
 82// overrides in particular, is refused rather than escaped, because a field that
 83// can only hold safe bytes needs no escaping at any of its render sites.
 84func urlByteOK(c byte) bool {
 85	switch {
 86	case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
 87		return true
 88	}
 89	switch c {
 90	case '-', '.', '_', '~', ':', '/', '%', '+':
 91		return true
 92	}
 93	return false
 94}
 95
 96// ValidDir reports whether s names the subdirectory of the repository holding
 97// the package. Empty means the repository root, which is the common case for a
 98// single-package repo and must stay expressible.
 99func ValidDir(s string) bool {
100	if s == "" {
101		return true
102	}
103	if len(s) > MaxDirLen {
104		return false
105	}
106	if strings.HasPrefix(s, "/") || strings.HasSuffix(s, "/") {
107		return false
108	}
109	for _, p := range strings.Split(s, "/") {
110		if p == "" || p == "." || p == ".." {
111			return false
112		}
113		for i := 0; i < len(p); i++ {
114			if !pathByteOK(p[i]) {
115				return false
116			}
117		}
118	}
119	return true
120}
121
122func pathByteOK(c byte) bool {
123	switch {
124	case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
125		return true
126	}
127	switch c {
128	case '-', '.', '_':
129		return true
130	}
131	return false
132}
133
134// ValidRef reports whether s is a fully-qualified git ref, or empty.
135//
136// The ref is optional and is never the thing that is verified: a ref moves, a
137// commit does not. It is recorded so a reader can tell a claim pinned to a
138// released tag from one pinned to a commit on nobody's branch, and so a
139// verifier can check the claimed commit is still reachable from it rather than
140// dangling behind a force-push.
141func ValidRef(s string) bool {
142	if s == "" {
143		return true
144	}
145	if len(s) > MaxRefLen || !strings.HasPrefix(s, "refs/") {
146		return false
147	}
148	if strings.Contains(s, "..") || strings.Contains(s, "@{") {
149		return false
150	}
151	if strings.HasSuffix(s, "/") || strings.HasSuffix(s, ".") {
152		return false
153	}
154	parts := strings.Split(s, "/")
155	if len(parts) < 2 {
156		return false
157	}
158	for _, p := range parts {
159		if p == "" || p == "@" || strings.HasPrefix(p, ".") || strings.HasSuffix(p, ".lock") {
160			return false
161		}
162		for i := 0; i < len(p); i++ {
163			if !refByteOK(p[i]) {
164				return false
165			}
166		}
167	}
168	return true
169}
170
171// refByteOK is an allowlist, and deliberately narrower than git's own rule.
172//
173// git rejects a handful of metacharacters and permits everything else, which
174// leaves a ref free to contain a backtick, a pipe or a bracket. Every other
175// field here is an allowlist precisely so that no consumer has to escape
176// anything, and one denylist field would undo that for all of them: a ref
177// carrying a pipe breaks out of a markdown table cell, and one carrying a
178// backtick breaks out of the inline-code span a renderer wraps it in.
179//
180// The cost is refs nobody writes. What stays expressible is every ref anyone
181// actually has: alphanumerics, and the four separators git tooling puts in a
182// name.
183func refByteOK(c byte) bool {
184	switch {
185	case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
186		return true
187	}
188	switch c {
189	case '-', '.', '_', '/', '+':
190		return true
191	}
192	return false
193}