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