Limit the maximum size of string/bytes values (#121)

* add tengo.MaxStringLen and tengo.MaxBytesLen to limit the maximum byte-length of string/bytes values

* add couple more tests
This commit is contained in:
Daniel 2019-03-01 10:48:02 -08:00 committed by GitHub
parent 880ee04ffe
commit 0c5e80b057
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 600 additions and 96 deletions

View file

@ -5,6 +5,7 @@ import (
"io"
"reflect"
"github.com/d5/tengo"
"github.com/d5/tengo/compiler/ast"
"github.com/d5/tengo/compiler/source"
"github.com/d5/tengo/compiler/token"
@ -195,6 +196,10 @@ func (c *Compiler) Compile(node ast.Node) error {
}
case *ast.StringLit:
if len(node.Value) > tengo.MaxStringLen {
return c.error(node, objects.ErrStringLimit)
}
c.emit(node, OpConstant, c.addConstant(&objects.String{Value: node.Value}))
case *ast.CharLit:
@ -332,6 +337,9 @@ func (c *Compiler) Compile(node ast.Node) error {
case *ast.MapLit:
for _, elt := range node.Elements {
// key
if len(elt.Key) > tengo.MaxStringLen {
return c.error(node, objects.ErrStringLimit)
}
c.emit(node, OpConstant, c.addConstant(&objects.String{Value: elt.Key}))
// value
@ -507,6 +515,10 @@ func (c *Compiler) Compile(node ast.Node) error {
case *ast.ImportExpr:
if c.builtinModules[node.ModuleName] {
if len(node.ModuleName) > tengo.MaxStringLen {
return c.error(node, objects.ErrStringLimit)
}
c.emit(node, OpConstant, c.addConstant(&objects.String{Value: node.ModuleName}))
c.emit(node, OpGetBuiltinModule)
} else {
@ -610,6 +622,14 @@ func (c *Compiler) fork(file *source.File, moduleName string, symbolTable *Symbo
return child
}
func (c *Compiler) error(node ast.Node, err error) error {
return &Error{
fileSet: c.file.Set(),
node: node,
error: err,
}
}
func (c *Compiler) errorf(node ast.Node, format string, args ...interface{}) error {
return &Error{
fileSet: c.file.Set(),

View file

@ -1,5 +1,7 @@
package objects
import "github.com/d5/tengo"
func builtinString(args ...Object) (Object, error) {
argsLen := len(args)
if !(argsLen == 1 || argsLen == 2) {
@ -12,6 +14,10 @@ func builtinString(args ...Object) (Object, error) {
v, ok := ToString(args[0])
if ok {
if len(v) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: v}, nil
}
@ -117,11 +123,19 @@ func builtinBytes(args ...Object) (Object, error) {
// bytes(N) => create a new bytes with given size N
if n, ok := args[0].(*Int); ok {
if n.Value > int64(tengo.MaxBytesLen) {
return nil, ErrBytesLimit
}
return &Bytes{Value: make([]byte, int(n.Value))}, nil
}
v, ok := ToByteSlice(args[0])
if ok {
if len(v) > tengo.MaxBytesLen {
return nil, ErrBytesLimit
}
return &Bytes{Value: v}, nil
}

View file

@ -2,6 +2,8 @@ package objects
import (
"encoding/json"
"github.com/d5/tengo"
)
// to_json(v object) => bytes
@ -15,6 +17,10 @@ func builtinToJSON(args ...Object) (Object, error) {
return &Error{Value: &String{Value: err.Error()}}, nil
}
if len(res) > tengo.MaxBytesLen {
return nil, ErrBytesLimit
}
return &Bytes{Value: res}, nil
}

View file

@ -2,6 +2,8 @@ package objects
import (
"fmt"
"github.com/d5/tengo"
)
// print(args...)
@ -71,5 +73,11 @@ func builtinSprintf(args ...Object) (Object, error) {
formatArgs[idx] = objectToInterface(arg)
}
return &String{Value: fmt.Sprintf(format.Value, formatArgs...)}, nil
s := fmt.Sprintf(format.Value, formatArgs...)
if len(s) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: s}, nil
}

View file

@ -3,6 +3,7 @@ package objects
import (
"bytes"
"github.com/d5/tengo"
"github.com/d5/tengo/compiler/token"
)
@ -27,6 +28,10 @@ func (o *Bytes) BinaryOp(op token.Token, rhs Object) (Object, error) {
case token.Add:
switch rhs := rhs.(type) {
case *Bytes:
if len(o.Value)+len(rhs.Value) > tengo.MaxBytesLen {
return nil, ErrBytesLimit
}
return &Bytes{Value: append(o.Value, rhs.Value...)}, nil
}
}

View file

@ -4,6 +4,8 @@ import (
"fmt"
"strconv"
"time"
"github.com/d5/tengo"
)
// ToString will try to convert object o to string value.
@ -194,6 +196,9 @@ func FromInterface(v interface{}) (Object, error) {
case nil:
return UndefinedValue, nil
case string:
if len(v) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: v}, nil
case int64:
return &Int{Value: v}, nil
@ -211,6 +216,9 @@ func FromInterface(v interface{}) (Object, error) {
case float64:
return &Float{Value: v}, nil
case []byte:
if len(v) > tengo.MaxBytesLen {
return nil, ErrBytesLimit
}
return &Bytes{Value: v}, nil
case error:
return &Error{Value: &String{Value: v.Error()}}, nil

View file

@ -20,6 +20,12 @@ var ErrInvalidOperator = errors.New("invalid operator")
// ErrWrongNumArguments represents a wrong number of arguments error.
var ErrWrongNumArguments = errors.New("wrong number of arguments")
// ErrBytesLimit represents an error where the size of bytes value exceeds the limit.
var ErrBytesLimit = errors.New("exceeding bytes size limit")
// ErrStringLimit represents an error where the size of string value exceeds the limit.
var ErrStringLimit = errors.New("exceeding string size limit")
// ErrInvalidArgumentType represents an invalid argument value type error.
type ErrInvalidArgumentType struct {
Name string

View file

@ -3,6 +3,7 @@ package objects
import (
"strconv"
"github.com/d5/tengo"
"github.com/d5/tengo/compiler/token"
)
@ -28,9 +29,16 @@ func (o *String) BinaryOp(op token.Token, rhs Object) (Object, error) {
case token.Add:
switch rhs := rhs.(type) {
case *String:
if len(o.Value)+len(rhs.Value) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: o.Value + rhs.Value}, nil
default:
return &String{Value: o.Value + rhs.String()}, nil
rhsStr := rhs.String()
if len(o.Value)+len(rhsStr) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: o.Value + rhsStr}, nil
}
}

View file

@ -193,8 +193,8 @@ out = func() {
`, 136)
// assigning different type value
expect(t, `a := 1; a = "foo"; out = a`, "foo") // global
expect(t, `func() { a := 1; a = "foo"; out = a }()`, "foo") // local
expect(t, `a := 1; a = "foo"; out = a`, "foo") // global
expect(t, `func() { a := 1; a = "foo"; out = a }()`, "foo") // local
expect(t, `
out = func() {
a := 5

View file

@ -3,6 +3,7 @@ package runtime_test
import (
"testing"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
@ -193,3 +194,17 @@ func TestBuiltinFunction(t *testing.T) {
expect(t, `a := func(x) { return func() { return x } }; out = is_callable(a(5))`, true) // closure
expectWithSymbols(t, `out = is_callable(x)`, true, SYM{"x": &StringArray{Value: []string{"foo", "bar"}}}) // user object
}
func TestBytesN(t *testing.T) {
curMaxBytesLen := tengo.MaxBytesLen
defer func() { tengo.MaxBytesLen = curMaxBytesLen }()
tengo.MaxBytesLen = 10
expect(t, `out = bytes(0)`, make([]byte, 0))
expect(t, `out = bytes(10)`, make([]byte, 10))
expectError(t, `bytes(11)`, "bytes size limit")
tengo.MaxBytesLen = 1000
expect(t, `out = bytes(1000)`, make([]byte, 1000))
expectError(t, `bytes(1001)`, "bytes size limit")
}

View file

@ -3,6 +3,7 @@ package stdlib
import (
"fmt"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
@ -124,7 +125,13 @@ func FuncARS(fn func() string) objects.CallableFunc {
return nil, objects.ErrWrongNumArguments
}
return &objects.String{Value: fn()}, nil
s := fn()
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -141,6 +148,10 @@ func FuncARSE(fn func() (string, error)) objects.CallableFunc {
return wrapError(err), nil
}
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: res}, nil
}
}
@ -158,6 +169,10 @@ func FuncARYE(fn func() ([]byte, error)) objects.CallableFunc {
return wrapError(err), nil
}
if len(res) > tengo.MaxBytesLen {
return nil, objects.ErrBytesLimit
}
return &objects.Bytes{Value: res}, nil
}
}
@ -183,8 +198,12 @@ func FuncARSs(fn func() []string) objects.CallableFunc {
}
arr := &objects.Array{}
for _, osArg := range fn() {
arr.Value = append(arr.Value, &objects.String{Value: osArg})
for _, elem := range fn() {
if len(elem) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: elem})
}
return arr, nil
@ -493,7 +512,13 @@ func FuncASRS(fn func(string) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(s1)}, nil
s := fn(s1)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -516,8 +541,12 @@ func FuncASRSs(fn func(string) []string) objects.CallableFunc {
res := fn(s1)
arr := &objects.Array{}
for _, osArg := range res {
arr.Value = append(arr.Value, &objects.String{Value: osArg})
for _, elem := range res {
if len(elem) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: elem})
}
return arr, nil
@ -546,6 +575,10 @@ func FuncASRSE(fn func(string) (string, error)) objects.CallableFunc {
return wrapError(err), nil
}
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: res}, nil
}
}
@ -628,6 +661,10 @@ func FuncASSRSs(fn func(string, string) []string) objects.CallableFunc {
arr := &objects.Array{}
for _, res := range fn(s1, s2) {
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: res})
}
@ -671,6 +708,10 @@ func FuncASSIRSs(fn func(string, string, int) []string) objects.CallableFunc {
arr := &objects.Array{}
for _, res := range fn(s1, s2, i3) {
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: res})
}
@ -732,7 +773,13 @@ func FuncASSRS(fn func(string, string) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(s1, s2)}, nil
s := fn(s1, s2)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -819,7 +866,12 @@ func FuncASsSRS(fn func([]string, string) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(ss1, s2)}, nil
s := fn(ss1, s2)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -909,7 +961,13 @@ func FuncASIRS(fn func(string, int) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(s1, i2)}, nil
s := fn(s1, i2)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -1028,6 +1086,10 @@ func FuncAIRSsE(fn func(int) ([]string, error)) objects.CallableFunc {
arr := &objects.Array{}
for _, r := range res {
if len(r) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: r})
}
@ -1052,6 +1114,12 @@ func FuncAIRS(fn func(int) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(i1)}, nil
s := fn(i1)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}

