25 changed files with 1547 additions and 44 deletions
@ -0,0 +1,5 @@ |
|||||||
|
package app |
||||||
|
|
||||||
|
func (s *Server) HandleMessage() { |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,100 @@ |
|||||||
|
package app |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/coder/websocket" |
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lol.mleku.dev/log" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// CloseMessage denotes a close control message. The optional message
|
||||||
|
// payload contains a numeric code and text. Use the FormatCloseMessage
|
||||||
|
// function to format a close message payload.
|
||||||
|
CloseMessage = 8 |
||||||
|
|
||||||
|
// PingMessage denotes a ping control message. The optional message payload
|
||||||
|
// is UTF-8 encoded text.
|
||||||
|
PingMessage = 9 |
||||||
|
|
||||||
|
// PongMessage denotes a pong control message. The optional message payload
|
||||||
|
// is UTF-8 encoded text.
|
||||||
|
PongMessage = 10 |
||||||
|
) |
||||||
|
|
||||||
|
func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) { |
||||||
|
remote := GetRemoteFromReq(r) |
||||||
|
var cancel context.CancelFunc |
||||||
|
s.Ctx, cancel = context.WithCancel(s.Ctx) |
||||||
|
defer cancel() |
||||||
|
var err error |
||||||
|
var conn *websocket.Conn |
||||||
|
if conn, err = websocket.Accept( |
||||||
|
w, r, &websocket.AcceptOptions{}, |
||||||
|
); chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
defer conn.CloseNow() |
||||||
|
|
||||||
|
go s.Pinger(s.Ctx, conn, time.NewTicker(time.Second*10), cancel) |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-s.Ctx.Done(): |
||||||
|
return |
||||||
|
default: |
||||||
|
} |
||||||
|
var typ websocket.MessageType |
||||||
|
var message []byte |
||||||
|
if typ, message, err = conn.Read(s.Ctx); err != nil { |
||||||
|
if strings.Contains( |
||||||
|
err.Error(), "use of closed network connection", |
||||||
|
) { |
||||||
|
return |
||||||
|
} |
||||||
|
status := websocket.CloseStatus(err) |
||||||
|
switch status { |
||||||
|
case websocket.StatusNormalClosure, |
||||||
|
websocket.StatusGoingAway, |
||||||
|
websocket.StatusNoStatusRcvd, |
||||||
|
websocket.StatusAbnormalClosure, |
||||||
|
websocket.StatusProtocolError: |
||||||
|
default: |
||||||
|
log.E.F("unexpected close error from %s: %v", remote, err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
if typ == PingMessage { |
||||||
|
if err = conn.Write(s.Ctx, PongMessage, message); chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
continue |
||||||
|
} |
||||||
|
go s.HandleMessage() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) Pinger( |
||||||
|
ctx context.Context, conn *websocket.Conn, ticker *time.Ticker, |
||||||
|
cancel context.CancelFunc, |
||||||
|
) { |
||||||
|
defer func() { |
||||||
|
cancel() |
||||||
|
ticker.Stop() |
||||||
|
}() |
||||||
|
var err error |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-ticker.C: |
||||||
|
if err = conn.Write(ctx, PingMessage, nil); err != nil { |
||||||
|
log.E.F("error writing ping: %v; closing websocket", err) |
||||||
|
return |
||||||
|
} |
||||||
|
case <-ctx.Done(): |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
package app |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// GetRemoteFromReq retrieves the originating IP address of the client from
|
||||||
|
// an HTTP request, considering standard and non-standard proxy headers.
|
||||||
|
//
|
||||||
|
// # Parameters
|
||||||
|
//
|
||||||
|
// - r: The HTTP request object containing details of the client and
|
||||||
|
// routing information.
|
||||||
|
//
|
||||||
|
// # Return Values
|
||||||
|
//
|
||||||
|
// - rr: A string value representing the IP address of the originating
|
||||||
|
// remote client.
|
||||||
|
//
|
||||||
|
// # Expected behaviour
|
||||||
|
//
|
||||||
|
// The function first checks for the standardized "Forwarded" header (RFC 7239)
|
||||||
|
// to identify the original client IP. If that isn't available, it falls back to
|
||||||
|
// the "X-Forwarded-For" header. If both headers are absent, it defaults to
|
||||||
|
// using the request's RemoteAddr.
|
||||||
|
//
|
||||||
|
// For the "Forwarded" header, it extracts the client IP from the "for"
|
||||||
|
// parameter. For the "X-Forwarded-For" header, if it contains one IP, it
|
||||||
|
// returns that. If it contains two IPs, it returns the second.
|
||||||
|
func GetRemoteFromReq(r *http.Request) (rr string) { |
||||||
|
// First check for the standardized Forwarded header (RFC 7239)
|
||||||
|
forwarded := r.Header.Get("Forwarded") |
||||||
|
if forwarded != "" { |
||||||
|
// Parse the Forwarded header which can contain multiple parameters
|
||||||
|
//
|
||||||
|
// Format:
|
||||||
|
//
|
||||||
|
// Forwarded: by=<identifier>;for=<identifier>;host=<host>;proto=<http|https>
|
||||||
|
parts := strings.Split(forwarded, ";") |
||||||
|
for _, part := range parts { |
||||||
|
part = strings.TrimSpace(part) |
||||||
|
if strings.HasPrefix(part, "for=") { |
||||||
|
// Extract the client IP from the "for" parameter
|
||||||
|
forValue := strings.TrimPrefix(part, "for=") |
||||||
|
// Remove quotes if present
|
||||||
|
forValue = strings.Trim(forValue, "\"") |
||||||
|
// Handle IPv6 addresses which are enclosed in square brackets
|
||||||
|
forValue = strings.Trim(forValue, "[]") |
||||||
|
return forValue |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
// If the Forwarded header is not available or doesn't contain "for"
|
||||||
|
// parameter, fall back to X-Forwarded-For
|
||||||
|
rem := r.Header.Get("X-Forwarded-For") |
||||||
|
if rem == "" { |
||||||
|
rr = r.RemoteAddr |
||||||
|
} else { |
||||||
|
splitted := strings.Split(rem, " ") |
||||||
|
if len(splitted) == 1 { |
||||||
|
rr = splitted[0] |
||||||
|
} |
||||||
|
if len(splitted) == 2 { |
||||||
|
rr = splitted[1] |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
@ -1,29 +1,9 @@ |
|||||||
package app |
package app |
||||||
|
|
||||||
import ( |
import ( |
||||||
"net/http" |
"github.com/coder/websocket" |
||||||
|
|
||||||
"lol.mleku.dev/log" |
|
||||||
"next.orly.dev/app/config" |
|
||||||
) |
) |
||||||
|
|
||||||
type Listener struct { |
type Listener struct { |
||||||
mux *http.ServeMux |
conn *websocket.Conn |
||||||
Config *config.C |
|
||||||
} |
|
||||||
|
|
||||||
func (l *Listener) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
||||||
log.I.F("path %v header %v", r.URL, r.Header) |
|
||||||
if r.Header.Get("Upgrade") == "websocket" { |
|
||||||
l.HandleWebsocket(w, r) |
|
||||||
} else if r.Header.Get("Accept") == "application/nostr+json" { |
|
||||||
l.HandleRelayInfo(w, r) |
|
||||||
} else { |
|
||||||
http.Error(w, "Upgrade required", http.StatusUpgradeRequired) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (l *Listener) HandleWebsocket(w http.ResponseWriter, r *http.Request) { |
|
||||||
log.I.F("websocket") |
|
||||||
return |
|
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,26 @@ |
|||||||
|
package app |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"lol.mleku.dev/log" |
||||||
|
"next.orly.dev/app/config" |
||||||
|
) |
||||||
|
|
||||||
|
type Server struct { |
||||||
|
mux *http.ServeMux |
||||||
|
Config *config.C |
||||||
|
Ctx context.Context |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||||
|
log.T.F("path %v header %v", r.URL, r.Header) |
||||||
|
if r.Header.Get("Upgrade") == "websocket" { |
||||||
|
s.HandleWebsocket(w, r) |
||||||
|
} else if r.Header.Get("Accept") == "application/nostr+json" { |
||||||
|
s.HandleRelayInfo(w, r) |
||||||
|
} else { |
||||||
|
http.Error(w, "Upgrade required", http.StatusUpgradeRequired) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,64 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/pkg/profile" |
||||||
|
lol "lol.mleku.dev" |
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lukechampine.com/frand" |
||||||
|
"next.orly.dev/pkg/encoders/event" |
||||||
|
"next.orly.dev/pkg/encoders/hex" |
||||||
|
"next.orly.dev/pkg/encoders/tag" |
||||||
|
"next.orly.dev/pkg/utils" |
||||||
|
"next.orly.dev/pkg/utils/bufpool" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
lol.SetLogLevel("info") |
||||||
|
prof := profile.Start(profile.CPUProfile) |
||||||
|
defer prof.Stop() |
||||||
|
for range 1000000 { |
||||||
|
ev := event.New() |
||||||
|
ev.ID = frand.Bytes(32) |
||||||
|
ev.Pubkey = frand.Bytes(32) |
||||||
|
ev.CreatedAt = time.Now().Unix() |
||||||
|
ev.Kind = 1 |
||||||
|
ev.Tags = &tag.S{ |
||||||
|
{T: [][]byte{[]byte("t"), []byte("hashtag")}}, |
||||||
|
{ |
||||||
|
T: [][]byte{ |
||||||
|
[]byte("e"), |
||||||
|
hex.EncAppend(nil, frand.Bytes(32)), |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
ev.Content = frand.Bytes(frand.Intn(1024) + 1) |
||||||
|
ev.Sig = frand.Bytes(64) |
||||||
|
// log.I.S(ev)
|
||||||
|
b, err := ev.MarshalJSON() |
||||||
|
if chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
var bc []byte |
||||||
|
bc = append(bc, b...) |
||||||
|
// log.I.F("%s", bc)
|
||||||
|
ev2 := event.New() |
||||||
|
if err = ev2.UnmarshalJSON(b); chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
var b2 []byte |
||||||
|
if b2, err = ev.MarshalJSON(); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if !utils.FastEqual(bc, b2) { |
||||||
|
return |
||||||
|
} |
||||||
|
// free up the resources for the next iteration
|
||||||
|
ev.Free() |
||||||
|
ev2.Free() |
||||||
|
bufpool.PutBytes(b) |
||||||
|
bufpool.PutBytes(b2) |
||||||
|
bufpool.PutBytes(bc) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,63 @@ |
|||||||
|
package event |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/pkg/profile" |
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lukechampine.com/frand" |
||||||
|
"next.orly.dev/pkg/encoders/hex" |
||||||
|
"next.orly.dev/pkg/encoders/tag" |
||||||
|
"next.orly.dev/pkg/utils" |
||||||
|
"next.orly.dev/pkg/utils/bufpool" |
||||||
|
) |
||||||
|
|
||||||
|
func TestMarshalJSON(t *testing.T) { |
||||||
|
// lol.SetLogLevel("trace")
|
||||||
|
prof := profile.Start(profile.MemProfile) |
||||||
|
defer prof.Stop() |
||||||
|
for range 1000000 { |
||||||
|
ev := New() |
||||||
|
ev.ID = frand.Bytes(32) |
||||||
|
ev.Pubkey = frand.Bytes(32) |
||||||
|
ev.CreatedAt = time.Now().Unix() |
||||||
|
ev.Kind = 1 |
||||||
|
ev.Tags = &tag.S{ |
||||||
|
{T: [][]byte{[]byte("t"), []byte("hashtag")}}, |
||||||
|
{ |
||||||
|
T: [][]byte{ |
||||||
|
[]byte("e"), |
||||||
|
hex.EncAppend(nil, frand.Bytes(32)), |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
ev.Content = frand.Bytes(frand.Intn(1024) + 1) |
||||||
|
ev.Sig = frand.Bytes(64) |
||||||
|
// log.I.S(ev)
|
||||||
|
b, err := ev.MarshalJSON() |
||||||
|
if err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
var bc []byte |
||||||
|
bc = append(bc, b...) |
||||||
|
// log.I.F("%s", bc)
|
||||||
|
ev2 := New() |
||||||
|
if err = ev2.UnmarshalJSON(b); chk.E(err) { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
var b2 []byte |
||||||
|
if b2, err = ev.MarshalJSON(); err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
if !utils.FastEqual(bc, b2) { |
||||||
|
t.Errorf("failed to re-marshal back original") |
||||||
|
} |
||||||
|
// free up the resources for the next iteration
|
||||||
|
ev.Free() |
||||||
|
ev2.Free() |
||||||
|
bufpool.PutBytes(b) |
||||||
|
bufpool.PutBytes(b2) |
||||||
|
bufpool.PutBytes(bc) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
// Package atag implements a special, optimized handling for keeping a tags
|
||||||
|
// (address) in a more memory efficient form while working with these tags.
|
||||||
|
package atag |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
|
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"next.orly.dev/pkg/encoders/hex" |
||||||
|
"next.orly.dev/pkg/encoders/ints" |
||||||
|
"next.orly.dev/pkg/encoders/kind" |
||||||
|
) |
||||||
|
|
||||||
|
// T is a data structure for what is found in an `a` tag: kind:pubkey:arbitrary data
|
||||||
|
type T struct { |
||||||
|
Kind *kind.K |
||||||
|
PubKey []byte |
||||||
|
DTag []byte |
||||||
|
} |
||||||
|
|
||||||
|
// Marshal an atag.T into raw bytes.
|
||||||
|
func (t *T) Marshal(dst []byte) (b []byte) { |
||||||
|
b = t.Kind.Marshal(dst) |
||||||
|
b = append(b, ':') |
||||||
|
b = hex.EncAppend(b, t.PubKey) |
||||||
|
b = append(b, ':') |
||||||
|
b = append(b, t.DTag...) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal an atag.T from its ascii encoding.
|
||||||
|
func (t *T) Unmarshal(b []byte) (r []byte, err error) { |
||||||
|
split := bytes.Split(b, []byte{':'}) |
||||||
|
if len(split) != 3 { |
||||||
|
return |
||||||
|
} |
||||||
|
// kind
|
||||||
|
kin := ints.New(uint16(0)) |
||||||
|
if _, err = kin.Unmarshal(split[0]); chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
t.Kind = kind.New(kin.Uint16()) |
||||||
|
// pubkey
|
||||||
|
if t.PubKey, err = hex.DecAppend(t.PubKey, split[1]); chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
// d-tag
|
||||||
|
t.DTag = split[2] |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,45 @@ |
|||||||
|
package atag |
||||||
|
|
||||||
|
import ( |
||||||
|
"math" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lol.mleku.dev/log" |
||||||
|
"lukechampine.com/frand" |
||||||
|
"next.orly.dev/pkg/crypto/ec/schnorr" |
||||||
|
"next.orly.dev/pkg/encoders/hex" |
||||||
|
"next.orly.dev/pkg/encoders/kind" |
||||||
|
"next.orly.dev/pkg/utils" |
||||||
|
) |
||||||
|
|
||||||
|
func TestT_Marshal_Unmarshal(t *testing.T) { |
||||||
|
k := kind.New(frand.Intn(math.MaxUint16)) |
||||||
|
pk := make([]byte, schnorr.PubKeyBytesLen) |
||||||
|
frand.Read(pk) |
||||||
|
d := make([]byte, frand.Intn(10)+3) |
||||||
|
frand.Read(d) |
||||||
|
var dtag string |
||||||
|
dtag = hex.Enc(d) |
||||||
|
t1 := &T{ |
||||||
|
Kind: k, |
||||||
|
PubKey: pk, |
||||||
|
DTag: []byte(dtag), |
||||||
|
} |
||||||
|
b1 := t1.Marshal(nil) |
||||||
|
log.I.F("%s", b1) |
||||||
|
t2 := &T{} |
||||||
|
var r []byte |
||||||
|
var err error |
||||||
|
if r, err = t2.Unmarshal(b1); chk.E(err) { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
if len(r) > 0 { |
||||||
|
log.I.S(r) |
||||||
|
t.Fatalf("remainder") |
||||||
|
} |
||||||
|
b2 := t2.Marshal(nil) |
||||||
|
if !utils.FastEqual(b1, b2) { |
||||||
|
t.Fatalf("failed to re-marshal back original") |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,75 @@ |
|||||||
|
// 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 ( |
||||||
|
"lol.mleku.dev/errorf" |
||||||
|
"next.orly.dev/pkg/encoders/text" |
||||||
|
"next.orly.dev/pkg/utils/bufpool" |
||||||
|
) |
||||||
|
|
||||||
|
// The tag position meanings, so they are clear when reading.
|
||||||
|
const ( |
||||||
|
Key = iota |
||||||
|
Value |
||||||
|
Relay |
||||||
|
) |
||||||
|
|
||||||
|
type T struct { |
||||||
|
T [][]byte |
||||||
|
b bufpool.B |
||||||
|
} |
||||||
|
|
||||||
|
func New(t ...[]byte) *T { |
||||||
|
return &T{T: t, b: bufpool.Get()} |
||||||
|
} |
||||||
|
|
||||||
|
func (t *T) Free() { |
||||||
|
bufpool.Put(t.b) |
||||||
|
t.T = nil |
||||||
|
} |
||||||
|
|
||||||
|
// Marshal encodes a tag.T as standard minified JSON array of strings.
|
||||||
|
//
|
||||||
|
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
||||||
|
func (t *T) Marshal() (b []byte) { |
||||||
|
dst := t.b |
||||||
|
dst = append(dst, '[') |
||||||
|
for i, s := range t.T { |
||||||
|
dst = text.AppendQuote(dst, s, text.NostrEscape) |
||||||
|
if i < len(t.T)-1 { |
||||||
|
dst = append(dst, ',') |
||||||
|
} |
||||||
|
} |
||||||
|
dst = append(dst, ']') |
||||||
|
return dst |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal decodes a standard minified JSON array of strings to a tags.T.
|
||||||
|
//
|
||||||
|
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
||||||
|
func (t *T) Unmarshal(b []byte) (r []byte, err error) { |
||||||
|
var inQuotes, openedBracket bool |
||||||
|
var quoteStart int |
||||||
|
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 |
||||||
|
t.T = append(t.T, text.NostrUnescape(b[quoteStart:i])) |
||||||
|
} |
||||||
|
} |
||||||
|
if !openedBracket || inQuotes { |
||||||
|
return nil, errorf.E("tag: failed to parse tag") |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
package tag |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lukechampine.com/frand" |
||||||
|
"next.orly.dev/pkg/utils" |
||||||
|
) |
||||||
|
|
||||||
|
func TestMarshalUnmarshal(t *testing.T) { |
||||||
|
for _ = range 1000 { |
||||||
|
n := frand.Intn(8) |
||||||
|
tg := New() |
||||||
|
for _ = range n { |
||||||
|
b1 := make([]byte, frand.Intn(8)) |
||||||
|
_, _ = frand.Read(b1) |
||||||
|
tg.T = append(tg.T, b1) |
||||||
|
} |
||||||
|
tb := tg.Marshal() |
||||||
|
var tbc []byte |
||||||
|
tbc = append(tbc, tb...) |
||||||
|
tg2 := New() |
||||||
|
if _, err := tg2.Unmarshal(tb); chk.E(err) { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
tb2 := tg2.Marshal() |
||||||
|
if !utils.FastEqual(tbc, tb2) { |
||||||
|
t.Fatalf("failed to re-marshal back original") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,78 @@ |
|||||||
|
package tag |
||||||
|
|
||||||
|
import ( |
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"next.orly.dev/pkg/utils/bufpool" |
||||||
|
) |
||||||
|
|
||||||
|
// S is a list of tag.T - which are lists of string elements with ordering and
|
||||||
|
// no uniqueness constraint (not a set).
|
||||||
|
type S []*T |
||||||
|
|
||||||
|
// MarshalJSON encodes a tags.T appended to a provided byte slice in JSON form.
|
||||||
|
//
|
||||||
|
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
||||||
|
func (s *S) MarshalJSON() (b []byte, err error) { |
||||||
|
b = bufpool.Get() |
||||||
|
b = append(b, '[') |
||||||
|
for i, ss := range *s { |
||||||
|
b = append(b, ss.Marshal()...) |
||||||
|
if i < len(*s)-1 { |
||||||
|
b = append(b, ',') |
||||||
|
} |
||||||
|
} |
||||||
|
b = append(b, ']') |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalJSON a tags.T from a provided byte slice and return what remains
|
||||||
|
// after the end of the array.
|
||||||
|
//
|
||||||
|
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
||||||
|
func (s *S) UnmarshalJSON(b []byte) (err error) { |
||||||
|
_, err = s.Unmarshal(b) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal a tags.T from a provided byte slice and return what remains after
|
||||||
|
// the end of the array.
|
||||||
|
func (s *S) Unmarshal(b []byte) (r []byte, err error) { |
||||||
|
r = b[:] |
||||||
|
for len(r) > 0 { |
||||||
|
switch r[0] { |
||||||
|
case '[': |
||||||
|
r = r[1:] |
||||||
|
goto inTags |
||||||
|
case ',': |
||||||
|
r = r[1:] |
||||||
|
// next
|
||||||
|
case ']': |
||||||
|
r = r[1:] |
||||||
|
// the end
|
||||||
|
return |
||||||
|
default: |
||||||
|
r = r[1:] |
||||||
|
} |
||||||
|
inTags: |
||||||
|
for len(r) > 0 { |
||||||
|
switch r[0] { |
||||||
|
case '[': |
||||||
|
tt := New() |
||||||
|
if r, err = tt.Unmarshal(r); chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
*s = append(*s, tt) |
||||||
|
case ',': |
||||||
|
r = r[1:] |
||||||
|
// next
|
||||||
|
case ']': |
||||||
|
r = r[1:] |
||||||
|
// the end
|
||||||
|
return |
||||||
|
default: |
||||||
|
r = r[1:] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
package tag |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lukechampine.com/frand" |
||||||
|
"next.orly.dev/pkg/utils" |
||||||
|
) |
||||||
|
|
||||||
|
func TestSMarshalUnmarshal(t *testing.T) { |
||||||
|
for _ = range 100 { |
||||||
|
tgs := new(S) |
||||||
|
n := frand.Intn(8) |
||||||
|
for _ = range n { |
||||||
|
n := frand.Intn(8) |
||||||
|
tg := New() |
||||||
|
for _ = range n { |
||||||
|
b1 := make([]byte, frand.Intn(8)) |
||||||
|
_, _ = frand.Read(b1) |
||||||
|
tg.T = append(tg.T, b1) |
||||||
|
} |
||||||
|
*tgs = append(*tgs, tg) |
||||||
|
} |
||||||
|
tgsb, _ := tgs.MarshalJSON() |
||||||
|
var tbc []byte |
||||||
|
tbc = append(tbc, tgsb...) |
||||||
|
tgs2 := new(S) |
||||||
|
if err := tgs2.UnmarshalJSON(tgsb); chk.E(err) { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
tgsb2, _ := tgs2.MarshalJSON() |
||||||
|
if !utils.FastEqual(tbc, tgsb2) { |
||||||
|
t.Fatalf("failed to re-marshal back original") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,255 @@ |
|||||||
|
package text |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
|
||||||
|
"github.com/templexxx/xhex" |
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lol.mleku.dev/errorf" |
||||||
|
"next.orly.dev/pkg/encoders/hex" |
||||||
|
"next.orly.dev/pkg/utils" |
||||||
|
) |
||||||
|
|
||||||
|
// JSONKey generates the JSON format for an object key and terminates with the semicolon.
|
||||||
|
func JSONKey(dst, k []byte) (b []byte) { |
||||||
|
dst = append(dst, '"') |
||||||
|
dst = append(dst, k...) |
||||||
|
dst = append(dst, '"', ':') |
||||||
|
b = dst |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalHex takes a byte string that should contain a quoted hexadecimal
|
||||||
|
// encoded value, decodes it using a SIMD hex codec and returns the decoded
|
||||||
|
// bytes in a newly allocated buffer.
|
||||||
|
func UnmarshalHex(b []byte) (h []byte, rem []byte, err error) { |
||||||
|
rem = b[:] |
||||||
|
var inQuote bool |
||||||
|
var start int |
||||||
|
for i := 0; i < len(b); i++ { |
||||||
|
if !inQuote { |
||||||
|
if b[i] == '"' { |
||||||
|
inQuote = true |
||||||
|
start = i + 1 |
||||||
|
} |
||||||
|
} else if b[i] == '"' { |
||||||
|
hexStr := b[start:i] |
||||||
|
rem = b[i+1:] |
||||||
|
l := len(hexStr) |
||||||
|
if l%2 != 0 { |
||||||
|
err = errorf.E( |
||||||
|
"invalid length for hex: %d, %0x", |
||||||
|
len(hexStr), hexStr, |
||||||
|
) |
||||||
|
return |
||||||
|
} |
||||||
|
// Allocate a new buffer for the decoded data
|
||||||
|
h = make([]byte, l/2) |
||||||
|
if err = xhex.Decode(h, hexStr); chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
if !inQuote { |
||||||
|
err = io.EOF |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalQuoted performs an in-place unquoting of NIP-01 quoted byte string.
|
||||||
|
func UnmarshalQuoted(b []byte) (content, rem []byte, err error) { |
||||||
|
if len(b) == 0 { |
||||||
|
err = io.EOF |
||||||
|
return |
||||||
|
} |
||||||
|
rem = b[:] |
||||||
|
for ; len(rem) >= 0; rem = rem[1:] { |
||||||
|
// advance to open quotes
|
||||||
|
if rem[0] == '"' { |
||||||
|
rem = rem[1:] |
||||||
|
content = rem |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if len(rem) == 0 { |
||||||
|
err = io.EOF |
||||||
|
return |
||||||
|
} |
||||||
|
var escaping bool |
||||||
|
var contentLen int |
||||||
|
for len(rem) > 0 { |
||||||
|
if rem[0] == '\\' { |
||||||
|
if !escaping { |
||||||
|
escaping = true |
||||||
|
contentLen++ |
||||||
|
rem = rem[1:] |
||||||
|
} else { |
||||||
|
escaping = false |
||||||
|
contentLen++ |
||||||
|
rem = rem[1:] |
||||||
|
} |
||||||
|
} else if rem[0] == '"' { |
||||||
|
if !escaping { |
||||||
|
rem = rem[1:] |
||||||
|
content = content[:contentLen] |
||||||
|
content = NostrUnescape(content) |
||||||
|
return |
||||||
|
} |
||||||
|
contentLen++ |
||||||
|
rem = rem[1:] |
||||||
|
escaping = false |
||||||
|
} else { |
||||||
|
escaping = false |
||||||
|
switch rem[0] { |
||||||
|
// none of these characters are allowed inside a JSON string:
|
||||||
|
//
|
||||||
|
// backspace, tab, newline, form feed or carriage return.
|
||||||
|
case '\b', '\t', '\n', '\f', '\r': |
||||||
|
err = errorf.E( |
||||||
|
"invalid character '%s' in quoted string", |
||||||
|
NostrEscape(nil, rem[:1]), |
||||||
|
) |
||||||
|
return |
||||||
|
} |
||||||
|
contentLen++ |
||||||
|
rem = rem[1:] |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func MarshalHexArray(dst []byte, ha [][]byte) (b []byte) { |
||||||
|
dst = append(dst, '[') |
||||||
|
for i := range ha { |
||||||
|
dst = AppendQuote(dst, ha[i], hex.EncAppend) |
||||||
|
if i != len(ha)-1 { |
||||||
|
dst = append(dst, ',') |
||||||
|
} |
||||||
|
} |
||||||
|
dst = append(dst, ']') |
||||||
|
b = dst |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalHexArray unpacks a JSON array containing strings with hexadecimal, and checks all
|
||||||
|
// values have the specified byte size.
|
||||||
|
func UnmarshalHexArray(b []byte, size int) (t [][]byte, rem []byte, err error) { |
||||||
|
rem = b |
||||||
|
var openBracket bool |
||||||
|
for ; len(rem) > 0; rem = rem[1:] { |
||||||
|
if rem[0] == '[' { |
||||||
|
openBracket = true |
||||||
|
} else if openBracket { |
||||||
|
if rem[0] == ',' { |
||||||
|
continue |
||||||
|
} else if rem[0] == ']' { |
||||||
|
rem = rem[1:] |
||||||
|
return |
||||||
|
} else if rem[0] == '"' { |
||||||
|
var h []byte |
||||||
|
if h, rem, err = UnmarshalHex(rem); chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
if len(h) != size { |
||||||
|
err = errorf.E( |
||||||
|
"invalid hex array size, got %d expect %d", |
||||||
|
2*len(h), 2*size, |
||||||
|
) |
||||||
|
return |
||||||
|
} |
||||||
|
t = append(t, h) |
||||||
|
if rem[0] == ']' { |
||||||
|
rem = rem[1:] |
||||||
|
// done
|
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalStringArray unpacks a JSON array containing strings.
|
||||||
|
func UnmarshalStringArray(b []byte) (t [][]byte, rem []byte, err error) { |
||||||
|
rem = b |
||||||
|
var openBracket bool |
||||||
|
for ; len(rem) > 0; rem = rem[1:] { |
||||||
|
if rem[0] == '[' { |
||||||
|
openBracket = true |
||||||
|
} else if openBracket { |
||||||
|
if rem[0] == ',' { |
||||||
|
continue |
||||||
|
} else if rem[0] == ']' { |
||||||
|
rem = rem[1:] |
||||||
|
return |
||||||
|
} else if rem[0] == '"' { |
||||||
|
var h []byte |
||||||
|
if h, rem, err = UnmarshalQuoted(rem); chk.E(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
t = append(t, h) |
||||||
|
if rem[0] == ']' { |
||||||
|
rem = rem[1:] |
||||||
|
// done
|
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func True() []byte { return []byte("true") } |
||||||
|
func False() []byte { return []byte("false") } |
||||||
|
|
||||||
|
func MarshalBool(src []byte, truth bool) []byte { |
||||||
|
if truth { |
||||||
|
return append(src, True()...) |
||||||
|
} |
||||||
|
return append(src, False()...) |
||||||
|
} |
||||||
|
|
||||||
|
func UnmarshalBool(src []byte) (rem []byte, truth bool, err error) { |
||||||
|
rem = src |
||||||
|
t, f := True(), False() |
||||||
|
for i := range rem { |
||||||
|
if rem[i] == t[0] { |
||||||
|
if len(rem) < i+len(t) { |
||||||
|
err = io.EOF |
||||||
|
return |
||||||
|
} |
||||||
|
if utils.FastEqual(t, rem[i:i+len(t)]) { |
||||||
|
truth = true |
||||||
|
rem = rem[i+len(t):] |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
if rem[i] == f[0] { |
||||||
|
if len(rem) < i+len(f) { |
||||||
|
err = io.EOF |
||||||
|
return |
||||||
|
} |
||||||
|
if utils.FastEqual(f, rem[i:i+len(f)]) { |
||||||
|
rem = rem[i+len(f):] |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
// if a truth value is not found in the string it will run to the end
|
||||||
|
err = io.EOF |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func Comma(b []byte) (rem []byte, err error) { |
||||||
|
rem = b |
||||||
|
for i := range rem { |
||||||
|
if rem[i] == ',' { |
||||||
|
rem = rem[i:] |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
err = io.EOF |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
package text |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lukechampine.com/frand" |
||||||
|
"next.orly.dev/pkg/crypto/sha256" |
||||||
|
"next.orly.dev/pkg/encoders/hex" |
||||||
|
"next.orly.dev/pkg/utils" |
||||||
|
) |
||||||
|
|
||||||
|
func TestUnmarshalHexArray(t *testing.T) { |
||||||
|
var ha [][]byte |
||||||
|
h := make([]byte, sha256.Size) |
||||||
|
frand.Read(h) |
||||||
|
var dst []byte |
||||||
|
for _ = range 20 { |
||||||
|
hh := sha256.Sum256(h) |
||||||
|
h = hh[:] |
||||||
|
ha = append(ha, h) |
||||||
|
} |
||||||
|
dst = append(dst, '[') |
||||||
|
for i := range ha { |
||||||
|
dst = AppendQuote(dst, ha[i], hex.EncAppend) |
||||||
|
if i != len(ha)-1 { |
||||||
|
dst = append(dst, ',') |
||||||
|
} |
||||||
|
} |
||||||
|
dst = append(dst, ']') |
||||||
|
var ha2 [][]byte |
||||||
|
var rem []byte |
||||||
|
var err error |
||||||
|
if ha2, rem, err = UnmarshalHexArray(dst, sha256.Size); chk.E(err) { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
if len(ha2) != len(ha) { |
||||||
|
t.Fatalf( |
||||||
|
"failed to unmarshal, got %d fields, expected %d", len(ha2), |
||||||
|
len(ha), |
||||||
|
) |
||||||
|
} |
||||||
|
if len(rem) > 0 { |
||||||
|
t.Fatalf("failed to unmarshal, remnant afterwards '%s'", rem) |
||||||
|
} |
||||||
|
for i := range ha2 { |
||||||
|
if !utils.FastEqual(ha[i], ha2[i]) { |
||||||
|
t.Fatalf( |
||||||
|
"failed to unmarshal at element %d; got %x, expected %x", |
||||||
|
i, ha[i], ha2[i], |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
package text |
||||||
|
|
||||||
|
// AppendBytesClosure is a function type for appending data from a source to a destination and
|
||||||
|
// returning the appended-to slice.
|
||||||
|
type AppendBytesClosure func(dst, src []byte) []byte |
||||||
|
|
||||||
|
// AppendClosure is a simple append where the caller appends to the destination and returns the
|
||||||
|
// appended-to slice.
|
||||||
|
type AppendClosure func(dst []byte) []byte |
||||||
|
|
||||||
|
// Unquote removes the quotes around a slice of bytes.
|
||||||
|
func Unquote(b []byte) []byte { return b[1 : len(b)-1] } |
||||||
|
|
||||||
|
// Noop simply appends the source to the destination slice and returns it.
|
||||||
|
func Noop(dst, src []byte) []byte { return append(dst, src...) } |
||||||
|
|
||||||
|
// AppendQuote appends a source of bytes, that have been processed by an AppendBytesClosure and
|
||||||
|
// returns the appended-to slice.
|
||||||
|
func AppendQuote(dst, src []byte, ac AppendBytesClosure) []byte { |
||||||
|
dst = append(dst, '"') |
||||||
|
dst = ac(dst, src) |
||||||
|
dst = append(dst, '"') |
||||||
|
return dst |
||||||
|
} |
||||||
|
|
||||||
|
// Quote simply quotes a provided source and attaches it to the provided destination slice.
|
||||||
|
func Quote(dst, src []byte) []byte { return AppendQuote(dst, src, Noop) } |
||||||
|
|
||||||
|
// AppendSingleQuote appends a provided AppendBytesClosure's output from a given source of
|
||||||
|
// bytes, wrapped in single quotes ”.
|
||||||
|
func AppendSingleQuote(dst, src []byte, ac AppendBytesClosure) []byte { |
||||||
|
dst = append(dst, '\'') |
||||||
|
dst = ac(dst, src) |
||||||
|
dst = append(dst, '\'') |
||||||
|
return dst |
||||||
|
} |
||||||
|
|
||||||
|
// AppendBackticks appends a provided AppendBytesClosure's output from a given source of
|
||||||
|
// bytes, wrapped in backticks “.
|
||||||
|
func AppendBackticks(dst, src []byte, ac AppendBytesClosure) []byte { |
||||||
|
dst = append(dst, '`') |
||||||
|
dst = ac(dst, src) |
||||||
|
dst = append(dst, '`') |
||||||
|
return dst |
||||||
|
} |
||||||
|
|
||||||
|
// AppendBrace appends a provided AppendBytesClosure's output from a given source of
|
||||||
|
// bytes, wrapped in braces ().
|
||||||
|
func AppendBrace(dst, src []byte, ac AppendBytesClosure) []byte { |
||||||
|
dst = append(dst, '(') |
||||||
|
dst = ac(dst, src) |
||||||
|
dst = append(dst, ')') |
||||||
|
return dst |
||||||
|
} |
||||||
|
|
||||||
|
// AppendParenthesis appends a provided AppendBytesClosure's output from a given source of
|
||||||
|
// bytes, wrapped in parentheses {}.
|
||||||
|
func AppendParenthesis(dst, src []byte, ac AppendBytesClosure) []byte { |
||||||
|
dst = append(dst, '{') |
||||||
|
dst = ac(dst, src) |
||||||
|
dst = append(dst, '}') |
||||||
|
return dst |
||||||
|
} |
||||||
|
|
||||||
|
// AppendBracket appends a provided AppendBytesClosure's output from a given source of
|
||||||
|
// bytes, wrapped in brackets [].
|
||||||
|
func AppendBracket(dst, src []byte, ac AppendBytesClosure) []byte { |
||||||
|
dst = append(dst, '[') |
||||||
|
dst = ac(dst, src) |
||||||
|
dst = append(dst, ']') |
||||||
|
return dst |
||||||
|
} |
||||||
|
|
||||||
|
// AppendList appends an input source bytes processed by an AppendBytesClosure and separates
|
||||||
|
// elements with the given separator byte.
|
||||||
|
func AppendList( |
||||||
|
dst []byte, src [][]byte, separator byte, |
||||||
|
ac AppendBytesClosure, |
||||||
|
) []byte { |
||||||
|
last := len(src) - 1 |
||||||
|
for i := range src { |
||||||
|
dst = append(dst, ac(dst, src[i])...) |
||||||
|
if i < last { |
||||||
|
dst = append(dst, separator) |
||||||
|
} |
||||||
|
} |
||||||
|
return dst |
||||||
|
} |
||||||
@ -0,0 +1,78 @@ |
|||||||
|
package bufpool |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"sync" |
||||||
|
"unsafe" |
||||||
|
|
||||||
|
"lol.mleku.dev/log" |
||||||
|
"next.orly.dev/pkg/utils/units" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// BufferSize is the size of each buffer in the pool (1kb)
|
||||||
|
BufferSize = units.Kb / 2 |
||||||
|
) |
||||||
|
|
||||||
|
type B []byte |
||||||
|
|
||||||
|
func (b B) ToBytes() []byte { return b } |
||||||
|
|
||||||
|
var Pool = sync.Pool{ |
||||||
|
New: func() interface{} { |
||||||
|
// Create a new buffer when the pool is empty
|
||||||
|
b := make([]byte, 0, BufferSize) |
||||||
|
log.T.C( |
||||||
|
func() string { |
||||||
|
ptr := unsafe.SliceData(b) |
||||||
|
return fmt.Sprintf("creating buffer at: %p", ptr) |
||||||
|
}, |
||||||
|
) |
||||||
|
return B(b) |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
// Get returns a buffer from the pool or creates a new one if the pool is empty.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// buf := bufpool.Get()
|
||||||
|
// defer bufpool.Put(buf)
|
||||||
|
// // Use buf...
|
||||||
|
func Get() B { |
||||||
|
b := Pool.Get().(B) |
||||||
|
log.T.C( |
||||||
|
func() string { |
||||||
|
ptr := unsafe.SliceData(b) |
||||||
|
return fmt.Sprintf("getting buffer at: %p", ptr) |
||||||
|
}, |
||||||
|
) |
||||||
|
return b |
||||||
|
} |
||||||
|
|
||||||
|
// Put returns a buffer to the pool.
|
||||||
|
// Buffers should be returned to the pool when no longer needed to allow reuse.
|
||||||
|
func Put(b B) { |
||||||
|
for i := range b { |
||||||
|
(b)[i] = 0 |
||||||
|
} |
||||||
|
b = b[:0] |
||||||
|
log.T.C( |
||||||
|
func() string { |
||||||
|
ptr := unsafe.SliceData(b) |
||||||
|
return fmt.Sprintf("returning to buffer: %p", ptr) |
||||||
|
}, |
||||||
|
) |
||||||
|
Pool.Put(b) |
||||||
|
} |
||||||
|
|
||||||
|
// PutBytes returns a buffer was not necessarily created by Get().
|
||||||
|
func PutBytes(b []byte) { |
||||||
|
log.T.C( |
||||||
|
func() string { |
||||||
|
ptr := unsafe.SliceData(b) |
||||||
|
return fmt.Sprintf("returning bytes to buffer: %p", ptr) |
||||||
|
}, |
||||||
|
) |
||||||
|
Put(b) |
||||||
|
} |
||||||
@ -0,0 +1,71 @@ |
|||||||
|
package bufpool |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
func TestBufferPoolGetPut(t *testing.T) { |
||||||
|
// Get a buffer from the pool
|
||||||
|
buf1 := Get() |
||||||
|
|
||||||
|
// Verify the buffer is the correct size
|
||||||
|
if len(*buf1) != BufferSize { |
||||||
|
t.Errorf("Expected buffer size of %d, got %d", BufferSize, len(*buf1)) |
||||||
|
} |
||||||
|
|
||||||
|
// Write some data to the buffer
|
||||||
|
(*buf1)[0] = 42 |
||||||
|
|
||||||
|
// Return the buffer to the pool
|
||||||
|
Put(buf1) |
||||||
|
|
||||||
|
// Get another buffer, which should be the same one we just returned
|
||||||
|
buf2 := Get() |
||||||
|
|
||||||
|
// Buffer may or may not be cleared, but we should be able to use it
|
||||||
|
// Let's check if we have the expected buffer size
|
||||||
|
if len(*buf2) != BufferSize { |
||||||
|
t.Errorf("Expected buffer size of %d, got %d", BufferSize, len(*buf2)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestMultipleBuffers(t *testing.T) { |
||||||
|
// Get multiple buffers at once to ensure the pool can handle it
|
||||||
|
const numBuffers = 10 |
||||||
|
buffers := make([]B, numBuffers) |
||||||
|
|
||||||
|
// Get buffers from the pool
|
||||||
|
for i := 0; i < numBuffers; i++ { |
||||||
|
buffers[i] = Get() |
||||||
|
// Verify each buffer is the correct size
|
||||||
|
if len(*buffers[i]) != BufferSize { |
||||||
|
t.Errorf( |
||||||
|
"Buffer %d: Expected size of %d, got %d", i, BufferSize, |
||||||
|
len(*buffers[i]), |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Return all buffers to the pool
|
||||||
|
for i := 0; i < numBuffers; i++ { |
||||||
|
Put(buffers[i]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkGetPut(b *testing.B) { |
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
buf := Get() |
||||||
|
Put(buf) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkGetPutParallel(b *testing.B) { |
||||||
|
b.RunParallel( |
||||||
|
func(pb *testing.PB) { |
||||||
|
for pb.Next() { |
||||||
|
buf := Get() |
||||||
|
Put(buf) |
||||||
|
} |
||||||
|
}, |
||||||
|
) |
||||||
|
} |
||||||
Loading…
Reference in new issue