parse.mx raw
1 package nostr
2
3 // Minimal JSON parsing for Nostr relay messages.
4 // No encoding/json. Hand-rolled for speed.
5
6 // ParseEvent parses a JSON event object into an Event.
7 func ParseEvent(s string) (e *Event) {
8 ev := &Event{}
9 i := skipWS(s, 0)
10 if i >= len(s) || s[i] != '{' {
11 return nil
12 }
13 i++
14 for i < len(s) {
15 i = skipWS(s, i)
16 if i >= len(s) {
17 return nil
18 }
19 if s[i] == '}' {
20 return ev
21 }
22 if s[i] == ',' {
23 i++
24 continue
25 }
26 // Key.
27 key, ni := parseString(s, i)
28 if ni < 0 {
29 return nil
30 }
31 i = skipWS(s, ni)
32 if i >= len(s) || s[i] != ':' {
33 return nil
34 }
35 i = skipWS(s, i+1)
36
37 switch key {
38 case "id":
39 ev.ID, i = parseString(s, i)
40 if i < 0 {
41 return nil
42 }
43 case "pubkey":
44 ev.PubKey, i = parseString(s, i)
45 if i < 0 {
46 return nil
47 }
48 case "created_at":
49 ev.CreatedAt, i = parseInt(s, i)
50 if i < 0 {
51 return nil
52 }
53 case "kind":
54 var k int64
55 k, i = parseInt(s, i)
56 if i < 0 {
57 return nil
58 }
59 if k < 0 || k > 0xffffffff {
60 return nil
61 }
62 ev.Kind = uint32(k)
63 case "content":
64 ev.Content, i = parseString(s, i)
65 if i < 0 {
66 return nil
67 }
68 case "sig":
69 ev.Sig, i = parseString(s, i)
70 if i < 0 {
71 return nil
72 }
73 case "tags":
74 ev.Tags, i = parseTags(s, i)
75 if i < 0 {
76 return nil
77 }
78 default:
79 // Skip unknown field value.
80 i = skipValue(s, i)
81 if i < 0 {
82 return nil
83 }
84 }
85 }
86 return ev
87 }
88
89 // ParseRelayMessage parses a relay message array.
90 // Returns (label, subscriptionID, payload) where:
91 // - EVENT: label="EVENT", subID set, payload = event JSON string
92 // - EOSE: label="EOSE", subID set
93 // - OK: label="OK", subID = eventID, payload = "true:<msg>" or "false:<msg>"
94 // - NOTICE: label="NOTICE", payload = message
95 // - AUTH: label="AUTH", payload = challenge
96 func ParseRelayMessage(s string) (label, subID, payload string) {
97 i := skipWS(s, 0)
98 if i >= len(s) || s[i] != '[' {
99 return
100 }
101 i = skipWS(s, i+1)
102
103 // First element: label string.
104 label, i = parseString(s, i)
105 if i < 0 {
106 label = ""
107 return
108 }
109
110 switch label {
111 case "EVENT":
112 i = skipWS(s, i)
113 if i >= len(s) || s[i] != ',' {
114 return
115 }
116 i = skipWS(s, i+1)
117 subID, i = parseString(s, i)
118 if i < 0 {
119 return
120 }
121 i = skipWS(s, i)
122 if i >= len(s) || s[i] != ',' {
123 return
124 }
125 i = skipWS(s, i+1)
126 // Rest until closing ] is the event JSON.
127 start := i
128 i = skipValue(s, i)
129 if i < 0 {
130 return
131 }
132 payload = s[start:i]
133
134 case "EOSE":
135 i = skipWS(s, i)
136 if i >= len(s) || s[i] != ',' {
137 return
138 }
139 i = skipWS(s, i+1)
140 subID, i = parseString(s, i)
141
142 case "OK":
143 i = skipWS(s, i)
144 if i >= len(s) || s[i] != ',' {
145 return
146 }
147 i = skipWS(s, i+1)
148 subID, i = parseString(s, i) // actually eventID
149 if i < 0 {
150 return
151 }
152 i = skipWS(s, i)
153 if i >= len(s) || s[i] != ',' {
154 return
155 }
156 i = skipWS(s, i+1)
157 // Boolean.
158 ok := false
159 if i+4 <= len(s) && s[i:i+4] == "true" {
160 ok = true
161 i += 4
162 } else if i+5 <= len(s) && s[i:i+5] == "false" {
163 i += 5
164 }
165 // Optional message.
166 i = skipWS(s, i)
167 msg := ""
168 if i < len(s) && s[i] == ',' {
169 i = skipWS(s, i+1)
170 msg, i = parseString(s, i)
171 }
172 if ok {
173 payload = "true:" | msg
174 } else {
175 payload = "false:" | msg
176 }
177
178 case "NOTICE":
179 i = skipWS(s, i)
180 if i >= len(s) || s[i] != ',' {
181 return
182 }
183 i = skipWS(s, i+1)
184 payload, i = parseString(s, i)
185
186 case "AUTH":
187 i = skipWS(s, i)
188 if i >= len(s) || s[i] != ',' {
189 return
190 }
191 i = skipWS(s, i+1)
192 payload, i = parseString(s, i)
193 }
194
195 return
196 }
197
198 // ParseFilter parses a JSON filter object into a Filter.
199 func ParseFilter(s string) (f *Filter) {
200 f = &Filter{}
201 i := skipWS(s, 0)
202 if i >= len(s) || s[i] != '{' {
203 return nil
204 }
205 i++
206 for i < len(s) {
207 i = skipWS(s, i)
208 if i >= len(s) {
209 return nil
210 }
211 if s[i] == '}' {
212 return f
213 }
214 if s[i] == ',' {
215 i++
216 continue
217 }
218 key, ni := parseString(s, i)
219 if ni < 0 {
220 return nil
221 }
222 i = skipWS(s, ni)
223 if i >= len(s) || s[i] != ':' {
224 return nil
225 }
226 i = skipWS(s, i+1)
227
228 switch key {
229 case "ids":
230 f.IDs, i = parseStrArray(s, i)
231 case "authors":
232 f.Authors, i = parseStrArray(s, i)
233 case "kinds":
234 f.Kinds, i = parseKindArray(s, i)
235 case "since":
236 f.Since, i = parseInt(s, i)
237 case "until":
238 f.Until, i = parseInt(s, i)
239 case "limit":
240 var l int64
241 l, i = parseInt(s, i)
242 if l < 0 {
243 l = 0
244 } else if l > 0xffffffff {
245 l = 0xffffffff
246 }
247 f.Limit = uint32(l)
248 case "search":
249 f.Search, i = parseString(s, i)
250 case "_proxy":
251 f.Proxy, i = parseStrArray(s, i)
252 default:
253 if len(key) == 2 && key[0] == '#' {
254 if f.Tags == nil {
255 f.Tags = map[string][]string{}
256 }
257 f.Tags[key], i = parseStrArray(s, i)
258 } else {
259 i = skipValue(s, i)
260 }
261 }
262 if i < 0 {
263 return nil
264 }
265 }
266 return f
267 }
268
269 // ParseEventsJSON parses a JSON array of event objects.
270 func ParseEventsJSON(s string) (es []*Event) {
271 i := skipWS(s, 0)
272 if i >= len(s) || s[i] != '[' {
273 return nil
274 }
275 i++
276 n, _ := countArray(s, i-1)
277 events := []*Event{:0:n}
278 for {
279 i = skipWS(s, i)
280 if i >= len(s) {
281 return events
282 }
283 if s[i] == ']' {
284 return events
285 }
286 if s[i] == ',' {
287 i++
288 continue
289 }
290 start := i
291 i = skipValue(s, i)
292 if i < 0 {
293 return events
294 }
295 ev := ParseEvent(s[start:i])
296 if ev != nil {
297 events = push(events, ev)
298 }
299 }
300 }
301
302 // countArray counts the top-level values in the JSON array starting at s[i]
303 // (which must be '['), returning the count and the index just past the closing
304 // ']'. On failure it returns the number of complete values seen so far with
305 // end == -1, so a caller can presize its sink exactly: push does not grow, and
306 // a lazily-initialized sink would fail loud on the fifth value.
307 func countArray(s string, i int32) (n, end int32) {
308 if i >= len(s) || s[i] != '[' {
309 return 0, -1
310 }
311 i++
312 for {
313 i = skipWS(s, i)
314 if i >= len(s) {
315 return n, -1
316 }
317 if s[i] == ']' {
318 return n, i + 1
319 }
320 if s[i] == ',' {
321 i++
322 continue
323 }
324 if s[i] == '[' {
325 i = skipBracketed(s, i, '[', ']')
326 } else if s[i] == '"' {
327 _, i = parseString(s, i)
328 } else {
329 i = skipValue(s, i)
330 }
331 if i < 0 {
332 return n, -1
333 }
334 n++
335 }
336 }
337
338 func parseStrArray(s string, i int32) (arr []string, next int32) {
339 i = skipWS(s, i)
340 if i >= len(s) || s[i] != '[' {
341 return nil, -1
342 }
343 n, _ := countArray(s, i)
344 i++
345 out := []string{:0:n}
346 for {
347 i = skipWS(s, i)
348 if i >= len(s) {
349 return nil, -1
350 }
351 if s[i] == ']' {
352 return out, i + 1
353 }
354 if s[i] == ',' {
355 i++
356 continue
357 }
358 v, ni := parseString(s, i)
359 if ni < 0 {
360 return nil, -1
361 }
362 out = push(out, v)
363 i = ni
364 }
365 }
366
367 func parseKindArray(s string, i int32) (arr []uint32, next int32) {
368 i = skipWS(s, i)
369 if i >= len(s) || s[i] != '[' {
370 return nil, -1
371 }
372 nk, _ := countArray(s, i)
373 i++
374 out := []uint32{:0:nk}
375 for {
376 i = skipWS(s, i)
377 if i >= len(s) {
378 return nil, -1
379 }
380 if s[i] == ']' {
381 return out, i + 1
382 }
383 if s[i] == ',' {
384 i++
385 continue
386 }
387 n, ni := parseInt(s, i)
388 if ni < 0 {
389 return nil, -1
390 }
391 // Clamp; kinds are uint32 on the wire. Out-of-range is dropped rather
392 // than erroring the whole filter (tolerant relay parsing).
393 if n < 0 || n > 0xffffffff {
394 i = ni
395 continue
396 }
397 out = push(out, uint32(n))
398 i = ni
399 }
400 }
401
402 // --- Low-level JSON parsing ---
403
404 func skipWS(s string, i int32) (n int32) {
405 for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') {
406 i++
407 }
408 return i
409 }
410
411 func parseString(s string, i int32) (str string, next int32) {
412 if i >= len(s) || s[i] != '"' {
413 return "", -1
414 }
415 i++
416 start := i
417 // Use string concat, not []byte - tinyjs strings are UTF-16, byte ops corrupt emoji.
418 result := ""
419 for i < len(s) {
420 if s[i] == '\\' {
421 result |= s[start:i]
422 i++
423 if i >= len(s) {
424 return "", -1
425 }
426 switch s[i] {
427 case '"', '\\', '/':
428 result |= s[i : i+1]
429 case 'n':
430 result |= "\n"
431 case 'r':
432 result |= "\r"
433 case 't':
434 result |= "\t"
435 case 'b':
436 result |= "\b"
437 case 'f':
438 result |= "\f"
439 case 'u':
440 if i+4 >= len(s) {
441 return "", -1
442 }
443 cp := hexVal(s[i+1])<<12 | hexVal(s[i+2])<<8 | hexVal(s[i+3])<<4 | hexVal(s[i+4])
444 // Surrogate pair: \uD800-\uDBFF followed by \uDC00-\uDFFF.
445 if cp >= 0xD800 && cp <= 0xDBFF && i+11 <= len(s) && s[i+5] == '\\' && s[i+6] == 'u' {
446 lo := hexVal(s[i+7])<<12 | hexVal(s[i+8])<<8 | hexVal(s[i+9])<<4 | hexVal(s[i+10])
447 if lo >= 0xDC00 && lo <= 0xDFFF {
448 cp = 0x10000 + (cp-0xD800)*0x400 + (lo - 0xDC00)
449 i += 6
450 }
451 }
452 result |= string(rune(cp))
453 i += 4
454 default:
455 result |= s[i : i+1]
456 }
457 i++
458 start = i
459 continue
460 }
461 if s[i] == '"' {
462 result |= s[start:i]
463 return result, i + 1
464 }
465 i++
466 }
467 return "", -1
468 }
469
470 func hexVal(c byte) (n int32) {
471 if c >= '0' && c <= '9' {
472 return int32(c - '0')
473 }
474 if c >= 'a' && c <= 'f' {
475 return int32(c-'a') + 10
476 }
477 if c >= 'A' && c <= 'F' {
478 return int32(c-'A') + 10
479 }
480 return 0
481 }
482
483 func parseInt(s string, i int32) (val int64, next int32) {
484 if i >= len(s) {
485 return 0, -1
486 }
487 neg := false
488 if s[i] == '-' {
489 neg = true
490 i++
491 }
492 if i >= len(s) || s[i] < '0' || s[i] > '9' {
493 return 0, -1
494 }
495 var n int64
496 for i < len(s) && s[i] >= '0' && s[i] <= '9' {
497 n = n*10 + int64(s[i]-'0')
498 i++
499 }
500 if neg {
501 n = -n
502 }
503 return n, i
504 }
505
506 func parseTags(s string, i int32) (ts [][]string, next int32) {
507 if i >= len(s) || s[i] != '[' {
508 return nil, -1
509 }
510 nt, _ := countArray(s, i)
511 i++
512 tags := [][]string{:0:nt}
513 for {
514 i = skipWS(s, i)
515 if i >= len(s) {
516 return nil, -1
517 }
518 if s[i] == ']' {
519 return tags, i + 1
520 }
521 if s[i] == ',' {
522 i++
523 continue
524 }
525 // Parse inner array.
526 if s[i] != '[' {
527 return nil, -1
528 }
529 ne, _ := countArray(s, i)
530 i++
531 tag := []string{:0:ne}
532 for {
533 i = skipWS(s, i)
534 if i >= len(s) {
535 return nil, -1
536 }
537 if s[i] == ']' {
538 i++
539 break
540 }
541 if s[i] == ',' {
542 i++
543 continue
544 }
545 var val string
546 val, i = parseString(s, i)
547 if i < 0 {
548 return nil, -1
549 }
550 tag = push(tag, val)
551 }
552 tags = push(tags, tag)
553 }
554 }
555
556 // skipValue skips a JSON value (string, number, object, array, bool, null).
557 func skipValue(s string, i int32) (n int32) {
558 if i >= len(s) {
559 return -1
560 }
561 switch s[i] {
562 case '"':
563 _, ni := parseString(s, i)
564 return ni
565 case '{':
566 return skipBracketed(s, i, '{', '}')
567 case '[':
568 return skipBracketed(s, i, '[', ']')
569 case 't': // true
570 if i+4 <= len(s) {
571 return i + 4
572 }
573 return -1
574 case 'f': // false
575 if i+5 <= len(s) {
576 return i + 5
577 }
578 return -1
579 case 'n': // null
580 if i+4 <= len(s) {
581 return i + 4
582 }
583 return -1
584 default:
585 // Number.
586 for i < len(s) && s[i] != ',' && s[i] != '}' && s[i] != ']' && s[i] != ' ' && s[i] != '\n' {
587 i++
588 }
589 return i
590 }
591 }
592
593 func skipBracketed(s string, i int32, open, closeB byte) (n int32) {
594 if i >= len(s) || s[i] != open {
595 return -1
596 }
597 depth := int32(1)
598 i++
599 inStr := false
600 for i < len(s) && depth > 0 {
601 if inStr {
602 if s[i] == '\\' {
603 i++
604 } else if s[i] == '"' {
605 inStr = false
606 }
607 } else {
608 if s[i] == '"' {
609 inStr = true
610 } else if s[i] == open {
611 depth++
612 } else if s[i] == closeB {
613 depth--
614 }
615 }
616 i++
617 }
618 if depth != 0 {
619 return -1
620 }
621 return i
622 }
623