Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f326ff0307 | |||
| 06063750e7 | |||
| 0addc61549 |
18
go.mod
18
go.mod
@@ -3,7 +3,7 @@ module next.orly.dev
|
||||
go 1.25.3
|
||||
|
||||
require (
|
||||
git.mleku.dev/mleku/nostr v1.0.9
|
||||
git.mleku.dev/mleku/nostr v1.0.11
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/aperturerobotics/go-indexeddb v0.2.3
|
||||
github.com/dgraph-io/badger/v4 v4.8.0
|
||||
@@ -21,7 +21,7 @@ require (
|
||||
github.com/vertex-lab/nostr-sqlite v0.3.2
|
||||
go-simpler.org/env v0.12.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/lint v0.0.0-20241112194109-818c5a804067
|
||||
honnef.co/go/tools v0.6.1
|
||||
lol.mleku.dev v1.0.5
|
||||
@@ -69,14 +69,14 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
p256k1.mleku.dev v1.0.3 // indirect
|
||||
|
||||
36
go.sum
36
go.sum
@@ -1,5 +1,5 @@
|
||||
git.mleku.dev/mleku/nostr v1.0.9 h1:aiN0ihnXzEpboXjW4u8qr5XokLQqg4P0XSZ1Y273qM0=
|
||||
git.mleku.dev/mleku/nostr v1.0.9/go.mod h1:iYTlg2WKJXJ0kcsM6QBGOJ0UDiJidMgL/i64cHyPjZc=
|
||||
git.mleku.dev/mleku/nostr v1.0.11 h1:xQ+rKPzTblerX/kRLDimOsH3rQK7/n9wYdG4DBKGcsg=
|
||||
git.mleku.dev/mleku/nostr v1.0.11/go.mod h1:kJwSMmLRnAJ7QJtgXDv2wGgceFU0luwVqrgAL3MI93M=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||
@@ -166,37 +166,37 @@ golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 h1:HDjDiATsGqvuqvkDvgJjD1IgPrVekcSXVVE21JwvzGE=
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
||||
golang.org/x/lint v0.0.0-20241112194109-818c5a804067 h1:adDmSQyFTCiv19j015EGKJBoaa7ElV0Q1Wovb/4G7NA=
|
||||
golang.org/x/lint v0.0.0-20241112194109-818c5a804067/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -120,7 +120,7 @@ func TestBinaryTagFilterRegression(t *testing.T) {
|
||||
// Verify we got the correct event
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if hex.Enc(r.Id) == testEventIdHex {
|
||||
if r.IDHex() == testEventIdHex {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func TestBinaryTagFilterRegression(t *testing.T) {
|
||||
// Verify we got the correct event
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if hex.Enc(r.Id) == testEventIdHex {
|
||||
if r.IDHex() == testEventIdHex {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func TestBinaryTagFilterRegression(t *testing.T) {
|
||||
// Verify we got the correct event
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if hex.Enc(r.Id) == testEventIdHex {
|
||||
if r.IDHex() == testEventIdHex {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -229,7 +229,7 @@ func TestBinaryTagFilterRegression(t *testing.T) {
|
||||
// Verify we got the correct event
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if hex.Enc(r.Id) == testEventIdHex {
|
||||
if r.IDHex() == testEventIdHex {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
currentVersion uint32 = 6
|
||||
currentVersion uint32 = 7
|
||||
)
|
||||
|
||||
func (d *D) RunMigrations() {
|
||||
@@ -107,6 +107,14 @@ func (d *D) RunMigrations() {
|
||||
// bump to version 6
|
||||
_ = d.writeVersionTag(6)
|
||||
}
|
||||
if dbVersion < 7 {
|
||||
log.I.F("migrating to version 7...")
|
||||
// Rebuild word indexes with unicode normalization (small caps, fraktur → ASCII)
|
||||
// This consolidates duplicate indexes from decorative unicode text
|
||||
d.RebuildWordIndexesWithNormalization()
|
||||
// bump to version 7
|
||||
_ = d.writeVersionTag(7)
|
||||
}
|
||||
}
|
||||
|
||||
// writeVersionTag writes a new version tag key to the database (no value)
|
||||
@@ -1018,3 +1026,56 @@ func (d *D) CleanupLegacyEventStorage() {
|
||||
log.I.F("legacy storage cleanup complete: removed %d evt entries, %d sev entries, reclaimed approximately %d bytes (%.2f MB)",
|
||||
cleanedEvt, cleanedSev, bytesReclaimed, float64(bytesReclaimed)/(1024.0*1024.0))
|
||||
}
|
||||
|
||||
// RebuildWordIndexesWithNormalization rebuilds all word indexes with unicode
|
||||
// normalization applied. This migration:
|
||||
// 1. Deletes all existing word indexes (wrd prefix)
|
||||
// 2. Re-tokenizes all events with normalizeRune() applied
|
||||
// 3. Creates new consolidated indexes where decorative unicode maps to ASCII
|
||||
//
|
||||
// After this migration, "ᴅᴇᴀᴛʜ" (small caps) and "𝔇𝔢𝔞𝔱𝔥" (fraktur) will index
|
||||
// the same as "death", eliminating duplicate entries and enabling proper search.
|
||||
func (d *D) RebuildWordIndexesWithNormalization() {
|
||||
log.I.F("rebuilding word indexes with unicode normalization...")
|
||||
var err error
|
||||
|
||||
// Step 1: Delete all existing word indexes
|
||||
var deletedCount int
|
||||
if err = d.Update(func(txn *badger.Txn) error {
|
||||
wrdPrf := new(bytes.Buffer)
|
||||
if err = indexes.WordEnc(nil, nil).MarshalWrite(wrdPrf); chk.E(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.Prefix = wrdPrf.Bytes()
|
||||
opts.PrefetchValues = false // Keys only for deletion
|
||||
|
||||
it := txn.NewIterator(opts)
|
||||
defer it.Close()
|
||||
|
||||
// Collect keys to delete (can't delete during iteration)
|
||||
var keysToDelete [][]byte
|
||||
for it.Rewind(); it.Valid(); it.Next() {
|
||||
keysToDelete = append(keysToDelete, it.Item().KeyCopy(nil))
|
||||
}
|
||||
|
||||
for _, key := range keysToDelete {
|
||||
if err = txn.Delete(key); err == nil {
|
||||
deletedCount++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); chk.E(err) {
|
||||
log.W.F("failed to delete old word indexes: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.I.F("deleted %d old word index entries", deletedCount)
|
||||
|
||||
// Step 2: Rebuild word indexes from all events
|
||||
// Reuse the existing UpdateWordIndexes logic which now uses normalizeRune
|
||||
d.UpdateWordIndexes()
|
||||
|
||||
log.I.F("word index rebuild with unicode normalization complete")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !(js && wasm)
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
@@ -65,7 +67,9 @@ func TokenHashes(content []byte) [][]byte {
|
||||
r2, size2 = utf8DecodeRuneInString(s[i:])
|
||||
}
|
||||
if unicode.IsLetter(r2) || unicode.IsNumber(r2) {
|
||||
runes = append(runes, unicode.ToLower(r2))
|
||||
// Normalize decorative unicode (small caps, fraktur) to ASCII
|
||||
// before lowercasing for consistent indexing
|
||||
runes = append(runes, unicode.ToLower(normalizeRune(r2)))
|
||||
i += size2
|
||||
continue
|
||||
}
|
||||
@@ -142,18 +146,39 @@ func allAlphaNum(s string) bool {
|
||||
|
||||
func isWordStart(r rune) bool { return unicode.IsLetter(r) || unicode.IsNumber(r) }
|
||||
|
||||
// Minimal utf8 rune decode without importing utf8 to avoid extra deps elsewhere
|
||||
// utf8DecodeRuneInString decodes the first UTF-8 rune from s.
|
||||
// Returns the rune and the number of bytes consumed.
|
||||
func utf8DecodeRuneInString(s string) (r rune, size int) {
|
||||
// Fallback to standard library if available; however, using basic decoding
|
||||
for i := 1; i <= 4 && i <= len(s); i++ {
|
||||
r, size = rune(s[0]), 1
|
||||
if r < 0x80 {
|
||||
return r, 1
|
||||
}
|
||||
// Use stdlib for correctness
|
||||
return []rune(s[:i])[0], len(string([]rune(s[:i])[0]))
|
||||
if len(s) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
return rune(s[0]), 1
|
||||
// ASCII fast path
|
||||
b := s[0]
|
||||
if b < 0x80 {
|
||||
return rune(b), 1
|
||||
}
|
||||
// Multi-byte: determine expected length from first byte
|
||||
var expectedLen int
|
||||
switch {
|
||||
case b&0xE0 == 0xC0: // 110xxxxx - 2 bytes
|
||||
expectedLen = 2
|
||||
case b&0xF0 == 0xE0: // 1110xxxx - 3 bytes
|
||||
expectedLen = 3
|
||||
case b&0xF8 == 0xF0: // 11110xxx - 4 bytes
|
||||
expectedLen = 4
|
||||
default:
|
||||
// Invalid UTF-8 start byte
|
||||
return 0xFFFD, 1
|
||||
}
|
||||
if len(s) < expectedLen {
|
||||
return 0xFFFD, 1
|
||||
}
|
||||
// Decode using Go's built-in rune conversion (simple and correct)
|
||||
runes := []rune(s[:expectedLen])
|
||||
if len(runes) == 0 {
|
||||
return 0xFFFD, 1
|
||||
}
|
||||
return runes[0], expectedLen
|
||||
}
|
||||
|
||||
// isHex64 returns true if s is exactly 64 hex characters (0-9, a-f)
|
||||
|
||||
135
pkg/database/unicode_normalize.go
Normal file
135
pkg/database/unicode_normalize.go
Normal file
@@ -0,0 +1,135 @@
|
||||
//go:build !(js && wasm)
|
||||
|
||||
package database
|
||||
|
||||
// normalizeRune maps decorative unicode characters (small caps, fraktur) back to
|
||||
// their ASCII equivalents for consistent word indexing. This ensures that text
|
||||
// written with decorative alphabets (e.g., "ᴅᴇᴀᴛʜ" or "𝔇𝔢𝔞𝔱𝔥") indexes the same
|
||||
// as regular ASCII ("death").
|
||||
//
|
||||
// Character sets normalized:
|
||||
// - Small Caps (used for DEATH-style text in Terry Pratchett tradition)
|
||||
// - Mathematical Fraktur lowercase (𝔞-𝔷)
|
||||
// - Mathematical Fraktur uppercase (𝔄-ℨ, including Letterlike Symbols block exceptions)
|
||||
func normalizeRune(r rune) rune {
|
||||
// Check small caps first (scattered codepoints)
|
||||
if mapped, ok := smallCapsToASCII[r]; ok {
|
||||
return mapped
|
||||
}
|
||||
|
||||
// Check fraktur lowercase: U+1D51E to U+1D537 (contiguous range)
|
||||
if r >= 0x1D51E && r <= 0x1D537 {
|
||||
return 'a' + (r - 0x1D51E)
|
||||
}
|
||||
|
||||
// Check fraktur uppercase main range: U+1D504 to U+1D51C (with gaps)
|
||||
if r >= 0x1D504 && r <= 0x1D51C {
|
||||
if mapped, ok := frakturUpperToASCII[r]; ok {
|
||||
return mapped
|
||||
}
|
||||
}
|
||||
|
||||
// Check fraktur uppercase exceptions from Letterlike Symbols block
|
||||
if mapped, ok := frakturLetterlikeToASCII[r]; ok {
|
||||
return mapped
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// smallCapsToASCII maps small capital letters to lowercase ASCII.
|
||||
// These are scattered across multiple Unicode blocks (IPA Extensions,
|
||||
// Phonetic Extensions, Latin Extended-D).
|
||||
var smallCapsToASCII = map[rune]rune{
|
||||
'ᴀ': 'a', // U+1D00 LATIN LETTER SMALL CAPITAL A
|
||||
'ʙ': 'b', // U+0299 LATIN LETTER SMALL CAPITAL B
|
||||
'ᴄ': 'c', // U+1D04 LATIN LETTER SMALL CAPITAL C
|
||||
'ᴅ': 'd', // U+1D05 LATIN LETTER SMALL CAPITAL D
|
||||
'ᴇ': 'e', // U+1D07 LATIN LETTER SMALL CAPITAL E
|
||||
'ꜰ': 'f', // U+A730 LATIN LETTER SMALL CAPITAL F
|
||||
'ɢ': 'g', // U+0262 LATIN LETTER SMALL CAPITAL G
|
||||
'ʜ': 'h', // U+029C LATIN LETTER SMALL CAPITAL H
|
||||
'ɪ': 'i', // U+026A LATIN LETTER SMALL CAPITAL I
|
||||
'ᴊ': 'j', // U+1D0A LATIN LETTER SMALL CAPITAL J
|
||||
'ᴋ': 'k', // U+1D0B LATIN LETTER SMALL CAPITAL K
|
||||
'ʟ': 'l', // U+029F LATIN LETTER SMALL CAPITAL L
|
||||
'ᴍ': 'm', // U+1D0D LATIN LETTER SMALL CAPITAL M
|
||||
'ɴ': 'n', // U+0274 LATIN LETTER SMALL CAPITAL N
|
||||
'ᴏ': 'o', // U+1D0F LATIN LETTER SMALL CAPITAL O
|
||||
'ᴘ': 'p', // U+1D18 LATIN LETTER SMALL CAPITAL P
|
||||
'ǫ': 'q', // U+01EB LATIN SMALL LETTER O WITH OGONEK (no true small cap Q)
|
||||
'ʀ': 'r', // U+0280 LATIN LETTER SMALL CAPITAL R
|
||||
'ꜱ': 's', // U+A731 LATIN LETTER SMALL CAPITAL S
|
||||
'ᴛ': 't', // U+1D1B LATIN LETTER SMALL CAPITAL T
|
||||
'ᴜ': 'u', // U+1D1C LATIN LETTER SMALL CAPITAL U
|
||||
'ᴠ': 'v', // U+1D20 LATIN LETTER SMALL CAPITAL V
|
||||
'ᴡ': 'w', // U+1D21 LATIN LETTER SMALL CAPITAL W
|
||||
// Note: no small cap X exists in standard use
|
||||
'ʏ': 'y', // U+028F LATIN LETTER SMALL CAPITAL Y
|
||||
'ᴢ': 'z', // U+1D22 LATIN LETTER SMALL CAPITAL Z
|
||||
}
|
||||
|
||||
// frakturUpperToASCII maps Mathematical Fraktur uppercase letters to lowercase ASCII.
|
||||
// The main range U+1D504-U+1D51C has gaps where C, H, I, R, Z use Letterlike Symbols.
|
||||
var frakturUpperToASCII = map[rune]rune{
|
||||
'𝔄': 'a', // U+1D504 MATHEMATICAL FRAKTUR CAPITAL A
|
||||
'𝔅': 'b', // U+1D505 MATHEMATICAL FRAKTUR CAPITAL B
|
||||
// C is at U+212D (Letterlike Symbols)
|
||||
'𝔇': 'd', // U+1D507 MATHEMATICAL FRAKTUR CAPITAL D
|
||||
'𝔈': 'e', // U+1D508 MATHEMATICAL FRAKTUR CAPITAL E
|
||||
'𝔉': 'f', // U+1D509 MATHEMATICAL FRAKTUR CAPITAL F
|
||||
'𝔊': 'g', // U+1D50A MATHEMATICAL FRAKTUR CAPITAL G
|
||||
// H is at U+210C (Letterlike Symbols)
|
||||
// I is at U+2111 (Letterlike Symbols)
|
||||
'𝔍': 'j', // U+1D50D MATHEMATICAL FRAKTUR CAPITAL J
|
||||
'𝔎': 'k', // U+1D50E MATHEMATICAL FRAKTUR CAPITAL K
|
||||
'𝔏': 'l', // U+1D50F MATHEMATICAL FRAKTUR CAPITAL L
|
||||
'𝔐': 'm', // U+1D510 MATHEMATICAL FRAKTUR CAPITAL M
|
||||
'𝔑': 'n', // U+1D511 MATHEMATICAL FRAKTUR CAPITAL N
|
||||
'𝔒': 'o', // U+1D512 MATHEMATICAL FRAKTUR CAPITAL O
|
||||
'𝔓': 'p', // U+1D513 MATHEMATICAL FRAKTUR CAPITAL P
|
||||
'𝔔': 'q', // U+1D514 MATHEMATICAL FRAKTUR CAPITAL Q
|
||||
// R is at U+211C (Letterlike Symbols)
|
||||
'𝔖': 's', // U+1D516 MATHEMATICAL FRAKTUR CAPITAL S
|
||||
'𝔗': 't', // U+1D517 MATHEMATICAL FRAKTUR CAPITAL T
|
||||
'𝔘': 'u', // U+1D518 MATHEMATICAL FRAKTUR CAPITAL U
|
||||
'𝔙': 'v', // U+1D519 MATHEMATICAL FRAKTUR CAPITAL V
|
||||
'𝔚': 'w', // U+1D51A MATHEMATICAL FRAKTUR CAPITAL W
|
||||
'𝔛': 'x', // U+1D51B MATHEMATICAL FRAKTUR CAPITAL X
|
||||
'𝔜': 'y', // U+1D51C MATHEMATICAL FRAKTUR CAPITAL Y
|
||||
// Z is at U+2128 (Letterlike Symbols)
|
||||
}
|
||||
|
||||
// frakturLetterlikeToASCII maps the Fraktur characters that live in the
|
||||
// Letterlike Symbols block (U+2100-U+214F) rather than Mathematical Alphanumeric Symbols.
|
||||
var frakturLetterlikeToASCII = map[rune]rune{
|
||||
'ℭ': 'c', // U+212D BLACK-LETTER CAPITAL C
|
||||
'ℌ': 'h', // U+210C BLACK-LETTER CAPITAL H
|
||||
'ℑ': 'i', // U+2111 BLACK-LETTER CAPITAL I
|
||||
'ℜ': 'r', // U+211C BLACK-LETTER CAPITAL R
|
||||
'ℨ': 'z', // U+2128 BLACK-LETTER CAPITAL Z
|
||||
}
|
||||
|
||||
// hasDecorativeUnicode checks if text contains any small caps or fraktur characters
|
||||
// that would need normalization. Used by migration to identify events needing re-indexing.
|
||||
func hasDecorativeUnicode(s string) bool {
|
||||
for _, r := range s {
|
||||
// Check small caps
|
||||
if _, ok := smallCapsToASCII[r]; ok {
|
||||
return true
|
||||
}
|
||||
// Check fraktur lowercase range
|
||||
if r >= 0x1D51E && r <= 0x1D537 {
|
||||
return true
|
||||
}
|
||||
// Check fraktur uppercase range
|
||||
if r >= 0x1D504 && r <= 0x1D51C {
|
||||
return true
|
||||
}
|
||||
// Check letterlike symbols fraktur
|
||||
if _, ok := frakturLetterlikeToASCII[r]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
205
pkg/database/unicode_normalize_test.go
Normal file
205
pkg/database/unicode_normalize_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
//go:build !(js && wasm)
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeRune(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input rune
|
||||
expected rune
|
||||
}{
|
||||
// Small caps
|
||||
{"small cap A", 'ᴀ', 'a'},
|
||||
{"small cap B", 'ʙ', 'b'},
|
||||
{"small cap C", 'ᴄ', 'c'},
|
||||
{"small cap D", 'ᴅ', 'd'},
|
||||
{"small cap E", 'ᴇ', 'e'},
|
||||
{"small cap F", 'ꜰ', 'f'},
|
||||
{"small cap G", 'ɢ', 'g'},
|
||||
{"small cap H", 'ʜ', 'h'},
|
||||
{"small cap I", 'ɪ', 'i'},
|
||||
{"small cap J", 'ᴊ', 'j'},
|
||||
{"small cap K", 'ᴋ', 'k'},
|
||||
{"small cap L", 'ʟ', 'l'},
|
||||
{"small cap M", 'ᴍ', 'm'},
|
||||
{"small cap N", 'ɴ', 'n'},
|
||||
{"small cap O", 'ᴏ', 'o'},
|
||||
{"small cap P", 'ᴘ', 'p'},
|
||||
{"small cap Q (ogonek)", 'ǫ', 'q'},
|
||||
{"small cap R", 'ʀ', 'r'},
|
||||
{"small cap S", 'ꜱ', 's'},
|
||||
{"small cap T", 'ᴛ', 't'},
|
||||
{"small cap U", 'ᴜ', 'u'},
|
||||
{"small cap V", 'ᴠ', 'v'},
|
||||
{"small cap W", 'ᴡ', 'w'},
|
||||
{"small cap Y", 'ʏ', 'y'},
|
||||
{"small cap Z", 'ᴢ', 'z'},
|
||||
|
||||
// Fraktur lowercase
|
||||
{"fraktur lower a", '𝔞', 'a'},
|
||||
{"fraktur lower b", '𝔟', 'b'},
|
||||
{"fraktur lower c", '𝔠', 'c'},
|
||||
{"fraktur lower d", '𝔡', 'd'},
|
||||
{"fraktur lower e", '𝔢', 'e'},
|
||||
{"fraktur lower f", '𝔣', 'f'},
|
||||
{"fraktur lower g", '𝔤', 'g'},
|
||||
{"fraktur lower h", '𝔥', 'h'},
|
||||
{"fraktur lower i", '𝔦', 'i'},
|
||||
{"fraktur lower j", '𝔧', 'j'},
|
||||
{"fraktur lower k", '𝔨', 'k'},
|
||||
{"fraktur lower l", '𝔩', 'l'},
|
||||
{"fraktur lower m", '𝔪', 'm'},
|
||||
{"fraktur lower n", '𝔫', 'n'},
|
||||
{"fraktur lower o", '𝔬', 'o'},
|
||||
{"fraktur lower p", '𝔭', 'p'},
|
||||
{"fraktur lower q", '𝔮', 'q'},
|
||||
{"fraktur lower r", '𝔯', 'r'},
|
||||
{"fraktur lower s", '𝔰', 's'},
|
||||
{"fraktur lower t", '𝔱', 't'},
|
||||
{"fraktur lower u", '𝔲', 'u'},
|
||||
{"fraktur lower v", '𝔳', 'v'},
|
||||
{"fraktur lower w", '𝔴', 'w'},
|
||||
{"fraktur lower x", '𝔵', 'x'},
|
||||
{"fraktur lower y", '𝔶', 'y'},
|
||||
{"fraktur lower z", '𝔷', 'z'},
|
||||
|
||||
// Fraktur uppercase (main range)
|
||||
{"fraktur upper A", '𝔄', 'a'},
|
||||
{"fraktur upper B", '𝔅', 'b'},
|
||||
{"fraktur upper D", '𝔇', 'd'},
|
||||
{"fraktur upper E", '𝔈', 'e'},
|
||||
{"fraktur upper F", '𝔉', 'f'},
|
||||
{"fraktur upper G", '𝔊', 'g'},
|
||||
{"fraktur upper J", '𝔍', 'j'},
|
||||
{"fraktur upper K", '𝔎', 'k'},
|
||||
{"fraktur upper L", '𝔏', 'l'},
|
||||
{"fraktur upper M", '𝔐', 'm'},
|
||||
{"fraktur upper N", '𝔑', 'n'},
|
||||
{"fraktur upper O", '𝔒', 'o'},
|
||||
{"fraktur upper P", '𝔓', 'p'},
|
||||
{"fraktur upper Q", '𝔔', 'q'},
|
||||
{"fraktur upper S", '𝔖', 's'},
|
||||
{"fraktur upper T", '𝔗', 't'},
|
||||
{"fraktur upper U", '𝔘', 'u'},
|
||||
{"fraktur upper V", '𝔙', 'v'},
|
||||
{"fraktur upper W", '𝔚', 'w'},
|
||||
{"fraktur upper X", '𝔛', 'x'},
|
||||
{"fraktur upper Y", '𝔜', 'y'},
|
||||
|
||||
// Fraktur uppercase (Letterlike Symbols block)
|
||||
{"fraktur upper C (letterlike)", 'ℭ', 'c'},
|
||||
{"fraktur upper H (letterlike)", 'ℌ', 'h'},
|
||||
{"fraktur upper I (letterlike)", 'ℑ', 'i'},
|
||||
{"fraktur upper R (letterlike)", 'ℜ', 'r'},
|
||||
{"fraktur upper Z (letterlike)", 'ℨ', 'z'},
|
||||
|
||||
// Regular ASCII should pass through unchanged
|
||||
{"regular lowercase a", 'a', 'a'},
|
||||
{"regular lowercase z", 'z', 'z'},
|
||||
{"regular uppercase A", 'A', 'A'},
|
||||
{"regular digit 5", '5', '5'},
|
||||
|
||||
// Other unicode should pass through unchanged
|
||||
{"cyrillic д", 'д', 'д'},
|
||||
{"greek α", 'α', 'α'},
|
||||
{"emoji", '🎉', '🎉'},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := normalizeRune(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("normalizeRune(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasDecorativeUnicode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"plain ASCII", "hello world", false},
|
||||
{"small caps word", "ᴅᴇᴀᴛʜ", true},
|
||||
{"fraktur lowercase", "𝔥𝔢𝔩𝔩𝔬", true},
|
||||
{"fraktur uppercase", "𝔇𝔈𝔄𝔗ℌ", true},
|
||||
{"mixed with ASCII", "hello ᴡᴏʀʟᴅ", true},
|
||||
{"single small cap", "aᴀa", true},
|
||||
{"cyrillic (no normalize)", "привет", false},
|
||||
{"empty string", "", false},
|
||||
{"letterlike fraktur C", "ℭool", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := hasDecorativeUnicode(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("hasDecorativeUnicode(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHashesNormalization(t *testing.T) {
|
||||
// All three representations should produce the same hash
|
||||
ascii := TokenHashes([]byte("death"))
|
||||
smallCaps := TokenHashes([]byte("ᴅᴇᴀᴛʜ"))
|
||||
frakturLower := TokenHashes([]byte("𝔡𝔢𝔞𝔱𝔥"))
|
||||
frakturUpper := TokenHashes([]byte("𝔇𝔈𝔄𝔗ℌ"))
|
||||
|
||||
if len(ascii) != 1 {
|
||||
t.Fatalf("expected 1 hash for 'death', got %d", len(ascii))
|
||||
}
|
||||
if len(smallCaps) != 1 {
|
||||
t.Fatalf("expected 1 hash for small caps, got %d", len(smallCaps))
|
||||
}
|
||||
if len(frakturLower) != 1 {
|
||||
t.Fatalf("expected 1 hash for fraktur lower, got %d", len(frakturLower))
|
||||
}
|
||||
if len(frakturUpper) != 1 {
|
||||
t.Fatalf("expected 1 hash for fraktur upper, got %d", len(frakturUpper))
|
||||
}
|
||||
|
||||
// All should match the ASCII version
|
||||
if !bytes.Equal(ascii[0], smallCaps[0]) {
|
||||
t.Errorf("small caps hash differs from ASCII\nASCII: %x\nsmall caps: %x", ascii[0], smallCaps[0])
|
||||
}
|
||||
if !bytes.Equal(ascii[0], frakturLower[0]) {
|
||||
t.Errorf("fraktur lower hash differs from ASCII\nASCII: %x\nfraktur lower: %x", ascii[0], frakturLower[0])
|
||||
}
|
||||
if !bytes.Equal(ascii[0], frakturUpper[0]) {
|
||||
t.Errorf("fraktur upper hash differs from ASCII\nASCII: %x\nfraktur upper: %x", ascii[0], frakturUpper[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHashesMixedContent(t *testing.T) {
|
||||
// Test that mixed content normalizes correctly
|
||||
content := []byte("ᴛʜᴇ quick 𝔟𝔯𝔬𝔴𝔫 fox")
|
||||
hashes := TokenHashes(content)
|
||||
|
||||
// Should get: "the", "quick", "brown", "fox" (4 unique words)
|
||||
if len(hashes) != 4 {
|
||||
t.Errorf("expected 4 hashes from mixed content, got %d", len(hashes))
|
||||
}
|
||||
|
||||
// Verify "the" matches between decorated and plain
|
||||
thePlain := TokenHashes([]byte("the"))
|
||||
theDecorated := TokenHashes([]byte("ᴛʜᴇ"))
|
||||
if !bytes.Equal(thePlain[0], theDecorated[0]) {
|
||||
t.Errorf("'the' hash mismatch: plain=%x, decorated=%x", thePlain[0], theDecorated[0])
|
||||
}
|
||||
|
||||
// Verify "brown" matches between decorated and plain
|
||||
brownPlain := TokenHashes([]byte("brown"))
|
||||
brownDecorated := TokenHashes([]byte("𝔟𝔯𝔬𝔴𝔫"))
|
||||
if !bytes.Equal(brownPlain[0], brownDecorated[0]) {
|
||||
t.Errorf("'brown' hash mismatch: plain=%x, decorated=%x", brownPlain[0], brownDecorated[0])
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/filter"
|
||||
"git.mleku.dev/mleku/nostr/encoders/tag"
|
||||
ntypes "git.mleku.dev/mleku/nostr/types"
|
||||
)
|
||||
|
||||
// I am a type for a persistence layer for nostr events handled by a relay.
|
||||
@@ -60,6 +61,9 @@ type Accountant interface {
|
||||
EventCount() (count uint64, err error)
|
||||
}
|
||||
|
||||
// IdPkTs holds event reference data with slice fields for backward compatibility.
|
||||
// For new code preferring stack-allocated, copy-on-assignment semantics,
|
||||
// use the IDFixed() and PubFixed() methods or convert to EventRef.
|
||||
type IdPkTs struct {
|
||||
Id []byte
|
||||
Pub []byte
|
||||
@@ -67,6 +71,87 @@ type IdPkTs struct {
|
||||
Ser uint64
|
||||
}
|
||||
|
||||
// IDFixed returns the event ID as a fixed-size array (stack-allocated, copied on assignment).
|
||||
func (i *IdPkTs) IDFixed() ntypes.EventID {
|
||||
return ntypes.EventIDFromBytes(i.Id)
|
||||
}
|
||||
|
||||
// PubFixed returns the pubkey as a fixed-size array (stack-allocated, copied on assignment).
|
||||
func (i *IdPkTs) PubFixed() ntypes.Pubkey {
|
||||
return ntypes.PubkeyFromBytes(i.Pub)
|
||||
}
|
||||
|
||||
// IDHex returns the event ID as a lowercase hex string.
|
||||
func (i *IdPkTs) IDHex() string {
|
||||
return ntypes.EventIDFromBytes(i.Id).Hex()
|
||||
}
|
||||
|
||||
// PubHex returns the pubkey as a lowercase hex string.
|
||||
func (i *IdPkTs) PubHex() string {
|
||||
return ntypes.PubkeyFromBytes(i.Pub).Hex()
|
||||
}
|
||||
|
||||
// ToEventRef converts IdPkTs to an EventRef (fully stack-allocated).
|
||||
func (i *IdPkTs) ToEventRef() EventRef {
|
||||
return NewEventRef(i.Id, i.Pub, i.Ts, i.Ser)
|
||||
}
|
||||
|
||||
// EventRef is a stack-friendly event reference using fixed-size arrays.
|
||||
// Total size: 80 bytes (32+32+8+8), fits in a cache line, copies stay on stack.
|
||||
// Use this type when you need safe, immutable event references.
|
||||
type EventRef struct {
|
||||
id ntypes.EventID // 32 bytes
|
||||
pub ntypes.Pubkey // 32 bytes
|
||||
ts int64 // 8 bytes
|
||||
ser uint64 // 8 bytes
|
||||
}
|
||||
|
||||
// NewEventRef creates an EventRef from byte slices.
|
||||
// The slices are copied into fixed-size arrays.
|
||||
func NewEventRef(id, pub []byte, ts int64, ser uint64) EventRef {
|
||||
return EventRef{
|
||||
id: ntypes.EventIDFromBytes(id),
|
||||
pub: ntypes.PubkeyFromBytes(pub),
|
||||
ts: ts,
|
||||
ser: ser,
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns the event ID (copy, stays on stack).
|
||||
func (r EventRef) ID() ntypes.EventID { return r.id }
|
||||
|
||||
// Pub returns the pubkey (copy, stays on stack).
|
||||
func (r EventRef) Pub() ntypes.Pubkey { return r.pub }
|
||||
|
||||
// Ts returns the timestamp.
|
||||
func (r EventRef) Ts() int64 { return r.ts }
|
||||
|
||||
// Ser returns the serial number.
|
||||
func (r EventRef) Ser() uint64 { return r.ser }
|
||||
|
||||
// IDHex returns the event ID as lowercase hex.
|
||||
func (r EventRef) IDHex() string { return r.id.Hex() }
|
||||
|
||||
// PubHex returns the pubkey as lowercase hex.
|
||||
func (r EventRef) PubHex() string { return r.pub.Hex() }
|
||||
|
||||
// IDSlice returns a slice view of the ID (shares memory, use carefully).
|
||||
func (r *EventRef) IDSlice() []byte { return r.id.Bytes() }
|
||||
|
||||
// PubSlice returns a slice view of the pubkey (shares memory, use carefully).
|
||||
func (r *EventRef) PubSlice() []byte { return r.pub.Bytes() }
|
||||
|
||||
// ToIdPkTs converts EventRef to IdPkTs for backward compatibility.
|
||||
// Note: This allocates new slices.
|
||||
func (r EventRef) ToIdPkTs() *IdPkTs {
|
||||
return &IdPkTs{
|
||||
Id: r.id.Copy(),
|
||||
Pub: r.pub.Copy(),
|
||||
Ts: r.ts,
|
||||
Ser: r.ser,
|
||||
}
|
||||
}
|
||||
|
||||
type Querier interface {
|
||||
QueryForIds(c context.Context, f *filter.F) (evs []*IdPkTs, err error)
|
||||
}
|
||||
|
||||
248
pkg/interfaces/store/store_interface_test.go
Normal file
248
pkg/interfaces/store/store_interface_test.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
ntypes "git.mleku.dev/mleku/nostr/types"
|
||||
)
|
||||
|
||||
func TestIdPkTsFixedMethods(t *testing.T) {
|
||||
// Create an IdPkTs with sample data
|
||||
id := make([]byte, 32)
|
||||
pub := make([]byte, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
id[i] = byte(i)
|
||||
pub[i] = byte(i + 32)
|
||||
}
|
||||
|
||||
ipk := &IdPkTs{
|
||||
Id: id,
|
||||
Pub: pub,
|
||||
Ts: 1234567890,
|
||||
Ser: 42,
|
||||
}
|
||||
|
||||
// Test IDFixed returns correct data
|
||||
idFixed := ipk.IDFixed()
|
||||
if !bytes.Equal(idFixed[:], id) {
|
||||
t.Errorf("IDFixed: got %x, want %x", idFixed[:], id)
|
||||
}
|
||||
|
||||
// Test IDFixed returns a copy
|
||||
idFixed[0] = 0xFF
|
||||
if ipk.Id[0] == 0xFF {
|
||||
t.Error("IDFixed should return a copy, not a reference")
|
||||
}
|
||||
|
||||
// Test PubFixed returns correct data
|
||||
pubFixed := ipk.PubFixed()
|
||||
if !bytes.Equal(pubFixed[:], pub) {
|
||||
t.Errorf("PubFixed: got %x, want %x", pubFixed[:], pub)
|
||||
}
|
||||
|
||||
// Test hex methods
|
||||
idHex := ipk.IDHex()
|
||||
expectedIDHex := "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
|
||||
if idHex != expectedIDHex {
|
||||
t.Errorf("IDHex: got %s, want %s", idHex, expectedIDHex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventRef(t *testing.T) {
|
||||
id := make([]byte, 32)
|
||||
pub := make([]byte, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
id[i] = byte(i)
|
||||
pub[i] = byte(i + 100)
|
||||
}
|
||||
|
||||
// Create EventRef
|
||||
ref := NewEventRef(id, pub, 1234567890, 42)
|
||||
|
||||
// Test accessors - need to get addressable values for slicing
|
||||
refID := ref.ID()
|
||||
refPub := ref.Pub()
|
||||
if !bytes.Equal(refID[:], id) {
|
||||
t.Error("ID() mismatch")
|
||||
}
|
||||
if !bytes.Equal(refPub[:], pub) {
|
||||
t.Error("Pub() mismatch")
|
||||
}
|
||||
if ref.Ts() != 1234567890 {
|
||||
t.Error("Ts() mismatch")
|
||||
}
|
||||
if ref.Ser() != 42 {
|
||||
t.Error("Ser() mismatch")
|
||||
}
|
||||
|
||||
// Test copy-on-assignment
|
||||
ref2 := ref
|
||||
testID := ref.ID()
|
||||
testID[0] = 0xFF
|
||||
ref2ID := ref2.ID()
|
||||
if ref2ID[0] == 0xFF {
|
||||
t.Error("EventRef should copy on assignment")
|
||||
}
|
||||
|
||||
// Test hex methods
|
||||
if len(ref.IDHex()) != 64 {
|
||||
t.Errorf("IDHex length: got %d, want 64", len(ref.IDHex()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventRefToIdPkTs(t *testing.T) {
|
||||
id := make([]byte, 32)
|
||||
pub := make([]byte, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
id[i] = byte(i)
|
||||
pub[i] = byte(i + 100)
|
||||
}
|
||||
|
||||
ref := NewEventRef(id, pub, 1234567890, 42)
|
||||
ipk := ref.ToIdPkTs()
|
||||
|
||||
// Verify conversion
|
||||
if !bytes.Equal(ipk.Id, id) {
|
||||
t.Error("ToIdPkTs: Id mismatch")
|
||||
}
|
||||
if !bytes.Equal(ipk.Pub, pub) {
|
||||
t.Error("ToIdPkTs: Pub mismatch")
|
||||
}
|
||||
if ipk.Ts != 1234567890 {
|
||||
t.Error("ToIdPkTs: Ts mismatch")
|
||||
}
|
||||
if ipk.Ser != 42 {
|
||||
t.Error("ToIdPkTs: Ser mismatch")
|
||||
}
|
||||
|
||||
// Verify independence (modifications don't affect original)
|
||||
ipk.Id[0] = 0xFF
|
||||
if ref.ID()[0] == 0xFF {
|
||||
t.Error("ToIdPkTs should create independent copy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdPkTsToEventRef(t *testing.T) {
|
||||
id := make([]byte, 32)
|
||||
pub := make([]byte, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
id[i] = byte(i)
|
||||
pub[i] = byte(i + 100)
|
||||
}
|
||||
|
||||
ipk := &IdPkTs{
|
||||
Id: id,
|
||||
Pub: pub,
|
||||
Ts: 1234567890,
|
||||
Ser: 42,
|
||||
}
|
||||
|
||||
ref := ipk.ToEventRef()
|
||||
|
||||
// Verify conversion - need addressable values for slicing
|
||||
refID := ref.ID()
|
||||
refPub := ref.Pub()
|
||||
if !bytes.Equal(refID[:], id) {
|
||||
t.Error("ToEventRef: ID mismatch")
|
||||
}
|
||||
if !bytes.Equal(refPub[:], pub) {
|
||||
t.Error("ToEventRef: Pub mismatch")
|
||||
}
|
||||
if ref.Ts() != 1234567890 {
|
||||
t.Error("ToEventRef: Ts mismatch")
|
||||
}
|
||||
if ref.Ser() != 42 {
|
||||
t.Error("ToEventRef: Ser mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEventRefCopy(b *testing.B) {
|
||||
id := make([]byte, 32)
|
||||
pub := make([]byte, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
id[i] = byte(i)
|
||||
pub[i] = byte(i + 100)
|
||||
}
|
||||
|
||||
ref := NewEventRef(id, pub, 1234567890, 42)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ref2 := ref // Copy (should stay on stack)
|
||||
_ = ref2
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIdPkTsToEventRef(b *testing.B) {
|
||||
id := make([]byte, 32)
|
||||
pub := make([]byte, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
id[i] = byte(i)
|
||||
pub[i] = byte(i + 100)
|
||||
}
|
||||
|
||||
ipk := &IdPkTs{
|
||||
Id: id,
|
||||
Pub: pub,
|
||||
Ts: 1234567890,
|
||||
Ser: 42,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ref := ipk.ToEventRef()
|
||||
_ = ref
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEventRefAccess(b *testing.B) {
|
||||
id := make([]byte, 32)
|
||||
pub := make([]byte, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
id[i] = byte(i)
|
||||
pub[i] = byte(i + 100)
|
||||
}
|
||||
|
||||
ref := NewEventRef(id, pub, 1234567890, 42)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
idCopy := ref.ID()
|
||||
pubCopy := ref.Pub()
|
||||
_ = idCopy
|
||||
_ = pubCopy
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIdPkTsFixedAccess(b *testing.B) {
|
||||
id := make([]byte, 32)
|
||||
pub := make([]byte, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
id[i] = byte(i)
|
||||
pub[i] = byte(i + 100)
|
||||
}
|
||||
|
||||
ipk := &IdPkTs{
|
||||
Id: id,
|
||||
Pub: pub,
|
||||
Ts: 1234567890,
|
||||
Ser: 42,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
idCopy := ipk.IDFixed()
|
||||
pubCopy := ipk.PubFixed()
|
||||
_ = idCopy
|
||||
_ = pubCopy
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure EventRef implements expected interface at compile time
|
||||
var _ interface {
|
||||
ID() ntypes.EventID
|
||||
Pub() ntypes.Pubkey
|
||||
Ts() int64
|
||||
Ser() uint64
|
||||
} = EventRef{}
|
||||
@@ -339,11 +339,11 @@ func TestGetFullIdPubkeyBySerial(t *testing.T) {
|
||||
t.Fatal("Expected non-nil result")
|
||||
}
|
||||
|
||||
if hex.Enc(idPkTs.Id) != hex.Enc(ev.ID[:]) {
|
||||
if idPkTs.IDHex() != hex.Enc(ev.ID[:]) {
|
||||
t.Fatalf("ID mismatch")
|
||||
}
|
||||
|
||||
if hex.Enc(idPkTs.Pub) != hex.Enc(ev.Pubkey[:]) {
|
||||
if idPkTs.PubHex() != hex.Enc(ev.Pubkey[:]) {
|
||||
t.Fatalf("Pubkey mismatch")
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.36.9
|
||||
v0.36.11
|
||||
|
||||
Reference in New Issue
Block a user