Skip to content

Commit 06f44fd

Browse files
committed
fix: reject nested key normalization collisions
1 parent 0fafc07 commit 06f44fd

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

binding.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,15 @@ func tryBindNestedField(
696696
if rawMap, ok := entry.value.(map[string]any); ok {
697697
nestedData := make(map[string]mergedEntry)
698698
for k, v := range rawMap {
699-
nestedData[strings.ToLower(k)] = mergedEntry{value: v, sourceName: entry.sourceName}
699+
normalizedKey := strings.ToLower(k)
700+
if _, exists := nestedData[normalizedKey]; exists {
701+
return true, []FieldError{{
702+
FieldPath: fieldPath + "." + normalizedKey,
703+
Code: ErrCodeInvalidType,
704+
Message: "duplicate configuration keys after normalization",
705+
}}
706+
}
707+
nestedData[normalizedKey] = mergedEntry{value: v, sourceName: entry.sourceName}
700708
}
701709
return true, bindNested(fieldValue, nestedData, "", fieldPath)
702710
}

loader_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,44 @@ func TestLoad_NestedStruct(t *testing.T) {
941941
t.Errorf("expected Database.Port=5432, got %d", cfg.Database.Port)
942942
}
943943
})
944+
945+
t.Run("direct nested map rejects case collisions", func(t *testing.T) {
946+
type ConfigWithDirectMap struct {
947+
Database Database
948+
}
949+
950+
source := &mockSource{
951+
data: map[string]any{
952+
"database": map[string]any{
953+
"Host": "localhost",
954+
"host": "db.example.com",
955+
},
956+
},
957+
}
958+
959+
cfg, err := NewLoader[ConfigWithDirectMap]().WithSource(source).Load(context.Background())
960+
if err == nil {
961+
t.Fatal("expected error for colliding nested map keys")
962+
}
963+
if cfg != nil {
964+
t.Fatal("expected nil cfg when nested key collision fails")
965+
}
966+
967+
valErr, ok := err.(*ValidationError)
968+
if !ok {
969+
t.Fatalf("expected ValidationError, got %T", err)
970+
}
971+
972+
foundCollision := false
973+
for _, fieldErr := range valErr.FieldErrors {
974+
if fieldErr.Code == ErrCodeInvalidType && fieldErr.FieldPath == "Database.host" {
975+
foundCollision = true
976+
}
977+
}
978+
if !foundCollision {
979+
t.Fatalf("expected invalid_type for Database.host collision, got %#v", valErr.FieldErrors)
980+
}
981+
})
944982
}
945983

946984
func TestLoad_NestedCollections(t *testing.T) {

0 commit comments

Comments
 (0)