package nostr // Minimal JSON parsing for Nostr relay messages. // No encoding/json. Hand-rolled for speed. // ParseEvent parses a JSON event object into an Event. func ParseEvent(s string) (e *Event) { ev := &Event{} i := skipWS(s, 0) if i >= len(s) || s[i] != '{' { return nil } i++ for i < len(s) { i = skipWS(s, i) if i >= len(s) { return nil } if s[i] == '}' { return ev } if s[i] == ',' { i++ continue } // Key. key, ni := parseString(s, i) if ni < 0 { return nil } i = skipWS(s, ni) if i >= len(s) || s[i] != ':' { return nil } i = skipWS(s, i+1) switch key { case "id": ev.ID, i = parseString(s, i) if i < 0 { return nil } case "pubkey": ev.PubKey, i = parseString(s, i) if i < 0 { return nil } case "created_at": ev.CreatedAt, i = parseInt(s, i) if i < 0 { return nil } case "kind": var k int64 k, i = parseInt(s, i) if i < 0 { return nil } if k < 0 || k > 0xffffffff { return nil } ev.Kind = uint32(k) case "content": ev.Content, i = parseString(s, i) if i < 0 { return nil } case "sig": ev.Sig, i = parseString(s, i) if i < 0 { return nil } case "tags": ev.Tags, i = parseTags(s, i) if i < 0 { return nil } default: // Skip unknown field value. i = skipValue(s, i) if i < 0 { return nil } } } return ev } // ParseRelayMessage parses a relay message array. // Returns (label, subscriptionID, payload) where: // - EVENT: label="EVENT", subID set, payload = event JSON string // - EOSE: label="EOSE", subID set // - OK: label="OK", subID = eventID, payload = "true:" or "false:" // - NOTICE: label="NOTICE", payload = message // - AUTH: label="AUTH", payload = challenge func ParseRelayMessage(s string) (label, subID, payload string) { i := skipWS(s, 0) if i >= len(s) || s[i] != '[' { return } i = skipWS(s, i+1) // First element: label string. label, i = parseString(s, i) if i < 0 { label = "" return } switch label { case "EVENT": i = skipWS(s, i) if i >= len(s) || s[i] != ',' { return } i = skipWS(s, i+1) subID, i = parseString(s, i) if i < 0 { return } i = skipWS(s, i) if i >= len(s) || s[i] != ',' { return } i = skipWS(s, i+1) // Rest until closing ] is the event JSON. start := i i = skipValue(s, i) if i < 0 { return } payload = s[start:i] case "EOSE": i = skipWS(s, i) if i >= len(s) || s[i] != ',' { return } i = skipWS(s, i+1) subID, i = parseString(s, i) case "OK": i = skipWS(s, i) if i >= len(s) || s[i] != ',' { return } i = skipWS(s, i+1) subID, i = parseString(s, i) // actually eventID if i < 0 { return } i = skipWS(s, i) if i >= len(s) || s[i] != ',' { return } i = skipWS(s, i+1) // Boolean. ok := false if i+4 <= len(s) && s[i:i+4] == "true" { ok = true i += 4 } else if i+5 <= len(s) && s[i:i+5] == "false" { i += 5 } // Optional message. i = skipWS(s, i) msg := "" if i < len(s) && s[i] == ',' { i = skipWS(s, i+1) msg, i = parseString(s, i) } if ok { payload = "true:" | msg } else { payload = "false:" | msg } case "NOTICE": i = skipWS(s, i) if i >= len(s) || s[i] != ',' { return } i = skipWS(s, i+1) payload, i = parseString(s, i) case "AUTH": i = skipWS(s, i) if i >= len(s) || s[i] != ',' { return } i = skipWS(s, i+1) payload, i = parseString(s, i) } return } // ParseFilter parses a JSON filter object into a Filter. func ParseFilter(s string) (f *Filter) { f = &Filter{} i := skipWS(s, 0) if i >= len(s) || s[i] != '{' { return nil } i++ for i < len(s) { i = skipWS(s, i) if i >= len(s) { return nil } if s[i] == '}' { return f } if s[i] == ',' { i++ continue } key, ni := parseString(s, i) if ni < 0 { return nil } i = skipWS(s, ni) if i >= len(s) || s[i] != ':' { return nil } i = skipWS(s, i+1) switch key { case "ids": f.IDs, i = parseStrArray(s, i) case "authors": f.Authors, i = parseStrArray(s, i) case "kinds": f.Kinds, i = parseKindArray(s, i) case "since": f.Since, i = parseInt(s, i) case "until": f.Until, i = parseInt(s, i) case "limit": var l int64 l, i = parseInt(s, i) if l < 0 { l = 0 } else if l > 0xffffffff { l = 0xffffffff } f.Limit = uint32(l) case "search": f.Search, i = parseString(s, i) case "_proxy": f.Proxy, i = parseStrArray(s, i) default: if len(key) == 2 && key[0] == '#' { if f.Tags == nil { f.Tags = map[string][]string{} } f.Tags[key], i = parseStrArray(s, i) } else { i = skipValue(s, i) } } if i < 0 { return nil } } return f } // ParseEventsJSON parses a JSON array of event objects. func ParseEventsJSON(s string) (es []*Event) { i := skipWS(s, 0) if i >= len(s) || s[i] != '[' { return nil } i++ n, _ := countArray(s, i-1) events := []*Event{:0:n} for { i = skipWS(s, i) if i >= len(s) { return events } if s[i] == ']' { return events } if s[i] == ',' { i++ continue } start := i i = skipValue(s, i) if i < 0 { return events } ev := ParseEvent(s[start:i]) if ev != nil { events = push(events, ev) } } } // countArray counts the top-level values in the JSON array starting at s[i] // (which must be '['), returning the count and the index just past the closing // ']'. On failure it returns the number of complete values seen so far with // end == -1, so a caller can presize its sink exactly: push does not grow, and // a lazily-initialized sink would fail loud on the fifth value. func countArray(s string, i int32) (n, end int32) { if i >= len(s) || s[i] != '[' { return 0, -1 } i++ for { i = skipWS(s, i) if i >= len(s) { return n, -1 } if s[i] == ']' { return n, i + 1 } if s[i] == ',' { i++ continue } if s[i] == '[' { i = skipBracketed(s, i, '[', ']') } else if s[i] == '"' { _, i = parseString(s, i) } else { i = skipValue(s, i) } if i < 0 { return n, -1 } n++ } } func parseStrArray(s string, i int32) (arr []string, next int32) { i = skipWS(s, i) if i >= len(s) || s[i] != '[' { return nil, -1 } n, _ := countArray(s, i) i++ out := []string{:0:n} for { i = skipWS(s, i) if i >= len(s) { return nil, -1 } if s[i] == ']' { return out, i + 1 } if s[i] == ',' { i++ continue } v, ni := parseString(s, i) if ni < 0 { return nil, -1 } out = push(out, v) i = ni } } func parseKindArray(s string, i int32) (arr []uint32, next int32) { i = skipWS(s, i) if i >= len(s) || s[i] != '[' { return nil, -1 } nk, _ := countArray(s, i) i++ out := []uint32{:0:nk} for { i = skipWS(s, i) if i >= len(s) { return nil, -1 } if s[i] == ']' { return out, i + 1 } if s[i] == ',' { i++ continue } n, ni := parseInt(s, i) if ni < 0 { return nil, -1 } // Clamp; kinds are uint32 on the wire. Out-of-range is dropped rather // than erroring the whole filter (tolerant relay parsing). if n < 0 || n > 0xffffffff { i = ni continue } out = push(out, uint32(n)) i = ni } } // --- Low-level JSON parsing --- func skipWS(s string, i int32) (n int32) { for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') { i++ } return i } func parseString(s string, i int32) (str string, next int32) { if i >= len(s) || s[i] != '"' { return "", -1 } i++ start := i // Use string concat, not []byte - tinyjs strings are UTF-16, byte ops corrupt emoji. result := "" for i < len(s) { if s[i] == '\\' { result |= s[start:i] i++ if i >= len(s) { return "", -1 } switch s[i] { case '"', '\\', '/': result |= s[i : i+1] case 'n': result |= "\n" case 'r': result |= "\r" case 't': result |= "\t" case 'b': result |= "\b" case 'f': result |= "\f" case 'u': if i+4 >= len(s) { return "", -1 } cp := hexVal(s[i+1])<<12 | hexVal(s[i+2])<<8 | hexVal(s[i+3])<<4 | hexVal(s[i+4]) // Surrogate pair: \uD800-\uDBFF followed by \uDC00-\uDFFF. if cp >= 0xD800 && cp <= 0xDBFF && i+11 <= len(s) && s[i+5] == '\\' && s[i+6] == 'u' { lo := hexVal(s[i+7])<<12 | hexVal(s[i+8])<<8 | hexVal(s[i+9])<<4 | hexVal(s[i+10]) if lo >= 0xDC00 && lo <= 0xDFFF { cp = 0x10000 + (cp-0xD800)*0x400 + (lo - 0xDC00) i += 6 } } result |= string(rune(cp)) i += 4 default: result |= s[i : i+1] } i++ start = i continue } if s[i] == '"' { result |= s[start:i] return result, i + 1 } i++ } return "", -1 } func hexVal(c byte) (n int32) { if c >= '0' && c <= '9' { return int32(c - '0') } if c >= 'a' && c <= 'f' { return int32(c-'a') + 10 } if c >= 'A' && c <= 'F' { return int32(c-'A') + 10 } return 0 } func parseInt(s string, i int32) (val int64, next int32) { if i >= len(s) { return 0, -1 } neg := false if s[i] == '-' { neg = true i++ } if i >= len(s) || s[i] < '0' || s[i] > '9' { return 0, -1 } var n int64 for i < len(s) && s[i] >= '0' && s[i] <= '9' { n = n*10 + int64(s[i]-'0') i++ } if neg { n = -n } return n, i } func parseTags(s string, i int32) (ts [][]string, next int32) { if i >= len(s) || s[i] != '[' { return nil, -1 } nt, _ := countArray(s, i) i++ tags := [][]string{:0:nt} for { i = skipWS(s, i) if i >= len(s) { return nil, -1 } if s[i] == ']' { return tags, i + 1 } if s[i] == ',' { i++ continue } // Parse inner array. if s[i] != '[' { return nil, -1 } ne, _ := countArray(s, i) i++ tag := []string{:0:ne} for { i = skipWS(s, i) if i >= len(s) { return nil, -1 } if s[i] == ']' { i++ break } if s[i] == ',' { i++ continue } var val string val, i = parseString(s, i) if i < 0 { return nil, -1 } tag = push(tag, val) } tags = push(tags, tag) } } // skipValue skips a JSON value (string, number, object, array, bool, null). func skipValue(s string, i int32) (n int32) { if i >= len(s) { return -1 } switch s[i] { case '"': _, ni := parseString(s, i) return ni case '{': return skipBracketed(s, i, '{', '}') case '[': return skipBracketed(s, i, '[', ']') case 't': // true if i+4 <= len(s) { return i + 4 } return -1 case 'f': // false if i+5 <= len(s) { return i + 5 } return -1 case 'n': // null if i+4 <= len(s) { return i + 4 } return -1 default: // Number. for i < len(s) && s[i] != ',' && s[i] != '}' && s[i] != ']' && s[i] != ' ' && s[i] != '\n' { i++ } return i } } func skipBracketed(s string, i int32, open, closeB byte) (n int32) { if i >= len(s) || s[i] != open { return -1 } depth := int32(1) i++ inStr := false for i < len(s) && depth > 0 { if inStr { if s[i] == '\\' { i++ } else if s[i] == '"' { inStr = false } } else { if s[i] == '"' { inStr = true } else if s[i] == open { depth++ } else if s[i] == closeB { depth-- } } i++ } if depth != 0 { return -1 } return i }