This commit is contained in:
parent
2568dc45c2
commit
d178c05b63
5 changed files with 232 additions and 3 deletions
|
|
@ -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)),
|
||||||
|
|
|
||||||
|
|
@ -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, "[")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
12
ucl/objs.go
12
ucl/objs.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue