--- /dev/null
+// GoKEKS -- Go KEKS codec implementation
+// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, version 3 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package atom
+
+import "io"
+
+type Decoder struct {
+ // You have to set one of R or B as a data source. Decoding from the
+ // B buffer takes less allocations, it is faster.
+ R io.Reader
+ B []byte
+
+ // Maximal allowable string length. 0 means no limits, but pay
+ // attention that if there is no sufficient memory available,
+ // then Go may panic.
+ MaxStrLen int64
+
+ // Disable UTF-8 codepoints validation check.
+ DisableUTF8Check bool
+}
+
+// Read n bytes from the data source. If data source is R, then buf is
+// allocated for each new read. If data source is B, then buf is a slice
+// of the original B buffer.
+func (ctx *Decoder) Want(n int) (buf []byte, err error) {
+ if ctx.B == nil {
+ buf = make([]byte, n)
+ _, err = io.ReadFull(ctx.R, buf)
+ return
+ }
+ if len(ctx.B) < n {
+ err = io.ErrUnexpectedEOF
+ return
+ }
+ buf, ctx.B = ctx.B[:n], ctx.B[n:]
+ return
+}
import (
"errors"
- "io"
"math/big"
"strings"
"unicode/utf8"
ErrBadInt = errors.New("bad int value")
)
-func strDecode(r io.Reader, tag byte) (read int64, v []byte, err error) {
+func (ctx *Decoder) strDecode(tag byte) (read int64, v []byte, err error) {
l := int64(tag & 63)
- var ll int64
+ var ll int
switch l {
case 61:
ll = 1
l += ((1 << 8) - 1) + ((1 << 16) - 1)
}
if ll != 0 {
- read += ll
- v = make([]byte, ll)
- _, err = io.ReadFull(r, v)
+ read += int64(ll)
+ v, err = ctx.Want(ll)
if err != nil {
return
}
err = ErrLenTooBig
return
}
- // TODO: check if it is too large for memory
- v = make([]byte, l)
- _, err = io.ReadFull(r, v)
+ if ctx.MaxStrLen > 0 && l > ctx.MaxStrLen {
+ err = ErrLenTooBig
+ return
+ }
+ v, err = ctx.Want(int(l))
return
}
// Decode a single KEKS-encoded atom. Atom means that it does not decode
// full lists, maps, blobs and may return types.EOC.
-func Decode(r io.Reader) (t types.Type, v any, read int64, err error) {
- buf := make([]byte, 1)
- _, err = io.ReadFull(r, buf)
+func (ctx *Decoder) Decode() (t types.Type, v any, read int64, err error) {
+ var buf []byte
+ buf, err = ctx.Want(1)
if err != nil {
return
}
t = types.Str
}
var strRead int64
- strRead, buf, err = strDecode(r, tag)
+ strRead, buf, err = ctx.strDecode(tag)
read += strRead
if err != nil {
return
} else {
s := unsafe.String(unsafe.SliceData(buf), len(buf))
v = s
- if !utf8.ValidString(s) {
- err = ErrBadUTF8
- }
- if strings.Contains(s, "\x00") {
- err = ErrBadUTF8
+ if !ctx.DisableUTF8Check {
+ if !utf8.ValidString(s) {
+ err = ErrBadUTF8
+ }
+ if strings.Contains(s, "\x00") {
+ err = ErrBadUTF8
+ }
}
}
return
case UUID:
t = types.UUID
read += 16
- buf = make([]byte, 16)
- _, err = io.ReadFull(r, buf)
+ buf, err = ctx.Want(16)
if err != nil {
return
}
case Blob:
t = types.Blob
read += 8
- buf = make([]byte, 8)
- _, err = io.ReadFull(r, buf)
+ buf, err = ctx.Want(8)
if err != nil {
return
}
t = types.Int
}
read += 1
- _, err = io.ReadFull(r, buf)
+ buf, err = ctx.Want(1)
if err != nil {
return
}
return
}
var binRead int64
- binRead, buf, err = strDecode(r, buf[0])
+ binRead, buf, err = ctx.strDecode(buf[0])
read += binRead
if err != nil {
return
l = 32
}
read += int64(l)
- buf = make([]byte, l)
- _, err = io.ReadFull(r, buf)
+ buf, err = ctx.Want(l)
if err != nil {
t = types.Float
return
}
t = types.TAI64
read += int64(l)
- buf = make([]byte, l)
- _, err = io.ReadFull(r, buf)
+ buf, err = ctx.Want(l)
if err != nil {
return
}
"os"
"go.cypherpunks.su/keks"
+ "go.cypherpunks.su/keks/atom"
)
func main() {
- item, read, err := keks.Decode(bufio.NewReader(os.Stdin))
+ item, read, err := keks.Decode(&atom.Decoder{R: bufio.NewReader(os.Stdin)})
if err != nil {
log.Fatal(err)
}
}
func decode(
- r io.Reader,
+ ctx *atom.Decoder,
allowContainers bool,
recursionDepth int,
) (item Item, read int64, err error) {
err = errors.New("deep recursion")
return
}
- item.T, item.V, read, err = atom.Decode(r)
+ item.T, item.V, read, err = ctx.Decode()
if err != nil {
return
}
var subRead int64
var v []Item
for {
- sub, subRead, err = decode(r, true, recursionDepth+1)
+ sub, subRead, err = decode(ctx, true, recursionDepth+1)
read += subRead
if err != nil {
return
var subRead int64
var keyPrev string
for {
- sub, subRead, err = decode(r, false, recursionDepth+1)
+ sub, subRead, err = decode(ctx, false, recursionDepth+1)
read += subRead
if err != nil {
return
}
keyPrev = s
}
- sub, subRead, err = decode(r, true, recursionDepth+1)
+ sub, subRead, err = decode(ctx, true, recursionDepth+1)
read += subRead
if err != nil {
return
err = atom.ErrUnknownType
return
}
- // TODO: check if it is too large for memory
chunkLen := int(item.V.(uint64))
+ if ctx.MaxStrLen != 0 && int64(chunkLen) > ctx.MaxStrLen {
+ err = atom.ErrLenTooBig
+ return
+ }
v := Blob{ChunkLen: chunkLen}
var sub Item
var subRead int64
var chunks []io.Reader
BlobCycle:
for {
- sub, subRead, err = decode(r, false, recursionDepth+1)
+ sub, subRead, err = decode(ctx, false, recursionDepth+1)
read += subRead
if err != nil {
return
}
switch sub.T {
case types.NIL:
- buf := make([]byte, chunkLen)
- read += int64(chunkLen)
- _, err = io.ReadFull(r, buf)
+ var buf []byte
+ buf, err = ctx.Want(chunkLen)
if err != nil {
return
}
+ read += int64(chunkLen)
chunks = append(chunks, bytes.NewReader(buf))
v.DecodedLen += int64(chunkLen)
case types.Bin:
}
// Decode single KEKS-encoded data item.
-func Decode(r io.Reader) (item Item, read int64, err error) {
- item, read, err = decode(r, true, 0)
+func Decode(ctx *atom.Decoder) (item Item, read int64, err error) {
+ item, read, err = decode(ctx, true, 0)
if item.T == types.EOC {
err = ErrUnexpectedEOC
}
import (
"bytes"
"testing"
+
+ "go.cypherpunks.su/keks/atom"
)
func FuzzItemDecode(f *testing.F) {
var e any
var buf bytes.Buffer
f.Fuzz(func(t *testing.T, b []byte) {
- item, _, err = Decode(bytes.NewReader(b))
+ item, _, err = Decode(&atom.Decoder{B: b, MaxStrLen: 1 << 20})
if err == nil {
e, err = item.ToGo()
if err != nil {
import (
"errors"
- "io"
"go.cypherpunks.su/keks"
+ "go.cypherpunks.su/keks/atom"
"go.cypherpunks.su/keks/types"
)
// Decode KEKS-encoded data to the dst structure.
// It will return an error if decoded data is not map.
-func Decode(dst any, src io.Reader) (err error) {
+func Decode(dst any, ctx *atom.Decoder) (err error) {
var item keks.Item
- item, _, err = keks.Decode(src)
+ item, _, err = keks.Decode(ctx)
if err != nil {
return
}
package pki
import (
- "bytes"
"crypto"
"errors"
"fmt"
+ "go.cypherpunks.su/keks/atom"
"go.cypherpunks.su/keks/mapstruct"
ed25519blake2b "go.cypherpunks.su/keks/pki/ed25519-blake2b"
"go.cypherpunks.su/keks/pki/gost"
func PrvParse(data []byte) (prv crypto.Signer, pub []byte, err error) {
var av AV
var tail []byte
- err = mapstruct.Decode(&av, bytes.NewReader(data))
+ err = mapstruct.Decode(&av, &atom.Decoder{B: data, MaxStrLen: 1<<16})
if err != nil {
return
}
package pki
import (
- "bytes"
"crypto"
"crypto/rand"
"errors"
"github.com/google/uuid"
"go.cypherpunks.su/keks"
+ "go.cypherpunks.su/keks/atom"
"go.cypherpunks.su/keks/mapstruct"
"go.cypherpunks.su/keks/types"
)
// SignedDataParseItem.
func SignedDataParse(data []byte) (sd *SignedData, err error) {
var item keks.Item
- item, _, err = keks.Decode(bytes.NewReader(data))
+ item, _, err = keks.Decode(&atom.Decoder{B: data})
if err != nil {
return
}