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