Add core packages, configuration system, and initial application structure
This commit is contained in:
127
pkg/encoders/text/escape.go
Normal file
127
pkg/encoders/text/escape.go
Normal 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
|
||||
}
|
||||
459
pkg/encoders/text/escape_test.go
Normal file
459
pkg/encoders/text/escape_test.go
Normal 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]
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user