Skip to content

Commit 18a2148

Browse files
authored
Handle array table into an empty slice (#997)
Fix #995
1 parent bc99583 commit 18a2148

File tree

2 files changed

+229
-6
lines changed

2 files changed

+229
-6
lines changed

unmarshaler.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,15 +416,39 @@ func (d *decoder) handleArrayTableCollection(key unstable.Iterator, v reflect.Va
416416

417417
return v, nil
418418
case reflect.Slice:
419-
elem := v.Index(v.Len() - 1)
419+
// Create a new element when the slice is empty; otherwise operate on
420+
// the last element.
421+
var (
422+
elem reflect.Value
423+
created bool
424+
)
425+
if v.Len() == 0 {
426+
created = true
427+
elemType := v.Type().Elem()
428+
if elemType.Kind() == reflect.Interface {
429+
elem = makeMapStringInterface()
430+
} else {
431+
elem = reflect.New(elemType).Elem()
432+
}
433+
} else {
434+
elem = v.Index(v.Len() - 1)
435+
}
436+
420437
x, err := d.handleArrayTable(key, elem)
421438
if err != nil || d.skipUntilTable {
422439
return reflect.Value{}, err
423440
}
424441
if x.IsValid() {
425-
elem.Set(x)
442+
if created {
443+
elem = x
444+
} else {
445+
elem.Set(x)
446+
}
426447
}
427448

449+
if created {
450+
return reflect.Append(v, elem), nil
451+
}
428452
return v, err
429453
case reflect.Array:
430454
idx := d.arrayIndex(false, v)

unmarshaler_test.go

Lines changed: 203 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ foo = "bar"`,
412412
assert: func(t *testing.T, test test) {
413413
// Despite the documentation:
414414
// Pointer variable equality is determined based on the equality of the
415-
// referenced values (as opposed to the memory addresses).
415+
// referenced values (as opposed to the memory addresses).
416416
// assert.Equal does not work properly with maps with pointer keys
417417
// https://github.com/stretchr/testify/issues/1143
418418
expected := make(map[unmarshalTextKey]string)
@@ -3884,9 +3884,9 @@ func TestUnmarshal_Nil(t *testing.T) {
38843884
{
38853885
desc: "simplest",
38863886
input: `
3887-
[foo]
3888-
[foo.foo]
3889-
`,
3887+
[foo]
3888+
[foo.foo]
3889+
`,
38903890
expected: "[foo]\n[foo.foo]\n",
38913891
},
38923892
}
@@ -4040,3 +4040,202 @@ func TestIssue994_OK(t *testing.T) {
40404040
assert.NoError(t, err)
40414041
assert.Equal(t, "bar from unmarshaler", d.S)
40424042
}
4043+
4044+
func TestIssue995(t *testing.T) {
4045+
type AllowList struct {
4046+
Description string
4047+
Condition string
4048+
Commits []string
4049+
Paths []string
4050+
RegexTarget string
4051+
Regexes []string
4052+
StopWords []string
4053+
}
4054+
4055+
type Rule struct {
4056+
ID string
4057+
Description string
4058+
Regex string
4059+
SecretGroup int
4060+
Entropy interface{}
4061+
Keywords []string
4062+
Path string
4063+
Tags []string
4064+
AllowList *AllowList
4065+
Allowlists []AllowList
4066+
}
4067+
4068+
type GitleaksConfig struct {
4069+
Description string
4070+
Rules []Rule
4071+
Allowlist struct {
4072+
Commits []string
4073+
Paths []string
4074+
RegexTarget string
4075+
Regexes []string
4076+
StopWords []string
4077+
}
4078+
}
4079+
4080+
doc := `
4081+
[[allowlists]]
4082+
description = "Exception for File "
4083+
files = [ '''app/src''']
4084+
4085+
[[rules.allowlists]]
4086+
description = "policies"
4087+
regexes = [
4088+
'''abc'''
4089+
]
4090+
`
4091+
4092+
var cfg GitleaksConfig
4093+
err := toml.Unmarshal([]byte(doc), &cfg)
4094+
assert.NoError(t, err)
4095+
4096+
// Ensure no panic and that nested array table was created.
4097+
if len(cfg.Rules) == 0 {
4098+
t.Fatalf("expected Rules to contain at least one element after unmarshaling nested array table")
4099+
}
4100+
if len(cfg.Rules[0].Allowlists) != 1 {
4101+
t.Fatalf("expected first Rule to have exactly one allowlists entry, got %d", len(cfg.Rules[0].Allowlists))
4102+
}
4103+
assert.Equal(t, "policies", cfg.Rules[0].Allowlists[0].Description)
4104+
assert.Equal(t, []string{"abc"}, cfg.Rules[0].Allowlists[0].Regexes)
4105+
}
4106+
4107+
func TestIssue995_InterfaceSlice_MultiNested(t *testing.T) {
4108+
type Root struct {
4109+
Rules []interface{}
4110+
}
4111+
4112+
doc := `
4113+
[[rules.allowlists]]
4114+
description = "a"
4115+
4116+
[[rules.allowlists]]
4117+
description = "b"
4118+
`
4119+
4120+
var r Root
4121+
err := toml.Unmarshal([]byte(doc), &r)
4122+
assert.NoError(t, err)
4123+
4124+
if len(r.Rules) != 1 {
4125+
t.Fatalf("expected one element in Rules, got %d", len(r.Rules))
4126+
}
4127+
4128+
m, ok := r.Rules[0].(map[string]interface{})
4129+
if !ok {
4130+
t.Fatalf("expected Rules[0] to be a map[string]any, got %T", r.Rules[0])
4131+
}
4132+
4133+
als, ok := m["allowlists"].([]interface{})
4134+
if !ok {
4135+
t.Fatalf("expected allowlists to be []any, got %T", m["allowlists"])
4136+
}
4137+
if len(als) != 2 {
4138+
t.Fatalf("expected 2 allowlists entries, got %d", len(als))
4139+
}
4140+
4141+
a0, ok := als[0].(map[string]interface{})
4142+
if !ok {
4143+
t.Fatalf("expected allowlists[0] to be map[string]any, got %T", als[0])
4144+
}
4145+
a1, ok := als[1].(map[string]interface{})
4146+
if !ok {
4147+
t.Fatalf("expected allowlists[1] to be map[string]any, got %T", als[1])
4148+
}
4149+
assert.Equal(t, "a", a0["description"])
4150+
assert.Equal(t, "b", a1["description"])
4151+
}
4152+
4153+
func TestIssue995_MultiNestedConcrete(t *testing.T) {
4154+
type AllowList struct {
4155+
Description string
4156+
}
4157+
type Rule struct {
4158+
Allowlists []AllowList
4159+
}
4160+
type Root struct {
4161+
Rules []Rule
4162+
}
4163+
4164+
doc := `
4165+
[[rules.allowlists]]
4166+
description = "a"
4167+
4168+
[[rules.allowlists]]
4169+
description = "b"
4170+
`
4171+
4172+
var r Root
4173+
err := toml.Unmarshal([]byte(doc), &r)
4174+
assert.NoError(t, err)
4175+
4176+
if len(r.Rules) != 1 {
4177+
t.Fatalf("expected one element in Rules, got %d", len(r.Rules))
4178+
}
4179+
assert.Equal(t, 2, len(r.Rules[0].Allowlists))
4180+
assert.Equal(t, "a", r.Rules[0].Allowlists[0].Description)
4181+
assert.Equal(t, "b", r.Rules[0].Allowlists[1].Description)
4182+
}
4183+
4184+
func TestIssue995_PointerToSlice_Rules(t *testing.T) {
4185+
type AllowList struct{ Description string }
4186+
type Rule struct{ Allowlists []AllowList }
4187+
type Root struct{ Rules *[]Rule }
4188+
4189+
doc := `
4190+
[[rules.allowlists]]
4191+
description = "a"
4192+
4193+
[[rules.allowlists]]
4194+
description = "b"
4195+
`
4196+
4197+
var r Root
4198+
err := toml.Unmarshal([]byte(doc), &r)
4199+
assert.NoError(t, err)
4200+
if r.Rules == nil {
4201+
t.Fatalf("expected Rules pointer to be initialized")
4202+
}
4203+
if len(*r.Rules) != 1 {
4204+
t.Fatalf("expected one element in Rules, got %d", len(*r.Rules))
4205+
}
4206+
rule := (*r.Rules)[0]
4207+
assert.Equal(t, 2, len(rule.Allowlists))
4208+
assert.Equal(t, "a", rule.Allowlists[0].Description)
4209+
assert.Equal(t, "b", rule.Allowlists[1].Description)
4210+
}
4211+
4212+
func TestIssue995_SliceNonEmpty_UsesLastElement(t *testing.T) {
4213+
type AllowList struct{ Description string }
4214+
type Rule struct{ Allowlists []AllowList }
4215+
type Root struct{ Rules []Rule }
4216+
4217+
// Pre-initialize with one Rule; nested array table should populate
4218+
// the last element, not create a new one at this level.
4219+
var r Root
4220+
r.Rules = []Rule{{}}
4221+
4222+
doc := `
4223+
[[rules.allowlists]]
4224+
description = "a"
4225+
4226+
[[rules.allowlists]]
4227+
description = "b"
4228+
`
4229+
4230+
err := toml.Unmarshal([]byte(doc), &r)
4231+
assert.NoError(t, err)
4232+
if len(r.Rules) != 1 {
4233+
t.Fatalf("expected one element in Rules, got %d", len(r.Rules))
4234+
}
4235+
assert.Equal(t, 2, len(r.Rules[0].Allowlists))
4236+
// Values presence check
4237+
got := []string{r.Rules[0].Allowlists[0].Description, r.Rules[0].Allowlists[1].Description}
4238+
if !(got[0] == "a" && got[1] == "b") && !(got[0] == "b" && got[1] == "a") {
4239+
t.Fatalf("unexpected values in allowlists: %v", got)
4240+
}
4241+
}

0 commit comments

Comments
 (0)