From b2a337d4bf9ced3209dc956d6a6b0fd2186f4c19 Mon Sep 17 00:00:00 2001 From: "Hagelstein, Kai" Date: Mon, 4 Mar 2019 16:08:36 +0100 Subject: [PATCH 01/11] Added function to disable template rendering abort on error --- global.go | 9 +++++++++ node.go | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 global.go diff --git a/global.go b/global.go new file mode 100644 index 0000000..1926b25 --- /dev/null +++ b/global.go @@ -0,0 +1,9 @@ +package jet + +var abortTemplateOnError = true + +// SetAbortTemplateOnError controls whether the template rendering process should be aborted when an error is encountered. +/// Default behavior is to abort the template rendering process when an error is encountered, so abortOnError == true. +func SetAbortTemplateOnError(abortOnError bool) { + abortTemplateOnError = abortOnError +} diff --git a/node.go b/node.go index 6ae3362..0abf378 100644 --- a/node.go +++ b/node.go @@ -61,7 +61,9 @@ func (node *NodeBase) error(err error) { } func (node *NodeBase) errorf(format string, v ...interface{}) { - panic(fmt.Errorf("Jet Runtime Error(%q:%d): %s", node.TemplateName, node.Line, fmt.Sprintf(format, v...))) + if abortTemplateOnError { + panic(fmt.Errorf("Jet Runtime Error(%q:%d): %s", node.TemplateName, node.Line, fmt.Sprintf(format, v...))) + } } // Type returns itself and provides an easy default implementation From 59195dff0fcfa495d925d191dfd05bd59df8ea76 Mon Sep 17 00:00:00 2001 From: "Hagelstein, Kai" Date: Thu, 14 Mar 2019 11:41:25 +0100 Subject: [PATCH 02/11] Added IsSet function --- func.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/func.go b/func.go index 8e6668b..7591ee2 100644 --- a/func.go +++ b/func.go @@ -26,6 +26,11 @@ type Arguments struct { argVal []reflect.Value } +// IsSet checks whether an argument is set or not. It behaves like the build-in isset function. +func (a *Arguments) IsSet(argumentIndex int) bool { + return a.runtime.isSet(a.argExpr[argumentIndex]) +} + // Get gets an argument by index. func (a *Arguments) Get(argumentIndex int) reflect.Value { if argumentIndex < len(a.argVal) { From 7e813ef90c23ed8b3338ba6f87468f7dd6e09ab2 Mon Sep 17 00:00:00 2001 From: Alexander Willing Date: Wed, 20 Mar 2019 16:30:21 +0100 Subject: [PATCH 03/11] make Ranger return interface{}, not reflect.Value --- eval.go | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/eval.go b/eval.go index 11d7c36..7c7d1f2 100644 --- a/eval.go +++ b/eval.go @@ -55,7 +55,7 @@ func (renderer RendererFunc) Render(r *Runtime) { // Ranger a value implementing a ranger interface is able to iterate on his value // and can be used directly in a range statement type Ranger interface { - Range() (reflect.Value, reflect.Value, bool) + Range() (interface{}, interface{}, bool) } type escapeeWriter struct { @@ -457,21 +457,21 @@ func (st *Runtime) executeList(list *ListNode) { if isSet { if isLet { if isKeyVal { - st.variables[node.Set.Left[0].String()] = indexValue - st.variables[node.Set.Left[1].String()] = rangeValue + st.variables[node.Set.Left[0].String()] = reflect.ValueOf(indexValue) + st.variables[node.Set.Left[1].String()] = reflect.ValueOf(rangeValue) } else { - st.variables[node.Set.Left[0].String()] = rangeValue + st.variables[node.Set.Left[0].String()] = reflect.ValueOf(rangeValue) } } else { if isKeyVal { - st.executeSet(node.Set.Left[0], indexValue) - st.executeSet(node.Set.Left[1], rangeValue) + st.executeSet(node.Set.Left[0], reflect.ValueOf(indexValue)) + st.executeSet(node.Set.Left[1], reflect.ValueOf(rangeValue)) } else { - st.executeSet(node.Set.Left[0], rangeValue) + st.executeSet(node.Set.Left[0], reflect.ValueOf(rangeValue)) } } } else { - st.context = rangeValue + st.context = reflect.ValueOf(rangeValue) } st.executeList(node.List) indexValue, rangeValue, end = ranger.Range() @@ -1586,11 +1586,11 @@ type sliceRanger struct { i int } -func (s *sliceRanger) Range() (index, value reflect.Value, end bool) { +func (s *sliceRanger) Range() (index, value interface{}, end bool) { s.i++ - index = reflect.ValueOf(&s.i).Elem() + index = s.i if s.i < s.len { - value = s.v.Index(s.i) + value = s.v.Index(s.i).Interface() return } pool_sliceRanger.Put(s) @@ -1602,8 +1602,9 @@ type chanRanger struct { v reflect.Value } -func (s *chanRanger) Range() (_, value reflect.Value, end bool) { - value, end = s.v.Recv() +func (s *chanRanger) Range() (_, value interface{}, end bool) { + _value, end := s.v.Recv() + value = _value.Interface() if end { pool_chanRanger.Put(s) } @@ -1617,10 +1618,11 @@ type mapRanger struct { i int } -func (s *mapRanger) Range() (index, value reflect.Value, end bool) { +func (s *mapRanger) Range() (index, value interface{}, end bool) { if s.i < s.len { - index = s.keys[s.i] - value = s.v.MapIndex(index) + _index := s.keys[s.i] + index = _index.Interface() + value = s.v.MapIndex(_index).Interface() s.i++ return } From 21e5963e201a5b4a27e14b8d2ee06f55a3300566 Mon Sep 17 00:00:00 2001 From: Alexander Willing Date: Wed, 27 Mar 2019 16:15:03 +0100 Subject: [PATCH 04/11] fix isset call with chain node argument --- eval.go | 39 ++++++++++++++++++++------------------- eval_test.go | 14 ++++++++++++++ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/eval.go b/eval.go index 7c7d1f2..943d25a 100644 --- a/eval.go +++ b/eval.go @@ -716,17 +716,8 @@ func (st *Runtime) isSet(node Node) bool { } } case NodeChain: - node := node.(*ChainNode) - var value = st.evalPrimaryExpressionGroup(node.Node) - if !value.IsValid() { - return false - } - for i := 0; i < len(node.Field); i++ { - value := getFieldOrMethodValue(node.Field[i], value) - if !value.IsValid() { - return false - } - } + resolved, _ := st.evalFieldAccessExpression(node.(*ChainNode)) + return resolved.IsValid() default: //todo: maybe work some edge cases if !(nodeType > beginExpressions && nodeType < endExpressions) { @@ -1104,14 +1095,9 @@ func (st *Runtime) evalBaseExpressionGroup(node Node) reflect.Value { } return resolved case NodeChain: - node := node.(*ChainNode) - var resolved = st.evalPrimaryExpressionGroup(node.Node) - for i := 0; i < len(node.Field); i++ { - fieldValue := getFieldOrMethodValue(node.Field[i], resolved) - if !fieldValue.IsValid() { - node.errorf("there is no field or method %q in %s", node.Field[i], getTypeString(resolved)) - } - resolved = fieldValue + resolved, err := st.evalFieldAccessExpression(node.(*ChainNode)) + if err != nil { + node.error(err) } return resolved case NodeNumber: @@ -1169,6 +1155,17 @@ func (st *Runtime) evalCommandExpression(node *CommandNode) (reflect.Value, bool return term, false } +func (st *Runtime) evalFieldAccessExpression(node *ChainNode) (reflect.Value, error) { + resolved := st.evalPrimaryExpressionGroup(node.Node) + for i := 0; i < len(node.Field); i++ { + resolved = getFieldOrMethodValue(node.Field[i], resolved) + if !resolved.IsValid() { + return resolved, fmt.Errorf("there is no field or method %q in %s", node.Field[i], getTypeString(resolved)) + } + } + return resolved, nil +} + type escapeWriter struct { rawWriter io.Writer safeWriter SafeWriter @@ -1446,6 +1443,10 @@ var cachedStructsMutex = sync.RWMutex{} var cachedStructsFieldIndex = map[reflect.Type]map[string][]int{} func getFieldOrMethodValue(key string, v reflect.Value) reflect.Value { + if !v.IsValid() { + return reflect.Value{} + } + value := getValue(key, v) if value.Kind() == reflect.Interface && !value.IsNil() { value = value.Elem() diff --git a/eval_test.go b/eval_test.go index 1110249..f7a07f5 100644 --- a/eval_test.go +++ b/eval_test.go @@ -460,6 +460,20 @@ func TestEvalBuiltinExpression(t *testing.T) { RunJetTest(t, data, nil, "LenExpression_1", `{{len("111")}}`, "3") RunJetTest(t, data, nil, "LenExpression_2", `{{isset(data)?len(data):0}}`, "0") RunJetTest(t, data, []string{"", "", "", ""}, "LenExpression_3", `{{len(.)}}`, "4") + data.Set( + "foo", map[string]interface{}{ + "asd": map[string]string{ + "bar": "baz", + }, + }, + ) + RunJetTest(t, data, nil, "IsSetExpression_1", `{{isset(foo)}}`, "true") + RunJetTest(t, data, nil, "IsSetExpression_2", `{{isset(foo.asd)}}`, "true") + RunJetTest(t, data, nil, "IsSetExpression_3", `{{isset(foo.asd.bar)}}`, "true") + RunJetTest(t, data, nil, "IsSetExpression_4", `{{isset(asd)}}`, "false") + RunJetTest(t, data, nil, "IsSetExpression_5", `{{isset(foo.bar)}}`, "false") + RunJetTest(t, data, nil, "IsSetExpression_6", `{{isset(foo.asd.foo)}}`, "false") + RunJetTest(t, data, nil, "IsSetExpression_7", `{{isset(foo.asd.bar.xyz)}}`, "false") } func TestEvalAutoescape(t *testing.T) { From a8defd51c21562ebe2af1fd254927cecf0ba3c73 Mon Sep 17 00:00:00 2001 From: Alexander Willing Date: Thu, 13 Jun 2019 16:08:26 +0200 Subject: [PATCH 05/11] support interface{} values in len() --- default.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.go b/default.go index ddde003..fc26062 100644 --- a/default.go +++ b/default.go @@ -65,7 +65,7 @@ func init() { a.RequireNumOfArguments("len", 1, 1) expression := a.Get(0) - if expression.Kind() == reflect.Ptr { + if expression.Kind() == reflect.Ptr || expression.Kind() == reflect.Interface { expression = expression.Elem() } From fdd3f82c7b461806e1a6b138bb1ed5bac634ec24 Mon Sep 17 00:00:00 2001 From: Alexander Willing Date: Wed, 19 Jun 2019 13:02:43 +0200 Subject: [PATCH 06/11] isset: return false when value is nil --- eval.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/eval.go b/eval.go index 943d25a..b5bc7a5 100644 --- a/eval.go +++ b/eval.go @@ -668,7 +668,7 @@ func (st *Runtime) isSet(node Node) bool { indexExpression := st.evalPrimaryExpressionGroup(node.Index) indexType := indexExpression.Type() - if baseExpression.Kind() == reflect.Ptr { + if baseExpression.Kind() == reflect.Ptr || baseExpression.Kind() == reflect.Interface { baseExpression = baseExpression.Elem() } @@ -682,7 +682,8 @@ func (st *Runtime) isSet(node Node) bool { node.errorf("%s is not assignable|convertible to map key %s", indexType.String(), key.String()) } } - return baseExpression.MapIndex(indexExpression).IsValid() + value := baseExpression.MapIndex(indexExpression) + return value.IsValid() && !value.IsNil() case reflect.Array, reflect.String, reflect.Slice: if canNumber(indexType.Kind()) { i := int(castInt64(indexExpression)) @@ -695,7 +696,8 @@ func (st *Runtime) isSet(node Node) bool { i := int(castInt64(indexExpression)) return i >= 0 && i < baseExpression.NumField() } else if indexType.Kind() == reflect.String { - return getFieldOrMethodValue(indexExpression.String(), baseExpression).IsValid() + fieldValue := getFieldOrMethodValue(indexExpression.String(), baseExpression) + return fieldValue.IsValid() && !fieldValue.IsNil() } else { node.errorf("non numeric value in index expression kind %s", baseExpression.Kind().String()) } @@ -703,21 +705,21 @@ func (st *Runtime) isSet(node Node) bool { node.errorf("indexing is not supported in value type %s", baseExpression.Kind().String()) } case NodeIdentifier: - if st.Resolve(node.String()).IsValid() == false { - return false - } + value := st.Resolve(node.String()) + return value.IsValid() && !value.IsNil() case NodeField: node := node.(*FieldNode) resolved := st.context for i := 0; i < len(node.Ident); i++ { resolved = getFieldOrMethodValue(node.Ident[i], resolved) - if !resolved.IsValid() { + if !resolved.IsValid() || resolved.IsNil() { return false } } case NodeChain: - resolved, _ := st.evalFieldAccessExpression(node.(*ChainNode)) - return resolved.IsValid() + node := node.(*ChainNode) + resolved, _ := st.evalFieldAccessExpression(node) + return resolved.IsValid() && !resolved.IsNil() default: //todo: maybe work some edge cases if !(nodeType > beginExpressions && nodeType < endExpressions) { From 93f8a90a308ccf1a3cf14f75cdfc734d9da28954 Mon Sep 17 00:00:00 2001 From: Alexander Willing Date: Wed, 19 Jun 2019 14:27:40 +0200 Subject: [PATCH 07/11] isset: only check for nil on values that can be nil --- eval.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/eval.go b/eval.go index b5bc7a5..b5d1d62 100644 --- a/eval.go +++ b/eval.go @@ -651,6 +651,20 @@ func (st *Runtime) evalPrimaryExpressionGroup(node Expression) reflect.Value { } func (st *Runtime) isSet(node Node) bool { + // notNil returns false when v.IsValid() == false + // or when v's kind can be nil and v.IsNil() == true + notNil := func(v reflect.Value) bool { + if !v.IsValid() { + return false + } + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return !v.IsNil() + default: + return true + } + } + nodeType := node.Type() switch nodeType { @@ -683,7 +697,7 @@ func (st *Runtime) isSet(node Node) bool { } } value := baseExpression.MapIndex(indexExpression) - return value.IsValid() && !value.IsNil() + return notNil(value) case reflect.Array, reflect.String, reflect.Slice: if canNumber(indexType.Kind()) { i := int(castInt64(indexExpression)) @@ -697,7 +711,8 @@ func (st *Runtime) isSet(node Node) bool { return i >= 0 && i < baseExpression.NumField() } else if indexType.Kind() == reflect.String { fieldValue := getFieldOrMethodValue(indexExpression.String(), baseExpression) - return fieldValue.IsValid() && !fieldValue.IsNil() + return notNil(fieldValue) + } else { node.errorf("non numeric value in index expression kind %s", baseExpression.Kind().String()) } @@ -706,20 +721,20 @@ func (st *Runtime) isSet(node Node) bool { } case NodeIdentifier: value := st.Resolve(node.String()) - return value.IsValid() && !value.IsNil() + return notNil(value) case NodeField: node := node.(*FieldNode) resolved := st.context for i := 0; i < len(node.Ident); i++ { resolved = getFieldOrMethodValue(node.Ident[i], resolved) - if !resolved.IsValid() || resolved.IsNil() { + if !notNil(resolved) { return false } } case NodeChain: node := node.(*ChainNode) resolved, _ := st.evalFieldAccessExpression(node) - return resolved.IsValid() && !resolved.IsNil() + return notNil(resolved) default: //todo: maybe work some edge cases if !(nodeType > beginExpressions && nodeType < endExpressions) { From 0287c20cf39cea4a577ed4c91f42797bf394878c Mon Sep 17 00:00:00 2001 From: Alexander Willing Date: Wed, 19 Jun 2019 14:53:40 +0200 Subject: [PATCH 08/11] make notNil a top-level function --- eval.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/eval.go b/eval.go index b5d1d62..b30c67e 100644 --- a/eval.go +++ b/eval.go @@ -650,21 +650,21 @@ func (st *Runtime) evalPrimaryExpressionGroup(node Expression) reflect.Value { return st.evalBaseExpressionGroup(node) } -func (st *Runtime) isSet(node Node) bool { - // notNil returns false when v.IsValid() == false - // or when v's kind can be nil and v.IsNil() == true - notNil := func(v reflect.Value) bool { - if !v.IsValid() { - return false - } - switch v.Kind() { - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: - return !v.IsNil() - default: - return true - } +// notNil returns false when v.IsValid() == false +// or when v's kind can be nil and v.IsNil() == true +func notNil(v reflect.Value) bool { + if !v.IsValid() { + return false + } + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return !v.IsNil() + default: + return true } +} +func (st *Runtime) isSet(node Node) bool { nodeType := node.Type() switch nodeType { From 6f7ec1b8f0c603d6740d016409a230c57829b100 Mon Sep 17 00:00:00 2001 From: Kabukky Date: Fri, 26 Jul 2019 10:54:29 +0200 Subject: [PATCH 09/11] Fixed context leak on panic When recovering from a panic, the runtime (state) was put back into the state pool. The state was then re-used by the next template execution. This led to the pre-crash-context (e. g. variables set on the state via Runtime.Set) being leaked into the next template execution. --- eval.go | 1 - 1 file changed, 1 deletion(-) diff --git a/eval.go b/eval.go index b30c67e..d629581 100644 --- a/eval.go +++ b/eval.go @@ -221,7 +221,6 @@ func (state *Runtime) Resolve(name string) reflect.Value { } func (st *Runtime) recover(err *error) { - pool_State.Put(st) if recovered := recover(); recovered != nil { var is bool if _, is = recovered.(runtime.Error); is { From 192d97b8d685b3c946b65bde42fa41fa0a297890 Mon Sep 17 00:00:00 2001 From: Kai Hagelstein Date: Fri, 2 Aug 2019 16:36:35 +0200 Subject: [PATCH 10/11] Update eval.go Changed how the state is reset after previous usage --- eval.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eval.go b/eval.go index d629581..ab0d728 100644 --- a/eval.go +++ b/eval.go @@ -221,6 +221,10 @@ func (state *Runtime) Resolve(name string) reflect.Value { } func (st *Runtime) recover(err *error) { + // reset state scope and context just to be safe (they might not be cleared properly if there was a panic while using the state) + st.scope = &scope{} + st.context = reflect.Value{} + pool_State.Put(st) if recovered := recover(); recovered != nil { var is bool if _, is = recovered.(runtime.Error); is { From 98d5663e6c76b083e7daed01018bfe807c9097b1 Mon Sep 17 00:00:00 2001 From: Alexander Willing Date: Mon, 9 Dec 2019 11:10:30 +0100 Subject: [PATCH 11/11] make sure index is in range before accessing an element of a reflect.Value --- eval.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eval.go b/eval.go index ab0d728..2763125 100644 --- a/eval.go +++ b/eval.go @@ -608,7 +608,12 @@ func (st *Runtime) evalPrimaryExpressionGroup(node Expression) reflect.Value { return baseExpression.MapIndex(indexExpression) case reflect.Array, reflect.String, reflect.Slice: if canNumber(indexType.Kind()) { - return baseExpression.Index(int(castInt64(indexExpression))) + index := int(castInt64(indexExpression)) + if 0 <= index && index < baseExpression.Len() { + return baseExpression.Index(index) + } else { + node.errorf("%s index out of range (index: %d, len: %d)", baseExpression.Kind().String(), index, baseExpression.Len()) + } } else { node.errorf("non numeric value in index expression kind %s", baseExpression.Kind().String()) }