helpers.go raw

   1  package text
   2  
   3  import (
   4  	"io"
   5  
   6  	"next.orly.dev/pkg/nostr/encoders/hex"
   7  	"next.orly.dev/pkg/nostr/utils"
   8  	"github.com/templexxx/xhex"
   9  	"next.orly.dev/pkg/lol/chk"
  10  	"next.orly.dev/pkg/lol/errorf"
  11  )
  12  
  13  // JSONKey generates the JSON format for an object key and terminates with the semicolon.
  14  func JSONKey(dst, k []byte) (b []byte) {
  15  	dst = append(dst, '"')
  16  	dst = append(dst, k...)
  17  	dst = append(dst, '"', ':')
  18  	b = dst
  19  	return
  20  }
  21  
  22  // UnmarshalHex takes a byte string that should contain a quoted hexadecimal
  23  // encoded value, decodes it using a SIMD hex codec and returns the decoded
  24  // bytes in a newly allocated buffer.
  25  func UnmarshalHex(b []byte) (h []byte, rem []byte, err error) {
  26  	rem = b[:]
  27  	var inQuote bool
  28  	var start int
  29  	for i := 0; i < len(b); i++ {
  30  		if !inQuote {
  31  			if b[i] == '"' {
  32  				inQuote = true
  33  				start = i + 1
  34  			}
  35  		} else if b[i] == '"' {
  36  			hexStr := b[start:i]
  37  			rem = b[i+1:]
  38  			l := len(hexStr)
  39  			if l%2 != 0 {
  40  				err = errorf.E(
  41  					"invalid length for hex: %d, %0x",
  42  					len(hexStr), hexStr,
  43  				)
  44  				return
  45  			}
  46  			// Allocate a new buffer for the decoded data
  47  			h = make([]byte, l/2)
  48  			if err = xhex.Decode(h, hexStr); chk.E(err) {
  49  				return
  50  			}
  51  			return
  52  		}
  53  	}
  54  	if !inQuote {
  55  		err = io.EOF
  56  		return
  57  	}
  58  	return
  59  }
  60  
  61  // UnmarshalQuoted performs an in-place unquoting of NIP-01 quoted byte string.
  62  func UnmarshalQuoted(b []byte) (content, rem []byte, err error) {
  63  	if len(b) == 0 {
  64  		err = io.EOF
  65  		return
  66  	}
  67  	rem = b[:]
  68  	for ; len(rem) >= 0; rem = rem[1:] {
  69  		if len(rem) == 0 {
  70  			err = io.EOF
  71  			return
  72  		}
  73  		// advance to open quotes
  74  		if rem[0] == '"' {
  75  			rem = rem[1:]
  76  			content = rem
  77  			break
  78  		}
  79  	}
  80  	if len(rem) == 0 {
  81  		err = io.EOF
  82  		return
  83  	}
  84  	var escaping bool
  85  	var contentLen int
  86  	for len(rem) > 0 {
  87  		if rem[0] == '\\' {
  88  			if !escaping {
  89  				escaping = true
  90  				contentLen++
  91  				rem = rem[1:]
  92  			} else {
  93  				escaping = false
  94  				contentLen++
  95  				rem = rem[1:]
  96  			}
  97  		} else if rem[0] == '"' {
  98  			if !escaping {
  99  				rem = rem[1:]
 100  				content = content[:contentLen]
 101  				// Create a copy of the content to avoid corrupting the original input buffer
 102  				contentCopy := make([]byte, len(content))
 103  				copy(contentCopy, content)
 104  				content = NostrUnescape(contentCopy)
 105  				return
 106  			}
 107  			contentLen++
 108  			rem = rem[1:]
 109  			escaping = false
 110  		} else {
 111  			escaping = false
 112  			switch rem[0] {
 113  			// none of these characters are allowed inside a JSON string:
 114  			//
 115  			// backspace, tab, newline, form feed or carriage return.
 116  			case '\b', '\t', '\n', '\f', '\r':
 117  				pos := len(content) - len(rem)
 118  				contextStart := pos - 10
 119  				if contextStart < 0 {
 120  					contextStart = 0
 121  				}
 122  				contextEnd := pos + 10
 123  				if contextEnd > len(content) {
 124  					contextEnd = len(content)
 125  				}
 126  				err = errorf.E(
 127  					"invalid character '%s' in quoted string (position %d, context: %q)",
 128  					NostrEscape(nil, rem[:1]),
 129  					pos,
 130  					string(content[contextStart:contextEnd]),
 131  				)
 132  				return
 133  			}
 134  			contentLen++
 135  			rem = rem[1:]
 136  		}
 137  	}
 138  	return
 139  }
 140  
 141  func MarshalHexArray(dst []byte, ha [][]byte) (b []byte) {
 142  	b = dst
 143  	// Pre-allocate buffer if nil to reduce reallocations
 144  	// Estimate: [ + (hex encoded item + quotes + comma) * n + ]
 145  	// Each hex item is 2*size + 2 quotes = 2*size + 2, plus comma for all but last
 146  	if b == nil && len(ha) > 0 {
 147  		estimatedSize := 2 // brackets
 148  		if len(ha) > 0 {
 149  			// Estimate based on first item size
 150  			itemSize := len(ha[0]) * 2                    // hex encoding doubles size
 151  			estimatedSize += len(ha) * (itemSize + 2 + 1) // item + quotes + comma
 152  		}
 153  		b = make([]byte, 0, estimatedSize)
 154  	}
 155  	b = append(b, '[')
 156  	for i := range ha {
 157  		b = AppendQuote(b, ha[i], hex.EncAppend)
 158  		if i != len(ha)-1 {
 159  			b = append(b, ',')
 160  		}
 161  	}
 162  	b = append(b, ']')
 163  	return
 164  }
 165  
 166  // UnmarshalHexArray unpacks a JSON array containing strings with hexadecimal, and checks all
 167  // values have the specified byte size.
 168  func UnmarshalHexArray(b []byte, size int) (t [][]byte, rem []byte, err error) {
 169  	rem = b
 170  	var openBracket bool
 171  	// Pre-allocate slice with estimated capacity to reduce reallocations
 172  	// Estimate based on typical array sizes (can grow if needed)
 173  	t = make([][]byte, 0, 16)
 174  	for ; len(rem) > 0; rem = rem[1:] {
 175  		if rem[0] == '[' {
 176  			openBracket = true
 177  		} else if openBracket {
 178  			if rem[0] == ',' {
 179  				continue
 180  			} else if rem[0] == ']' {
 181  				rem = rem[1:]
 182  				return
 183  			} else if rem[0] == '"' {
 184  				var h []byte
 185  				if h, rem, err = UnmarshalHex(rem); chk.E(err) {
 186  					return
 187  				}
 188  				if len(h) != size {
 189  					err = errorf.E(
 190  						"invalid hex array size, got %d expect %d",
 191  						2*len(h), 2*size,
 192  					)
 193  					return
 194  				}
 195  				t = append(t, h)
 196  				if rem[0] == ']' {
 197  					rem = rem[1:]
 198  					// done
 199  					return
 200  				}
 201  			}
 202  		}
 203  	}
 204  	return
 205  }
 206  
 207  // UnmarshalStringArray unpacks a JSON array containing strings.
 208  func UnmarshalStringArray(b []byte) (t [][]byte, rem []byte, err error) {
 209  	rem = b
 210  	var openBracket bool
 211  	// Pre-allocate slice with estimated capacity to reduce reallocations
 212  	// Estimate based on typical array sizes (can grow if needed)
 213  	t = make([][]byte, 0, 16)
 214  	for ; len(rem) > 0; rem = rem[1:] {
 215  		if rem[0] == '[' {
 216  			openBracket = true
 217  		} else if openBracket {
 218  			if rem[0] == ',' {
 219  				continue
 220  			} else if rem[0] == ']' {
 221  				rem = rem[1:]
 222  				return
 223  			} else if rem[0] == '"' {
 224  				var h []byte
 225  				if h, rem, err = UnmarshalQuoted(rem); chk.E(err) {
 226  					return
 227  				}
 228  				t = append(t, h)
 229  				if rem[0] == ']' {
 230  					rem = rem[1:]
 231  					// done
 232  					return
 233  				}
 234  			}
 235  		}
 236  	}
 237  	return
 238  }
 239  
 240  func True() []byte  { return []byte("true") }
 241  func False() []byte { return []byte("false") }
 242  
 243  func MarshalBool(src []byte, truth bool) []byte {
 244  	if truth {
 245  		return append(src, True()...)
 246  	}
 247  	return append(src, False()...)
 248  }
 249  
 250  func UnmarshalBool(src []byte) (rem []byte, truth bool, err error) {
 251  	rem = src
 252  	t, f := True(), False()
 253  	for i := range rem {
 254  		if rem[i] == t[0] {
 255  			if len(rem) < i+len(t) {
 256  				err = io.EOF
 257  				return
 258  			}
 259  			if utils.FastEqual(t, rem[i:i+len(t)]) {
 260  				truth = true
 261  				rem = rem[i+len(t):]
 262  				return
 263  			}
 264  		}
 265  		if rem[i] == f[0] {
 266  			if len(rem) < i+len(f) {
 267  				err = io.EOF
 268  				return
 269  			}
 270  			if utils.FastEqual(f, rem[i:i+len(f)]) {
 271  				rem = rem[i+len(f):]
 272  				return
 273  			}
 274  		}
 275  	}
 276  	// if a truth value is not found in the string it will run to the end
 277  	err = io.EOF
 278  	return
 279  }
 280  
 281  func Comma(b []byte) (rem []byte, err error) {
 282  	rem = b
 283  	for i := range rem {
 284  		if rem[i] == ',' {
 285  			rem = rem[i:]
 286  			return
 287  		}
 288  	}
 289  	err = io.EOF
 290  	return
 291  }
 292