Add core packages, configuration system, and initial application structure

This commit is contained in:
2025-08-21 11:04:03 +01:00
parent b8db587d7b
commit ecaf52b98f
18 changed files with 12889 additions and 0 deletions

270
app/config/config.go Normal file
View File

@@ -0,0 +1,270 @@
// Package config provides a go-simpler.org/env configuration table and helpers
// for working with the list of key/value lists stored in .env files.
package config
import (
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
"github.com/adrg/xdg"
"go-simpler.org/env"
lol "lol.mleku.dev"
"lol.mleku.dev/chk"
"next.orly.dev/pkg/version"
)
// C holds application configuration settings loaded from environment variables
// and default values. It defines parameters for app behaviour, storage
// locations, logging, and network settings used across the relay service.
type C struct {
AppName string `env:"ORLY_APP_NAME" usage:"set a name to display on information about the relay" default:"ORLY"`
DataDir string `env:"ORLY_DATA_DIR" usage:"storage location for the event store" default:"~/.local/share/ORLY"`
Listen string `env:"ORLY_LISTEN" default:"0.0.0.0" usage:"network listen address"`
Port int `env:"ORLY_PORT" default:"3334" usage:"port to listen on"`
LogLevel string `env:"ORLY_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
Pprof string `env:"ORLY_PPROF" usage:"enable pprof in modes: cpu,memory,allocation"`
}
// New creates and initializes a new configuration object for the relay
// application
//
// # Return Values
//
// - cfg: A pointer to the initialized configuration struct containing default
// or environment-provided values
//
// - err: An error object that is non-nil if any operation during
// initialization fails
//
// # Expected Behaviour:
//
// Initializes a new configuration instance by loading environment variables and
// checking for a .env file in the default configuration directory. Sets logging
// levels based on configuration values and returns the populated configuration
// or an error if any step fails
func New() (cfg *C, err error) {
cfg = &C{}
if err = env.Load(cfg, &env.Options{SliceSep: ","}); chk.T(err) {
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s\n\n", err)
}
PrintHelp(cfg, os.Stderr)
os.Exit(0)
}
if cfg.DataDir == "" || strings.Contains(cfg.DataDir, "~") {
cfg.DataDir = filepath.Join(xdg.DataHome, cfg.AppName)
}
if GetEnv() {
PrintEnv(cfg, os.Stdout)
os.Exit(0)
}
if HelpRequested() {
PrintHelp(cfg, os.Stderr)
os.Exit(0)
}
lol.SetLogLevel(cfg.LogLevel)
return
}
// HelpRequested determines if the command line arguments indicate a request for help
//
// # Return Values
//
// - help: A boolean value indicating true if a help flag was detected in the
// command line arguments, false otherwise
//
// # Expected Behaviour
//
// The function checks the first command line argument for common help flags and
// returns true if any of them are present. Returns false if no help flag is found
func HelpRequested() (help bool) {
if len(os.Args) > 1 {
switch strings.ToLower(os.Args[1]) {
case "help", "-h", "--h", "-help", "--help", "?":
help = true
}
}
return
}
// GetEnv checks if the first command line argument is "env" and returns
// whether the environment configuration should be printed.
//
// # Return Values
//
// - requested: A boolean indicating true if the 'env' argument was
// provided, false otherwise.
//
// # Expected Behaviour
//
// The function returns true when the first command line argument is "env"
// (case-insensitive), signalling that the environment configuration should be
// printed. Otherwise, it returns false.
func GetEnv() (requested bool) {
if len(os.Args) > 1 {
switch strings.ToLower(os.Args[1]) {
case "env":
requested = true
}
}
return
}
// KV is a key/value pair.
type KV struct{ Key, Value string }
// KVSlice is a sortable slice of key/value pairs, designed for managing
// configuration data and enabling operations like merging and sorting based on
// keys.
type KVSlice []KV
func (kv KVSlice) Len() int { return len(kv) }
func (kv KVSlice) Less(i, j int) bool { return kv[i].Key < kv[j].Key }
func (kv KVSlice) Swap(i, j int) { kv[i], kv[j] = kv[j], kv[i] }
// Compose merges two KVSlice instances into a new slice where key-value pairs
// from the second slice override any duplicate keys from the first slice.
//
// # Parameters
//
// - kv2: The second KVSlice whose entries will be merged with the receiver.
//
// # Return Values
//
// - out: A new KVSlice containing all entries from both slices, with keys
// from kv2 taking precedence over keys from the receiver.
//
// # Expected Behaviour
//
// The method returns a new KVSlice that combines the contents of the receiver
// and kv2. If any key exists in both slices, the value from kv2 is used. The
// resulting slice remains sorted by keys as per the KVSlice implementation.
func (kv KVSlice) Compose(kv2 KVSlice) (out KVSlice) {
// duplicate the initial KVSlice
for _, p := range kv {
out = append(out, p)
}
out:
for i, p := range kv2 {
for j, q := range out {
// if the key is repeated, replace the value
if p.Key == q.Key {
out[j].Value = kv2[i].Value
continue out
}
}
out = append(out, p)
}
return
}
// EnvKV generates key/value pairs from a configuration object's struct tags
//
// # Parameters
//
// - cfg: A configuration object whose struct fields are processed for env tags
//
// # Return Values
//
// - m: A KVSlice containing key/value pairs derived from the config's env tags
//
// # Expected Behaviour
//
// Processes each field of the config object, extracting values tagged with
// "env" and converting them to strings. Skips fields without an "env" tag.
// Handles various value types including strings, integers, booleans, durations,
// and string slices by joining elements with commas.
func EnvKV(cfg any) (m KVSlice) {
t := reflect.TypeOf(cfg)
for i := 0; i < t.NumField(); i++ {
k := t.Field(i).Tag.Get("env")
v := reflect.ValueOf(cfg).Field(i).Interface()
var val string
switch v.(type) {
case string:
val = v.(string)
case int, bool, time.Duration:
val = fmt.Sprint(v)
case []string:
arr := v.([]string)
if len(arr) > 0 {
val = strings.Join(arr, ",")
}
}
// this can happen with embedded structs
if k == "" {
continue
}
m = append(m, KV{k, val})
}
return
}
// PrintEnv outputs sorted environment key/value pairs from a configuration object
// to the provided writer
//
// # Parameters
//
// - cfg: Pointer to the configuration object containing env tags
//
// - printer: Destination for the output, typically an io.Writer implementation
//
// # Expected Behaviour
//
// Outputs each environment variable derived from the config's struct tags in
// sorted order, formatted as "key=value\n" to the specified writer
func PrintEnv(cfg *C, printer io.Writer) {
kvs := EnvKV(*cfg)
sort.Sort(kvs)
for _, v := range kvs {
_, _ = fmt.Fprintf(printer, "%s=%s\n", v.Key, v.Value)
}
}
// PrintHelp prints help information including application version, environment
// variable configuration, and details about .env file handling to the provided
// writer
//
// # Parameters
//
// - cfg: Configuration object containing app name and config directory path
//
// - printer: Output destination for the help text
//
// # Expected Behaviour
//
// Prints application name and version followed by environment variable
// configuration details, explains .env file behaviour including automatic
// loading and custom path options, and displays current configuration values
// using PrintEnv. Outputs all information to the specified writer
func PrintHelp(cfg *C, printer io.Writer) {
_, _ = fmt.Fprintf(
printer,
"%s %s\n\n", cfg.AppName, version.V,
)
_, _ = fmt.Fprintf(
printer,
`Usage: %s [env|help]
- env: print environment variables configuring %s
- help: print this help text
`,
cfg.AppName, cfg.AppName,
)
_, _ = fmt.Fprintf(
printer,
"Environment variables that configure %s:\n\n", cfg.AppName,
)
env.Usage(cfg, printer, &env.Options{SliceSep: ","})
fmt.Fprintf(printer, "\ncurrent configuration:\n\n")
PrintEnv(cfg, printer)
fmt.Fprintln(printer)
return
}

23
app/main.go Normal file
View File

@@ -0,0 +1,23 @@
package app
import (
"context"
"lol.mleku.dev/log"
"next.orly.dev/app/config"
)
func Run(ctx context.Context, cfg *config.C) (quit chan struct{}) {
// shutdown handler
go func() {
select {
case <-ctx.Done():
log.I.F("shutting down")
close(quit)
}
}()
// start listener
quit = make(chan struct{})
return
}

7
go.mod
View File

@@ -3,15 +3,22 @@ module next.orly.dev
go 1.25.0
require (
github.com/adrg/xdg v0.5.3
github.com/davecgh/go-spew v1.1.1
github.com/klauspost/cpuid/v2 v2.3.0
github.com/pkg/profile v1.7.0
github.com/stretchr/testify v1.10.0
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b
go-simpler.org/env v0.12.0
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
lol.mleku.dev v1.0.2
lukechampine.com/frand v1.5.1
)
require (
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/fgprof v0.9.3 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect

25
go.sum
View File

@@ -1,27 +1,52 @@
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY=
github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b h1:XeDLE6c9mzHpdv3Wb1+pWBaWv/BlHK0ZYIu/KaL6eHg=
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b/go.mod h1:7rwmCH0wC2fQvNEvPZ3sKXukhyCTyiaZ5VTZMQYpZKQ=
go-simpler.org/env v0.12.0 h1:kt/lBts0J1kjWJAnB740goNdvwNxt5emhYngL0Fzufs=
go-simpler.org/env v0.12.0/go.mod h1:cc/5Md9JCUM7LVLtN0HYjPTDcI3Q8TDaPlNTAlDU+WI=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lol.mleku.dev v1.0.2 h1:bSV1hHnkmt1hq+9nSvRwN6wgcI7itbM3XRZ4dMB438c=
lol.mleku.dev v1.0.2/go.mod h1:DQ0WnmkntA9dPLCXgvtIgYt5G0HSqx3wSTLolHgWeLA=
lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w=
lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q=

38
main.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/app"
"next.orly.dev/app/config"
"next.orly.dev/pkg/version"
)
func main() {
var err error
var cfg *config.C
if cfg, err = config.New(); chk.T(err) {
}
log.I.F("starting %s %s", cfg.AppName, version.V)
startProfiler(cfg.Pprof)
ctx, cancel := context.WithCancel(context.Background())
quit := app.Run(ctx, cfg)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt)
for {
select {
case <-sigs:
fmt.Printf("\r")
cancel()
case <-quit:
cancel()
return
}
}
}

View File

@@ -0,0 +1,47 @@
package event
// E is the primary datatype of nostr. This is the form of the structure that
// defines its JSON string-based format.
type E struct {
// ID is the SHA256 hash of the canonical encoding of the event in binary format
ID []byte
// Pubkey is the public key of the event creator in binary format
Pubkey []byte
// CreatedAt is the UNIX timestamp of the event according to the event
// creator (never trust a timestamp!)
CreatedAt int64
// Kind is the nostr protocol code for the type of event. See kind.T
Kind uint16
// Tags are a list of tags, which are a list of strings usually structured
// as a 3-layer scheme indicating specific features of an event.
Tags [][]byte
// Content is an arbitrary string that can contain anything, but usually
// conforming to a specification relating to the Kind and the Tags.
Content []byte
// Sig is the signature on the ID hash that validates as coming from the
// Pubkey in binary format.
Sig []byte
}
// S is an array of event.E that sorts in reverse chronological order.
type S []*E
// Len returns the length of the event.Es.
func (ev S) Len() int { return len(ev) }
// Less returns whether the first is newer than the second (larger unix
// timestamp).
func (ev S) Less(i, j int) bool { return ev[i].CreatedAt > ev[j].CreatedAt }
// Swap two indexes of the event.Es with each other.
func (ev S) Swap(i, j int) { ev[i], ev[j] = ev[j], ev[i] }
// C is a channel that carries event.E.
type C chan *E

View File

@@ -0,0 +1,10 @@
// Package examples is an embedded jsonl format of a collection of events
// intended to be used to test an event codec.
package examples
import (
_ "embed"
)
//go:embed out.jsonl
var Cache []byte

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
// Package main is a generator for the base10000 (4 digit) encoding of the ints
// library.
package main
import (
"fmt"
"os"
"lol.mleku.dev/chk"
)
func main() {
fh, err := os.Create("pkg/ints/base10k.txt")
if chk.E(err) {
panic(err)
}
for i := range 10000 {
fmt.Fprintf(fh, "%04d", i)
}
}

135
pkg/encoders/ints/ints.go Normal file
View File

@@ -0,0 +1,135 @@
// Package ints is an optimised encoder for decimal numbers in ASCII format,
// that simplifies and accelerates encoding and decoding decimal strings. It is
// faster than strconv in part because it uses a base of 10000 and a lookup
// table.
package ints
import (
_ "embed"
"io"
"golang.org/x/exp/constraints"
"lol.mleku.dev/errorf"
)
// run this to regenerate (pointlessly) the base 10 array of 4 places per entry
//go:generate go run ./gen/.
//go:embed base10k.txt
var base10k []byte
const base = 10000
// T is an integer with a fast codec to decimal ASCII.
type T struct {
N uint64
}
func New[V constraints.Integer](n V) *T {
return &T{uint64(n)}
}
// Uint64 returns the int.T as a uint64 (the base type)
func (n *T) Uint64() uint64 { return n.N }
// Int64 returns an int64 from the base number (may cause truncation)
func (n *T) Int64() int64 { return int64(n.N) }
// Uint16 returns an uint16 from the base number (may cause truncation)
func (n *T) Uint16() uint16 { return uint16(n.N) }
var powers = []*T{
{1},
{1_0000},
{1_0000_0000},
{1_0000_0000_0000},
{1_0000_0000_0000_0000},
}
const zero = '0'
const nine = '9'
// Marshal the int.T into a byte string.
func (n *T) Marshal(dst []byte) (b []byte) {
nn := n.N
b = dst
if n.N == 0 {
b = append(b, '0')
return
}
var i int
var trimmed bool
k := len(powers)
for k > 0 {
k--
q := n.N / powers[k].N
if !trimmed && q == 0 {
continue
}
offset := q * 4
bb := base10k[offset : offset+4]
if !trimmed {
for i = range bb {
if bb[i] != '0' {
bb = bb[i:]
trimmed = true
break
}
}
}
b = append(b, bb...)
n.N = n.N - q*powers[k].N
}
n.N = nn
return
}
// Unmarshal reads a string, which must be a positive integer no larger than math.MaxUint64,
// skipping any non-numeric content before it.
//
// Note that leading zeros are not considered valid, but basically no such thing as machine
// generated JSON integers with leading zeroes. Until this is disproven, this is the fastest way
// to read a positive json integer, and a leading zero is decoded as a zero, and the remainder
// returned.
func (n *T) Unmarshal(b []byte) (r []byte, err error) {
if len(b) < 1 {
err = errorf.E("zero length number")
return
}
var sLen int
if b[0] == zero {
r = b[1:]
n.N = 0
return
}
// skip non-number characters
for i, v := range b {
if v >= '0' && v <= '9' {
b = b[i:]
break
}
}
if len(b) == 0 {
err = io.EOF
return
}
// count the digits
for ; sLen < len(b) && b[sLen] >= zero && b[sLen] <= nine && b[sLen] != ','; sLen++ {
}
if sLen == 0 {
err = errorf.E("zero length number")
return
}
if sLen > 20 {
err = errorf.E("too big number for uint64")
return
}
// the length of the string found
r = b[sLen:]
b = b[:sLen]
for _, ch := range b {
ch -= zero
n.N = n.N*10 + uint64(ch)
}
return
}

View File

@@ -0,0 +1,87 @@
package ints
import (
"math"
"strconv"
"testing"
"lol.mleku.dev/chk"
"lukechampine.com/frand"
)
func TestMarshalUnmarshal(t *testing.T) {
b := make([]byte, 0, 8)
var rem []byte
var n *T
var err error
for _ = range 10000000 {
n = New(uint64(frand.Intn(math.MaxInt64)))
b = n.Marshal(b)
m := New(0)
if rem, err = m.Unmarshal(b); chk.E(err) {
t.Fatal(err)
}
if n.N != m.N {
t.Fatalf("failed to convert to int64 at %d %s %d", n.N, b, m.N)
}
if len(rem) > 0 {
t.Fatalf("leftover bytes after converting back: '%s'", rem)
}
b = b[:0]
}
}
func BenchmarkByteStringToInt64(bb *testing.B) {
b := make([]byte, 0, 19)
var i int
const nTests = 10000000
testInts := make([]*T, nTests)
for i = range nTests {
testInts[i] = New(frand.Intn(math.MaxInt64))
}
bb.Run(
"Marshal", func(bb *testing.B) {
bb.ReportAllocs()
for i = 0; i < bb.N; i++ {
n := testInts[i%10000]
b = n.Marshal(b)
b = b[:0]
}
},
)
bb.Run(
"Itoa", func(bb *testing.B) {
bb.ReportAllocs()
var s string
for i = 0; i < bb.N; i++ {
n := testInts[i%10000]
s = strconv.Itoa(int(n.N))
_ = s
}
},
)
bb.Run(
"MarshalUnmarshal", func(bb *testing.B) {
bb.ReportAllocs()
m := New(0)
for i = 0; i < bb.N; i++ {
n := testInts[i%10000]
b = m.Marshal(b)
_, _ = n.Unmarshal(b)
b = b[:0]
}
},
)
bb.Run(
"ItoaAtoi", func(bb *testing.B) {
bb.ReportAllocs()
var s string
for i = 0; i < bb.N; i++ {
n := testInts[i%10000]
s = strconv.Itoa(int(n.N))
_, _ = strconv.Atoi(s)
}
},
)
}

127
pkg/encoders/text/escape.go Normal file
View File

@@ -0,0 +1,127 @@
package text
// NostrEscape for JSON encoding according to RFC8259.
//
// This is the efficient implementation based on the NIP-01 specification:
//
// To prevent implementation differences from creating a different event ID for
// the same event, the following rules MUST be followed while serializing:
//
// No whitespace, line breaks or other unnecessary formatting should be included
// in the output JSON. No characters except the following should be escaped, and
// instead should be included verbatim:
//
// - A line break, 0x0A, as \n
// - A double quote, 0x22, as \"
// - A backslash, 0x5C, as \\
// - A carriage return, 0x0D, as \r
// - A tab character, 0x09, as \t
// - A backspace, 0x08, as \b
// - A form feed, 0x0C, as \f
//
// UTF-8 should be used for encoding.
func NostrEscape(dst, src []byte) []byte {
l := len(src)
for i := 0; i < l; i++ {
c := src[i]
switch {
case c == '"':
dst = append(dst, '\\', '"')
case c == '\\':
// if i+1 < l && src[i+1] == 'u' || i+1 < l && src[i+1] == '/' {
if i+1 < l && src[i+1] == 'u' {
dst = append(dst, '\\')
} else {
dst = append(dst, '\\', '\\')
}
case c == '\b':
dst = append(dst, '\\', 'b')
case c == '\t':
dst = append(dst, '\\', 't')
case c == '\n':
dst = append(dst, '\\', 'n')
case c == '\f':
dst = append(dst, '\\', 'f')
case c == '\r':
dst = append(dst, '\\', 'r')
default:
dst = append(dst, c)
}
}
return dst
}
// NostrUnescape reverses the operation of NostrEscape except instead of
// appending it to the provided slice, it rewrites it, eliminating a memory
// copy. Keep in mind that the original JSON will be mangled by this operation,
// but the resultant slices will cost zero allocations.
func NostrUnescape(dst []byte) (b []byte) {
var r, w int
for ; r < len(dst); r++ {
if dst[r] == '\\' {
r++
c := dst[r]
switch {
// nip-01 specifies the following single letter C-style escapes for control
// codes under 0x20.
//
// no others are specified but must be preserved, so only these can be
// safely decoded at runtime as they must be re-encoded when marshalled.
case c == '"':
dst[w] = '"'
w++
case c == '\\':
dst[w] = '\\'
w++
case c == 'b':
dst[w] = '\b'
w++
case c == 't':
dst[w] = '\t'
w++
case c == 'n':
dst[w] = '\n'
w++
case c == 'f':
dst[w] = '\f'
w++
case c == 'r':
dst[w] = '\r'
w++
// special cases for non-nip-01 specified json escapes (must be preserved for ID
// generation).
case c == 'u':
dst[w] = '\\'
w++
dst[w] = 'u'
w++
case c == '/':
dst[w] = '\\'
w++
dst[w] = '/'
w++
// special case for octal escapes (must be preserved for ID generation).
case c >= '0' && c <= '9':
dst[w] = '\\'
w++
dst[w] = c
w++
// anything else after a reverse solidus just preserve it.
default:
dst[w] = dst[r]
w++
dst[w] = c
w++
}
} else {
dst[w] = dst[r]
w++
}
}
b = dst[:w]
return
}

View File

@@ -0,0 +1,459 @@
package text
import (
"testing"
"lol.mleku.dev/chk"
"next.orly.dev/pkg/crypto/sha256"
"lukechampine.com/frand"
)
func TestUnescapeByteString(t *testing.T) {
b := make([]byte, 256)
for i := range b {
b[i] = byte(i)
}
escaped := NostrEscape(nil, b)
unescaped := NostrUnescape(escaped)
if string(b) != string(unescaped) {
t.Log(b)
t.Log(unescaped)
t.FailNow()
}
}
func GenRandString(l int, src *frand.RNG) (str []byte) {
return src.Bytes(l)
}
var seed = sha256.Sum256(
[]byte(`
The tao that can be told
is not the eternal Tao
The name that can be named
is not the eternal Name
The unnamable is the eternally real
Naming is the origin of all particular things
Free from desire, you realize the mystery
Caught in desire, you see only the manifestations
Yet mystery and manifestations arise from the same source
This source is called darkness
Darkness within darkness
The gateway to all understanding
`),
)
var src = frand.NewCustom(seed[:], 32, 12)
func TestRandomEscapeByteString(t *testing.T) {
// this is a kind of fuzz test, does a massive number of iterations of
// random content that ensures the escaping is correct without creating a
// fixed set of test vectors.
for i := 0; i < 1000; i++ {
l := src.Intn(1<<8) + 32
s1 := GenRandString(l, src)
s2 := make([]byte, l)
orig := make([]byte, l)
copy(s2, s1)
copy(orig, s1)
// first we are checking our implementation comports to the one from go-nostr.
escapeStringVersion := NostrEscape([]byte{}, s1)
escapeJSONStringAndWrapVersion := NostrEscape(nil, s2)
if len(escapeJSONStringAndWrapVersion) != len(escapeStringVersion) {
t.Logf(
"escapeString\nlength: %d\n%s\n%v\n",
len(escapeStringVersion), string(escapeStringVersion),
escapeStringVersion,
)
t.Logf(
"escapJSONStringAndWrap\nlength: %d\n%s\n%v\n",
len(escapeJSONStringAndWrapVersion),
escapeJSONStringAndWrapVersion,
escapeJSONStringAndWrapVersion,
)
t.FailNow()
}
for i := range escapeStringVersion {
if i > len(escapeJSONStringAndWrapVersion) {
t.Fatal("escapeString version is shorter")
}
if escapeStringVersion[i] != escapeJSONStringAndWrapVersion[i] {
t.Logf(
"escapeString version differs at index %d from "+
"escapeJSONStringAndWrap version\n%s\n%s\n%v\n%v", i,
escapeStringVersion[i-4:],
escapeJSONStringAndWrapVersion[i-4:],
escapeStringVersion[i-4:],
escapeJSONStringAndWrapVersion[i-4:],
)
t.Logf(
"escapeString\nlength: %d %s\n",
len(escapeStringVersion), escapeStringVersion,
)
t.Logf(
"escapJSONStringAndWrap\nlength: %d %s\n",
len(escapeJSONStringAndWrapVersion),
escapeJSONStringAndWrapVersion,
)
t.Logf(
"got '%s' %d expected '%s' %d\n",
string(escapeJSONStringAndWrapVersion[i]),
escapeJSONStringAndWrapVersion[i],
string(escapeStringVersion[i]),
escapeStringVersion[i],
)
t.FailNow()
}
}
// next, unescape the output and see if it matches the original
unescaped := NostrUnescape(escapeJSONStringAndWrapVersion)
// t.Logf("unescaped: \n%s\noriginal: \n%s", unescaped, orig)
if string(unescaped) != string(orig) {
t.Fatalf(
"\ngot %d %v\nexpected %d %v\n",
len(unescaped),
unescaped,
len(orig),
orig,
)
}
}
}
func BenchmarkNostrEscapeNostrUnescape(b *testing.B) {
const size = 65536
b.Run(
"frand64k", func(b *testing.B) {
b.ReportAllocs()
in := make([]byte, size)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
}
},
)
b.Run(
"NostrEscape64k", func(b *testing.B) {
b.ReportAllocs()
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
out = out[:0]
}
},
)
b.Run(
"NostrEscapeNostrUnescape64k", func(b *testing.B) {
b.ReportAllocs()
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
in = in[:0]
out = NostrUnescape(out)
out = out[:0]
}
},
)
b.Run(
"frand32k", func(b *testing.B) {
b.ReportAllocs()
size := size / 2
in := make([]byte, size)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
}
},
)
b.Run(
"NostrEscape32k", func(b *testing.B) {
b.ReportAllocs()
size := size / 2
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
out = out[:0]
}
},
)
b.Run(
"NostrEscapeNostrUnescape32k", func(b *testing.B) {
b.ReportAllocs()
size := size / 2
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
in = in[:0]
out = NostrUnescape(out)
out = out[:0]
}
},
)
b.Run(
"frand16k", func(b *testing.B) {
b.ReportAllocs()
size := size / 4
in := make([]byte, size)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
}
},
)
b.Run(
"NostrEscape16k", func(b *testing.B) {
b.ReportAllocs()
size := size / 4
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
out = out[:0]
}
},
)
b.Run(
"NostrEscapeNostrUnescape16k", func(b *testing.B) {
b.ReportAllocs()
size := size / 4
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
in = in[:0]
out = NostrUnescape(out)
out = out[:0]
}
},
)
b.Run(
"frand8k", func(b *testing.B) {
b.ReportAllocs()
size := size / 8
in := make([]byte, size)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
}
},
)
b.Run(
"NostrEscape8k", func(b *testing.B) {
b.ReportAllocs()
size := size / 8
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
out = out[:0]
}
},
)
b.Run(
"NostrEscapeNostrUnescape8k", func(b *testing.B) {
b.ReportAllocs()
size := size / 8
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
in = in[:0]
out = NostrUnescape(out)
out = out[:0]
}
},
)
b.Run(
"frand4k", func(b *testing.B) {
b.ReportAllocs()
size := size / 16
in := make([]byte, size)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
}
},
)
b.Run(
"NostrEscape4k", func(b *testing.B) {
b.ReportAllocs()
size := size / 16
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
out = out[:0]
}
},
)
b.Run(
"NostrEscapeNostrUnescape4k", func(b *testing.B) {
b.ReportAllocs()
size := size / 16
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
in = in[:0]
out = NostrUnescape(out)
out = out[:0]
}
},
)
b.Run(
"frand2k", func(b *testing.B) {
b.ReportAllocs()
size := size / 32
in := make([]byte, size)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
}
},
)
b.Run(
"NostrEscape2k", func(b *testing.B) {
b.ReportAllocs()
size := size / 32
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
out = out[:0]
}
},
)
b.Run(
"NostrEscapeNostrUnescape2k", func(b *testing.B) {
b.ReportAllocs()
size := size / 32
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
in = in[:0]
out = NostrUnescape(out)
out = out[:0]
}
},
)
b.Run(
"frand1k", func(b *testing.B) {
b.ReportAllocs()
size := size / 64
in := make([]byte, size)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
}
},
)
b.Run(
"NostrEscape1k", func(b *testing.B) {
b.ReportAllocs()
size := size / 64
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
out = out[:0]
}
},
)
b.Run(
"NostrEscapeNostrUnescape1k", func(b *testing.B) {
b.ReportAllocs()
size := size / 64
in := make([]byte, size)
out := make([]byte, size*2)
var err error
for i := 0; i < b.N; i++ {
if _, err = frand.Read(in); chk.E(err) {
b.Fatal(err)
}
out = NostrEscape(out, in)
in = in[:0]
out = NostrUnescape(out)
out = out[:0]
}
},
)
}

12
pkg/utils/units/units.go Normal file
View File

@@ -0,0 +1,12 @@
// Package units is a convenient set of names designating data sizes in bytes
// using common ISO names (base 10).
package units
const (
Kilobyte = 1000
Kb = Kilobyte
Megabyte = Kilobyte * Kilobyte
Mb = Megabyte
Gigabyte = Megabyte * Kilobyte
Gb = Gigabyte
)

1
pkg/version/version Normal file
View File

@@ -0,0 +1 @@
v0.0.1

12
pkg/version/version.go Normal file
View File

@@ -0,0 +1,12 @@
package version
import (
_ "embed"
)
//go:embed version
var V string
var Description = "relay powered by the orly framework https://next.orly.dev"
var URL = "https://nextorly.dev"

19
pprof.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"github.com/pkg/profile"
)
func startProfiler(mode string) {
switch mode {
case "cpu":
prof := profile.Start(profile.CPUProfile)
defer prof.Stop()
case "memory":
prof := profile.Start(profile.MemProfile)
defer prof.Stop()
case "allocation":
prof := profile.Start(profile.MemProfileAllocs)
defer prof.Stop()
}
}