Skip to content

Commit 17f32e1

Browse files
committed
handle edgecases for mergetworanges
1 parent f0cb44d commit 17f32e1

3 files changed

Lines changed: 191 additions & 22 deletions

File tree

vulnfeeds/conversion/common.go

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,16 @@ func BuildVersionRange(intro string, lastAff string, fixed string) *osvschema.Ra
284284
return &versionRange
285285
}
286286

287-
func MergeTwoRanges(range1, range2 *osvschema.Range) *osvschema.Range {
287+
// MergeTwoRanges combines two osvschema.Range objects into a single range.
288+
// It merges the events and the DatabaseSpecific fields. If the ranges are
289+
// not for the same repository or are of different types, it returns an error.
290+
// When merging DatabaseSpecific fields, it handles lists, maps, and simple
291+
// strings. If there are mismatching types for the same key, it returns an error.
292+
func MergeTwoRanges(range1, range2 *osvschema.Range) (*osvschema.Range, error) {
288293
// check if the ranges are the same
289294
if range1.GetRepo() != range2.GetRepo() || range1.GetType() != range2.GetType() {
290-
return nil
295+
// return an error if not the case
296+
return nil, fmt.Errorf("ranges are not the same repo or type")
291297
}
292298

293299
mergedRange := &osvschema.Range{
@@ -300,7 +306,7 @@ func MergeTwoRanges(range1, range2 *osvschema.Range) *osvschema.Range {
300306
db2 := range2.GetDatabaseSpecific()
301307

302308
if db1 == nil && db2 == nil {
303-
return mergedRange
309+
return mergedRange, nil
304310
}
305311

306312
mergedMap := make(map[string]any)
@@ -313,17 +319,16 @@ func MergeTwoRanges(range1, range2 *osvschema.Range) *osvschema.Range {
313319

314320
if db2 != nil {
315321
for k, v := range db2.GetFields() {
322+
val2 := v.AsInterface()
316323
if existing, ok := mergedMap[k]; ok {
317-
// If both are lists, append them
318-
if list1, ok := existing.([]any); ok {
319-
if list2, ok := v.AsInterface().([]any); ok {
320-
mergedMap[k] = append(list1, list2...)
321-
continue
322-
}
324+
mergedVal, err := mergeDatabaseSpecificValues(existing, val2)
325+
if err != nil {
326+
return nil, fmt.Errorf("failed to merge database specific key %q: %w", k, err)
323327
}
328+
mergedMap[k] = mergedVal
329+
} else {
330+
mergedMap[k] = val2
324331
}
325-
// Otherwise overwrite or add new
326-
mergedMap[k] = v.AsInterface()
327332
}
328333
}
329334

@@ -335,5 +340,55 @@ func MergeTwoRanges(range1, range2 *osvschema.Range) *osvschema.Range {
335340
}
336341
}
337342

338-
return mergedRange
343+
return mergedRange, nil
339344
}
345+
346+
// mergeDatabaseSpecificValues is a helper function that recursively merges two
347+
// values from a DatabaseSpecific field. It handles lists (by appending), maps
348+
// (by recursively merging keys), and simple strings (by creating a list if they
349+
// differ). It returns an error if the types of the two values do not match.
350+
func mergeDatabaseSpecificValues(val1, val2 any) (any, error) {
351+
switch v1 := val1.(type) {
352+
case []any:
353+
if v2, ok := val2.([]any); ok {
354+
return append(v1, v2...), nil
355+
}
356+
return nil, fmt.Errorf("mismatching types: %T and %T", val1, val2)
357+
case map[string]any:
358+
if v2, ok := val2.(map[string]any); ok {
359+
merged := make(map[string]any)
360+
for k, v := range v1 {
361+
merged[k] = v
362+
}
363+
for k, v := range v2 {
364+
if existing, ok := merged[k]; ok {
365+
mergedVal, err := mergeDatabaseSpecificValues(existing, v)
366+
if err != nil {
367+
return nil, err
368+
}
369+
merged[k] = mergedVal
370+
} else {
371+
merged[k] = v
372+
}
373+
}
374+
return merged, nil
375+
}
376+
return nil, fmt.Errorf("mismatching types: %T and %T", val1, val2)
377+
case string:
378+
if v2, ok := val2.(string); ok {
379+
if v1 == v2 {
380+
return v1, nil
381+
}
382+
return []any{v1, v2}, nil
383+
}
384+
return nil, fmt.Errorf("mismatching types: %T and %T", val1, val2)
385+
default:
386+
if fmt.Sprintf("%T", val1) != fmt.Sprintf("%T", val2) {
387+
return nil, fmt.Errorf("mismatching types: %T and %T", val1, val2)
388+
}
389+
if val1 == val2 {
390+
return val1, nil
391+
}
392+
return []any{val1, val2}, nil
393+
}
394+
}

vulnfeeds/conversion/common_test.go

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,11 @@ func TestBuildVersionRange(t *testing.T) {
7070

7171
func TestMergeTwoRanges(t *testing.T) {
7272
tests := []struct {
73-
name string
74-
range1 *osvschema.Range
75-
range2 *osvschema.Range
76-
want *osvschema.Range
73+
name string
74+
range1 *osvschema.Range
75+
range2 *osvschema.Range
76+
want *osvschema.Range
77+
wantErr bool
7778
}{
7879
{
7980
name: "Merge identical ranges",
@@ -101,7 +102,7 @@ func TestMergeTwoRanges(t *testing.T) {
101102
},
102103
},
103104
{
104-
name: "Different repos should return nil",
105+
name: "Different repos should return nil and error",
105106
range1: &osvschema.Range{
106107
Type: osvschema.Range_GIT,
107108
Repo: "https://github.com/example/repo1",
@@ -110,10 +111,11 @@ func TestMergeTwoRanges(t *testing.T) {
110111
Type: osvschema.Range_GIT,
111112
Repo: "https://github.com/example/repo2",
112113
},
113-
want: nil,
114+
want: nil,
115+
wantErr: true,
114116
},
115117
{
116-
name: "Different types should return nil",
118+
name: "Different types should return nil and error",
117119
range1: &osvschema.Range{
118120
Type: osvschema.Range_GIT,
119121
Repo: "https://github.com/example/repo",
@@ -122,7 +124,8 @@ func TestMergeTwoRanges(t *testing.T) {
122124
Type: osvschema.Range_ECOSYSTEM,
123125
Repo: "https://github.com/example/repo",
124126
},
125-
want: nil,
127+
want: nil,
128+
wantErr: true,
126129
},
127130
{
128131
name: "Merge with DatabaseSpecific",
@@ -198,10 +201,117 @@ func TestMergeTwoRanges(t *testing.T) {
198201

199202
for _, tt := range tests {
200203
t.Run(tt.name, func(t *testing.T) {
201-
got := MergeTwoRanges(tt.range1, tt.range2)
204+
got, err := MergeTwoRanges(tt.range1, tt.range2)
205+
if (err != nil) != tt.wantErr {
206+
t.Errorf("MergeTwoRanges() error = %v, wantErr %v", err, tt.wantErr)
207+
return
208+
}
202209
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
203210
t.Errorf("mergeTwoRanges() mismatch (-want +got):\n%s", diff)
204211
}
205212
})
206213
}
207214
}
215+
216+
func TestMergeDatabaseSpecificValues(t *testing.T) {
217+
tests := []struct {
218+
name string
219+
val1 any
220+
val2 any
221+
want any
222+
wantErr bool
223+
}{
224+
{
225+
name: "Merge lists",
226+
val1: []any{"a", "b"},
227+
val2: []any{"c", "d"},
228+
want: []any{"a", "b", "c", "d"},
229+
},
230+
{
231+
name: "List and string mismatch",
232+
val1: []any{"a", "b"},
233+
val2: "c",
234+
wantErr: true,
235+
},
236+
{
237+
name: "Merge maps",
238+
val1: map[string]any{"key1": "value1"},
239+
val2: map[string]any{"key2": "value2"},
240+
want: map[string]any{"key1": "value1", "key2": "value2"},
241+
},
242+
{
243+
name: "Merge nested maps",
244+
val1: map[string]any{
245+
"nested": map[string]any{
246+
"key1": "value1",
247+
},
248+
},
249+
val2: map[string]any{
250+
"nested": map[string]any{
251+
"key2": "value2",
252+
},
253+
},
254+
want: map[string]any{
255+
"nested": map[string]any{
256+
"key1": "value1",
257+
"key2": "value2",
258+
},
259+
},
260+
},
261+
{
262+
name: "Map and string mismatch",
263+
val1: map[string]any{"key1": "value1"},
264+
val2: "string",
265+
wantErr: true,
266+
},
267+
{
268+
name: "Merge same strings",
269+
val1: "value1",
270+
val2: "value1",
271+
want: "value1",
272+
},
273+
{
274+
name: "Merge different strings",
275+
val1: "value1",
276+
val2: "value2",
277+
want: []any{"value1", "value2"},
278+
},
279+
{
280+
name: "String and int mismatch",
281+
val1: "value1",
282+
val2: 123,
283+
wantErr: true,
284+
},
285+
{
286+
name: "Merge same ints",
287+
val1: 123,
288+
val2: 123,
289+
want: 123,
290+
},
291+
{
292+
name: "Merge different ints",
293+
val1: 123,
294+
val2: 456,
295+
want: []any{123, 456},
296+
},
297+
{
298+
name: "Int and float64 mismatch",
299+
val1: 123,
300+
val2: 456.0,
301+
wantErr: true,
302+
},
303+
}
304+
305+
for _, tt := range tests {
306+
t.Run(tt.name, func(t *testing.T) {
307+
got, err := mergeDatabaseSpecificValues(tt.val1, tt.val2)
308+
if (err != nil) != tt.wantErr {
309+
t.Errorf("mergeDatabaseSpecificValues() error = %v, wantErr %v", err, tt.wantErr)
310+
return
311+
}
312+
if !tt.wantErr && !cmp.Equal(got, tt.want) {
313+
t.Errorf("mergeDatabaseSpecificValues() mismatch (-want +got):\n%s", cmp.Diff(tt.want, got))
314+
}
315+
})
316+
}
317+
}

vulnfeeds/conversion/nvd/converter.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,11 @@ func MergeRangesAndCreateAffected(resolvedRanges []*osvschema.Range, unresolvedR
357357
if mergedRange == nil {
358358
mergedRange = vr
359359
} else {
360-
mergedRange = conversion.MergeTwoRanges(mergedRange, vr)
360+
var err error
361+
mergedRange, err = conversion.MergeTwoRanges(mergedRange, vr)
362+
if err != nil {
363+
metrics.AddNote("Failed to merge ranges: %v", err)
364+
}
361365
}
362366
}
363367
}

0 commit comments

Comments
 (0)