Skip to content

Commit

Permalink
Fix parsing of environment variables to prevent object parsing. elast…
Browse files Browse the repository at this point in the history
  • Loading branch information
blakerouse committed Jan 16, 2020
1 parent 7e7dbca commit c9ab797
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 11 deletions.
65 changes: 60 additions & 5 deletions internal/parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,33 @@ import (
"unicode"
)

// ParserConfig allows enabling and disabling parser features.
type ParserConfig struct {
Array bool
Object bool
StringDQuote bool
StringSQuote bool
}

// DefaultParserConfig is the default config with all parser features enabled.
var DefaultParserConfig = ParserConfig{
Array: true,
Object: true,
StringDQuote: true,
StringSQuote: true,
}

// EnvParserConfig is configuration for parser when the value comes from environmental variable.
var EnvParserConfig = ParserConfig{
Array: true,
Object: false,
StringDQuote: true,
StringSQuote: true,
}

type flagParser struct {
input string
cfg ParserConfig
}

// stopSet definitions for handling unquoted strings
Expand All @@ -52,7 +77,25 @@ const (
// In addition, top-level values can be separated by ',' to build arrays
// without having to use [].
func Value(content string) (interface{}, error) {
p := &flagParser{strings.TrimSpace(content)}
return ValueWithConfig(content, DefaultParserConfig)
}

// ValueWithConfig parses command line arguments, supporting
// boolean, numbers, strings, arrays, objects when enabled.
//
// The parser implements a superset of JSON, but only a subset of YAML by
// allowing for arrays and objects having a trailing comma. In addition 3
// strings types are supported:
//
// 1. single quoted string (no unescaping of any characters)
// 2. double quoted strings (characters are escaped)
// 3. strings without quotes. String parsing stops in
// special characters like '[]{},:'
//
// In addition, top-level values can be separated by ',' to build arrays
// without having to use [].
func ValueWithConfig(content string, cfg ParserConfig) (interface{}, error) {
p := &flagParser{strings.TrimSpace(content), cfg}
v, err := p.parse()
if err != nil {
return nil, fmt.Errorf("%v when parsing '%v'", err.Error(), content)
Expand Down Expand Up @@ -99,13 +142,25 @@ func (p *flagParser) parseValue(stopSet string) (interface{}, error) {

switch in[0] {
case '[':
return p.parseArray()
if p.cfg.Array {
return p.parseArray()
}
return p.parsePrimitive(stopSet)
case '{':
return p.parseObj()
if p.cfg.Object {
return p.parseObj()
}
return p.parsePrimitive(stopSet)
case '"':
return p.parseStringDQuote()
if p.cfg.StringDQuote {
return p.parseStringDQuote()
}
return p.parsePrimitive(stopSet)
case '\'':
return p.parseStringSQuote()
if p.cfg.StringSQuote {
return p.parseStringSQuote()
}
return p.parsePrimitive(stopSet)
default:
return p.parsePrimitive(stopSet)
}
Expand Down
95 changes: 94 additions & 1 deletion internal/parse/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"github.com/stretchr/testify/assert"
)

func TestFlagValueParsing(t *testing.T) {
func TestFlagValueParsingWithDefault(t *testing.T) {
tests := []struct {
input string
expected interface{}
Expand Down Expand Up @@ -133,6 +133,99 @@ func TestFlagValueParsing(t *testing.T) {

}

func TestFlagValueParsingWithEnv(t *testing.T) {
tests := []struct {
input string
expected interface{}
}{
// null
{"", nil},
{"null", nil},

// booleans
{`true`, true},
{`false`, false},
{`on`, true},
{`off`, false},

// unsigned numbers
{`23`, uint64(23)},

// negative number
{`-42`, int64(-42)},

// floating point
{`3.14`, float64(3.14)},

// strings
{`'single quoted'`, `single quoted`},
{`'single quoted \"'`, `single quoted \"`},
{`"double quoted"`, `double quoted`},
{`"double quoted \""`, `double quoted "`},
{`plain string`, `plain string`},
{`string : with :: colons`, `string : with :: colons`},
{`C:\Windows\Style\Path`, `C:\Windows\Style\Path`},

// test arrays
{`[]`, nil},
{
`a,b,c`,
[]interface{}{"a", "b", "c"},
},
{
`C:\Windows\Path1,C:\Windows\Path2`,
[]interface{}{
`C:\Windows\Path1`,
`C:\Windows\Path2`,
},
},
{
`[array, 1, true, "abc"]`,
[]interface{}{"array", uint64(1), true, "abc"},
},
{
`[test, [1,2,3], on]`,
[]interface{}{
"test",
[]interface{}{uint64(1), uint64(2), uint64(3)},
true,
},
},
{
`[host1:1234, host2:1234]`,
[]interface{}{
"host1:1234",
"host2:1234",
},
},

// test dictionaries (won't parse)
{`{string}`, "{string}"},

// array of top-level dictionaries
{
`{key: 1},{key: 2}`,
[]interface{}{
"{key: 1}",
"{key: 2}",
},
},
}

for i, test := range tests {
t.Logf("run test (%v): %v", i, test.input)

v, err := ValueWithConfig(test.input, EnvParserConfig)
if err != nil {
t.Error(err)
continue
}

assert.Equal(t, test.expected, v)
}

}

func TestFlagValueParsingFails(t *testing.T) {
tests := []string{
// strings:
Expand Down
9 changes: 9 additions & 0 deletions reify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,23 @@ func TestUnpackPrimitivesValuesResolve(t *testing.T) {
U uint
F float64
S string
W string
}{},
&struct {
B interface{}
I interface{}
U interface{}
F interface{}
S interface{}
W interface{}
}{},
&struct {
B *bool
I *int
U *uint
F *float64
S *string
W *string
}{},
}

Expand All @@ -141,6 +144,7 @@ func TestUnpackPrimitivesValuesResolve(t *testing.T) {
"v_u": "23",
"v_f": "3.14",
"v_s": "string",
"v_w": "{string}",
}[name], nil
}),
}
Expand All @@ -151,6 +155,7 @@ func TestUnpackPrimitivesValuesResolve(t *testing.T) {
"u": "${v_u}",
"f": "${v_f}",
"s": "${v_s}",
"w": "${v_w}",
}, cfgOpts...)

for i, out := range tests {
Expand Down Expand Up @@ -186,11 +191,15 @@ func TestUnpackPrimitivesValuesResolve(t *testing.T) {
s, err := c.String("s", -1, cfgOpts...)
assert.NoError(t, err)

w, err := c.String("w", -1, cfgOpts...)
assert.NoError(t, err)

assert.Equal(t, true, b)
assert.Equal(t, 42, int(i))
assert.Equal(t, 23, int(u))
assert.Equal(t, 3.14, f)
assert.Equal(t, "string", s)
assert.Equal(t, "{string}", w)
}
}

Expand Down
12 changes: 11 additions & 1 deletion structs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ func TestStructMergeUnpackTyped(t *testing.T) {
},
env: testEnv{"env_strings": "one"},
},

{
t: &struct {
Hosts []string
Expand All @@ -79,6 +78,17 @@ func TestStructMergeUnpackTyped(t *testing.T) {
},
env: testEnv{"hosts_from_env": "host1:1234,host2:4567"},
},
{
t: &struct {
Hosts []string
}{
Hosts: []string{"{host1}:1234", "host2:4567"},
},
cfg: map[string]interface{}{
"hosts": "${hosts_from_env}",
},
env: testEnv{"hosts_from_env": "{host1}:1234,host2:4567"},
},
{
t: &struct {
Hosts []string
Expand Down
8 changes: 4 additions & 4 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ func (r *refDynValue) getValue(
}
return nil, err
}
return parseValue(p, opts, str)
return parseValue(p, opts, str, parse.EnvParserConfig)
}

func (s spliceDynValue) getValue(
Expand All @@ -546,19 +546,19 @@ func (s spliceDynValue) getValue(
return nil, err
}

return parseValue(p, opts, str)
return parseValue(p, opts, str, parse.DefaultParserConfig)
}

func (s spliceDynValue) String() string {
return "<splice>"
}

func parseValue(p *cfgPrimitive, opts *options, str string) (value, error) {
func parseValue(p *cfgPrimitive, opts *options, str string, parserCfg parse.ParserConfig) (value, error) {
if opts.noParse {
return nil, raiseNoParse(p.ctx, p.meta())
}

ifc, err := parse.Value(str)
ifc, err := parse.ValueWithConfig(str, parserCfg)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit c9ab797

Please sign in to comment.