Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fx-annotate] Verify invalid tags passed to fx.Annotate #835 #1051

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions annotated.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,75 @@ type paramTagsAnnotation struct {
}

var _ Annotation = paramTagsAnnotation{}
var (
errTagSyntaxSpace = errors.New(`multiple tags are not separated by space`)
errTagKeySyntax = errors.New("tag key is invalid, Use group, name or optional as tag keys")
errTagValueSyntaxQuote = errors.New(`tag value should start with double quote. i.e. key:"value" `)
errTagValueSyntaxEndingQuote = errors.New(`tag value should end in double quote. i.e. key:"value" `)
)

// Collections of key value pairs within a tag should be separated by a space.
// Eg: `group:"some" optional:"true"`.
func verifyTagsSpaceSeparated(tagIdx int, tag string) error {
if tagIdx > 0 && tag != "" && tag[0] != ' ' {
return errTagSyntaxSpace
}
return nil
}

// verify tag values are delimited with double quotes.
func verifyValueQuote(value string) (string, error) {
// starting quote should be a double quote
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just check if the value string is prefixed and suffixed with double quotes. Could the step to validate tag values is within quotes be replaced by counting the total number of double quotes and ensuring that is exactly 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The total number of double quotes can be more than 2, as it's valid for a struct string to have quotes in it.
key:"val/"ue" would be a valid tag with more than 2 double quotes. Hence, I don't think it'd be possible unless I misunderstood?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah understood that this approach does not work because value actually contains the rest of the tag strings.

if value[0] != '"' {
return "", errTagValueSyntaxQuote
}
// validate tag value is within quotes
i := 1
for i < len(value) && value[i] != '"' {
if value[i] == '\\' {
i++
}
i++
}
if i >= len(value) {
return "", errTagValueSyntaxEndingQuote
}
return value[i+1:], nil

}

// Check whether the tag follows valid struct.
// format and returns an error if it's invalid. (i.e. not following
// tag:"value" space-separated list )
// Currently dig accepts only 'name', 'group', 'optional' as valid tag keys.
func verifyAnnotateTag(tag string) error {
tagIdx := 0
validKeys := map[string]struct{}{"group": {}, "optional": {}, "name": {}}
for ; tag != ""; tagIdx++ {
if err := verifyTagsSpaceSeparated(tagIdx, tag); err != nil {
return err
}
i := 0
if strings.TrimSpace(tag) == "" {
return nil
}
// parsing the key i.e. till reaching colon :
for i < len(tag) && tag[i] != ':' {
i++
}
key := strings.TrimSpace(tag[:i])
if _, ok := validKeys[key]; !ok {
return errTagKeySyntax
}
value, err := verifyValueQuote(tag[i+1:])
if err != nil {
return err
}
tag = value
}
return nil

}

// Given func(T1, T2, T3, ..., TN), this generates a type roughly
// equivalent to,
Expand All @@ -159,11 +228,19 @@ var _ Annotation = paramTagsAnnotation{}
//
// If there has already been a ParamTag that was applied, this
// will return an error.
//
// If the tag is invalid and has mismatched quotation for example,
// (`tag_name:"tag_value') , this will return an error.

func (pt paramTagsAnnotation) apply(ann *annotated) error {
if len(ann.ParamTags) > 0 {
return errors.New("cannot apply more than one line of ParamTags")
}
for _, tag := range pt.tags {
if err := verifyAnnotateTag(tag); err != nil {
return err
}
}
ann.ParamTags = pt.tags
return nil
}
Expand Down Expand Up @@ -285,10 +362,18 @@ var _ Annotation = resultTagsAnnotation{}
//
// If there has already been a ResultTag that was applied, this
// will return an error.
//
// If the tag is invalid and has mismatched quotation for example,
// (`tag_name:"tag_value') , this will return an error.
func (rt resultTagsAnnotation) apply(ann *annotated) error {
if len(ann.ResultTags) > 0 {
return errors.New("cannot apply more than one line of ResultTags")
}
for _, tag := range rt.tags {
if err := verifyAnnotateTag(tag); err != nil {
return err
}
}
ann.ResultTags = rt.tags
return nil
}
Expand Down
125 changes: 125 additions & 0 deletions annotated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,131 @@ func TestAnnotate(t *testing.T) {
})
}

