diff --git a/compiler/compiler.go b/compiler/compiler.go index 141ea8f..021542c 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -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(), diff --git a/objects/builtin_convert.go b/objects/builtin_convert.go index 7d9a873..b5f2d05 100644 --- a/objects/builtin_convert.go +++ b/objects/builtin_convert.go @@ -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 } diff --git a/objects/builtin_json.go b/objects/builtin_json.go index c0810f7..b341365 100644 --- a/objects/builtin_json.go +++ b/objects/builtin_json.go @@ -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 } diff --git a/objects/builtin_print.go b/objects/builtin_print.go index c5fe36d..58f2261 100644 --- a/objects/builtin_print.go +++ b/objects/builtin_print.go @@ -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 } diff --git a/objects/bytes.go b/objects/bytes.go index 7d8d669..16b6168 100644 --- a/objects/bytes.go +++ b/objects/bytes.go @@ -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 } } diff --git a/objects/conversion.go b/objects/conversion.go index c78e8f1..f80090a 100644 --- a/objects/conversion.go +++ b/objects/conversion.go @@ -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 diff --git a/objects/errors.go b/objects/errors.go index e401231..bcd480a 100644 --- a/objects/errors.go +++ b/objects/errors.go @@ -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 diff --git a/objects/string.go b/objects/string.go index 6a53b44..c25b050 100644 --- a/objects/string.go +++ b/objects/string.go @@ -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 } } diff --git a/runtime/vm_assignment_test.go b/runtime/vm_assignment_test.go index 4a122fc..2ec4201 100644 --- a/runtime/vm_assignment_test.go +++ b/runtime/vm_assignment_test.go @@ -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 diff --git a/runtime/vm_builtin_test.go b/runtime/vm_builtin_test.go index 2dad8f8..b5198b2 100644 --- a/runtime/vm_builtin_test.go +++ b/runtime/vm_builtin_test.go @@ -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") +} diff --git a/stdlib/func_typedefs.go b/stdlib/func_typedefs.go index a85619f..26c7ddd 100644 --- a/stdlib/func_typedefs.go +++ b/stdlib/func_typedefs.go @@ -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 } } diff --git a/stdlib/os.go b/stdlib/os.go index e68d510..a7890cc 100644 --- a/stdlib/os.go +++ b/stdlib/os.go @@ -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 diff --git a/stdlib/os_exec.go b/stdlib/os_exec.go index 809c581..5274c36 100644 --- a/stdlib/os_exec.go +++ b/stdlib/os_exec.go @@ -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 diff --git a/stdlib/os_file.go b/stdlib/os_file.go index 4fc41dd..ee9f625 100644 --- a/stdlib/os_file.go +++ b/stdlib/os_file.go @@ -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 diff --git a/stdlib/os_process.go b/stdlib/os_process.go index 0f4a9f7..801ccde 100644 --- a/stdlib/os_process.go +++ b/stdlib/os_process.go @@ -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 diff --git a/stdlib/os_test.go b/stdlib/os_test.go index 80d0121..012f584 100644 --- a/stdlib/os_test.go +++ b/stdlib/os_test.go @@ -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() +} diff --git a/stdlib/rand.go b/stdlib/rand.go index 248d8e7..6efe1de 100644 --- a/stdlib/rand.go +++ b/stdlib/rand.go @@ -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 diff --git a/stdlib/text.go b/stdlib/text.go index 053bebf..9f9770b 100644 --- a/stdlib/text.go +++ b/stdlib/text.go @@ -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 +} diff --git a/stdlib/text_regexp.go b/stdlib/text_regexp.go index 3fb8b3b..16f135b 100644 --- a/stdlib/text_regexp.go +++ b/stdlib/text_regexp.go @@ -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 +} diff --git a/stdlib/text_test.go b/stdlib/text_test.go index 5509fa6..3a625a8 100644 --- a/stdlib/text_test.go +++ b/stdlib/text_test.go @@ -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() +} diff --git a/stdlib/times.go b/stdlib/times.go index 16d6d14..111c877 100644 --- a/stdlib/times.go +++ b/stdlib/times.go @@ -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 } diff --git a/tengo.go b/tengo.go index f3bf76e..a883bbd 100644 --- a/tengo.go +++ b/tengo.go @@ -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 +)