1 // Package database provides filter utilities for normalizing tag values.
2 //
3 // The nostr library optimizes e/p tag values by storing them in binary format
4 // (32 bytes + null terminator) rather than hex strings (64 chars). However,
5 // filter tags from client queries come as hex strings and don't go through
6 // the same binary encoding during unmarshalling.
7 //
8 // This file provides utilities to normalize filter tags to match the binary
9 // encoding used in stored events, ensuring consistent index lookups and
10 // tag comparisons.
11 package database
12 13 import (
14 "next.orly.dev/pkg/nostr/encoders/filter"
15 "next.orly.dev/pkg/nostr/encoders/hex"
16 "next.orly.dev/pkg/nostr/encoders/tag"
17 )
18 19 // Tag binary encoding constants (matching the nostr library)
20 const (
21 // BinaryEncodedLen is the length of a binary-encoded 32-byte hash with null terminator
22 BinaryEncodedLen = 33
23 // HexEncodedLen is the length of a hex-encoded 32-byte hash
24 HexEncodedLen = 64
25 // HashLen is the raw length of a hash (pubkey/event ID)
26 HashLen = 32
27 )
28 29 // binaryOptimizedTags defines which tag keys use binary encoding optimization
30 var binaryOptimizedTags = map[byte]bool{
31 'e': true, // event references
32 'p': true, // pubkey references
33 }
34 35 // IsBinaryOptimizedTag returns true if the given tag key uses binary encoding
36 func IsBinaryOptimizedTag(key byte) bool {
37 return binaryOptimizedTags[key]
38 }
39 40 // IsBinaryEncoded checks if a value field is stored in optimized binary format
41 func IsBinaryEncoded(val []byte) bool {
42 return len(val) == BinaryEncodedLen && val[HashLen] == 0
43 }
44 45 // IsValidHexValue checks if a byte slice is a valid 64-character hex string
46 func IsValidHexValue(b []byte) bool {
47 if len(b) != HexEncodedLen {
48 return false
49 }
50 return IsHexString(b)
51 }
52 53 // HexToBinary converts a 64-character hex string to 33-byte binary format
54 // Returns nil if the input is not a valid hex string
55 func HexToBinary(hexVal []byte) []byte {
56 if !IsValidHexValue(hexVal) {
57 return nil
58 }
59 binVal := make([]byte, BinaryEncodedLen)
60 if _, err := hex.DecBytes(binVal[:HashLen], hexVal); err != nil {
61 return nil
62 }
63 binVal[HashLen] = 0 // null terminator
64 return binVal
65 }
66 67 // BinaryToHex converts a 33-byte binary value to 64-character hex string
68 // Returns nil if the input is not in binary format
69 func BinaryToHex(binVal []byte) []byte {
70 if !IsBinaryEncoded(binVal) {
71 return nil
72 }
73 return hex.EncAppend(nil, binVal[:HashLen])
74 }
75 76 // NormalizeTagValue normalizes a tag value for the given key.
77 // For e/p tags, hex values are converted to binary format.
78 // Other tags are returned unchanged.
79 func NormalizeTagValue(key byte, val []byte) []byte {
80 if !IsBinaryOptimizedTag(key) {
81 return val
82 }
83 // If already binary, return as-is
84 if IsBinaryEncoded(val) {
85 return val
86 }
87 // If valid hex, convert to binary
88 if binVal := HexToBinary(val); binVal != nil {
89 return binVal
90 }
91 // Otherwise return as-is
92 return val
93 }
94 95 // NormalizeTagToHex returns the hex representation of a tag value.
96 // For binary-encoded values, converts to hex. For hex values, returns as-is.
97 func NormalizeTagToHex(val []byte) []byte {
98 if IsBinaryEncoded(val) {
99 return BinaryToHex(val)
100 }
101 return val
102 }
103 104 // NormalizeFilterTag creates a new tag with binary-encoded values for e/p tags.
105 // The original tag is not modified.
106 func NormalizeFilterTag(t *tag.T) *tag.T {
107 if t == nil || t.Len() < 2 {
108 return t
109 }
110 111 keyBytes := t.Key()
112 var key byte
113 114 // Handle both "e" and "#e" style keys
115 if len(keyBytes) == 1 {
116 key = keyBytes[0]
117 } else if len(keyBytes) == 2 && keyBytes[0] == '#' {
118 key = keyBytes[1]
119 } else {
120 return t // Not a single-letter tag
121 }
122 123 if !IsBinaryOptimizedTag(key) {
124 return t // Not an optimized tag type
125 }
126 127 // Create new tag with normalized values
128 normalized := tag.NewWithCap(t.Len())
129 normalized.T = append(normalized.T, t.T[0]) // Keep key as-is
130 131 // Normalize each value
132 for _, val := range t.T[1:] {
133 normalizedVal := NormalizeTagValue(key, val)
134 normalized.T = append(normalized.T, normalizedVal)
135 }
136 137 return normalized
138 }
139 140 // NormalizeFilterTags normalizes all tags in a tag.S, converting e/p hex values to binary.
141 // Returns a new tag.S with normalized tags.
142 func NormalizeFilterTags(tags *tag.S) *tag.S {
143 if tags == nil || tags.Len() == 0 {
144 return tags
145 }
146 147 normalized := tag.NewSWithCap(tags.Len())
148 for _, t := range *tags {
149 normalizedTag := NormalizeFilterTag(t)
150 normalized.Append(normalizedTag)
151 }
152 return normalized
153 }
154 155 // NormalizeFilter normalizes a filter's tags for consistent database queries.
156 // This should be called before using a filter for database lookups.
157 // The original filter is not modified; a copy with normalized tags is returned.
158 func NormalizeFilter(f *filter.F) *filter.F {
159 if f == nil {
160 return nil
161 }
162 163 // Create a shallow copy of the filter
164 normalized := &filter.F{
165 Ids: f.Ids,
166 Kinds: f.Kinds,
167 Authors: f.Authors,
168 Since: f.Since,
169 Until: f.Until,
170 Search: f.Search,
171 Limit: f.Limit,
172 }
173 174 // Normalize the tags
175 normalized.Tags = NormalizeFilterTags(f.Tags)
176 177 return normalized
178 }
179 180 // TagValuesMatch compares two tag values, handling both binary and hex encodings.
181 // This is useful for post-query tag matching where event values may be binary
182 // and filter values may be hex (or vice versa).
183 func TagValuesMatch(key byte, eventVal, filterVal []byte) bool {
184 // If both are the same, they match
185 if len(eventVal) == len(filterVal) {
186 for i := range eventVal {
187 if eventVal[i] != filterVal[i] {
188 goto different
189 }
190 }
191 return true
192 }
193 different:
194 195 // For non-optimized tags, require exact match
196 if !IsBinaryOptimizedTag(key) {
197 return false
198 }
199 200 // Normalize both to hex and compare
201 eventHex := NormalizeTagToHex(eventVal)
202 filterHex := NormalizeTagToHex(filterVal)
203 204 if len(eventHex) != len(filterHex) {
205 return false
206 }
207 for i := range eventHex {
208 if eventHex[i] != filterHex[i] {
209 return false
210 }
211 }
212 return true
213 }
214 215 // TagValuesMatchUsingTagMethods compares an event tag's value with a filter value
216 // using the tag.T methods. This leverages the nostr library's ValueHex() method
217 // for proper binary/hex conversion.
218 func TagValuesMatchUsingTagMethods(eventTag *tag.T, filterVal []byte) bool {
219 if eventTag == nil {
220 return false
221 }
222 223 keyBytes := eventTag.Key()
224 if len(keyBytes) != 1 {
225 // Not a single-letter tag, use direct comparison
226 return bytesEqual(eventTag.Value(), filterVal)
227 }
228 229 key := keyBytes[0]
230 if !IsBinaryOptimizedTag(key) {
231 // Not an optimized tag, use direct comparison
232 return bytesEqual(eventTag.Value(), filterVal)
233 }
234 235 // For e/p tags, use ValueHex() for proper conversion
236 eventHex := eventTag.ValueHex()
237 filterHex := NormalizeTagToHex(filterVal)
238 239 return bytesEqual(eventHex, filterHex)
240 }
241 242 // bytesEqual is a fast equality check that avoids allocation
243 func bytesEqual(a, b []byte) bool {
244 if len(a) != len(b) {
245 return false
246 }
247 for i := range a {
248 if a[i] != b[i] {
249 return false
250 }
251 }
252 return true
253 }
254