helpers.go raw
1 package text
2
3 import (
4 "io"
5
6 "next.orly.dev/pkg/nostr/encoders/hex"
7 "next.orly.dev/pkg/nostr/utils"
8 "github.com/templexxx/xhex"
9 "next.orly.dev/pkg/lol/chk"
10 "next.orly.dev/pkg/lol/errorf"
11 )
12
13 // JSONKey generates the JSON format for an object key and terminates with the semicolon.
14 func JSONKey(dst, k []byte) (b []byte) {
15 dst = append(dst, '"')
16 dst = append(dst, k...)
17 dst = append(dst, '"', ':')
18 b = dst
19 return
20 }
21
22 // UnmarshalHex takes a byte string that should contain a quoted hexadecimal
23 // encoded value, decodes it using a SIMD hex codec and returns the decoded
24 // bytes in a newly allocated buffer.
25 func UnmarshalHex(b []byte) (h []byte, rem []byte, err error) {
26 rem = b[:]
27 var inQuote bool
28 var start int
29 for i := 0; i < len(b); i++ {
30 if !inQuote {
31 if b[i] == '"' {
32 inQuote = true
33 start = i + 1
34 }
35 } else if b[i] == '"' {
36 hexStr := b[start:i]
37 rem = b[i+1:]
38 l := len(hexStr)
39 if l%2 != 0 {
40 err = errorf.E(
41 "invalid length for hex: %d, %0x",
42 len(hexStr), hexStr,
43 )
44 return
45 }
46 // Allocate a new buffer for the decoded data
47 h = make([]byte, l/2)
48 if err = xhex.Decode(h, hexStr); chk.E(err) {
49 return
50 }
51 return
52 }
53 }
54 if !inQuote {
55 err = io.EOF
56 return
57 }
58 return
59 }
60
61 // UnmarshalQuoted performs an in-place unquoting of NIP-01 quoted byte string.
62 func UnmarshalQuoted(b []byte) (content, rem []byte, err error) {
63 if len(b) == 0 {
64 err = io.EOF
65 return
66 }
67 rem = b[:]
68 for ; len(rem) >= 0; rem = rem[1:] {
69 if len(rem) == 0 {
70 err = io.EOF
71 return
72 }
73 // advance to open quotes
74 if rem[0] == '"' {
75 rem = rem[1:]
76 content = rem
77 break
78 }
79 }
80 if len(rem) == 0 {
81 err = io.EOF
82 return
83 }
84 var escaping bool
85 var contentLen int
86 for len(rem) > 0 {
87 if rem[0] == '\\' {
88 if !escaping {
89 escaping = true
90 contentLen++
91 rem = rem[1:]
92 } else {
93 escaping = false
94 contentLen++
95 rem = rem[1:]
96 }
97 } else if rem[0] == '"' {
98 if !escaping {
99 rem = rem[1:]
100 content = content[:contentLen]
101 // Create a copy of the content to avoid corrupting the original input buffer
102 contentCopy := make([]byte, len(content))
103 copy(contentCopy, content)
104 content = NostrUnescape(contentCopy)
105 return
106 }
107 contentLen++
108 rem = rem[1:]
109 escaping = false
110 } else {
111 escaping = false
112 switch rem[0] {
113 // none of these characters are allowed inside a JSON string:
114 //
115 // backspace, tab, newline, form feed or carriage return.
116 case '\b', '\t', '\n', '\f', '\r':
117 pos := len(content) - len(rem)
118 contextStart := pos - 10
119 if contextStart < 0 {
120 contextStart = 0
121 }
122 contextEnd := pos + 10
123 if contextEnd > len(content) {
124 contextEnd = len(content)
125 }
126 err = errorf.E(
127 "invalid character '%s' in quoted string (position %d, context: %q)",
128 NostrEscape(nil, rem[:1]),
129 pos,
130 string(content[contextStart:contextEnd]),
131 )
132 return
133 }
134 contentLen++
135 rem = rem[1:]
136 }
137 }
138 return
139 }
140
141 func MarshalHexArray(dst []byte, ha [][]byte) (b []byte) {
142 b = dst
143 // Pre-allocate buffer if nil to reduce reallocations
144 // Estimate: [ + (hex encoded item + quotes + comma) * n + ]
145 // Each hex item is 2*size + 2 quotes = 2*size + 2, plus comma for all but last
146 if b == nil && len(ha) > 0 {
147 estimatedSize := 2 // brackets
148 if len(ha) > 0 {
149 // Estimate based on first item size
150 itemSize := len(ha[0]) * 2 // hex encoding doubles size
151 estimatedSize += len(ha) * (itemSize + 2 + 1) // item + quotes + comma
152 }
153 b = make([]byte, 0, estimatedSize)
154 }
155 b = append(b, '[')
156 for i := range ha {
157 b = AppendQuote(b, ha[i], hex.EncAppend)
158 if i != len(ha)-1 {
159 b = append(b, ',')
160 }
161 }
162 b = append(b, ']')
163 return
164 }
165
166 // UnmarshalHexArray unpacks a JSON array containing strings with hexadecimal, and checks all
167 // values have the specified byte size.
168 func UnmarshalHexArray(b []byte, size int) (t [][]byte, rem []byte, err error) {
169 rem = b
170 var openBracket bool
171 // Pre-allocate slice with estimated capacity to reduce reallocations
172 // Estimate based on typical array sizes (can grow if needed)
173 t = make([][]byte, 0, 16)
174 for ; len(rem) > 0; rem = rem[1:] {
175 if rem[0] == '[' {
176 openBracket = true
177 } else if openBracket {
178 if rem[0] == ',' {
179 continue
180 } else if rem[0] == ']' {
181 rem = rem[1:]
182 return
183 } else if rem[0] == '"' {
184 var h []byte
185 if h, rem, err = UnmarshalHex(rem); chk.E(err) {
186 return
187 }
188 if len(h) != size {
189 err = errorf.E(
190 "invalid hex array size, got %d expect %d",
191 2*len(h), 2*size,
192 )
193 return
194 }
195 t = append(t, h)
196 if rem[0] == ']' {
197 rem = rem[1:]
198 // done
199 return
200 }
201 }
202 }
203 }
204 return
205 }
206
207 // UnmarshalStringArray unpacks a JSON array containing strings.
208 func UnmarshalStringArray(b []byte) (t [][]byte, rem []byte, err error) {
209 rem = b
210 var openBracket bool
211 // Pre-allocate slice with estimated capacity to reduce reallocations
212 // Estimate based on typical array sizes (can grow if needed)
213 t = make([][]byte, 0, 16)
214 for ; len(rem) > 0; rem = rem[1:] {
215 if rem[0] == '[' {
216 openBracket = true
217 } else if openBracket {
218 if rem[0] == ',' {
219 continue
220 } else if rem[0] == ']' {
221 rem = rem[1:]
222 return
223 } else if rem[0] == '"' {
224 var h []byte
225 if h, rem, err = UnmarshalQuoted(rem); chk.E(err) {
226 return
227 }
228 t = append(t, h)
229 if rem[0] == ']' {
230 rem = rem[1:]
231 // done
232 return
233 }
234 }
235 }
236 }
237 return
238 }
239
240 func True() []byte { return []byte("true") }
241 func False() []byte { return []byte("false") }
242
243 func MarshalBool(src []byte, truth bool) []byte {
244 if truth {
245 return append(src, True()...)
246 }
247 return append(src, False()...)
248 }
249
250 func UnmarshalBool(src []byte) (rem []byte, truth bool, err error) {
251 rem = src
252 t, f := True(), False()
253 for i := range rem {
254 if rem[i] == t[0] {
255 if len(rem) < i+len(t) {
256 err = io.EOF
257 return
258 }
259 if utils.FastEqual(t, rem[i:i+len(t)]) {
260 truth = true
261 rem = rem[i+len(t):]
262 return
263 }
264 }
265 if rem[i] == f[0] {
266 if len(rem) < i+len(f) {
267 err = io.EOF
268 return
269 }
270 if utils.FastEqual(f, rem[i:i+len(f)]) {
271 rem = rem[i+len(f):]
272 return
273 }
274 }
275 }
276 // if a truth value is not found in the string it will run to the end
277 err = io.EOF
278 return
279 }
280
281 func Comma(b []byte) (rem []byte, err error) {
282 rem = b
283 for i := range rem {
284 if rem[i] == ',' {
285 rem = rem[i:]
286 return
287 }
288 }
289 err = io.EOF
290 return
291 }
292