helpers.mx raw
1 package text
2
3 import (
4 "bytes"
5 "encoding/hex"
6 "io"
7
8 "smesh.lol/pkg/lol/chk"
9 "smesh.lol/pkg/lol/errorf"
10 )
11
12 // JSONKey generates the JSON format for an object key terminated with colon.
13 func JSONKey(dst, k []byte) []byte {
14 dst = append(dst, '"')
15 dst = append(dst, k...)
16 dst = append(dst, '"', ':')
17 return dst
18 }
19
20 // UnmarshalHex decodes a quoted hex value from b.
21 func UnmarshalHex(b []byte) (h []byte, rem []byte, err error) {
22 rem = b[:]
23 var inQuote bool
24 var start int
25 for i := 0; i < len(b); i++ {
26 if !inQuote {
27 if b[i] == '"' {
28 inQuote = true
29 start = i + 1
30 }
31 } else if b[i] == '"' {
32 hexStr := b[start:i]
33 rem = b[i+1:]
34 l := len(hexStr)
35 if l%2 != 0 {
36 err = errorf.E([]byte("invalid length for hex: %d, %0x"), len(hexStr), hexStr)
37 return
38 }
39 h = []byte{:l/2}
40 if _, err = hex.Decode(h, hexStr); chk.E(err) {
41 return
42 }
43 return
44 }
45 }
46 if !inQuote {
47 err = io.EOF
48 }
49 return
50 }
51
52 // UnmarshalQuoted performs in-place unquoting of a NIP-01 quoted byte string.
53 func UnmarshalQuoted(b []byte) (content, rem []byte, err error) {
54 if len(b) == 0 {
55 err = io.EOF
56 return
57 }
58 rem = b[:]
59 for ; len(rem) >= 0; rem = rem[1:] {
60 if len(rem) == 0 {
61 err = io.EOF
62 return
63 }
64 if rem[0] == '"' {
65 rem = rem[1:]
66 content = rem
67 break
68 }
69 }
70 if len(rem) == 0 {
71 err = io.EOF
72 return
73 }
74 var escaping bool
75 var contentLen int
76 for len(rem) > 0 {
77 if rem[0] == '\\' {
78 if !escaping {
79 escaping = true
80 contentLen++
81 rem = rem[1:]
82 } else {
83 escaping = false
84 contentLen++
85 rem = rem[1:]
86 }
87 } else if rem[0] == '"' {
88 if !escaping {
89 rem = rem[1:]
90 content = content[:contentLen]
91 contentCopy := []byte{:len(content)}
92 copy(contentCopy, content)
93 content = NostrUnescape(contentCopy)
94 return
95 }
96 contentLen++
97 rem = rem[1:]
98 escaping = false
99 } else {
100 escaping = false
101 switch rem[0] {
102 case '\b', '\t', '\n', '\f', '\r':
103 pos := len(content) - len(rem)
104 contextStart := pos - 10
105 if contextStart < 0 {
106 contextStart = 0
107 }
108 contextEnd := pos + 10
109 if contextEnd > len(content) {
110 contextEnd = len(content)
111 }
112 err = errorf.E(
113 []byte("invalid character '%s' in quoted string (position %d, context: %q)"),
114 NostrEscape(nil, rem[:1]), pos, string(content[contextStart:contextEnd]),
115 )
116 return
117 }
118 contentLen++
119 rem = rem[1:]
120 }
121 }
122 return
123 }
124
125 // MarshalHexArray encodes a slice of byte slices as a JSON hex array.
126 func MarshalHexArray(dst []byte, ha [][]byte) []byte {
127 b := dst
128 b = append(b, '[')
129 for i := range ha {
130 b = AppendQuote(b, ha[i], hexEncAppend)
131 if i != len(ha)-1 {
132 b = append(b, ',')
133 }
134 }
135 b = append(b, ']')
136 return b
137 }
138
139 func hexEncAppend(dst, src []byte) []byte {
140 l := len(dst)
141 dst = append(dst, []byte{:len(src)*2}...)
142 hex.Encode(dst[l:], src)
143 return dst
144 }
145
146 // UnmarshalHexArray unpacks a JSON array of hex strings with specified byte size.
147 func UnmarshalHexArray(b []byte, size int) (t [][]byte, rem []byte, err error) {
148 rem = b
149 var openBracket bool
150 t = [][]byte{:0:16}
151 for ; len(rem) > 0; rem = rem[1:] {
152 if rem[0] == '[' {
153 openBracket = true
154 } else if openBracket {
155 if rem[0] == ',' {
156 continue
157 } else if rem[0] == ']' {
158 rem = rem[1:]
159 return
160 } else if rem[0] == '"' {
161 var h []byte
162 if h, rem, err = UnmarshalHex(rem); chk.E(err) {
163 return
164 }
165 if len(h) != size {
166 err = errorf.E([]byte("invalid hex array size, got %d expect %d"), 2*len(h), 2*size)
167 return
168 }
169 t = append(t, h)
170 if rem[0] == ']' {
171 rem = rem[1:]
172 return
173 }
174 }
175 }
176 }
177 return
178 }
179
180 // UnmarshalStringArray unpacks a JSON array of strings.
181 func UnmarshalStringArray(b []byte) (t [][]byte, rem []byte, err error) {
182 rem = b
183 var openBracket bool
184 t = [][]byte{:0:16}
185 for ; len(rem) > 0; rem = rem[1:] {
186 if rem[0] == '[' {
187 openBracket = true
188 } else if openBracket {
189 if rem[0] == ',' {
190 continue
191 } else if rem[0] == ']' {
192 rem = rem[1:]
193 return
194 } else if rem[0] == '"' {
195 var h []byte
196 if h, rem, err = UnmarshalQuoted(rem); chk.E(err) {
197 return
198 }
199 t = append(t, h)
200 if rem[0] == ']' {
201 rem = rem[1:]
202 return
203 }
204 }
205 }
206 }
207 return
208 }
209
210 func True() []byte { return []byte("true") }
211 func False() []byte { return []byte("false") }
212
213 func MarshalBool(src []byte, truth bool) []byte {
214 if truth {
215 return append(src, True()...)
216 }
217 return append(src, False()...)
218 }
219
220 func UnmarshalBool(src []byte) (rem []byte, truth bool, err error) {
221 rem = src
222 t, f := True(), False()
223 for i := range rem {
224 if rem[i] == t[0] {
225 if len(rem) < i+len(t) {
226 err = io.EOF
227 return
228 }
229 if bytes.Equal(t, rem[i:i+len(t)]) {
230 truth = true
231 rem = rem[i+len(t):]
232 return
233 }
234 }
235 if rem[i] == f[0] {
236 if len(rem) < i+len(f) {
237 err = io.EOF
238 return
239 }
240 if bytes.Equal(f, rem[i:i+len(f)]) {
241 rem = rem[i+len(f):]
242 return
243 }
244 }
245 }
246 err = io.EOF
247 return
248 }
249
250 func Comma(b []byte) (rem []byte, err error) {
251 rem = b
252 for i := range rem {
253 if rem[i] == ',' {
254 rem = rem[i:]
255 return
256 }
257 }
258 err = io.EOF
259 return
260 }
261