diff --git a/builtin/lib.go b/builtin/lib.go index 579393161..6f6a3b6cd 100644 --- a/builtin/lib.go +++ b/builtin/lib.go @@ -421,10 +421,14 @@ func get(params ...any) (out any, err error) { fieldName := i.(string) value := v.FieldByNameFunc(func(name string) bool { field, _ := v.Type().FieldByName(name) - if field.Tag.Get("expr") == fieldName { + switch field.Tag.Get("expr") { + case "-": + return false + case fieldName: return true + default: + return name == fieldName } - return name == fieldName }) if value.IsValid() { return value.Interface(), nil diff --git a/checker/nature/utils.go b/checker/nature/utils.go index 179459874..c1551546c 100644 --- a/checker/nature/utils.go +++ b/checker/nature/utils.go @@ -6,11 +6,15 @@ import ( "github.com/expr-lang/expr/internal/deref" ) -func fieldName(field reflect.StructField) string { - if taggedName := field.Tag.Get("expr"); taggedName != "" { - return taggedName +func fieldName(field reflect.StructField) (string, bool) { + switch taggedName := field.Tag.Get("expr"); taggedName { + case "-": + return "", false + case "": + return field.Name, true + default: + return taggedName, true } - return field.Name } func fetchField(t reflect.Type, name string) (reflect.StructField, bool) { @@ -23,7 +27,7 @@ func fetchField(t reflect.Type, name string) (reflect.StructField, bool) { for i := 0; i < t.NumField(); i++ { field := t.Field(i) // Search all fields, even embedded structs. - if fieldName(field) == name { + if n, ok := fieldName(field); ok && n == name { return field, true } } @@ -69,7 +73,11 @@ func StructFields(t reflect.Type) map[string]Nature { } } - table[fieldName(f)] = Nature{ + name, ok := fieldName(f) + if !ok { + continue + } + table[name] = Nature{ Type: f.Type, FieldIndex: f.Index, } diff --git a/expr_test.go b/expr_test.go index 57d2cfbbb..32ec6dfb8 100644 --- a/expr_test.go +++ b/expr_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "reflect" + "strings" "sync" "testing" "time" @@ -139,7 +140,86 @@ func ExampleEnv_tagged_field_names() { fmt.Printf("%v", output) - // Output : Hello World + // Output: Hello World +} + +func ExampleEnv_hidden_tagged_field_names() { + type Internal struct { + Visible string + Hidden string `expr:"-"` + } + type environment struct { + Visible string + Hidden string `expr:"-"` + HiddenInternal Internal `expr:"-"` + VisibleInternal Internal + } + + env := environment{ + Hidden: "First level secret", + HiddenInternal: Internal{ + Visible: "Second level secret", + Hidden: "Also hidden", + }, + VisibleInternal: Internal{ + Visible: "Not a secret", + Hidden: "Hidden too", + }, + } + + hiddenValues := []string{ + `Hidden`, + `HiddenInternal`, + `HiddenInternal.Visible`, + `HiddenInternal.Hidden`, + `VisibleInternal["Hidden"]`, + } + for _, expression := range hiddenValues { + output, err := expr.Eval(expression, env) + if err == nil || !strings.Contains(err.Error(), "cannot fetch") { + fmt.Printf("unexpected output: %v; err: %v\n", output, err) + return + } + fmt.Printf("%q is hidden as expected\n", expression) + } + + visibleValues := []string{ + `Visible`, + `VisibleInternal`, + `VisibleInternal["Visible"]`, + } + for _, expression := range visibleValues { + _, err := expr.Eval(expression, env) + if err != nil { + fmt.Printf("unexpected error: %v\n", err) + return + } + fmt.Printf("%q is visible as expected\n", expression) + } + + testWithIn := []string{ + `not ("Hidden" in $env)`, + `"Visible" in $env`, + `not ("Hidden" in VisibleInternal)`, + `"Visible" in VisibleInternal`, + } + for _, expression := range testWithIn { + val, err := expr.Eval(expression, env) + shouldBeTrue, ok := val.(bool) + if err != nil || !ok || !shouldBeTrue { + fmt.Printf("unexpected result; value: %v; error: %v\n", val, err) + return + } + } + + // Output: "Hidden" is hidden as expected + // "HiddenInternal" is hidden as expected + // "HiddenInternal.Visible" is hidden as expected + // "HiddenInternal.Hidden" is hidden as expected + // "VisibleInternal[\"Hidden\"]" is hidden as expected + // "Visible" is visible as expected + // "VisibleInternal" is visible as expected + // "VisibleInternal[\"Visible\"]" is visible as expected } func ExampleAsKind() { @@ -529,7 +609,7 @@ func ExamplePatch() { } fmt.Printf("%v", output) - // Output : Hello, you, world! + // Output: Hello, you, world! } func ExampleWithContext() { @@ -2765,3 +2845,20 @@ func TestMemoryBudget(t *testing.T) { }) } } + +func TestIssue807(t *testing.T) { + type MyStruct struct { + nonExported string + } + out, err := expr.Eval(` "nonExported" in $env `, MyStruct{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + b, ok := out.(bool) + if !ok { + t.Fatalf("expected boolean type, got %T: %v", b, b) + } + if b { + t.Fatalf("expected 'in' operator to return false for unexported field") + } +} diff --git a/vm/runtime/runtime.go b/vm/runtime/runtime.go index cd48a280d..fa14c4d0a 100644 --- a/vm/runtime/runtime.go +++ b/vm/runtime/runtime.go @@ -65,10 +65,14 @@ func Fetch(from, i any) any { fieldName := i.(string) value := v.FieldByNameFunc(func(name string) bool { field, _ := v.Type().FieldByName(name) - if field.Tag.Get("expr") == fieldName { + switch field.Tag.Get("expr") { + case "-": + return false + case fieldName: return true + default: + return name == fieldName } - return name == fieldName }) if value.IsValid() { return value.Interface() @@ -213,7 +217,11 @@ func In(needle any, array any) bool { if !n.IsValid() || n.Kind() != reflect.String { panic(fmt.Sprintf("cannot use %T as field name of %T", needle, array)) } - value := v.FieldByName(n.String()) + field, ok := v.Type().FieldByName(n.String()) + if !ok || !field.IsExported() || field.Tag.Get("expr") == "-" { + return false + } + value := v.FieldByIndex(field.Index) if value.IsValid() { return true }