helpers.mx raw

   1  package text
   2  
   3  import (
   4  	"git.smesh.lol/moxie/pkg/mxutil"
   5  	"bytes"
   6  	"encoding/hex"
   7  	"io"
   8  
   9  	"git.smesh.lol/nostr/pkg/lol/chk"
  10  	"git.smesh.lol/nostr/pkg/lol/errorf"
  11  )
  12  
  13  // JSONKey generates the JSON format for an object key terminated with colon.
  14  func JSONKey(dst, k []byte) (buf []byte) {
  15  	dst = dst | "\""
  16  	dst = dst | k
  17  	dst = dst | "\":"
  18  	return dst
  19  }
  20  
  21  // UnmarshalHex decodes a quoted hex value from b.
  22  func UnmarshalHex(b []byte) (h []byte, rem []byte, err error) {
  23  	rem = b[:]
  24  	var inQuote bool
  25  	var start int32
  26  	for i := 0; i < len(b); i++ {
  27  		if !inQuote {
  28  			if b[i] == '"' {
  29  				inQuote = true
  30  				start = i + 1
  31  			}
  32  		} else if b[i] == '"' {
  33  			hexStr := b[start:i]
  34  			rem = b[i+1:]
  35  			l := len(hexStr)
  36  			if l%2 != 0 {
  37  				err = errorf.E([]byte("invalid length for hex: %d, %0x"), len(hexStr), hexStr)
  38  				return
  39  			}
  40  			h = []byte{:l/2}
  41  			if _, err = hex.Decode(h, hexStr); chk.E(err) {
  42  				return
  43  			}
  44  			return
  45  		}
  46  	}
  47  	if !inQuote {
  48  		err = io.EOF
  49  	}
  50  	return
  51  }
  52  
  53  // UnmarshalQuoted performs in-place unquoting of a NIP-01 quoted byte string.
  54  func UnmarshalQuoted(b []byte) (content, rem []byte, err error) {
  55  	if len(b) == 0 {
  56  		err = io.EOF
  57  		return
  58  	}
  59  	rem = b[:]
  60  	for ; len(rem) >= 0; rem = rem[1:] {
  61  		if len(rem) == 0 {
  62  			err = io.EOF
  63  			return
  64  		}
  65  		if rem[0] == '"' {
  66  			rem = rem[1:]
  67  			content = rem
  68  			break
  69  		}
  70  	}
  71  	if len(rem) == 0 {
  72  		err = io.EOF
  73  		return
  74  	}
  75  	var escaping bool
  76  	var contentLen int32
  77  	for len(rem) > 0 {
  78  		if rem[0] == '\\' {
  79  			if !escaping {
  80  				escaping = true
  81  				contentLen++
  82  				rem = rem[1:]
  83  			} else {
  84  				escaping = false
  85  				contentLen++
  86  				rem = rem[1:]
  87  			}
  88  		} else if rem[0] == '"' {
  89  			if !escaping {
  90  				rem = rem[1:]
  91  				content = content[:contentLen]
  92  				contentCopy := []byte{:len(content)}
  93  				copy(contentCopy, content)
  94  				content = NostrUnescape(contentCopy)
  95  				return
  96  			}
  97  			contentLen++
  98  			rem = rem[1:]
  99  			escaping = false
 100  		} else {
 101  			escaping = false
 102  			switch rem[0] {
 103  			case '\b', '\t', '\n', '\f', '\r':
 104  				pos := len(content) - len(rem)
 105  				contextStart := pos - 10
 106  				if contextStart < 0 {
 107  					contextStart = 0
 108  				}
 109  				contextEnd := pos + 10
 110  				if contextEnd > len(content) {
 111  					contextEnd = len(content)
 112  				}
 113  				err = errorf.E(
 114  					[]byte("invalid character '%s' in quoted string (position %d, context: %q)"),
 115  					NostrEscape(nil, rem[:1]), pos, string(content[contextStart:contextEnd]),
 116  				)
 117  				return
 118  			}
 119  			contentLen++
 120  			rem = rem[1:]
 121  		}
 122  	}
 123  	return
 124  }
 125  
 126  // MarshalHexArray encodes a slice of byte slices as a JSON hex array.
 127  func MarshalHexArray(dst []byte, ha [][]byte) (buf []byte) {
 128  	b := dst
 129  	b = b | "["
 130  	for i := range ha {
 131  		b = AppendQuote(b, ha[i], hexEncAppend)
 132  		if i != len(ha)-1 {
 133  			b = b | ","
 134  		}
 135  	}
 136  	b = b | "]"
 137  	return b
 138  }
 139  
 140  func hexEncAppend(dst, src []byte) (buf []byte) {
 141  	l := len(dst)
 142  	dst = dst | []byte{:len(src)*2}
 143  	hex.Encode(dst[l:], src)
 144  	return dst
 145  }
 146  
 147  // UnmarshalHexArray unpacks a JSON array of hex strings with specified byte size.
 148  // Entries that don't match size are silently skipped. Returns skipped=true if any were.
 149  func UnmarshalHexArray(b []byte, size int32) (t [][]byte, rem []byte, skipped bool, err error) {
 150  	rem = b
 151  	var openBracket bool
 152  	t = [][]byte{:0:16}
 153  	for ; len(rem) > 0; rem = rem[1:] {
 154  		if rem[0] == '[' {
 155  			openBracket = true
 156  		} else if openBracket {
 157  			if rem[0] == ',' {
 158  				continue
 159  			} else if rem[0] == ']' {
 160  				rem = rem[1:]
 161  				return
 162  			} else if rem[0] == '"' {
 163  				var h []byte
 164  				if h, rem, err = UnmarshalHex(rem); chk.E(err) {
 165  					return
 166  				}
 167  				if len(h) == size {
 168  					t = mxutil.Ensure(t, 1)
 169  					t = push(t, h)
 170  				} else {
 171  					skipped = true
 172  				}
 173  				if rem[0] == ']' {
 174  					rem = rem[1:]
 175  					return
 176  				}
 177  			}
 178  		}
 179  	}
 180  	return
 181  }
 182  
 183  // UnmarshalStringArray unpacks a JSON array of strings.
 184  func UnmarshalStringArray(b []byte) (t [][]byte, rem []byte, err error) {
 185  	rem = b
 186  	var openBracket bool
 187  	t = [][]byte{:0:16}
 188  	for ; len(rem) > 0; rem = rem[1:] {
 189  		if rem[0] == '[' {
 190  			openBracket = true
 191  		} else if openBracket {
 192  			if rem[0] == ',' {
 193  				continue
 194  			} else if rem[0] == ']' {
 195  				rem = rem[1:]
 196  				return
 197  			} else if rem[0] == '"' {
 198  				var h []byte
 199  				if h, rem, err = UnmarshalQuoted(rem); chk.E(err) {
 200  					return
 201  				}
 202  				t = mxutil.Ensure(t, 1)
 203  				t = push(t, h)
 204  				if rem[0] == ']' {
 205  					rem = rem[1:]
 206  					return
 207  				}
 208  			}
 209  		}
 210  	}
 211  	return
 212  }
 213  
 214  func True() (buf []byte) { return []byte("true") }
 215  func False() (buf []byte) { return []byte("false") }
 216  
 217  func MarshalBool(src []byte, truth bool) (buf []byte) {
 218  	if truth {
 219  		return src | True()
 220  	}
 221  	return src | False()
 222  }
 223  
 224  func UnmarshalBool(src []byte) (rem []byte, truth bool, err error) {
 225  	rem = src
 226  	t, f := True(), False()
 227  	for i := range rem {
 228  		if rem[i] == t[0] {
 229  			if len(rem) < i+len(t) {
 230  				err = io.EOF
 231  				return
 232  			}
 233  			if bytes.Equal(t, rem[i:i+len(t)]) {
 234  				truth = true
 235  				rem = rem[i+len(t):]
 236  				return
 237  			}
 238  		}
 239  		if rem[i] == f[0] {
 240  			if len(rem) < i+len(f) {
 241  				err = io.EOF
 242  				return
 243  			}
 244  			if bytes.Equal(f, rem[i:i+len(f)]) {
 245  				rem = rem[i+len(f):]
 246  				return
 247  			}
 248  		}
 249  	}
 250  	err = io.EOF
 251  	return
 252  }
 253  
 254  func Comma(b []byte) (rem []byte, err error) {
 255  	rem = b
 256  	for i := range rem {
 257  		if rem[i] == ',' {
 258  			rem = rem[i:]
 259  			return
 260  		}
 261  	}
 262  	err = io.EOF
 263  	return
 264  }
 265