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) *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  			ev.Kind = int(k)
  60  		case "content":
  61  			ev.Content, i = parseString(s, i)
  62  			if i < 0 {
  63  				return nil
  64  			}
  65  		case "sig":
  66  			ev.Sig, i = parseString(s, i)
  67  			if i < 0 {
  68  				return nil
  69  			}
  70  		case "tags":
  71  			ev.Tags, i = parseTags(s, i)
  72  			if i < 0 {
  73  				return nil
  74  			}
  75  		default:
  76  			// Skip unknown field value.
  77  			i = skipValue(s, i)
  78  			if i < 0 {
  79  				return nil
  80  			}
  81  		}
  82  	}
  83  	return ev
  84  }
  85  
  86  // ParseRelayMessage parses a relay message array.
  87  // Returns (label, subscriptionID, payload) where:
  88  //   - EVENT:  label="EVENT", subID set, payload = event JSON string
  89  //   - EOSE:   label="EOSE", subID set
  90  //   - OK:     label="OK", subID = eventID, payload = "true:<msg>" or "false:<msg>"
  91  //   - NOTICE: label="NOTICE", payload = message
  92  //   - AUTH:   label="AUTH", payload = challenge
  93  func ParseRelayMessage(s string) (label, subID, payload string) {
  94  	i := skipWS(s, 0)
  95  	if i >= len(s) || s[i] != '[' {
  96  		return
  97  	}
  98  	i = skipWS(s, i+1)
  99  
 100  	// First element: label string.
 101  	label, i = parseString(s, i)
 102  	if i < 0 {
 103  		label = ""
 104  		return
 105  	}
 106  
 107  	switch label {
 108  	case "EVENT":
 109  		i = skipWS(s, i)
 110  		if i >= len(s) || s[i] != ',' {
 111  			return
 112  		}
 113  		i = skipWS(s, i+1)
 114  		subID, i = parseString(s, i)
 115  		if i < 0 {
 116  			return
 117  		}
 118  		i = skipWS(s, i)
 119  		if i >= len(s) || s[i] != ',' {
 120  			return
 121  		}
 122  		i = skipWS(s, i+1)
 123  		// Rest until closing ] is the event JSON.
 124  		start := i
 125  		i = skipValue(s, i)
 126  		if i < 0 {
 127  			return
 128  		}
 129  		payload = s[start:i]
 130  
 131  	case "EOSE":
 132  		i = skipWS(s, i)
 133  		if i >= len(s) || s[i] != ',' {
 134  			return
 135  		}
 136  		i = skipWS(s, i+1)
 137  		subID, i = parseString(s, i)
 138  
 139  	case "OK":
 140  		i = skipWS(s, i)
 141  		if i >= len(s) || s[i] != ',' {
 142  			return
 143  		}
 144  		i = skipWS(s, i+1)
 145  		subID, i = parseString(s, i) // actually eventID
 146  		if i < 0 {
 147  			return
 148  		}
 149  		i = skipWS(s, i)
 150  		if i >= len(s) || s[i] != ',' {
 151  			return
 152  		}
 153  		i = skipWS(s, i+1)
 154  		// Boolean.
 155  		ok := false
 156  		if i+4 <= len(s) && s[i:i+4] == "true" {
 157  			ok = true
 158  			i += 4
 159  		} else if i+5 <= len(s) && s[i:i+5] == "false" {
 160  			i += 5
 161  		}
 162  		// Optional message.
 163  		i = skipWS(s, i)
 164  		msg := ""
 165  		if i < len(s) && s[i] == ',' {
 166  			i = skipWS(s, i+1)
 167  			msg, i = parseString(s, i)
 168  		}
 169  		if ok {
 170  			payload = "true:" + msg
 171  		} else {
 172  			payload = "false:" + msg
 173  		}
 174  
 175  	case "NOTICE":
 176  		i = skipWS(s, i)
 177  		if i >= len(s) || s[i] != ',' {
 178  			return
 179  		}
 180  		i = skipWS(s, i+1)
 181  		payload, i = parseString(s, i)
 182  
 183  	case "AUTH":
 184  		i = skipWS(s, i)
 185  		if i >= len(s) || s[i] != ',' {
 186  			return
 187  		}
 188  		i = skipWS(s, i+1)
 189  		payload, i = parseString(s, i)
 190  	}
 191  
 192  	return
 193  }
 194  
 195  // ParseFilter parses a JSON filter object into a Filter.
 196  func ParseFilter(s string) *Filter {
 197  	f := &Filter{}
 198  	i := skipWS(s, 0)
 199  	if i >= len(s) || s[i] != '{' {
 200  		return nil
 201  	}
 202  	i++
 203  	for i < len(s) {
 204  		i = skipWS(s, i)
 205  		if i >= len(s) {
 206  			return nil
 207  		}
 208  		if s[i] == '}' {
 209  			return f
 210  		}
 211  		if s[i] == ',' {
 212  			i++
 213  			continue
 214  		}
 215  		key, ni := parseString(s, i)
 216  		if ni < 0 {
 217  			return nil
 218  		}
 219  		i = skipWS(s, ni)
 220  		if i >= len(s) || s[i] != ':' {
 221  			return nil
 222  		}
 223  		i = skipWS(s, i+1)
 224  
 225  		switch key {
 226  		case "ids":
 227  			f.IDs, i = parseStrArray(s, i)
 228  		case "authors":
 229  			f.Authors, i = parseStrArray(s, i)
 230  		case "kinds":
 231  			f.Kinds, i = parseIntArray(s, i)
 232  		case "since":
 233  			f.Since, i = parseInt(s, i)
 234  		case "until":
 235  			f.Until, i = parseInt(s, i)
 236  		case "limit":
 237  			var l int64
 238  			l, i = parseInt(s, i)
 239  			f.Limit = int(l)
 240  		case "_proxy":
 241  			f.Proxy, i = parseStrArray(s, i)
 242  		default:
 243  			if len(key) == 2 && key[0] == '#' {
 244  				if f.Tags == nil {
 245  					f.Tags = map[string][]string{}
 246  				}
 247  				f.Tags[key], i = parseStrArray(s, i)
 248  			} else {
 249  				i = skipValue(s, i)
 250  			}
 251  		}
 252  		if i < 0 {
 253  			return nil
 254  		}
 255  	}
 256  	return f
 257  }
 258  
 259  // ParseEventsJSON parses a JSON array of event objects.
 260  func ParseEventsJSON(s string) []*Event {
 261  	i := skipWS(s, 0)
 262  	if i >= len(s) || s[i] != '[' {
 263  		return nil
 264  	}
 265  	i++
 266  	var events []*Event
 267  	for {
 268  		i = skipWS(s, i)
 269  		if i >= len(s) {
 270  			return events
 271  		}
 272  		if s[i] == ']' {
 273  			return events
 274  		}
 275  		if s[i] == ',' {
 276  			i++
 277  			continue
 278  		}
 279  		start := i
 280  		i = skipValue(s, i)
 281  		if i < 0 {
 282  			return events
 283  		}
 284  		ev := ParseEvent(s[start:i])
 285  		if ev != nil {
 286  			events = append(events, ev)
 287  		}
 288  	}
 289  }
 290  
 291  func parseStrArray(s string, i int) ([]string, int) {
 292  	i = skipWS(s, i)
 293  	if i >= len(s) || s[i] != '[' {
 294  		return nil, -1
 295  	}
 296  	i++
 297  	var out []string
 298  	for {
 299  		i = skipWS(s, i)
 300  		if i >= len(s) {
 301  			return nil, -1
 302  		}
 303  		if s[i] == ']' {
 304  			return out, i + 1
 305  		}
 306  		if s[i] == ',' {
 307  			i++
 308  			continue
 309  		}
 310  		v, ni := parseString(s, i)
 311  		if ni < 0 {
 312  			return nil, -1
 313  		}
 314  		out = append(out, v)
 315  		i = ni
 316  	}
 317  }
 318  
 319  func parseIntArray(s string, i int) ([]int, int) {
 320  	i = skipWS(s, i)
 321  	if i >= len(s) || s[i] != '[' {
 322  		return nil, -1
 323  	}
 324  	i++
 325  	var out []int
 326  	for {
 327  		i = skipWS(s, i)
 328  		if i >= len(s) {
 329  			return nil, -1
 330  		}
 331  		if s[i] == ']' {
 332  			return out, i + 1
 333  		}
 334  		if s[i] == ',' {
 335  			i++
 336  			continue
 337  		}
 338  		n, ni := parseInt(s, i)
 339  		if ni < 0 {
 340  			return nil, -1
 341  		}
 342  		out = append(out, int(n))
 343  		i = ni
 344  	}
 345  }
 346  
 347  // --- Low-level JSON parsing ---
 348  
 349  func skipWS(s string, i int) int {
 350  	for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') {
 351  		i++
 352  	}
 353  	return i
 354  }
 355  
 356  func parseString(s string, i int) (string, int) {
 357  	if i >= len(s) || s[i] != '"' {
 358  		return "", -1
 359  	}
 360  	i++
 361  	start := i
 362  	// Use string concat, not []byte — tinyjs strings are UTF-16, byte ops corrupt emoji.
 363  	result := ""
 364  	for i < len(s) {
 365  		if s[i] == '\\' {
 366  			result += s[start:i]
 367  			i++
 368  			if i >= len(s) {
 369  				return "", -1
 370  			}
 371  			switch s[i] {
 372  			case '"', '\\', '/':
 373  				result += s[i : i+1]
 374  			case 'n':
 375  				result += "\n"
 376  			case 'r':
 377  				result += "\r"
 378  			case 't':
 379  				result += "\t"
 380  			case 'b':
 381  				result += "\b"
 382  			case 'f':
 383  				result += "\f"
 384  			case 'u':
 385  				if i+4 >= len(s) {
 386  					return "", -1
 387  				}
 388  				cp := hexVal(s[i+1])<<12 | hexVal(s[i+2])<<8 | hexVal(s[i+3])<<4 | hexVal(s[i+4])
 389  				// Surrogate pair: \uD800-\uDBFF followed by \uDC00-\uDFFF.
 390  				if cp >= 0xD800 && cp <= 0xDBFF && i+10 <= len(s) && s[i+5] == '\\' && s[i+6] == 'u' {
 391  					lo := hexVal(s[i+7])<<12 | hexVal(s[i+8])<<8 | hexVal(s[i+9])<<4 | hexVal(s[i+10])
 392  					if lo >= 0xDC00 && lo <= 0xDFFF {
 393  						cp = 0x10000 + (cp-0xD800)*0x400 + (lo - 0xDC00)
 394  						i += 6
 395  					}
 396  				}
 397  				result += string(rune(cp))
 398  				i += 4
 399  			default:
 400  				result += s[i : i+1]
 401  			}
 402  			i++
 403  			start = i
 404  			continue
 405  		}
 406  		if s[i] == '"' {
 407  			result += s[start:i]
 408  			return result, i + 1
 409  		}
 410  		i++
 411  	}
 412  	return "", -1
 413  }
 414  
 415  func hexVal(c byte) int {
 416  	if c >= '0' && c <= '9' {
 417  		return int(c - '0')
 418  	}
 419  	if c >= 'a' && c <= 'f' {
 420  		return int(c-'a') + 10
 421  	}
 422  	if c >= 'A' && c <= 'F' {
 423  		return int(c-'A') + 10
 424  	}
 425  	return 0
 426  }
 427  
 428  func parseInt(s string, i int) (int64, int) {
 429  	if i >= len(s) {
 430  		return 0, -1
 431  	}
 432  	neg := false
 433  	if s[i] == '-' {
 434  		neg = true
 435  		i++
 436  	}
 437  	if i >= len(s) || s[i] < '0' || s[i] > '9' {
 438  		return 0, -1
 439  	}
 440  	var n int64
 441  	for i < len(s) && s[i] >= '0' && s[i] <= '9' {
 442  		n = n*10 + int64(s[i]-'0')
 443  		i++
 444  	}
 445  	if neg {
 446  		n = -n
 447  	}
 448  	return n, i
 449  }
 450  
 451  func parseTags(s string, i int) (Tags, int) {
 452  	if i >= len(s) || s[i] != '[' {
 453  		return nil, -1
 454  	}
 455  	i++
 456  	var tags Tags
 457  	for {
 458  		i = skipWS(s, i)
 459  		if i >= len(s) {
 460  			return nil, -1
 461  		}
 462  		if s[i] == ']' {
 463  			return tags, i + 1
 464  		}
 465  		if s[i] == ',' {
 466  			i++
 467  			continue
 468  		}
 469  		// Parse inner array.
 470  		if s[i] != '[' {
 471  			return nil, -1
 472  		}
 473  		i++
 474  		var tag Tag
 475  		for {
 476  			i = skipWS(s, i)
 477  			if i >= len(s) {
 478  				return nil, -1
 479  			}
 480  			if s[i] == ']' {
 481  				i++
 482  				break
 483  			}
 484  			if s[i] == ',' {
 485  				i++
 486  				continue
 487  			}
 488  			var val string
 489  			val, i = parseString(s, i)
 490  			if i < 0 {
 491  				return nil, -1
 492  			}
 493  			tag = append(tag, val)
 494  		}
 495  		tags = append(tags, tag)
 496  	}
 497  }
 498  
 499  // skipValue skips a JSON value (string, number, object, array, bool, null).
 500  func skipValue(s string, i int) int {
 501  	if i >= len(s) {
 502  		return -1
 503  	}
 504  	switch s[i] {
 505  	case '"':
 506  		_, ni := parseString(s, i)
 507  		return ni
 508  	case '{':
 509  		return skipBracketed(s, i, '{', '}')
 510  	case '[':
 511  		return skipBracketed(s, i, '[', ']')
 512  	case 't': // true
 513  		if i+4 <= len(s) {
 514  			return i + 4
 515  		}
 516  		return -1
 517  	case 'f': // false
 518  		if i+5 <= len(s) {
 519  			return i + 5
 520  		}
 521  		return -1
 522  	case 'n': // null
 523  		if i+4 <= len(s) {
 524  			return i + 4
 525  		}
 526  		return -1
 527  	default:
 528  		// Number.
 529  		for i < len(s) && s[i] != ',' && s[i] != '}' && s[i] != ']' && s[i] != ' ' && s[i] != '\n' {
 530  			i++
 531  		}
 532  		return i
 533  	}
 534  }
 535  
 536  func skipBracketed(s string, i int, open, close byte) int {
 537  	if i >= len(s) || s[i] != open {
 538  		return -1
 539  	}
 540  	depth := 1
 541  	i++
 542  	inStr := false
 543  	for i < len(s) && depth > 0 {
 544  		if inStr {
 545  			if s[i] == '\\' {
 546  				i++
 547  			} else if s[i] == '"' {
 548  				inStr = false
 549  			}
 550  		} else {
 551  			if s[i] == '"' {
 552  				inStr = true
 553  			} else if s[i] == open {
 554  				depth++
 555  			} else if s[i] == close {
 556  				depth--
 557  			}
 558  		}
 559  		i++
 560  	}
 561  	if depth != 0 {
 562  		return -1
 563  	}
 564  	return i
 565  }
 566