1
votes

Trying to make a function that walks a struct recursively, and modifies any fields that are strings, based on on a certain tag.

Reflection is very tedious to work with. First time using it and having some troubles.

I'm getting a panic from one of my lines of code:

panic: reflect: Field of non-struct type

The panic comes from this line:

tf := vf.Type().Field(i)

I'm trying to get the type field so I can get a tag from it.

Here is the full function:

func Sanitize(s interface{}) error {
    v := reflect.ValueOf(s)

    // It's a pointer struct, convert to the value that it points to.
    if v.Kind() == reflect.Ptr && !v.IsNil() {
        v = v.Elem()
    }

    // Not a struct, return an error.
    if v.Kind() != reflect.Struct {
        return &InvalidSanitizerError{Type: reflect.TypeOf(s)}
    }

    for i := 0; i < v.NumField(); i++ {
        vf := v.Field(i)

        if vf.Kind() == reflect.Struct {
            // Recurse.
            err := Sanitize(v.Field(i).Interface())
            if err != nil {
                return err
            }

            // Move onto the next field.
            continue
        }


        if vf.Kind() == reflect.String {
            tf := vf.Type().Field(i) // <-- TROUBLE MAKER

            // Get the field tag value
            tag := tf.Tag.Get("sanitize")

            // Skip if tag is not defined or ignored
            if tag == "" || tag == "-" {
                continue
            }
            shouldSanitize, err := strconv.ParseBool(tag)
            if err != nil {
                return err
            }

            if shouldSanitize && vf.CanSet() {
                vf.SetString(policy.Sanitize(vf.String()))
            }
        }
    }

    return nil
}

This is an example of how the function should be used:

type User struct {
    Name string `sanitize:"true"`
    Bio *Bio
}

type Bio struct {
    Text string `sanitize:"true"`
}

func main() {
    user := &User{
        Name: "Lansana<script>alert('rekt');</script>",
        Bio: &Bio{
            Text: "Hello world</script>alert('rekt');</script>",
        },
    }
    if err := Sanitize(user); err != nil {
        panic(err)
    }

    fmt.Println(user.Name) // Lansana
    fmt.Println(user.Bio.Text) // Hello world
}

Any insight on the panic would be greatly appreciated.

1
Did you mean v.Type().Field(i)? - Andy Schweig
Ahhh darn, good catch. The panic is no more :) - Lansana Camara

1 Answers

4
votes

After working all day on this, this was the solution I finally came up with to walk a struct recursively and modify the value of all string values that have a specific tag.

The Sanitize function only allows pointer to structs, but the nested structs can be either pointers or values; it doesn't matter.

The example in my question will work with the function below, and it passes all my tests.

func Sanitize(s interface{}) error {
    if s == nil {
        return nil
    }

    val := reflect.ValueOf(s)

    // If it's an interface or a pointer, unwrap it.
    if val.Kind() == reflect.Ptr && val.Elem().Kind() == reflect.Struct {
        val = val.Elem()
    } else {
        return &InvalidStructError{message: "s must be a struct"}
    }

    valNumFields := val.NumField()

    for i := 0; i < valNumFields; i++ {
        field := val.Field(i)
        fieldKind := field.Kind()

        // Check if it's a pointer to a struct.
        if fieldKind == reflect.Ptr && field.Elem().Kind() == reflect.Struct {
            if field.CanInterface() {
                // Recurse using an interface of the field.
                err := Sanitize(field.Interface())
                if err != nil {
                    return err
                }
            }

            // Move onto the next field.
            continue
        }

        // Check if it's a struct value.
        if fieldKind == reflect.Struct {
            if field.CanAddr() && field.Addr().CanInterface() {
                // Recurse using an interface of the pointer value of the field.
                err := Sanitize(field.Addr().Interface())
                if err != nil {
                    return err
                }
            }

            // Move onto the next field.
            continue
        }

        // Check if it's a string or a pointer to a string.
        if fieldKind == reflect.String || (fieldKind == reflect.Ptr && field.Elem().Kind() == reflect.String) {
            typeField := val.Type().Field(i)

            // Get the field tag value.
            tag := typeField.Tag.Get("sanitize")

            // Skip if tag is not defined or ignored.
            if tag == "" || tag == "-" {
                continue
            }

            // Check if the tag is allowed.
            if tag != "true" && tag != "false" {
                return &InvalidTagError{message: "tag must be either 'true' or 'false'."}
            }

            // Parse it to a bool.
            shouldSanitize, err := strconv.ParseBool(tag)
            if err != nil {
                return err
            } else if !shouldSanitize {
                continue
            }

            // Set the string value to the sanitized string if it's allowed.
            // It should always be allowed at this point.
            if field.CanSet() {
                field.SetString(policy.Sanitize(field.String()))
            }

            continue
        }
    }

    return nil
}