Files
realy/tag/tag.go
mleku bbebbe2b02 Add tracing with lol.Tracer in multiple functions.
Introduced `lol.Tracer` for function entry/exit logging across various packages. This improves traceability and debugging of function executions while preserving existing behavior. Removed unused files `doc.go` and `nothing.go` to clean up the repository.
2025-06-29 07:32:24 +01:00

444 lines
12 KiB
Go

// Package tag provides an implementation of a nostr tag list, an array of
// strings with a usually single letter first "key" field, including methods to
// compare, marshal/unmarshal and access elements with their proper semantics.
package tag
import (
"bytes"
"golang.org/x/exp/constraints"
"realy.lol/errorf"
"realy.lol/log"
"realy.lol/lol"
"realy.lol/normalize"
"realy.lol/text"
)
// The tag position meanings, so they are clear when reading.
const (
Key = iota
Value
Relay
)
// T marker strings for e (reference) tags.
const (
MarkerReply = "reply"
MarkerRoot = "root"
MarkerMention = "mention"
)
// BS is an abstract data type that can process strings and byte slices as byte slices.
type BS[Z []byte | string] []byte
// T is a list of strings with a literal ordering.
//
// Not a set, there can be repeating elements.
type T struct {
field []BS[[]byte]
}
// New creates a new tag.T from a variadic parameter that can be either string or byte slice.
func New[V string | []byte](fields ...V) (t *T) {
lol.Tracer("New", fields)
defer func() { lol.Tracer("end New", t) }()
t = &T{field: make([]BS[[]byte], len(fields))}
for i, field := range fields {
t.field[i] = []byte(field)
}
return
}
// NewWithCap creates a new empty tag.T with a pre-allocated capacity for some number of fields.
func NewWithCap[V constraints.Integer](c V) (t *T) {
lol.Tracer("NewWithCap", c)
defer func() { lol.Tracer("end NewWithCap", t) }()
return &T{make([]BS[[]byte], 0, c)}
}
// S returns a field of a tag.T as a string.
func (t *T) S(i int) (s string) {
lol.Tracer("S", i)
defer func() { lol.Tracer("end S", s) }()
if t == nil {
return
}
if t.Len() <= i {
return
}
s = string(t.field[i])
return
}
// B returns a field of a tag.T as a byte slice.
func (t *T) B(i int) (b []byte) {
lol.Tracer("B", i)
defer func() { lol.Tracer("end B", b) }()
if t == nil {
return
}
if t.Len() <= i {
return
}
b = t.field[i]
return
}
// Len returns the number of elements in a tag.T.
func (t *T) Len() int {
if t == nil {
return 0
}
return len(t.field)
}
// Less returns whether one field of a tag.T is lexicographically less than another (smaller).
// This uses bytes.Compare, which sorts strings and byte slices as though they are numbers.
func (t *T) Less(i, j int) bool {
// Added nil checks for robustness
if t == nil || i < 0 || j < 0 || i >= t.Len() || j >= t.Len() {
return false // Or panic, depending on desired error handling
}
return bytes.Compare(t.field[i], t.field[j]) < 0
}
// Swap flips the position of two fields of a tag.T with each other.
func (t *T) Swap(i, j int) { t.field[i], t.field[j] = t.field[j], t.field[i] }
// FromBytesSlice creates a tag.T from a slice of slice of bytes.
func FromBytesSlice(fields ...[]byte) (t *T) {
lol.Tracer("FromBytesSlice", fields)
defer func() { lol.Tracer("end FromBytesSlice", t) }()
t = &T{field: make([]BS[[]byte], len(fields))}
for i, field := range fields {
t.field[i] = field
}
return
}
// Clone makes a new tag.T with the same members.
func (t *T) Clone() (c *T) {
lol.Tracer("Clone")
defer func() { lol.Tracer("end Clone", c) }()
if t == nil {
log.I.F("nil tag %s", lol.GetNLoc(7)) // This line is present in the `tags.go` code.
return nil // Or return &T{} or panic, depending on desired behavior for nil receiver
}
c = &T{field: make([]BS[[]byte], 0, len(t.field))}
for _, f := range t.field {
l := len(f)
b := make([]byte, l)
copy(b, f)
c.field = append(c.field, b)
}
return
}
// Append a slice of slice of bytes to a tag.T.
func (t *T) Append(b ...[]byte) (tt *T) {
lol.Tracer("Append", b)
defer func() { lol.Tracer("end Append", tt) }()
tt = t
if t == nil {
// we are propagating back this to tt if t was nil, else it appends
tt = &T{}
}
for _, bb := range b {
tt.field = append(tt.field, bb)
}
return
}
// Cap returns the capacity of a tag.T (how much elements it can hold without a re-allocation).
func (t *T) Cap() int { return cap(t.field) }
// Clear sets the length of the tag.T to zero so new elements can be appended.
func (t *T) Clear() { t.field = t.field[:0] }
// Slice cuts out a given start and end (exclusive) segment of the tag.T. This
// function must be called after using the Len function to ensure the `end`
// parameter does not exceed the bounds of the array.
func (t *T) Slice(start, end int) (tt *T) {
lol.Tracer("Slice")
defer func() { lol.Tracer("end Slice", tt) }()
tt = &T{t.field[start:end]}
return
}
// ToSliceOfBytes renders a tag.T as a slice of slice of bytes.
func (t *T) ToSliceOfBytes() (b [][]byte) {
lol.Tracer("ToSliceOfBytes")
defer func() { lol.Tracer("end ToSliceOfBytes", b) }()
if t == nil {
return [][]byte{}
}
b = make([][]byte, t.Len())
for i := range t.field {
b[i] = t.B(i)
}
return
}
// ToStringSlice converts a tag.T to a slice of strings.
func (t *T) ToStringSlice() (b []string) {
lol.Tracer("ToStringSlice")
defer func() { lol.Tracer("end ToStringSlice", b) }()
b = make([]string, 0, len(t.field))
for i := range t.field {
b = append(b, string(t.field[i]))
}
return
}
// StartsWith checks a tag has the same initial set of elements.
//
// The last element is treated specially in that it is considered to match if
// the candidate has the same initial substring as its corresponding element.
func (t *T) StartsWith(prefix *T) (startsWith bool) {
lol.Tracer("StartsWith", prefix)
defer func() { lol.Tracer("end StartsWith", startsWith) }()
prefixLen := len(prefix.field)
if prefixLen > len(t.field) {
return
}
// check initial elements for equality
for i := 0; i < prefixLen-1; i++ {
if !bytes.Equal(prefix.field[i], t.field[i]) {
return
}
}
// check last element just for a prefix
startsWith = bytes.HasPrefix(t.field[prefixLen-1], prefix.field[prefixLen-1])
return
}
// Key returns the first element of the tags.
func (t *T) Key() (key []byte) {
lol.Tracer("Key")
defer func() { lol.Tracer("end Key", key) }()
if t == nil {
return nil
}
if len(t.field) > Key {
return t.field[Key]
}
return nil
}
// KeyString returns the first element of the tags as a string.
func (t *T) KeyString() (key string) {
lol.Tracer("KeyString")
defer func() { lol.Tracer("end KeyString", key) }()
if t == nil {
return
}
// Get the first element.
keyElement := t.field[Key]
// Ensure the key element has at least two bytes to perform the slice [1:].
// If it has 0 or 1 byte, slicing from index 1 will cause a panic or unexpected behavior.
// A common pattern for filter keys is like "#e", "#p", so they should be at least 2 chars.
if len(keyElement) >= 2 {
key = string(keyElement[1:])
return
}
// If the key element is too short, return an empty slice or the original key,
// depending on desired behavior. Returning nil or an empty slice seems safer
// than panicking. The comment implies removing '#', so if it's not present
// or the string is too short, an empty or original string could be returned.
// Returning nil in this context is consistent with other nil returns in this package.
return
}
// FilterKey returns the first element of a filter tag (the key) with the # removed
func (t *T) FilterKey() (key []byte) {
lol.Tracer("FilterKey")
defer func() { lol.Tracer("end FilterKey", key) }()
if t == nil {
return nil
}
// Get the first element.
keyElement := t.field[Key]
// Ensure the key element has at least two bytes to perform the slice [1:].
// If it has 0 or 1 byte, slicing from index 1 will cause a panic or unexpected behavior.
// A common pattern for filter keys is like "#e", "#p", so they should be at least 2 chars.
if len(keyElement) >= 2 {
return keyElement[1:]
}
// If the key element is too short, return an empty slice or the original key,
// depending on desired behavior. Returning nil or an empty slice seems safer
// than panicking. The comment implies removing '#', so if it's not present
// or the string is too short, an empty or original string could be returned.
// Returning nil in this context is consistent with other nil returns in this package.
return nil
}
// Value returns the second element of the tag.
func (t *T) Value() (val []byte) {
lol.Tracer("Value")
defer func() { lol.Tracer("end Value", val) }()
if t == nil {
return
}
if len(t.field) > Value {
val = t.field[Value]
return
}
return
}
var etag, ptag = []byte("e"), []byte("p")
// Relay returns the third element of the tag.
func (t *T) Relay() (relay []byte) {
lol.Tracer("Relay")
defer func() { lol.Tracer("end Relay", relay) }()
if t == nil {
return
}
// Check if the key is 'e' or 'p' and if there are enough fields for the Relay.
if (bytes.Equal(t.Key(), etag) ||
bytes.Equal(t.Key(), ptag)) &&
len(t.field) > Relay {
relay = normalize.URL([]byte(t.field[Relay]))
return
}
return
}
// Marshal encodes a tag.T as standard minified JSON array of strings.
func (t *T) Marshal(dst []byte) (b []byte) {
lol.Tracer("Marshal", dst)
defer func() { lol.Tracer("end Marshal", b) }()
if t == nil {
// A nil tag should marshal to an empty JSON array.
b = append(dst, []byte("[]")...)
return
}
dst = append(dst, '[')
for i, s := range t.field {
if i > 0 {
dst = append(dst, ',')
}
dst = text.AppendQuote(dst, s, text.NostrEscape)
}
dst = append(dst, ']')
b = dst
return
}
// Unmarshal decodes a standard minified JSON array of strings to a tags.T.
func (t *T) Unmarshal(b []byte) (r []byte, err error) {
lol.Tracer("Unmarshal", b)
defer func() { lol.Tracer("end Unmarshal", r, err) }()
var inQuotes, openedBracket bool
var quoteStart int
t.field = []BS[[]byte]{} // Clear the field to ensure a fresh unmarshal
for i := 0; i < len(b); i++ {
if !openedBracket {
if b[i] == '[' {
openedBracket = true
if i+1 == len(b) { // Handle empty array "[]" if it's the end of input
return nil, nil // No remaining bytes, no error
}
continue // Move to the next character after '['
} else {
// If we haven't opened a bracket yet and current char isn't '[', it's an error.
err = errorf.E("tag: failed to parse tag: expected opening bracket '['")
return
}
}
// We are inside the bracket now
if !inQuotes {
switch b[i] {
case '"':
inQuotes, quoteStart = true, i+1
case ']':
// Found the closing bracket. Return the remaining bytes after it.
r = b[i+1:] // Correctly return remaining bytes and no error
return
case ',':
// Expecting a comma only if we've already parsed at least one tag.
// This case covers a comma before the first element or multiple commas.
if len(t.field) == 0 {
err = errorf.E("tag: failed to parse tag: unexpected comma before first element")
return
}
case ' ':
// Allow spaces outside quotes but within the array structure
continue
default:
// Unexpected character outside of quotes, e.g., "invalid"
err = errorf.E("tag: failed to parse tag: unexpected character '%c' outside quotes", b[i])
return
}
} else { // In quotes
if b[i] == '\\' && i < len(b)-1 {
i++ // Skip escaped character
} else if b[i] == '"' {
inQuotes = false
t.field = append(t.field, text.NostrUnescape(b[quoteStart:i]))
}
// If it's not '\' or '"', just continue as it's part of the string content.
}
}
// If we reach here, it means we didn't find a closing bracket or are still in quotes
// when the input ended. This indicates an incomplete or malformed tag.
if inQuotes {
err = errorf.E("tag: failed to parse tag: unclosed quote")
return
}
if openedBracket {
err = errorf.E("tag: failed to parse tag: unclosed bracket")
return
}
err = errorf.E("tag: failed to parse tag: unexpected end of input")
return
}
// Contains returns true if the provided element is found in the tag slice.
func (t *T) Contains(s []byte) (contains bool) {
lol.Tracer("Contains", s)
defer func() { lol.Tracer("end Contains", contains) }()
if t == nil {
return // A nil tag list cannot contain any elements.
}
for i := range t.field {
if bytes.Equal(t.field[i], s) {
contains = true
return
}
}
return
}
// Equal checks that the provided tag list matches.
func (t *T) Equal(ta *T) (eq bool) {
lol.Tracer("Equal", ta)
defer func() { lol.Tracer("end Equal", eq) }()
// Handle nil cases:
// If both are nil, they are equal.
if t == nil && ta == nil {
eq = true
return
}
// If one is nil and the other is not, they are not equal.
if t == nil || ta == nil {
return
}
if len(t.field) != len(ta.field) {
return
}
for i := range t.field {
if !bytes.Equal(t.field[i], ta.field[i]) {
return
}
}
eq = true
return
}