Added additional byte builtins
Some checks failed
Build / build (push) Failing after 2m47s

This commit is contained in:
Leon Mika 2026-09-08 21:57:15 +10:00
parent 2568dc45c2
commit d178c05b63
5 changed files with 232 additions and 3 deletions

View file

@ -19,6 +19,7 @@ func main() {
defer rl.Close() defer rl.Close()
instRepl := repl.New( instRepl := repl.New(
ucl.WithModule(builtins.Bytes()),
ucl.WithModule(builtins.CSV(nil)), ucl.WithModule(builtins.CSV(nil)),
ucl.WithModule(builtins.FS(nil)), ucl.WithModule(builtins.FS(nil)),
ucl.WithModule(builtins.Log(nil)), ucl.WithModule(builtins.Log(nil)),

View file

@ -60,6 +60,28 @@ func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise
if _, err = fmt.Fprintln(w, "(nil)"); err != nil { if _, err = fmt.Fprintln(w, "(nil)"); err != nil {
return err return err
} }
case []byte:
if _, err = fmt.Fprint(w, "bytes["); err != nil {
return err
}
for i, x := range v {
if i > 0 {
if _, err = fmt.Fprint(w, " "); err != nil {
return err
}
}
if _, err = fmt.Fprintf(w, "%02x", x); err != nil {
return err
}
}
if _, err = fmt.Fprint(w, "]"); err != nil {
return err
}
if !concise {
if _, err = fmt.Fprintln(w); err != nil {
return err
}
}
case ucl.Listable: case ucl.Listable:
if concise { if concise {
fmt.Fprintf(w, "[") fmt.Fprintf(w, "[")

View file

@ -1,12 +1,15 @@
package builtins package builtins
import ( import (
"bytes"
"context" "context"
"crypto/md5" "crypto/md5"
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/hex"
"errors" "errors"
"fmt"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
) )
@ -18,11 +21,15 @@ type bytesHandlers struct{}
// Perform operations over byte slices. // Perform operations over byte slices.
func Bytes() ucl.Module { func Bytes() ucl.Module {
bh := bytesHandlers{}
return ucl.Module{ return ucl.Module{
Name: "bytes", Name: "bytes",
Builtins: map[string]ucl.BuiltinHandler{ Builtins: map[string]ucl.BuiltinHandler{
"hash": bytesHandlers{}.hash, "from": bh.from,
"base64": bytesHandlers{}.base64, "hash": bh.hash,
"base64": bh.base64,
"hex": bh.hex,
}, },
} }
} }
@ -82,6 +89,50 @@ func (c bytesHandlers) base64(ctx context.Context, args ucl.CallArgs) (any, erro
return res, nil return res, nil
} }
// :fn from
//
// :syntax INPUT ...
//
// Returns a byte slice from a given input. Input can be one of the following:
//
// - Nil, which will produce an empty byte slice
// - A string, which would produce a byte slice containing the UTF-8 encoded string
// - A number, which would produce a single byte slice containing the number as a byte
// - A byte slice, which would produce a copy of the byte slice
//
// A list or iter will consume the elements and apply the byte slice conversion recursively, combining
// the results into a single byte slice.
func (c bytesHandlers) from(ctx context.Context, args ucl.CallArgs) (any, error) {
var bfr bytes.Buffer
var o ucl.Object
for args.NArgs() > 0 {
if err := args.Bind(&o); err != nil {
return nil, fmt.Errorf("failed to bind object: %w", err)
}
if err := writeObjToBytesBuffer(ctx, &bfr, o); err != nil {
return nil, err
}
}
return bfr.Bytes(), nil
}
// :fn hex
//
// :syntax BYTES
//
// Returns a string encoding the bytes slice as a hex string
func (c bytesHandlers) hex(ctx context.Context, args ucl.CallArgs) (any, error) {
inputBts, args, err := consumeStringOrBytes(args)
if err != nil {
return nil, err
}
res := hex.EncodeToString(inputBts)
return res, nil
}
func consumeStringOrBytes(args ucl.CallArgs) (bs ucl.BytesObject, _ ucl.CallArgs, _ error) { func consumeStringOrBytes(args ucl.CallArgs) (bs ucl.BytesObject, _ ucl.CallArgs, _ error) {
if objs := args.RestAsObjects(); len(objs) > 0 { if objs := args.RestAsObjects(); len(objs) > 0 {
if b, ok := objs[0].(ucl.BytesObject); ok { if b, ok := objs[0].(ucl.BytesObject); ok {
@ -96,3 +147,45 @@ func consumeStringOrBytes(args ucl.CallArgs) (bs ucl.BytesObject, _ ucl.CallArgs
} }
return ucl.BytesObject(s), args, nil return ucl.BytesObject(s), args, nil
} }
func writeObjToBytesBuffer(ctx context.Context, bfr *bytes.Buffer, o ucl.Object) error {
if o == nil {
return nil
}
switch v := o.(type) {
case ucl.StringObject:
bfr.Write([]byte(v))
case ucl.IntObject:
bfr.WriteByte(byte(v))
case ucl.StringListObject:
for _, el := range v {
bfr.Write([]byte(el))
}
case ucl.BytesObject:
bfr.Write(v)
case ucl.Listable:
l := v.Len()
for i := 0; i < l; i++ {
if err := writeObjToBytesBuffer(ctx, bfr, v.Index(i)); err != nil {
return err
}
}
case ucl.Iterable:
for v.HasNext() {
n, err := v.Next(ctx)
if err != nil {
return err
}
if n == nil {
break
}
if err := writeObjToBytesBuffer(ctx, bfr, n); err != nil {
return err
}
}
default:
return fmt.Errorf("unsupported type %T", o)
}
return nil
}

View file

@ -10,6 +10,76 @@ import (
"ucl.lmika.dev/ucl/builtins" "ucl.lmika.dev/ucl/builtins"
) )
func TestBytes_From(t *testing.T) {
tests := []struct {
desc string
eval string
want any
wantErr bool
}{
{desc: "no args", eval: `bytes:from`},
{desc: "nil", eval: `bytes:from ()`},
{desc: "string 1", eval: `bytes:from "hello"`, want: []byte("hello")},
{desc: "string 2", eval: `bytes:from ""`},
{desc: "string utf-8", eval: `bytes:from "héllo"`, want: []byte("héllo")},
{desc: "single int", eval: `bytes:from 104`, want: []byte{104}},
{desc: "zero int", eval: `bytes:from 0`, want: []byte{0x00}},
{desc: "int wraps to byte", eval: `bytes:from 260`, want: []byte{4}},
{desc: "bytes input", eval: `bytes:from (bytes:from "hello")`, want: []byte("hello")},
{desc: "multiple args", eval: `bytes:from "he" 108 "lo"`, want: []byte("hello")},
{desc: "list of ints", eval: `bytes:from [104 101 108 108 111]`, want: []byte("hello")},
{desc: "empty list", eval: `bytes:from []`},
{desc: "list of strings", eval: `bytes:from ["he" "llo"]`, want: []byte("hello")},
{desc: "list with nil", eval: `bytes:from ["he" () "llo"]`, want: []byte("hello")},
{desc: "nested lists", eval: `bytes:from [["he"] [108 "lo"]]`, want: []byte("hello")},
{desc: "list of bytes", eval: `bytes:from [(bytes:from "he") (bytes:from "llo")]`, want: []byte("hello")},
{desc: "iterator of ints", eval: `bytes:from (itrs:from [104 105])`, want: []byte("hi")},
{desc: "iterator from seq", eval: `seq 4 | map { |x| add $x 97 } | bytes:from`, want: []byte("abcd")},
{desc: "round trip through str", eval: `str (bytes:from "hello")`, want: "hello"},
{desc: "err unsupported bool", eval: `bytes:from $true`, wantErr: true},
{desc: "err unsupported map", eval: `bytes:from [a: 1]`, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
inst := ucl.New(
ucl.WithModule(builtins.Bytes()),
ucl.WithModule(builtins.Itrs()),
)
inst.SetVar("true", true)
res, err := inst.EvalString(context.Background(), tt.eval)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if wantStr, ok := tt.want.(string); ok {
assert.Equal(t, wantStr, res)
return
}
resBts, ok := res.([]byte)
if assert.True(t, ok, "expected []byte result, got %T", res) {
if tt.want == nil {
assert.Empty(t, resBts)
} else {
assert.Equal(t, tt.want, resBts)
}
}
}
})
}
}
func TestBytes_Hash(t *testing.T) { func TestBytes_Hash(t *testing.T) {
tests := []struct { tests := []struct {
desc string desc string
@ -83,3 +153,36 @@ func TestBytes_Base64(t *testing.T) {
}) })
} }
} }
func TestBytes_Hex(t *testing.T) {
tests := []struct {
desc string
eval string
want string
wantErr bool
}{
{desc: "encode string", eval: `bytes:hex "hello"`, want: "68656c6c6f"},
{desc: "encode empty string", eval: `bytes:hex ""`, want: ""},
{desc: "encode string with utf-8", eval: `bytes:hex "héllo"`, want: "68c3a96c6c6f"},
{desc: "encode bytes input", eval: `bytes:hex (bytes:from "hello")`, want: "68656c6c6f"},
{desc: "encode hash result", eval: `bytes:hex (bytes:hash "hello" md5)`, want: "5d41402abc4b2a76b9719d911017c592"},
{desc: "err missing args", eval: `bytes:hex`, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
inst := ucl.New(
ucl.WithModule(builtins.Bytes()),
)
res, err := inst.EvalString(context.Background(), tt.eval)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
}
})
}
}

View file

@ -121,7 +121,17 @@ func (ss StringListObject) Index(i int) Object {
type BytesObject []byte type BytesObject []byte
func (bs BytesObject) String() string { func (bs BytesObject) String() string {
return fmt.Sprintf("[%v]bytes", len(bs)) var sb strings.Builder
sb.WriteString("bytes[")
for i, b := range bs {
if i > 0 {
sb.WriteString(" ")
}
sb.WriteString(fmt.Sprintf("%02x", b))
}
sb.WriteString("]")
return sb.String()
} }
func (bs BytesObject) Truthy() bool { func (bs BytesObject) Truthy() bool {