// Package tag provides nostr tag lists — arrays of byte slices with a // single-letter key field. package tag import ( "bytes" "smesh.lol/pkg/nostr/hex" "smesh.lol/pkg/nostr/text" "smesh.lol/pkg/lol/errorf" ) const ( Key = iota Value Relay ) const ( BinaryEncodedLen = 33 HexEncodedLen = 64 HashLen = 32 ) var binaryOptimizedTags = map[byte]bool{ 'e': true, 'p': true, } type T struct { T [][]byte } func New() *T { return &T{} } func NewFromBytesSlice(t ...[]byte) *T { return &T{T: t} } func NewWithCap(c int) *T { return &T{T: [][]byte{:0:c}} } func (t *T) Free() { t.T = nil } func (t *T) Len() int { if t == nil { return 0 } return len(t.T) } func (t *T) Less(i, j int) bool { return bytes.Compare(t.T[i], t.T[j]) < 0 } func (t *T) Swap(i, j int) { t.T[i], t.T[j] = t.T[j], t.T[i] } func (t *T) Contains(s []byte) bool { for i := range t.T { if bytes.Equal(t.T[i], s) { return true } } return false } func (t *T) Marshal(dst []byte) (b []byte) { b = dst b = append(b, '[') for i, s := range t.T { if i == Value && isBinaryEncoded(s) { hexVal := hex.EncAppend(nil, s[:HashLen]) b = text.AppendQuote(b, hexVal, text.NostrEscape) } else { b = text.AppendQuote(b, s, text.NostrEscape) } if i < len(t.T)-1 { b = append(b, ',') } } b = append(b, ']') return } func (t *T) MarshalJSON() ([]byte, error) { return t.Marshal(nil), nil } func (t *T) Unmarshal(b []byte) (r []byte, err error) { var inQuotes, openedBracket bool var quoteStart int t.T = [][]byte{:0:4} for i := 0; i < len(b); i++ { if !openedBracket && b[i] == '[' { openedBracket = true } else if !inQuotes { if b[i] == '"' { inQuotes, quoteStart = true, i+1 } else if b[i] == ']' { return b[i+1:], err } } else if b[i] == '\\' && i < len(b)-1 { i++ } else if b[i] == '"' { inQuotes = false copyBuf := []byte{:i-quoteStart} copy(copyBuf, b[quoteStart:i]) unescaped := text.NostrUnescape(copyBuf) fieldIdx := len(t.T) if fieldIdx == Value && len(t.T) > 0 && shouldOptimize(t.T[Key], unescaped) { binVal := []byte{:BinaryEncodedLen} if _, decErr := hex.DecBytes(binVal[:HashLen], unescaped); decErr == nil { binVal[HashLen] = 0 t.T = append(t.T, binVal) } else { t.T = append(t.T, unescaped) } } else { t.T = append(t.T, unescaped) } } } if !openedBracket || inQuotes { return nil, errorf.E([]byte("tag: failed to parse tag")) } return } func (t *T) UnmarshalJSON(b []byte) error { _, err := t.Unmarshal(b) return err } func (t *T) Key() []byte { if len(t.T) > Key { return t.T[Key] } return nil } func (t *T) Value() []byte { if t == nil { return nil } if len(t.T) > Value { return t.T[Value] } return nil } func (t *T) Relay() []byte { if len(t.T) > Relay { return t.T[Relay] } return nil } func isBinaryEncoded(val []byte) bool { return len(val) == BinaryEncodedLen && val[HashLen] == 0 } func shouldOptimize(key []byte, val []byte) bool { if len(key) != 1 { return false } if !binaryOptimizedTags[key[0]] { return false } return len(val) == HexEncodedLen && isValidHex(val) } func isValidHex(b []byte) bool { for _, c := range b { if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { return false } } return true } func (t *T) ValueHex() []byte { if t == nil || len(t.T) <= Value { return nil } val := t.T[Value] if isBinaryEncoded(val) { return hex.EncAppend(nil, val[:HashLen]) } return val } func NewFromAny(t ...any) (tt *T) { tt = &T{} for _, v := range t { switch vv := v.(type) { case []byte: tt.T = append(tt.T, vv) } } return } func (t *T) ValueBinary() []byte { if t == nil || len(t.T) <= Value { return nil } val := t.T[Value] if isBinaryEncoded(val) { return val[:HashLen] } return nil } func (t *T) Equals(other *T) bool { if t == nil && other == nil { return true } if t == nil || other == nil { return false } if len(t.T) != len(other.T) { return false } for i := range t.T { if i == Value && len(t.T) > Value { tVal := t.T[Value] oVal := other.T[Value] tIsBinary := isBinaryEncoded(tVal) oIsBinary := isBinaryEncoded(oVal) if tIsBinary && oIsBinary { if !bytes.Equal(tVal[:HashLen], oVal[:HashLen]) { return false } } else if tIsBinary || oIsBinary { var binBytes, hexBytes []byte if tIsBinary { binBytes = tVal[:HashLen] hexBytes = oVal } else { binBytes = oVal[:HashLen] hexBytes = tVal } if len(hexBytes) != HexEncodedLen { return false } for j := 0; j < HashLen; j++ { hi := hexBytes[j*2] lo := hexBytes[j*2+1] var hiByte, loByte byte if hi >= '0' && hi <= '9' { hiByte = hi - '0' } else if hi >= 'a' && hi <= 'f' { hiByte = hi - 'a' + 10 } else if hi >= 'A' && hi <= 'F' { hiByte = hi - 'A' + 10 } else { return false } if lo >= '0' && lo <= '9' { loByte = lo - '0' } else if lo >= 'a' && lo <= 'f' { loByte = lo - 'a' + 10 } else if lo >= 'A' && lo <= 'F' { loByte = lo - 'A' + 10 } else { return false } if binBytes[j] != (hiByte<<4)|loByte { return false } } } else { if !bytes.Equal(tVal, oVal) { return false } } } else { if !bytes.Equal(t.T[i], other.T[i]) { return false } } } return true }