skip.go raw

   1  package filter
   2  
   3  import (
   4  	"next.orly.dev/pkg/lol/errorf"
   5  )
   6  
   7  // skipJSONValue skips over an arbitrary JSON value and returns the raw bytes and remainder.
   8  // It handles: objects {}, arrays [], strings "", numbers, true, false, null.
   9  // The input `b` should start at the first character of the value (after the colon in "key":value).
  10  func skipJSONValue(b []byte) (val []byte, r []byte, err error) {
  11  	if len(b) == 0 {
  12  		err = errorf.E("empty input")
  13  		return
  14  	}
  15  
  16  	start := 0
  17  	end := 0
  18  
  19  	switch b[0] {
  20  	case '{':
  21  		// Object - find matching closing brace
  22  		end, err = findMatchingBrace(b, '{', '}')
  23  	case '[':
  24  		// Array - find matching closing bracket
  25  		end, err = findMatchingBrace(b, '[', ']')
  26  	case '"':
  27  		// String - find closing quote (handling escapes)
  28  		end, err = findClosingQuote(b)
  29  	case 't':
  30  		// true
  31  		if len(b) >= 4 && string(b[:4]) == "true" {
  32  			end = 4
  33  		} else {
  34  			err = errorf.E("invalid JSON value starting with 't'")
  35  		}
  36  	case 'f':
  37  		// false
  38  		if len(b) >= 5 && string(b[:5]) == "false" {
  39  			end = 5
  40  		} else {
  41  			err = errorf.E("invalid JSON value starting with 'f'")
  42  		}
  43  	case 'n':
  44  		// null
  45  		if len(b) >= 4 && string(b[:4]) == "null" {
  46  			end = 4
  47  		} else {
  48  			err = errorf.E("invalid JSON value starting with 'n'")
  49  		}
  50  	case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
  51  		// Number - scan until we hit a non-number character
  52  		end = scanNumber(b)
  53  	default:
  54  		err = errorf.E("invalid JSON value starting with '%c'", b[0])
  55  	}
  56  
  57  	if err != nil {
  58  		return
  59  	}
  60  
  61  	val = b[start:end]
  62  	r = b[end:]
  63  	return
  64  }
  65  
  66  // findMatchingBrace finds the index after the closing brace/bracket that matches the opening one.
  67  // It handles nested structures and strings.
  68  func findMatchingBrace(b []byte, open, close byte) (end int, err error) {
  69  	if len(b) == 0 || b[0] != open {
  70  		err = errorf.E("expected '%c'", open)
  71  		return
  72  	}
  73  
  74  	depth := 0
  75  	inString := false
  76  	escaped := false
  77  
  78  	for i := 0; i < len(b); i++ {
  79  		c := b[i]
  80  
  81  		if escaped {
  82  			escaped = false
  83  			continue
  84  		}
  85  
  86  		if c == '\\' && inString {
  87  			escaped = true
  88  			continue
  89  		}
  90  
  91  		if c == '"' {
  92  			inString = !inString
  93  			continue
  94  		}
  95  
  96  		if inString {
  97  			continue
  98  		}
  99  
 100  		if c == open {
 101  			depth++
 102  		} else if c == close {
 103  			depth--
 104  			if depth == 0 {
 105  				end = i + 1
 106  				return
 107  			}
 108  		}
 109  	}
 110  
 111  	err = errorf.E("unmatched '%c'", open)
 112  	return
 113  }
 114  
 115  // findClosingQuote finds the index after the closing quote of a JSON string.
 116  // Handles escape sequences.
 117  func findClosingQuote(b []byte) (end int, err error) {
 118  	if len(b) == 0 || b[0] != '"' {
 119  		err = errorf.E("expected '\"'")
 120  		return
 121  	}
 122  
 123  	escaped := false
 124  	for i := 1; i < len(b); i++ {
 125  		c := b[i]
 126  
 127  		if escaped {
 128  			escaped = false
 129  			continue
 130  		}
 131  
 132  		if c == '\\' {
 133  			escaped = true
 134  			continue
 135  		}
 136  
 137  		if c == '"' {
 138  			end = i + 1
 139  			return
 140  		}
 141  	}
 142  
 143  	err = errorf.E("unclosed string")
 144  	return
 145  }
 146  
 147  // scanNumber scans a JSON number and returns the index after it.
 148  // Handles integers, decimals, and scientific notation.
 149  func scanNumber(b []byte) (end int) {
 150  	for i := 0; i < len(b); i++ {
 151  		c := b[i]
 152  		// Number characters: digits, minus, plus, dot, e, E
 153  		if (c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E' {
 154  			continue
 155  		}
 156  		end = i
 157  		return
 158  	}
 159  	end = len(b)
 160  	return
 161  }
 162