func TestAnnotateApplyFail(t *testing.T) {
type a struct{}
type b struct{ a *a }
newA := func() *a { return &a{} }
newB := func(a *a) *b {
return &b{a}
}

var (
errTagSyntaxSpace = `multiple tags are not separated by space`
errTagKeySyntax = "tag key is invalid, Use group, name or optional as tag keys"
errTagValueSyntaxQuote = `tag value should start with double quote. i.e. key:"value" `
errTagValueSyntaxEndingQuote = `tag value should end in double quote. i.e. key:"value" `
)
tests := []struct {
give string
wantErr string
giveAnnotationParam fx.Annotation
giveAnnotationResult fx.Annotation
}{
{
give: "Tags value invalid ending quote",
wantErr: errTagValueSyntaxEndingQuote,
giveAnnotationParam: fx.ParamTags(`name:"something'`),
giveAnnotationResult: fx.ResultTags(`name:"something'`),
},
{
give: "Tags value wrong starting quote",
wantErr: errTagValueSyntaxQuote,
giveAnnotationParam: fx.ParamTags(`name:"something" optional:'true"`),
giveAnnotationResult: fx.ResultTags(`name:"something" optional:'true"`),
},
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The set of tests for ResultTags looks similar to for ParamTags, can we de-duplicate these tests?

give: "Tags multiple tags not separated by space",
wantErr: errTagSyntaxSpace,
giveAnnotationParam: fx.ParamTags(`name:"something"group:"something"`),
giveAnnotationResult: fx.ResultTags(`name:"something"group:"something"`),
},
{
give: "Tags key not equal to group, name or optional",
wantErr: errTagKeySyntax,
giveAnnotationParam: fx.ParamTags(`name1:"something"`),
giveAnnotationResult: fx.ResultTags(`name1:"something"`),
},
{
give: "Tags key empty",
wantErr: errTagKeySyntax,
giveAnnotationParam: fx.ParamTags(`:"something"`),
giveAnnotationResult: fx.ResultTags(`:"something"`),
},
}
for _, tt := range tests {
t.Run("Param "+tt.give, func(t *testing.T) {
app := NewForTest(t,
fx.Provide(
fx.Annotate(
newA,
tt.giveAnnotationParam,
),
),
fx.Invoke(newB),
)
assert.ErrorContains(t, app.Err(), tt.wantErr)
})
t.Run("Result "+tt.give, func(t *testing.T) {
app := NewForTest(t,
fx.Provide(
fx.Annotate(
newA,
tt.giveAnnotationResult,
),
),
fx.Invoke(newB),
)
assert.ErrorContains(t, app.Err(), tt.wantErr)
})
}
}

func TestAnnotateApplySuccess(t *testing.T) {
type a struct{}
type b struct{ a *a }
newA := func() *a { return &a{} }
newB := func(a *a) *b {
return &b{a}
}

tests := []struct {
give string
giveAnnotationParam fx.Annotation
giveAnnotationResult fx.Annotation
}{
{
give: "ParamTags Tag Empty",
giveAnnotationParam: fx.ParamTags(` `),
giveAnnotationResult: fx.ResultTags(` `),
},
{
give: "ParamTags Tag Empty with extra spaces",
giveAnnotationParam: fx.ParamTags(`name:"versionNum"`, ` `),
giveAnnotationResult: fx.ResultTags(` `, `group:"versionNum"`),
},
{
give: "ParamTags Tag with \\ ",
giveAnnotationParam: fx.ParamTags(`name:"version\\Num"`, ` `),
giveAnnotationResult: fx.ResultTags(``, `group:"version\\Num"`),
},
}
for _, tt := range tests {
t.Run(tt.give, func(t *testing.T) {
app := NewForTest(t,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you're also using t.Parallel() at the top-level. You'll also need to call t.Parallel() within these subtests if you want them to run in parallel.

fx.Provide(
fx.Annotate(
newA,
tt.giveAnnotationParam,
tt.giveAnnotationResult,
),
),
fx.Invoke(newB),
)
require.NoError(t, app.Err())
})
}

}
func assertApp(
t *testing.T,
app interface {
Expand Down
4 changes: 2 additions & 2 deletions module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,8 @@ func TestModuleFailures(t *testing.T) {
app := NewForTest(t,
fx.Module("module",
fx.Provide(fx.Annotate(newA,
fx.ParamTags(`"name:"A1"`),
fx.ParamTags(`"name:"A2"`),
fx.ParamTags(`name:"A1"`),
fx.ParamTags(`name:"A2"`),
)),
),
)
Expand Down