tag.mx raw

   1  // Package tag provides nostr tag lists — arrays of byte slices with a
   2  // single-letter key field.
   3  package tag
   4  
   5  import (
   6  	"bytes"
   7  
   8  	"smesh.lol/pkg/nostr/hex"
   9  	"smesh.lol/pkg/nostr/text"
  10  	"smesh.lol/pkg/lol/errorf"
  11  )
  12  
  13  const (
  14  	Key = iota
  15  	Value
  16  	Relay
  17  )
  18  
  19  const (
  20  	BinaryEncodedLen = 33
  21  	HexEncodedLen    = 64
  22  	HashLen          = 32
  23  )
  24  
  25  var binaryOptimizedTags = map[byte]bool{
  26  	'e': true,
  27  	'p': true,
  28  }
  29  
  30  type T struct {
  31  	T [][]byte
  32  }
  33  
  34  func New() *T                              { return &T{} }
  35  func NewFromBytesSlice(t ...[]byte) *T     { return &T{T: t} }
  36  func NewWithCap(c int) *T                  { return &T{T: [][]byte{:0:c}} }
  37  func (t *T) Free()                         { t.T = nil }
  38  
  39  func (t *T) Len() int {
  40  	if t == nil {
  41  		return 0
  42  	}
  43  	return len(t.T)
  44  }
  45  
  46  func (t *T) Less(i, j int) bool { return bytes.Compare(t.T[i], t.T[j]) < 0 }
  47  func (t *T) Swap(i, j int)     { t.T[i], t.T[j] = t.T[j], t.T[i] }
  48  
  49  func (t *T) Contains(s []byte) bool {
  50  	for i := range t.T {
  51  		if bytes.Equal(t.T[i], s) {
  52  			return true
  53  		}
  54  	}
  55  	return false
  56  }
  57  
  58  func (t *T) Marshal(dst []byte) (b []byte) {
  59  	b = dst
  60  	b = append(b, '[')
  61  	for i, s := range t.T {
  62  		if i == Value && isBinaryEncoded(s) {
  63  			hexVal := hex.EncAppend(nil, s[:HashLen])
  64  			b = text.AppendQuote(b, hexVal, text.NostrEscape)
  65  		} else {
  66  			b = text.AppendQuote(b, s, text.NostrEscape)
  67  		}
  68  		if i < len(t.T)-1 {
  69  			b = append(b, ',')
  70  		}
  71  	}
  72  	b = append(b, ']')
  73  	return
  74  }
  75  
  76  func (t *T) MarshalJSON() ([]byte, error) {
  77  	return t.Marshal(nil), nil
  78  }
  79  
  80  func (t *T) Unmarshal(b []byte) (r []byte, err error) {
  81  	var inQuotes, openedBracket bool
  82  	var quoteStart int
  83  	t.T = [][]byte{:0:4}
  84  	for i := 0; i < len(b); i++ {
  85  		if !openedBracket && b[i] == '[' {
  86  			openedBracket = true
  87  		} else if !inQuotes {
  88  			if b[i] == '"' {
  89  				inQuotes, quoteStart = true, i+1
  90  			} else if b[i] == ']' {
  91  				return b[i+1:], err
  92  			}
  93  		} else if b[i] == '\\' && i < len(b)-1 {
  94  			i++
  95  		} else if b[i] == '"' {
  96  			inQuotes = false
  97  			copyBuf := []byte{:i-quoteStart}
  98  			copy(copyBuf, b[quoteStart:i])
  99  			unescaped := text.NostrUnescape(copyBuf)
 100  
 101  			fieldIdx := len(t.T)
 102  			if fieldIdx == Value && len(t.T) > 0 && shouldOptimize(t.T[Key], unescaped) {
 103  				binVal := []byte{:BinaryEncodedLen}
 104  				if _, decErr := hex.DecBytes(binVal[:HashLen], unescaped); decErr == nil {
 105  					binVal[HashLen] = 0
 106  					t.T = append(t.T, binVal)
 107  				} else {
 108  					t.T = append(t.T, unescaped)
 109  				}
 110  			} else {
 111  				t.T = append(t.T, unescaped)
 112  			}
 113  		}
 114  	}
 115  	if !openedBracket || inQuotes {
 116  		return nil, errorf.E([]byte("tag: failed to parse tag"))
 117  	}
 118  	return
 119  }
 120  
 121  func (t *T) UnmarshalJSON(b []byte) error {
 122  	_, err := t.Unmarshal(b)
 123  	return err
 124  }
 125  
 126  func (t *T) Key() []byte {
 127  	if len(t.T) > Key {
 128  		return t.T[Key]
 129  	}
 130  	return nil
 131  }
 132  
 133  func (t *T) Value() []byte {
 134  	if t == nil {
 135  		return nil
 136  	}
 137  	if len(t.T) > Value {
 138  		return t.T[Value]
 139  	}
 140  	return nil
 141  }
 142  
 143  func (t *T) Relay() []byte {
 144  	if len(t.T) > Relay {
 145  		return t.T[Relay]
 146  	}
 147  	return nil
 148  }
 149  
 150  func isBinaryEncoded(val []byte) bool {
 151  	return len(val) == BinaryEncodedLen && val[HashLen] == 0
 152  }
 153  
 154  func shouldOptimize(key []byte, val []byte) bool {
 155  	if len(key) != 1 {
 156  		return false
 157  	}
 158  	if !binaryOptimizedTags[key[0]] {
 159  		return false
 160  	}
 161  	return len(val) == HexEncodedLen && isValidHex(val)
 162  }
 163  
 164  func isValidHex(b []byte) bool {
 165  	for _, c := range b {
 166  		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
 167  			return false
 168  		}
 169  	}
 170  	return true
 171  }
 172  
 173  func (t *T) ValueHex() []byte {
 174  	if t == nil || len(t.T) <= Value {
 175  		return nil
 176  	}
 177  	val := t.T[Value]
 178  	if isBinaryEncoded(val) {
 179  		return hex.EncAppend(nil, val[:HashLen])
 180  	}
 181  	return val
 182  }
 183  
 184  func NewFromAny(t ...any) (tt *T) {
 185  	tt = &T{}
 186  	for _, v := range t {
 187  		switch vv := v.(type) {
 188  		case []byte:
 189  			tt.T = append(tt.T, vv)
 190  		}
 191  	}
 192  	return
 193  }
 194  
 195  func (t *T) ValueBinary() []byte {
 196  	if t == nil || len(t.T) <= Value {
 197  		return nil
 198  	}
 199  	val := t.T[Value]
 200  	if isBinaryEncoded(val) {
 201  		return val[:HashLen]
 202  	}
 203  	return nil
 204  }
 205  
 206  func (t *T) Equals(other *T) bool {
 207  	if t == nil && other == nil {
 208  		return true
 209  	}
 210  	if t == nil || other == nil {
 211  		return false
 212  	}
 213  	if len(t.T) != len(other.T) {
 214  		return false
 215  	}
 216  	for i := range t.T {
 217  		if i == Value && len(t.T) > Value {
 218  			tVal := t.T[Value]
 219  			oVal := other.T[Value]
 220  			tIsBinary := isBinaryEncoded(tVal)
 221  			oIsBinary := isBinaryEncoded(oVal)
 222  			if tIsBinary && oIsBinary {
 223  				if !bytes.Equal(tVal[:HashLen], oVal[:HashLen]) {
 224  					return false
 225  				}
 226  			} else if tIsBinary || oIsBinary {
 227  				var binBytes, hexBytes []byte
 228  				if tIsBinary {
 229  					binBytes = tVal[:HashLen]
 230  					hexBytes = oVal
 231  				} else {
 232  					binBytes = oVal[:HashLen]
 233  					hexBytes = tVal
 234  				}
 235  				if len(hexBytes) != HexEncodedLen {
 236  					return false
 237  				}
 238  				for j := 0; j < HashLen; j++ {
 239  					hi := hexBytes[j*2]
 240  					lo := hexBytes[j*2+1]
 241  					var hiByte, loByte byte
 242  					if hi >= '0' && hi <= '9' {
 243  						hiByte = hi - '0'
 244  					} else if hi >= 'a' && hi <= 'f' {
 245  						hiByte = hi - 'a' + 10
 246  					} else if hi >= 'A' && hi <= 'F' {
 247  						hiByte = hi - 'A' + 10
 248  					} else {
 249  						return false
 250  					}
 251  					if lo >= '0' && lo <= '9' {
 252  						loByte = lo - '0'
 253  					} else if lo >= 'a' && lo <= 'f' {
 254  						loByte = lo - 'a' + 10
 255  					} else if lo >= 'A' && lo <= 'F' {
 256  						loByte = lo - 'A' + 10
 257  					} else {
 258  						return false
 259  					}
 260  					if binBytes[j] != (hiByte<<4)|loByte {
 261  						return false
 262  					}
 263  				}
 264  			} else {
 265  				if !bytes.Equal(tVal, oVal) {
 266  					return false
 267  				}
 268  			}
 269  		} else {
 270  			if !bytes.Equal(t.T[i], other.T[i]) {
 271  				return false
 272  			}
 273  		}
 274  	}
 275  	return true
 276  }
 277