Support filtering nested structures by tag

Allow tagging a nested property structure with
`blueprint:"filter(key:\"value\")"`, which will only allow property
assignments to properites in the nested structure that are tagged
with `key:"value"`.  Moving the filter into Blueprint instead of
the project build logic allows more reliable and consistent error
messages.

Change-Id: I06bc673dde647776fc5552673bdc0cdcd7216462
diff --git a/unpack.go b/unpack.go
index 37bc158..3e9fe00 100644
--- a/unpack.go
+++ b/unpack.go
@@ -16,10 +16,12 @@
 
 import (
 	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+
 	"github.com/google/blueprint/parser"
 	"github.com/google/blueprint/proptools"
-	"reflect"
-	"strings"
 )
 
 type packedProperty struct {
@@ -47,7 +49,7 @@
 			panic("properties must be a pointer to a struct")
 		}
 
-		newErrs := unpackStructValue("", propertiesValue, propertyMap)
+		newErrs := unpackStructValue("", propertiesValue, propertyMap, "", "")
 		errs = append(errs, newErrs...)
 
 		if len(errs) >= maxErrors {
@@ -118,7 +120,7 @@
 }
 
 func unpackStructValue(namePrefix string, structValue reflect.Value,
-	propertyMap map[string]*packedProperty) []error {
+	propertyMap map[string]*packedProperty, filterKey, filterValue string) []error {
 
 	structType := structValue.Type()
 
@@ -169,6 +171,7 @@
 				panic(fmt.Errorf("field %s contains a non-struct pointer",
 					field.Name))
 			}
+
 		case reflect.Int, reflect.Uint:
 			if !hasTag(field, "blueprint", "mutated") {
 				panic(fmt.Errorf(`int field %s must be tagged blueprint:"mutated"`, field.Name))
@@ -187,18 +190,33 @@
 			continue
 		}
 
-		var newErrs []error
+		packedProperty.unpacked = true
 
 		if hasTag(field, "blueprint", "mutated") {
 			errs = append(errs,
-				fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName))
+				&Error{
+					Err: fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName),
+					Pos: packedProperty.property.Pos,
+				})
 			if len(errs) >= maxErrors {
 				return errs
 			}
 			continue
 		}
 
-		packedProperty.unpacked = true
+		if filterKey != "" && !hasTag(field, filterKey, filterValue) {
+			errs = append(errs,
+				&Error{
+					Err: fmt.Errorf("filtered field %s cannot be set in a Blueprint file", propertyName),
+					Pos: packedProperty.property.Pos,
+				})
+			if len(errs) >= maxErrors {
+				return errs
+			}
+			continue
+		}
+
+		var newErrs []error
 
 		switch kind := fieldValue.Kind(); kind {
 		case reflect.Bool:
@@ -207,13 +225,29 @@
 			newErrs = unpackString(fieldValue, packedProperty.property)
 		case reflect.Slice:
 			newErrs = unpackSlice(fieldValue, packedProperty.property)
-		case reflect.Struct:
-			newErrs = unpackStruct(propertyName+".", fieldValue,
-				packedProperty.property, propertyMap)
 		case reflect.Ptr, reflect.Interface:
-			structValue := fieldValue.Elem()
-			newErrs = unpackStruct(propertyName+".", structValue,
-				packedProperty.property, propertyMap)
+			fieldValue = fieldValue.Elem()
+			fallthrough
+		case reflect.Struct:
+			localFilterKey, localFilterValue := filterKey, filterValue
+			if k, v, err := hasFilter(field); err != nil {
+				errs = append(errs, err)
+				if len(errs) >= maxErrors {
+					return errs
+				}
+			} else if k != "" {
+				if filterKey != "" {
+					errs = append(errs, fmt.Errorf("nested filter tag not supported on field %q",
+						field.Name))
+					if len(errs) >= maxErrors {
+						return errs
+					}
+				} else {
+					localFilterKey, localFilterValue = k, v
+				}
+			}
+			newErrs = unpackStruct(propertyName+".", fieldValue,
+				packedProperty.property, propertyMap, localFilterKey, localFilterValue)
 		}
 		errs = append(errs, newErrs...)
 		if len(errs) >= maxErrors {
@@ -273,8 +307,8 @@
 }
 
 func unpackStruct(namePrefix string, structValue reflect.Value,
-	property *parser.Property,
-	propertyMap map[string]*packedProperty) []error {
+	property *parser.Property, propertyMap map[string]*packedProperty,
+	filterKey, filterValue string) []error {
 
 	if property.Value.Type != parser.Map {
 		return []error{
@@ -289,7 +323,7 @@
 		return errs
 	}
 
-	return unpackStructValue(namePrefix, structValue, propertyMap)
+	return unpackStructValue(namePrefix, structValue, propertyMap, filterKey, filterValue)
 }
 
 func hasTag(field reflect.StructField, name, value string) bool {
@@ -302,3 +336,29 @@
 
 	return false
 }
+
+func hasFilter(field reflect.StructField) (k, v string, err error) {
+	tag := field.Tag.Get("blueprint")
+	for _, entry := range strings.Split(tag, ",") {
+		if strings.HasPrefix(entry, "filter") {
+			if !strings.HasPrefix(entry, "filter(") || !strings.HasSuffix(entry, ")") {
+				return "", "", fmt.Errorf("unexpected format for filter %q: missing ()", entry)
+			}
+			entry = strings.TrimPrefix(entry, "filter(")
+			entry = strings.TrimSuffix(entry, ")")
+
+			s := strings.Split(entry, ":")
+			if len(s) != 2 {
+				return "", "", fmt.Errorf("unexpected format for filter %q: expected single ':'", entry)
+			}
+			k = s[0]
+			v, err = strconv.Unquote(s[1])
+			if err != nil {
+				return "", "", fmt.Errorf("unexpected format for filter %q: %s", entry, err.Error())
+			}
+			return k, v, nil
+		}
+	}
+
+	return "", "", nil
+}
diff --git a/unpack_test.go b/unpack_test.go
index bd4dd8f..393631c 100644
--- a/unpack_test.go
+++ b/unpack_test.go
@@ -16,15 +16,19 @@
 
 import (
 	"bytes"
-	"github.com/google/blueprint/parser"
-	"github.com/google/blueprint/proptools"
+	"fmt"
 	"reflect"
 	"testing"
+	"text/scanner"
+
+	"github.com/google/blueprint/parser"
+	"github.com/google/blueprint/proptools"
 )
 
 var validUnpackTestCases = []struct {
 	input  string
 	output interface{}
+	errs   []error
 }{
 	{`
 		m {
@@ -36,6 +40,7 @@
 		}{
 			Name: "abc",
 		},
+		nil,
 	},
 
 	{`
@@ -48,6 +53,7 @@
 		}{
 			IsGood: true,
 		},
+		nil,
 	},
 
 	{`
@@ -61,6 +67,7 @@
 		}{
 			Stuff: []string{"asdf", "jkl;", "qwert", "uiop", "bnm,"},
 		},
+		nil,
 	},
 
 	{`
@@ -79,6 +86,7 @@
 				Name: "abc",
 			},
 		},
