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}