json.gno
4.67 Kb · 243 lines
1package game31
2
3import (
4 "strconv"
5 "strings"
6
7 "gno.land/p/nt/avl/v0"
8)
9
10// jsonStr returns s as a quoted, escaped JSON string (game28 concatenated raw
11// user strings into JSON, so a guild name with a quote broke every reader).
12func jsonStr(s string) string {
13 var b strings.Builder
14 b.WriteByte('"')
15 for i := 0; i < len(s); i++ {
16 c := s[i]
17 switch c {
18 case '"':
19 b.WriteString(`\"`)
20 case '\\':
21 b.WriteString(`\\`)
22 case '\n':
23 b.WriteString(`\n`)
24 case '\r':
25 b.WriteString(`\r`)
26 case '\t':
27 b.WriteString(`\t`)
28 default:
29 if c < 0x20 {
30 b.WriteString(`\u00`)
31 b.WriteByte("0123456789abcdef"[c>>4])
32 b.WriteByte("0123456789abcdef"[c&0xf])
33 } else {
34 b.WriteByte(c)
35 }
36 }
37 }
38 b.WriteByte('"')
39 return b.String()
40}
41
42// sanitizeName trims and strips control characters from a display name.
43func sanitizeName(s string) string {
44 var b strings.Builder
45 for _, r := range strings.TrimSpace(s) {
46 if r < 0x20 || r == 0x7f {
47 continue
48 }
49 b.WriteRune(r)
50 }
51 return b.String()
52}
53
54// isSafeId reports whether s is a short [a-z0-9_.-] identifier.
55func isSafeId(s string, maxLen int) bool {
56 if s == "" || len(s) > maxLen {
57 return false
58 }
59 for i := 0; i < len(s); i++ {
60 c := s[i]
61 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.') {
62 return false
63 }
64 }
65 return true
66}
67
68func isHex64(s string) bool {
69 if len(s) != 64 {
70 return false
71 }
72 for i := 0; i < len(s); i++ {
73 c := s[i]
74 if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
75 return false
76 }
77 }
78 return true
79}
80
81// parseStringRows parses a JSON array of arrays of strings, e.g.
82// [["g1..","grin","10"],["g1..","joy","4"]]. Numbers are accepted unquoted.
83// Panics on malformed input.
84func parseStringRows(s string) [][]string {
85 p := &jparser{s: s}
86 p.ws()
87 p.expect('[')
88 var rows [][]string
89 p.ws()
90 if p.peek() == ']' {
91 p.i++
92 return rows
93 }
94 for {
95 p.ws()
96 p.expect('[')
97 var row []string
98 p.ws()
99 if p.peek() == ']' {
100 p.i++
101 } else {
102 for {
103 p.ws()
104 row = append(row, p.value())
105 p.ws()
106 if p.peek() == ',' {
107 p.i++
108 continue
109 }
110 p.expect(']')
111 break
112 }
113 }
114 rows = append(rows, row)
115 p.ws()
116 if p.peek() == ',' {
117 p.i++
118 continue
119 }
120 p.expect(']')
121 break
122 }
123 p.ws()
124 if p.i != len(p.s) {
125 panic("invalid JSON: trailing data")
126 }
127 return rows
128}
129
130// parseStringList parses ["a","b",...].
131func parseStringList(s string) []string {
132 rows := parseStringRows("[" + s + "]")
133 if len(rows) != 1 {
134 panic("invalid JSON list")
135 }
136 return rows[0]
137}
138
139type jparser struct {
140 s string
141 i int
142}
143
144func (p *jparser) peek() byte {
145 if p.i >= len(p.s) {
146 panic("invalid JSON: unexpected end")
147 }
148 return p.s[p.i]
149}
150
151func (p *jparser) ws() {
152 for p.i < len(p.s) && (p.s[p.i] == ' ' || p.s[p.i] == '\n' || p.s[p.i] == '\t' || p.s[p.i] == '\r') {
153 p.i++
154 }
155}
156
157func (p *jparser) expect(c byte) {
158 if p.peek() != c {
159 panic("invalid JSON: expected " + string(c) + " at " + strconv.Itoa(p.i))
160 }
161 p.i++
162}
163
164func (p *jparser) value() string {
165 if p.peek() != '"' {
166 st := p.i
167 for p.i < len(p.s) && (p.s[p.i] >= '0' && p.s[p.i] <= '9' || p.s[p.i] == '-') {
168 p.i++
169 }
170 if st == p.i {
171 panic("invalid JSON: expected string or number at " + strconv.Itoa(st))
172 }
173 return p.s[st:p.i]
174 }
175 p.i++
176 var b strings.Builder
177 for {
178 c := p.peek()
179 p.i++
180 if c == '"' {
181 return b.String()
182 }
183 if c == '\\' {
184 e := p.peek()
185 p.i++
186 switch e {
187 case '"', '\\', '/':
188 b.WriteByte(e)
189 case 'n':
190 b.WriteByte('\n')
191 case 't':
192 b.WriteByte('\t')
193 case 'r':
194 b.WriteByte('\r')
195 default:
196 panic("invalid JSON escape")
197 }
198 continue
199 }
200 b.WriteByte(c)
201 }
202}
203
204// ── avl uint helpers ─────────────────────────────────────────────────────────
205
206func getU(t *avl.Tree, key string) uint64 {
207 if !t.Has(key) {
208 return 0
209 }
210 v, _ := strconv.ParseUint(t.Get(key).(string), 10, 64)
211 return v
212}
213
214func setU(t *avl.Tree, key string, v uint64) {
215 t.Set(key, strconv.FormatUint(v, 10))
216}
217
218// addU adds d to a stored counter; panics instead of wrapping on overflow.
219func addU(t *avl.Tree, key string, d uint64) uint64 {
220 old := getU(t, key)
221 v := old + d
222 if v < old {
223 panic("math overflow")
224 }
225 setU(t, key, v)
226 return v
227}
228
229func balOf(addr, coin string) uint64 { return getU(&balances, addr+":"+coin) }
230
231func creditBal(addr, coin string, amt uint64) {
232 if amt > 0 {
233 addU(&balances, addr+":"+coin, amt)
234 }
235}
236
237func debitBal(addr, coin string, amt uint64) {
238 have := balOf(addr, coin)
239 if have < amt {
240 panic("insufficient " + coin + ": need " + strconv.FormatUint(amt, 10) + " have " + strconv.FormatUint(have, 10))
241 }
242 setU(&balances, addr+":"+coin, have-amt)
243}