+		nil,
 	},
 
 	{`
@@ -95,6 +103,7 @@
 				Name: "def",
 			},
 		},
+		nil,
 	},
 
 	{`
@@ -119,6 +128,64 @@
 			Bar: false,
 			Baz: []string{"def", "ghi"},
 		},
+		nil,
+	},
+
+	{`
+		m {
+			nested: {
+				foo: "abc",
+			},
+			bar: false,
+			baz: ["def", "ghi"],
+		}
+		`,
+		struct {
+			Nested struct {
+				Foo string `allowNested:"true"`
+			} `blueprint:"filter(allowNested:\"true\")"`
+			Bar bool
+			Baz []string
+		}{
+			Nested: struct {
+				Foo string `allowNested:"true"`
+			}{
+				Foo: "abc",
+			},
+			Bar: false,
+			Baz: []string{"def", "ghi"},
+		},
+		nil,
+	},
+
+	{`
+		m {
+			nested: {
+				foo: "abc",
+			},
+			bar: false,
+			baz: ["def", "ghi"],
+		}
+		`,
+		struct {
+			Nested struct {
+				Foo string
+			} `blueprint:"filter(allowNested:\"true\")"`
+			Bar bool
+			Baz []string
+		}{
+			Nested: struct{ Foo string }{
+				Foo: "",
+			},
+			Bar: false,
+			Baz: []string{"def", "ghi"},
+		},
+		[]error{
+			&Error{
+				Err: fmt.Errorf("filtered field nested.foo cannot be set in a Blueprint file"),
+				Pos: scanner.Position{"", 27, 4, 8},
+			},
+		},
 	},
 }
 
@@ -139,13 +206,18 @@
 		properties := proptools.CloneProperties(reflect.ValueOf(testCase.output))
 		proptools.ZeroProperties(properties.Elem())
 		_, errs = unpackProperties(module.Properties, properties.Interface())
-		if len(errs) != 0 {
+		if len(errs) != 0 && len(testCase.errs) == 0 {
 			t.Errorf("test case: %s", testCase.input)
 			t.Errorf("unexpected unpack errors:")
 			for _, err := range errs {
 				t.Errorf("  %s", err)
 			}
 			t.FailNow()
+		} else if !reflect.DeepEqual(errs, testCase.errs) {
+			t.Errorf("test case: %s", testCase.input)
+			t.Errorf("incorrect errors:")
+			t.Errorf("  expected: %+v", testCase.errs)
+			t.Errorf("       got: %+v", errs)
 		}
 
 		output := properties.Elem().Interface()