validate.gno
3.15 Kb · 129 lines
1package gnotif
2
3import "strings"
4
5const (
6 targetPrefix = "gno.land/r/"
7 maxTarget = 249
8 maxName = 64
9 maxFilterPairs = 8
10 maxBody = 255
11 maxLink = 256
12 maxPerTarget = 64
13)
14
15func validate(t Trigger) {
16 switch {
17 case !validName(t.Event, 1, maxName):
18 panic("invalid event")
19 case !validFilter(t.Filter):
20 panic("invalid filter")
21 case !validName(t.Param, 0, maxName):
22 panic("invalid param")
23 case !sized(t.Title, 1, maxName):
24 panic("invalid title")
25 case !sized(t.Body, 0, maxBody):
26 panic("invalid body")
27 case !validLink(t.Link):
28 panic("invalid link")
29 }
30}
31
32// namespaceOf returns the namespace a valid target lives in, and panics on a
33// target that is not a valid realm path.
34func namespaceOf(target string) string {
35 if !validTarget(target) {
36 panic("invalid target " + target)
37 }
38 namespace, _, _ := strings.Cut(target[len(targetPrefix):], "/")
39 return namespace
40}
41
42func sized(s string, lo, hi int) bool {
43 return len(s) >= lo && len(s) <= hi
44}
45
46// validTarget accepts a realm path whose segments each match the chain's
47// Re_name, so a target names a path where a realm can exist.
48func validTarget(target string) bool {
49 if !strings.HasPrefix(target, targetPrefix) || len(target) > maxTarget {
50 return false
51 }
52 rest := target[len(targetPrefix):]
53 start := 0 // index of the current segment's first byte
54 for i := 0; i <= len(rest); i++ {
55 if i == len(rest) || rest[i] == '/' {
56 if i == start || !isLowerAlnum(rest[i-1]) {
57 return false
58 }
59 start = i + 1
60 continue
61 }
62 c := rest[i]
63 switch {
64 case i == start:
65 if !(c >= 'a' && c <= 'z') {
66 return false
67 }
68 case c == '_' || c == '-':
69 if !isLowerAlnum(rest[i-1]) {
70 return false
71 }
72 case !isLowerAlnum(c):
73 return false
74 }
75 }
76 return true
77}
78
79func isLowerAlnum(c byte) bool {
80 return c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
81}
82
83// validName accepts identifier bytes only, so a name cannot carry spaces or
84// symbols that imitate the verified mark in rendered output.
85func validName(s string, lo, hi int) bool {
86 if !sized(s, lo, hi) {
87 return false
88 }
89 for i := 0; i < len(s); i++ {
90 c := s[i]
91 if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_') {
92 return false
93 }
94 }
95 return true
96}
97
98func validFilter(filter string) bool {
99 if filter == "" {
100 return true
101 }
102 pairs := strings.Split(filter, ",")
103 if len(pairs) > maxFilterPairs {
104 return false
105 }
106 for _, pair := range pairs {
107 key, value, ok := strings.Cut(pair, "=")
108 if !ok || !validName(key, 1, maxName) || !sized(value, 1, maxName) || strings.Contains(value, "=") {
109 return false
110 }
111 }
112 return true
113}
114
115// validLink accepts only a path, so a click always stays on the dapp's origin.
116// "//host" starts with a slash yet resolves to another host. A URL parser
117// drops tab, LF and CR and reads a backslash as a slash, so "/\t/host" and
118// "/\host" would do the same; control bytes and backslashes are refused.
119func validLink(link string) bool {
120 if !sized(link, 1, maxLink) || !strings.HasPrefix(link, "/") || strings.HasPrefix(link, "//") {
121 return false
122 }
123 for i := 0; i < len(link); i++ {
124 if c := link[i]; c < 0x20 || c == 0x7f || c == '\\' {
125 return false
126 }
127 }
128 return true
129}