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