Kind Pure package
Name v0
Namespace moul / forge
Exported functions n/a — not supported for pure packages by the node (vm/qfuncs)
Module gno.land/p/moul/forge/v0
gno 0.9 validate.gno gno
⧉
1 package forge 2 3 import "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). not supported for pure packages by the node (vm/qfuncs)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
8
const (
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.
25 func 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.
44 func 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.
79 func 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).
98 func 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.
109 func 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.
121 func 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).
135 const 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.
140 func 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
160 func 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.
167 func 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.
186 func 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.
196 func 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.
209 func ValidLabel(s string ) bool {
210 return s != "" && ValidLine(s, MaxLabelLen) && !strings.Contains(s, "," )
211 }
212