View file

@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
@ -39,14 +40,14 @@ var osModule = map[string]objects.Object{
"seek_set": &objects.Int{Value: int64(io.SeekStart)},
"seek_cur": &objects.Int{Value: int64(io.SeekCurrent)},
"seek_end": &objects.Int{Value: int64(io.SeekEnd)},
"args": &objects.UserFunction{Value: osArgs}, // args() => array(string)
"args": &objects.UserFunction{Name: "args", Value: osArgs}, // args() => array(string)
"chdir": &objects.UserFunction{Name: "chdir", Value: FuncASRE(os.Chdir)}, // chdir(dir string) => error
"chmod": osFuncASFmRE(os.Chmod), // chmod(name string, mode int) => error
"chmod": osFuncASFmRE("chmod", os.Chmod), // chmod(name string, mode int) => error
"chown": &objects.UserFunction{Name: "chown", Value: FuncASIIRE(os.Chown)}, // chown(name string, uid int, gid int) => error
"clearenv": &objects.UserFunction{Name: "clearenv", Value: FuncAR(os.Clearenv)}, // clearenv()
"environ": &objects.UserFunction{Name: "environ", Value: FuncARSs(os.Environ)}, // environ() => array(string)
"exit": &objects.UserFunction{Name: "exit", Value: FuncAIR(os.Exit)}, // exit(code int)
"expand_env": &objects.UserFunction{Name: "expand_env", Value: FuncASRS(os.ExpandEnv)}, // expand_env(s string) => string
"expand_env": &objects.UserFunction{Name: "expand_env", Value: osExpandEnv}, // expand_env(s string) => string
"getegid": &objects.UserFunction{Name: "getegid", Value: FuncARI(os.Getegid)}, // getegid() => int
"getenv": &objects.UserFunction{Name: "getenv", Value: FuncASRS(os.Getenv)}, // getenv(s string) => string
"geteuid": &objects.UserFunction{Name: "geteuid", Value: FuncARI(os.Geteuid)}, // geteuid() => int
@ -60,9 +61,9 @@ var osModule = map[string]objects.Object{
"hostname": &objects.UserFunction{Name: "hostname", Value: FuncARSE(os.Hostname)}, // hostname() => string/error
"lchown": &objects.UserFunction{Name: "lchown", Value: FuncASIIRE(os.Lchown)}, // lchown(name string, uid int, gid int) => error
"link": &objects.UserFunction{Name: "link", Value: FuncASSRE(os.Link)}, // link(oldname string, newname string) => error
"lookup_env": &objects.UserFunction{Value: osLookupEnv}, // lookup_env(key string) => string/false
"mkdir": osFuncASFmRE(os.Mkdir), // mkdir(name string, perm int) => error
"mkdir_all": osFuncASFmRE(os.MkdirAll), // mkdir_all(name string, perm int) => error
"lookup_env": &objects.UserFunction{Name: "lookup_env", Value: osLookupEnv}, // lookup_env(key string) => string/false
"mkdir": osFuncASFmRE("mkdir", os.Mkdir), // mkdir(name string, perm int) => error
"mkdir_all": osFuncASFmRE("mkdir_all", os.MkdirAll), // mkdir_all(name string, perm int) => error
"readlink": &objects.UserFunction{Name: "readlink", Value: FuncASRSE(os.Readlink)}, // readlink(name string) => string/error
"remove": &objects.UserFunction{Name: "remove", Value: FuncASRE(os.Remove)}, // remove(name string) => error
"remove_all": &objects.UserFunction{Name: "remove_all", Value: FuncASRE(os.RemoveAll)}, // remove_all(name string) => error
@ -72,15 +73,15 @@ var osModule = map[string]objects.Object{
"temp_dir": &objects.UserFunction{Name: "temp_dir", Value: FuncARS(os.TempDir)}, // temp_dir() => string
"truncate": &objects.UserFunction{Name: "truncate", Value: FuncASI64RE(os.Truncate)}, // truncate(name string, size int) => error
"unsetenv": &objects.UserFunction{Name: "unsetenv", Value: FuncASRE(os.Unsetenv)}, // unsetenv(key string) => error
"create": &objects.UserFunction{Value: osCreate}, // create(name string) => imap(file)/error
"open": &objects.UserFunction{Value: osOpen}, // open(name string) => imap(file)/error
"open_file": &objects.UserFunction{Value: osOpenFile}, // open_file(name string, flag int, perm int) => imap(file)/error
"find_process": &objects.UserFunction{Value: osFindProcess}, // find_process(pid int) => imap(process)/error
"start_process": &objects.UserFunction{Value: osStartProcess}, // start_process(name string, argv array(string), dir string, env array(string)) => imap(process)/error
"create": &objects.UserFunction{Name: "create", Value: osCreate}, // create(name string) => imap(file)/error
"open": &objects.UserFunction{Name: "open", Value: osOpen}, // open(name string) => imap(file)/error
"open_file": &objects.UserFunction{Name: "open_file", Value: osOpenFile}, // open_file(name string, flag int, perm int) => imap(file)/error
"find_process": &objects.UserFunction{Name: "find_process", Value: osFindProcess}, // find_process(pid int) => imap(process)/error
"start_process": &objects.UserFunction{Name: "start_process", Value: osStartProcess}, // start_process(name string, argv array(string), dir string, env array(string)) => imap(process)/error
"exec_look_path": &objects.UserFunction{Name: "exec_look_path", Value: FuncASRSE(exec.LookPath)}, // exec_look_path(file) => string/error
"exec": &objects.UserFunction{Value: osExec}, // exec(name, args...) => command
"stat": &objects.UserFunction{Value: osStat}, // stat(name) => imap(fileinfo)/error
"read_file": &objects.UserFunction{Value: osReadFile}, // readfile(name) => array(byte)/error
"exec": &objects.UserFunction{Name: "exec", Value: osExec}, // exec(name, args...) => command
"stat": &objects.UserFunction{Name: "stat", Value: osStat}, // stat(name) => imap(fileinfo)/error
"read_file": &objects.UserFunction{Name: "read_file", Value: osReadFile}, // readfile(name) => array(byte)/error
}
func osReadFile(args ...objects.Object) (ret objects.Object, err error) {
@ -102,6 +103,10 @@ func osReadFile(args ...objects.Object) (ret objects.Object, err error) {
return wrapError(err), nil
}
if len(bytes) > tengo.MaxBytesLen {
return nil, objects.ErrBytesLimit
}
return &objects.Bytes{Value: bytes}, nil
}
@ -233,14 +238,19 @@ func osArgs(args ...objects.Object) (objects.Object, error) {
arr := &objects.Array{}
for _, osArg := range os.Args {
if len(osArg) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: osArg})
}
return arr, nil
}
func osFuncASFmRE(fn func(string, os.FileMode) error) *objects.UserFunction {
func osFuncASFmRE(name string, fn func(string, os.FileMode) error) *objects.UserFunction {
return &objects.UserFunction{
Name: name,
Value: func(args ...objects.Object) (objects.Object, error) {
if len(args) != 2 {
return nil, objects.ErrWrongNumArguments
@ -287,9 +297,54 @@ func osLookupEnv(args ...objects.Object) (objects.Object, error) {
return objects.FalseValue, nil
}
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: res}, nil
}
func osExpandEnv(args ...objects.Object) (objects.Object, error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
}
s1, ok := objects.ToString(args[0])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "first",
Expected: "string(compatible)",
Found: args[0].TypeName(),
}
}
var vlen int
var failed bool
s := os.Expand(s1, func(k string) string {
if failed {
return ""
}
v := os.Getenv(k)
// this does not count the other texts that are not being replaced
// but the code checks the final length at the end
vlen += len(v)
if vlen > tengo.MaxStringLen {
failed = true
return ""
}
return v
})
if failed || len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
func osExec(args ...objects.Object) (objects.Object, error) {
if len(args) == 0 {
return nil, objects.ErrWrongNumArguments

View file

@ -21,6 +21,7 @@ func makeOSExecCommand(cmd *exec.Cmd) *objects.ImmutableMap {
"wait": &objects.UserFunction{Name: "wait", Value: FuncARE(cmd.Wait)}, //
// set_path(path string)
"set_path": &objects.UserFunction{
Name: "set_path",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -42,6 +43,7 @@ func makeOSExecCommand(cmd *exec.Cmd) *objects.ImmutableMap {
},
// set_dir(dir string)
"set_dir": &objects.UserFunction{
Name: "set_dir",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -63,6 +65,7 @@ func makeOSExecCommand(cmd *exec.Cmd) *objects.ImmutableMap {
},
// set_env(env array(string))
"set_env": &objects.UserFunction{
Name: "set_env",
Value: func(args ...objects.Object) (objects.Object, error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -96,6 +99,7 @@ func makeOSExecCommand(cmd *exec.Cmd) *objects.ImmutableMap {
},
// process() => imap(process)
"process": &objects.UserFunction{
Name: "process",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 0 {
return nil, objects.ErrWrongNumArguments

View file

@ -29,6 +29,7 @@ func makeOSFile(file *os.File) *objects.ImmutableMap {
"read": &objects.UserFunction{Name: "read", Value: FuncAYRIE(file.Read)}, //
// chmod(mode int) => error
"chmod": &objects.UserFunction{
Name: "chmod",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -48,6 +49,7 @@ func makeOSFile(file *os.File) *objects.ImmutableMap {
},
// seek(offset int, whence int) => int/error
"seek": &objects.UserFunction{
Name: "seek",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 2 {
return nil, objects.ErrWrongNumArguments
@ -80,6 +82,7 @@ func makeOSFile(file *os.File) *objects.ImmutableMap {
},
// stat() => imap(fileinfo)/error
"stat": &objects.UserFunction{
Name: "start",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 0 {
return nil, objects.ErrWrongNumArguments

View file

@ -24,6 +24,7 @@ func makeOSProcess(proc *os.Process) *objects.ImmutableMap {
"kill": &objects.UserFunction{Name: "kill", Value: FuncARE(proc.Kill)}, //
"release": &objects.UserFunction{Name: "release", Value: FuncARE(proc.Release)}, //
"signal": &objects.UserFunction{
Name: "signal",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -42,6 +43,7 @@ func makeOSProcess(proc *os.Process) *objects.ImmutableMap {
},
},
"wait": &objects.UserFunction{
Name: "wait",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 0 {
return nil, objects.ErrWrongNumArguments

View file

@ -5,6 +5,7 @@ import (
"os"
"testing"
"github.com/d5/tengo"
"github.com/d5/tengo/assert"
"github.com/d5/tengo/objects"
)
@ -86,3 +87,33 @@ func TestFileStatDir(t *testing.T) {
},
})
}
func TestOSExpandEnv(t *testing.T) {
curMaxStringLen := tengo.MaxStringLen
defer func() { tengo.MaxStringLen = curMaxStringLen }()
tengo.MaxStringLen = 12
_ = os.Setenv("TENGO", "FOO BAR")
module(t, "os").call("expand_env", "$TENGO").expect("FOO BAR")
_ = os.Setenv("TENGO", "FOO")
module(t, "os").call("expand_env", "$TENGO $TENGO").expect("FOO FOO")
_ = os.Setenv("TENGO", "123456789012")
module(t, "os").call("expand_env", "$TENGO").expect("123456789012")
_ = os.Setenv("TENGO", "1234567890123")
module(t, "os").call("expand_env", "$TENGO").expectError()
_ = os.Setenv("TENGO", "123456")
module(t, "os").call("expand_env", "$TENGO$TENGO").expect("123456123456")
_ = os.Setenv("TENGO", "123456")
module(t, "os").call("expand_env", "${TENGO}${TENGO}").expect("123456123456")
_ = os.Setenv("TENGO", "123456")
module(t, "os").call("expand_env", "$TENGO $TENGO").expectError()
_ = os.Setenv("TENGO", "123456")
module(t, "os").call("expand_env", "${TENGO} ${TENGO}").expectError()
}

View file

@ -15,6 +15,7 @@ var randModule = map[string]objects.Object{
"perm": &objects.UserFunction{Name: "perm", Value: FuncAIRIs(rand.Perm)},
"seed": &objects.UserFunction{Name: "seed", Value: FuncAI64R(rand.Seed)},
"read": &objects.UserFunction{
Name: "read",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -39,6 +40,7 @@ var randModule = map[string]objects.Object{
},
},
"rand": &objects.UserFunction{
Name: "rand",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -71,6 +73,7 @@ func randRand(r *rand.Rand) *objects.ImmutableMap {
"perm": &objects.UserFunction{Name: "perm", Value: FuncAIRIs(r.Perm)},
"seed": &objects.UserFunction{Name: "seed", Value: FuncAI64R(r.Seed)},
"read": &objects.UserFunction{
Name: "read",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments

View file

@ -1,19 +1,22 @@
package stdlib
import (
"fmt"
"regexp"
"strconv"
"strings"
"unicode/utf8"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
var textModule = map[string]objects.Object{
"re_match": &objects.UserFunction{Value: textREMatch}, // re_match(pattern, text) => bool/error
"re_find": &objects.UserFunction{Value: textREFind}, // re_find(pattern, text, count) => [[{text:,begin:,end:}]]/undefined
"re_replace": &objects.UserFunction{Value: textREReplace}, // re_replace(pattern, text, repl) => string/error
"re_split": &objects.UserFunction{Value: textRESplit}, // re_split(pattern, text, count) => [string]/error
"re_compile": &objects.UserFunction{Value: textRECompile}, // re_compile(pattern) => Regexp/error
"re_match": &objects.UserFunction{Name: "re_match", Value: textREMatch}, // re_match(pattern, text) => bool/error
"re_find": &objects.UserFunction{Name: "re_find", Value: textREFind}, // re_find(pattern, text, count) => [[{text:,begin:,end:}]]/undefined
"re_replace": &objects.UserFunction{Name: "re_replace", Value: textREReplace}, // re_replace(pattern, text, repl) => string/error
"re_split": &objects.UserFunction{Name: "re_split", Value: textRESplit}, // re_split(pattern, text, count) => [string]/error
"re_compile": &objects.UserFunction{Name: "re_compile", Value: textRECompile}, // re_compile(pattern) => Regexp/error
"compare": &objects.UserFunction{Name: "compare", Value: FuncASSRI(strings.Compare)}, // compare(a, b) => int
"contains": &objects.UserFunction{Name: "contains", Value: FuncASSRB(strings.Contains)}, // contains(s, substr) => bool
"contains_any": &objects.UserFunction{Name: "contains_any", Value: FuncASSRB(strings.ContainsAny)}, // contains_any(s, chars) => bool
@ -24,11 +27,11 @@ var textModule = map[string]objects.Object{
"has_suffix": &objects.UserFunction{Name: "has_suffix", Value: FuncASSRB(strings.HasSuffix)}, // has_suffix(s, suffix) => bool
"index": &objects.UserFunction{Name: "index", Value: FuncASSRI(strings.Index)}, // index(s, substr) => int
"index_any": &objects.UserFunction{Name: "index_any", Value: FuncASSRI(strings.IndexAny)}, // index_any(s, chars) => int
"join": &objects.UserFunction{Name: "join", Value: FuncASsSRS(strings.Join)}, // join(arr, sep) => string
"join": &objects.UserFunction{Name: "join", Value: textJoin}, // join(arr, sep) => string
"last_index": &objects.UserFunction{Name: "last_index", Value: FuncASSRI(strings.LastIndex)}, // last_index(s, substr) => int
"last_index_any": &objects.UserFunction{Name: "last_index_any", Value: FuncASSRI(strings.LastIndexAny)}, // last_index_any(s, chars) => int
"repeat": &objects.UserFunction{Name: "repeat", Value: FuncASIRS(strings.Repeat)}, // repeat(s, count) => string
"replace": &objects.UserFunction{Value: textReplace}, // replace(s, old, new, n) => string
"repeat": &objects.UserFunction{Name: "repeat", Value: textRepeat}, // repeat(s, count) => string
"replace": &objects.UserFunction{Name: "replace", Value: textReplace}, // replace(s, old, new, n) => string
"split": &objects.UserFunction{Name: "split", Value: FuncASSRSs(strings.Split)}, // split(s, sep) => [string]
"split_after": &objects.UserFunction{Name: "split_after", Value: FuncASSRSs(strings.SplitAfter)}, // split_after(s, sep) => [string]
"split_after_n": &objects.UserFunction{Name: "split_after_n", Value: FuncASSIRSs(strings.SplitAfterN)}, // split_after_n(s, sep, n) => [string]
@ -43,13 +46,13 @@ var textModule = map[string]objects.Object{
"trim_space": &objects.UserFunction{Name: "trim_space", Value: FuncASRS(strings.TrimSpace)}, // trim_space(s) => string
"trim_suffix": &objects.UserFunction{Name: "trim_suffix", Value: FuncASSRS(strings.TrimSuffix)}, // trim_suffix(s, suffix) => string
"atoi": &objects.UserFunction{Name: "atoi", Value: FuncASRIE(strconv.Atoi)}, // atoi(str) => int/error
"format_bool": &objects.UserFunction{Value: textFormatBool}, // format_bool(b) => string
"format_float": &objects.UserFunction{Value: textFormatFloat}, // format_float(f, fmt, prec, bits) => string
"format_int": &objects.UserFunction{Value: textFormatInt}, // format_int(i, base) => string
"format_bool": &objects.UserFunction{Name: "format_bool", Value: textFormatBool}, // format_bool(b) => string
"format_float": &objects.UserFunction{Name: "format_float", Value: textFormatFloat}, // format_float(f, fmt, prec, bits) => string
"format_int": &objects.UserFunction{Name: "format_int", Value: textFormatInt}, // format_int(i, base) => string
"itoa": &objects.UserFunction{Name: "itoa", Value: FuncAIRS(strconv.Itoa)}, // itoa(i) => string
"parse_bool": &objects.UserFunction{Value: textParseBool}, // parse_bool(str) => bool/error
"parse_float": &objects.UserFunction{Value: textParseFloat}, // parse_float(str, bits) => float/error
"parse_int": &objects.UserFunction{Value: textParseInt}, // parse_int(str, base, bits) => int/error
"parse_bool": &objects.UserFunction{Name: "parse_bool", Value: textParseBool}, // parse_bool(str) => bool/error
"parse_float": &objects.UserFunction{Name: "parse_float", Value: textParseFloat}, // parse_float(str, bits) => float/error
"parse_int": &objects.UserFunction{Name: "parse_int", Value: textParseInt}, // parse_int(str, base, bits) => int/error
"quote": &objects.UserFunction{Name: "quote", Value: FuncASRS(strconv.Quote)}, // quote(str) => string
"unquote": &objects.UserFunction{Name: "unquote", Value: FuncASRSE(strconv.Unquote)}, // unquote(str) => string/error
}
@ -223,7 +226,12 @@ func textREReplace(args ...objects.Object) (ret objects.Object, err error) {
if err != nil {
ret = wrapError(err)
} else {
ret = &objects.String{Value: re.ReplaceAllString(s2, s3)}
s, ok := doTextRegexpReplace(re, s2, s3)
if !ok {
return nil, objects.ErrStringLimit
}
ret = &objects.String{Value: s}
}
return
@ -357,11 +365,106 @@ func textReplace(args ...objects.Object) (ret objects.Object, err error) {
return
}
ret = &objects.String{Value: strings.Replace(s1, s2, s3, i4)}
s, ok := doTextReplace(s1, s2, s3, i4)
if !ok {
err = objects.ErrStringLimit
return
}
ret = &objects.String{Value: s}
return
}
func textRepeat(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 2 {
return nil, objects.ErrWrongNumArguments
}
s1, ok := objects.ToString(args[0])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "first",
Expected: "string(compatible)",
Found: args[0].TypeName(),
}
}
i2, ok := objects.ToInt(args[1])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "second",
Expected: "int(compatible)",
Found: args[1].TypeName(),
}
}
if len(s1)*i2 > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: strings.Repeat(s1, i2)}, nil
}
func textJoin(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 2 {
return nil, objects.ErrWrongNumArguments
}
var slen int
var ss1 []string
switch arg0 := args[0].(type) {
case *objects.Array:
for idx, a := range arg0.Value {
as, ok := objects.ToString(a)
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: fmt.Sprintf("first[%d]", idx),
Expected: "string(compatible)",
Found: a.TypeName(),
}
}
slen += len(as)
ss1 = append(ss1, as)
}
case *objects.ImmutableArray:
for idx, a := range arg0.Value {
as, ok := objects.ToString(a)
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: fmt.Sprintf("first[%d]", idx),
Expected: "string(compatible)",
Found: a.TypeName(),
}
}
slen += len(as)
ss1 = append(ss1, as)
}
default:
return nil, objects.ErrInvalidArgumentType{
Name: "first",
Expected: "array",
Found: args[0].TypeName(),
}
}
s2, ok := objects.ToString(args[1])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "second",
Expected: "string(compatible)",
Found: args[1].TypeName(),
}
}
// make sure output length does not exceed the limit
if slen+len(s2)*(len(ss1)-1) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: strings.Join(ss1, s2)}, nil
}
func textFormatBool(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
err = objects.ErrWrongNumArguments
@ -583,3 +686,52 @@ func textParseInt(args ...objects.Object) (ret objects.Object, err error) {
return
}
// Modified implementation of strings.Replace
// to limit the maximum length of output string.
func doTextReplace(s, old, new string, n int) (string, bool) {
if old == new || n == 0 {
return s, true // avoid allocation
}
// Compute number of replacements.
if m := strings.Count(s, old); m == 0 {
return s, true // avoid allocation
} else if n < 0 || m < n {
n = m
}
// Apply replacements to buffer.
t := make([]byte, len(s)+n*(len(new)-len(old)))
w := 0
start := 0
for i := 0; i < n; i++ {
j := start
if len(old) == 0 {
if i > 0 {
_, wid := utf8.DecodeRuneInString(s[start:])
j += wid
}
} else {
j += strings.Index(s[start:], old)
}
ssj := s[start:j]
if w+len(ssj)+len(new) > tengo.MaxStringLen {
return "", false
}
w += copy(t[w:], ssj)
w += copy(t[w:], new)
start = j + len(old)
}
ss := s[start:]
if w+len(ss) > tengo.MaxStringLen {
return "", false
}
w += copy(t[w:], ss)
return string(t[0:w]), true
}

View file

@ -3,6 +3,7 @@ package stdlib
import (
"regexp"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
@ -141,7 +142,12 @@ func makeTextRegexp(re *regexp.Regexp) *objects.ImmutableMap {
return
}
ret = &objects.String{Value: re.ReplaceAllString(s1, s2)}
s, ok := doTextRegexpReplace(re, s1, s2)
if !ok {
return nil, objects.ErrStringLimit
}
ret = &objects.String{Value: s}
return
},
@ -193,3 +199,30 @@ func makeTextRegexp(re *regexp.Regexp) *objects.ImmutableMap {
},
}
}
// Size-limit checking implementation of regexp.ReplaceAllString.
func doTextRegexpReplace(re *regexp.Regexp, src, repl string) (string, bool) {
idx := 0
out := ""
for _, m := range re.FindAllStringSubmatchIndex(src, -1) {
var exp []byte
exp = re.ExpandString(exp, repl, src, m)
if len(out)+m[0]-idx+len(exp) > tengo.MaxStringLen {
return "", false
}
out += src[idx:m[0]] + string(exp)
idx = m[1]
}
if idx < len(src) {
if len(out)+len(src)-idx > tengo.MaxStringLen {
return "", false
}
out += src[idx:]
}
return string(out), true
}

View file

@ -1,27 +1,29 @@
package stdlib_test
import (
"regexp"
"testing"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
func TestTextRE(t *testing.T) {
// re_match(pattern, text)
for _, d := range []struct {
pattern string
text string
expected interface{}
pattern string
text string
}{
{"abc", "", false},
{"abc", "abc", true},
{"a", "abc", true},
{"b", "abc", true},
{"^a", "abc", true},
{"^b", "abc", false},
{"abc", ""},
{"abc", "abc"},
{"a", "abc"},
{"b", "abc"},
{"^a", "abc"},
{"^b", "abc"},
} {
module(t, "text").call("re_match", d.pattern, d.text).expect(d.expected, "pattern: %q, src: %q", d.pattern, d.text)
module(t, "text").call("re_compile", d.pattern).call("match", d.text).expect(d.expected, "patter: %q, src: %q", d.pattern, d.text)
expected := regexp.MustCompile(d.pattern).MatchString(d.text)
module(t, "text").call("re_match", d.pattern, d.text).expect(expected, "pattern: %q, src: %q", d.pattern, d.text)
module(t, "text").call("re_compile", d.pattern).call("match", d.text).expect(expected, "patter: %q, src: %q", d.pattern, d.text)
}
// re_find(pattern, text)
@ -106,58 +108,71 @@ func TestTextRE(t *testing.T) {
// re_replace(pattern, text, repl)
for _, d := range []struct {
pattern string
text string
repl string
expected interface{}
pattern string
text string
repl string
}{
{"a", "", "b", ""},
{"a", "a", "b", "b"},
{"a", "acac", "b", "bcbc"},
{"a", "acac", "123", "123c123c"},
{"ac", "acac", "99", "9999"},
{"ac$", "acac", "foo", "acfoo"},
{"a", "", "b"},
{"a", "a", "b"},
{"a", "acac", "b"},
{"b", "acac", "x"},
{"a", "acac", "123"},
{"ac", "acac", "99"},
{"ac$", "acac", "foo"},
{"a(b)", "ababab", "$1"},
{"a(b)(c)", "abcabcabc", "$2$1"},
{"(a(b)c)", "abcabcabc", "$1$2"},
{"(일(2)삼)", "일2삼12삼일23", "$1$2"},
{"((일)(2)3)", "일23\n일이3\n일23", "$1$2$3"},
{"(a(b)c)", "abc\nabc\nabc", "$1$2"},
} {
module(t, "text").call("re_replace", d.pattern, d.text, d.repl).expect(d.expected, "pattern: %q, text: %q, repl: %q", d.pattern, d.text, d.repl)
module(t, "text").call("re_compile", d.pattern).call("replace", d.text, d.repl).expect(d.expected, "pattern: %q, text: %q, repl: %q", d.pattern, d.text, d.repl)
expected := regexp.MustCompile(d.pattern).ReplaceAllString(d.text, d.repl)
module(t, "text").call("re_replace", d.pattern, d.text, d.repl).expect(expected, "pattern: %q, text: %q, repl: %q", d.pattern, d.text, d.repl)
module(t, "text").call("re_compile", d.pattern).call("replace", d.text, d.repl).expect(expected, "pattern: %q, text: %q, repl: %q", d.pattern, d.text, d.repl)
}
// re_split(pattern, text)
for _, d := range []struct {
pattern string
text string
expected interface{}
pattern string
text string
}{
{"a", "", ARR{""}},
{"a", "abcabc", ARR{"", "bc", "bc"}},
{"ab", "abcabc", ARR{"", "c", "c"}},
{"^a", "abcabc", ARR{"", "bcabc"}},
{"a", ""},
{"a", "abcabc"},
{"ab", "abcabc"},
{"^a", "abcabc"},
} {
module(t, "text").call("re_split", d.pattern, d.text).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text)
module(t, "text").call("re_compile", d.pattern).call("split", d.text).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text)
var expected []interface{}
for _, ex := range regexp.MustCompile(d.pattern).Split(d.text, -1) {
expected = append(expected, ex)
}
module(t, "text").call("re_split", d.pattern, d.text).expect(expected, "pattern: %q, text: %q", d.pattern, d.text)
module(t, "text").call("re_compile", d.pattern).call("split", d.text).expect(expected, "pattern: %q, text: %q", d.pattern, d.text)
}
// re_split(pattern, text, count))
for _, d := range []struct {
pattern string
text string
count int
expected interface{}
pattern string
text string
count int
}{
{"a", "", -1, ARR{""}},
{"a", "abcabc", -1, ARR{"", "bc", "bc"}},
{"ab", "abcabc", -1, ARR{"", "c", "c"}},
{"^a", "abcabc", -1, ARR{"", "bcabc"}},
{"a", "abcabc", 0, ARR{}},
{"a", "abcabc", 1, ARR{"abcabc"}},
{"a", "abcabc", 2, ARR{"", "bcabc"}},
{"a", "abcabc", 3, ARR{"", "bc", "bc"}},
{"b", "abcabc", 1, ARR{"abcabc"}},
{"b", "abcabc", 2, ARR{"a", "cabc"}},
{"b", "abcabc", 3, ARR{"a", "ca", "c"}},
{"a", "", -1},
{"a", "abcabc", -1},
{"ab", "abcabc", -1},
{"^a", "abcabc", -1},
{"a", "abcabc", 0},
{"a", "abcabc", 1},
{"a", "abcabc", 2},
{"a", "abcabc", 3},
{"b", "abcabc", 1},
{"b", "abcabc", 2},
{"b", "abcabc", 3},
} {
module(t, "text").call("re_split", d.pattern, d.text, d.count).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text)
module(t, "text").call("re_compile", d.pattern).call("split", d.text, d.count).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text)
var expected []interface{}
for _, ex := range regexp.MustCompile(d.pattern).Split(d.text, d.count) {
expected = append(expected, ex)
}
module(t, "text").call("re_split", d.pattern, d.text, d.count).expect(expected, "pattern: %q, text: %q", d.pattern, d.text)
module(t, "text").call("re_compile", d.pattern).call("split", d.text, d.count).expect(expected, "pattern: %q, text: %q", d.pattern, d.text)
}
}
@ -198,3 +213,34 @@ func TestText(t *testing.T) {
module(t, "text").call("parse_float", "-19.84", 64).expect(-19.84)
module(t, "text").call("parse_int", "-1984", 10, 64).expect(-1984)
}
func TestReplaceLimit(t *testing.T) {
curMaxStringLen := tengo.MaxStringLen
defer func() { tengo.MaxStringLen = curMaxStringLen }()
tengo.MaxStringLen = 12
module(t, "text").call("replace", "123456789012", "1", "x", -1).expect("x234567890x2")
module(t, "text").call("replace", "123456789012", "12", "x", -1).expect("x34567890x")
module(t, "text").call("replace", "123456789012", "1", "xy", -1).expectError()
module(t, "text").call("replace", "123456789012", "0", "xy", -1).expectError()
module(t, "text").call("replace", "123456789012", "012", "xyz", -1).expect("123456789xyz")
module(t, "text").call("replace", "123456789012", "012", "xyzz", -1).expectError()
module(t, "text").call("re_replace", "1", "123456789012", "x").expect("x234567890x2")
module(t, "text").call("re_replace", "12", "123456789012", "x").expect("x34567890x")
module(t, "text").call("re_replace", "1", "123456789012", "xy").expectError()
module(t, "text").call("re_replace", "1(2)", "123456789012", "x$1").expect("x234567890x2")
module(t, "text").call("re_replace", "(1)(2)", "123456789012", "$2$1").expect("213456789021")
module(t, "text").call("re_replace", "(1)(2)", "123456789012", "${2}${1}x").expectError()
}
func TestTextRepeat(t *testing.T) {
curMaxStringLen := tengo.MaxStringLen
defer func() { tengo.MaxStringLen = curMaxStringLen }()
tengo.MaxStringLen = 12
module(t, "text").call("repeat", "1234", "3").expect("123412341234")
module(t, "text").call("repeat", "1234", "4").expectError()
module(t, "text").call("repeat", "1", "12").expect("111111111111")
module(t, "text").call("repeat", "1", "13").expectError()
}

View file

@ -3,6 +3,7 @@ package stdlib
import (
"time"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
@ -867,7 +868,13 @@ func timesTimeFormat(args ...objects.Object) (ret objects.Object, err error) {
return
}
ret = &objects.String{Value: t1.Format(s2)}
s := t1.Format(s2)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
ret = &objects.String{Value: s}
return
}

View file

@ -1 +1,11 @@
package tengo
var (
// MaxStringLen is the maximum byte-length for string value.
// Note this limit applies to all compiler/VM instances in the process.
MaxStringLen = 2147483647
// MaxBytesLen is the maximum length for bytes value.
// Note this limit applies to all compiler/VM instances in the process.
MaxBytesLen = 2147483647
)