• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

spdx / tools-golang / 18390436414

09 Oct 2025 10:09PM UTC coverage: 87.135% (-8.3%) from 95.483%
18390436414

Pull #247

github

kzantow
chore: cleanup test logging, misc fixes

Signed-off-by: Keith Zantow <kzantow@gmail.com>
Pull Request #247: feat: SPDX 3

1476 of 1606 new or added lines in 8 files covered. (91.91%)

11 existing lines in 1 file now uncovered.

9042 of 10377 relevant lines covered (87.14%)

22.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.51
/spdx/v3/v3_0/convert.go
1
package v3_0
2

3
import (
4
        "fmt"
5
        "os"
6
        "reflect"
7
        "regexp"
8
        "slices"
9
        "strings"
10
        "time"
11

12
        "github.com/kzantow/go-ld"
13

14
        "github.com/spdx/tools-golang/spdx/v2/common"
15
        "github.com/spdx/tools-golang/spdx/v2/v2_3"
16
        "github.com/spdx/tools-golang/spdx/v3/internal"
17
)
18

19
func From_v2_3(doc v2_3.Document, d *Document) {
2✔
20
        c := newDocumentConverter(d)
2✔
21

2✔
22
        // namespace is used to prefix document IDs
2✔
23
        if doc.DocumentNamespace != "" {
4✔
24
                d.NamespaceMaps = NamespaceMapList{
2✔
25
                        &NamespaceMap{
2✔
26
                                Namespace: URI(doc.DocumentNamespace),
2✔
27
                        },
2✔
28
                }
2✔
29
                c.namespace = "ns"
2✔
30
        }
2✔
31

32
        // set creationInfo of the converter so all created objects use the original
33
        c.creationInfo = c.convert23creationInfo(doc.CreationInfo)
2✔
34

2✔
35
        if d.CreationInfo == nil {
4✔
36
                d.CreationInfo = c.creationInfo
2✔
37
        }
2✔
38

39
        if len(d.ProfileConformances) == 0 {
4✔
40
                d.ProfileConformances = []ProfileIdentifierType{ProfileIdentifierType_Core, ProfileIdentifierType_Software}
2✔
41
        }
2✔
42

43
        d.ID = string(doc.SPDXIdentifier)
2✔
44
        d.Comment = doc.DocumentComment
2✔
45
        d.Imports = list[ExternalMapList](c.convert23externalDocumentRef, doc.ExternalDocumentReferences...)
2✔
46
        d.Name = doc.DocumentName
2✔
47
        d.DataLicense = c.convert23licenseString(doc.DataLicense)
2✔
48

2✔
49
        var converted ElementList
2✔
50
        for _, l := range doc.OtherLicenses {
6✔
51
                converted = append(converted, c.convert23license(l))
4✔
52
        }
4✔
53

54
        for _, pkg := range doc.Packages {
6✔
55
                converted = append(converted, c.convert23package(pkg))
4✔
56
        }
4✔
57

58
        for _, file := range doc.Files {
6✔
59
                converted = append(converted, c.convert23file(file))
4✔
60
        }
4✔
61

62
        for _, a := range doc.Annotations {
6✔
63
                converted = append(converted, c.convert23annotation(a))
4✔
64
        }
4✔
65

66
        for _, s := range doc.Snippets {
6✔
67
                converted = append(converted, c.convert23snippet(s))
4✔
68
        }
4✔
69

70
        var relationships ElementList
2✔
71
        for _, rel := range doc.Relationships {
8✔
72
                r := c.convert23relationship(rel)
6✔
73
                if r != nil {
8✔
74
                        c.sbom.Elements = append(c.sbom.Elements, r)
2✔
75
                        converted = append(relationships, r)
2✔
76
                }
2✔
77
        }
78

79
        // if the user did not add elements to the document via relationships,
80
        // just add all elements directly
81
        if len(c.sbom.RootElements) == 0 {
2✔
NEW
82
                c.sbom.RootElements = append(notNil(converted), notNil(relationships)...)
×
83
        } else {
2✔
84
                c.sbom.Elements = append(c.sbom.Elements, notNil(converted)...)
2✔
85
        }
2✔
86
}
87

88
func newDocumentConverter(d *Document) *documentConverter {
10✔
89
        if d.LDContext == nil {
12✔
90
                d.LDContext = context()
2✔
91
        }
2✔
92
        sbom := &SBOM{}
10✔
93
        d.RootElements = ElementList{sbom}
10✔
94
        c := &documentConverter{
10✔
95
                emailExtractor:  regexp.MustCompile(`(.*)\s*[(<]([^)>]+)[)>]$`),
10✔
96
                relationshipMap: map[any][]*Relationship{},
10✔
97
                sbom:            sbom,
10✔
98
                idMap: duplicateLower(map[string]any{
10✔
99
                        "DOCUMENT":         sbom,
10✔
100
                        "SPDXRef-DOCUMENT": sbom,
10✔
101
                }),
10✔
102
                lifecycleMap: duplicateLower(map[string]LifecycleScopeType{
10✔
103
                        "BUILD_TOOL_OF":         LifecycleScopeType_Build,
10✔
104
                        "DEPENDS_ON":            LifecycleScopeType_Build,
10✔
105
                        "DEV_DEPENDENCY_OF":     LifecycleScopeType_Development,
10✔
106
                        "DEV_TOOL_OF":           LifecycleScopeType_Development,
10✔
107
                        "RUNTIME_DEPENDENCY_OF": LifecycleScopeType_Runtime,
10✔
108
                        "TEST_DEPENDENCY_OF":    LifecycleScopeType_Test,
10✔
109
                        "TEST_TOOL_OF":          LifecycleScopeType_Test,
10✔
110
                }),
10✔
111
                inverseRelationshipMap: duplicateLower(map[string]RelationshipType{
10✔
112
                        "DESCRIBED_BY":                RelationshipType_Describes,
10✔
113
                        "BUILD_TOOL_OF":               RelationshipType_UsesTool,
10✔
114
                        "CONTAINED_BY":                RelationshipType_Contains,
10✔
115
                        "COPY_OF":                     RelationshipType_CopiedTo,
10✔
116
                        "DATA_FILE_OF":                RelationshipType_HasDataFile,
10✔
117
                        "DOCUMENTATION_OF":            RelationshipType_HasDocumentation,
10✔
118
                        "DYNAMIC_LINK":                RelationshipType_HasDynamicLink,
10✔
119
                        "EXPANDED_FROM_ARCHIVE":       RelationshipType_ExpandsTo,
10✔
120
                        "FILE_ADDED":                  RelationshipType_HasAddedFile,
10✔
121
                        "FILE_DELETED":                RelationshipType_HasDeletedFile,
10✔
122
                        "GENERATED_FROM":              RelationshipType_Generates,
10✔
123
                        "METAFILE_OF":                 RelationshipType_HasMetadata,
10✔
124
                        "OPTIONAL_COMPONENT_OF":       RelationshipType_HasOptionalComponent,
10✔
125
                        "PACKAGE_OF":                  RelationshipType_PackagedBy,
10✔
126
                        "PATCH_APPLIED":               RelationshipType_PatchedBy,
10✔
127
                        "PATCH_FOR":                   RelationshipType_PatchedBy,
10✔
128
                        "AMENDS":                      RelationshipType_AmendedBy,
10✔
129
                        "TEST_CASE_OF":                RelationshipType_HasTestCase,
10✔
130
                        "PREREQUISITE_FOR":            RelationshipType_HasPrerequisite,
10✔
131
                        "VARIANT_OF":                  RelationshipType_HasVariant,
10✔
132
                        "BUILD_DEPENDENCY_OF":         RelationshipType_DependsOn,
10✔
133
                        "DEPENDENCY_MANIFEST_OF":      RelationshipType_HasDependencyManifest,
10✔
134
                        "DEPENDENCY_OF":               RelationshipType_DependsOn,
10✔
135
                        "DEV_DEPENDENCY_OF":           RelationshipType_DependsOn,
10✔
136
                        "DEV_TOOL_OF":                 RelationshipType_UsesTool,
10✔
137
                        "EXAMPLE_OF":                  RelationshipType_HasExample,
10✔
138
                        "OPTIONAL_DEPENDENCY_OF":      RelationshipType_HasOptionalDependency,
10✔
139
                        "PROVIDED_DEPENDENCY_OF":      RelationshipType_HasProvidedDependency,
10✔
140
                        "RUNTIME_DEPENDENCY_OF":       RelationshipType_DependsOn,
10✔
141
                        "TEST_DEPENDENCY_OF":          RelationshipType_DependsOn,
10✔
142
                        "TEST_OF":                     RelationshipType_HasTest,
10✔
143
                        "TEST_TOOL_OF":                RelationshipType_UsesTool,
10✔
144
                        "REQUIREMENT_DESCRIPTION_FOR": RelationshipType_HasRequirement,
10✔
145
                        "SPECIFICATION_FOR":           RelationshipType_HasSpecification,
10✔
146
                }),
10✔
147
                relationshipTypeMap: duplicateLower(map[string]RelationshipType{
10✔
148
                        "DESCRIBES":             RelationshipType_Describes,
10✔
149
                        "ANCESTOR_OF":           RelationshipType_AncestorOf,
10✔
150
                        "CONTAINS":              RelationshipType_Contains,
10✔
151
                        "DESCENDANT_OF":         RelationshipType_DescendantOf,
10✔
152
                        "DISTRIBUTION_ARTIFACT": RelationshipType_HasDistributionArtifact,
10✔
153
                        "FILE_MODIFIED":         RelationshipType_ModifiedBy,
10✔
154
                        "GENERATES":             RelationshipType_Generates,
10✔
155
                        "OTHER":                 RelationshipType_Other,
10✔
156
                        "STATIC_LINK":           RelationshipType_HasStaticLink,
10✔
157
                        "HAS_PREREQUISITE":      RelationshipType_HasPrerequisite,
10✔
158
                        "DEPENDS_ON":            RelationshipType_DependsOn,
10✔
159
                }),
10✔
160
                hashAlgorithmMap: duplicateLower(map[string]HashAlgorithm{
10✔
161
                        "ADLER32":     HashAlgorithm_Other,
10✔
162
                        "BLAKE2b_256": HashAlgorithm_Blake2b256,
10✔
163
                        "BLAKE2b_384": HashAlgorithm_Blake2b384,
10✔
164
                        "BLAKE2b_512": HashAlgorithm_Blake2b512,
10✔
165
                        "BLAKE3":      HashAlgorithm_Blake3,
10✔
166
                        "MD2":         HashAlgorithm_Md2,
10✔
167
                        "MD4":         HashAlgorithm_Md4,
10✔
168
                        "MD5":         HashAlgorithm_Md5,
10✔
169
                        "MD6":         HashAlgorithm_Md6,
10✔
170
                        "SHA1":        HashAlgorithm_Sha1,
10✔
171
                        "SHA224":      HashAlgorithm_Sha224,
10✔
172
                        "SHA256":      HashAlgorithm_Sha256,
10✔
173
                        "SHA384":      HashAlgorithm_Sha384,
10✔
174
                        "SHA3_256":    HashAlgorithm_Sha3_256,
10✔
175
                        "SHA3_384":    HashAlgorithm_Sha3_384,
10✔
176
                        "SHA3_512":    HashAlgorithm_Sha3_512,
10✔
177
                        "SHA512":      HashAlgorithm_Sha3_512,
10✔
178
                }),
10✔
179
                annotationTypeMap: duplicateLower(map[string]AnnotationType{
10✔
180
                        "OTHER":  AnnotationType_Other,
10✔
181
                        "REVIEW": AnnotationType_Review,
10✔
182
                }),
10✔
183
                contentIdentifierTypeMap: duplicateLower(map[string]ContentIdentifierType{
10✔
184
                        "gitoid": ContentIdentifierType_Gitoid,
10✔
185
                        "swh":    ContentIdentifierType_Swhid,
10✔
186
                }),
10✔
187
                externalIdentifierTypeMap: duplicateLower(map[string]ExternalIdentifierType{
10✔
188
                        "cpe22Type": ExternalIdentifierType_Cpe22,
10✔
189
                        "cpe23Type": ExternalIdentifierType_Cpe23,
10✔
190
                        "swid":      ExternalIdentifierType_Swid,
10✔
191
                        "purl":      ExternalIdentifierType_PackageURL,
10✔
192
                }),
10✔
193
                externalRefTypeMap: duplicateLower(map[string]ExternalRefType{
10✔
194
                        "maven-central": ExternalRefType_MavenCentral,
10✔
195
                        "npm":           ExternalRefType_Npm,
10✔
196
                        "nuget":         ExternalRefType_Nuget,
10✔
197
                        "bower":         ExternalRefType_Bower,
10✔
198
                        "advisory":      ExternalRefType_SecurityAdvisory,
10✔
199
                        "fix":           ExternalRefType_SecurityFix,
10✔
200
                        "url":           ExternalRefType_SecurityOther,
10✔
201
                }),
10✔
202
                primaryPurposeMap: duplicateLower(map[string]SoftwarePurpose{
10✔
203
                        "APPLICATION":      SoftwarePurpose_Application,
10✔
204
                        "ARCHIVE":          SoftwarePurpose_Archive,
10✔
205
                        "CONTAINER":        SoftwarePurpose_Container,
10✔
206
                        "DEVICE":           SoftwarePurpose_Device,
10✔
207
                        "FILE":             SoftwarePurpose_File,
10✔
208
                        "FIRMWARE":         SoftwarePurpose_Firmware,
10✔
209
                        "FRAMEWORK":        SoftwarePurpose_Framework,
10✔
210
                        "INSTALL":          SoftwarePurpose_Install,
10✔
211
                        "LIBRARY":          SoftwarePurpose_Library,
10✔
212
                        "OPERATING_SYSTEM": SoftwarePurpose_OperatingSystem,
10✔
213
                        "OTHER":            SoftwarePurpose_Other,
10✔
214
                        "SOURCE":           SoftwarePurpose_Source,
10✔
215
                }),
10✔
216
        }
10✔
217
        return c
10✔
218
}
219

220
type documentConverter struct {
221
        sbom                      *SBOM
222
        namespace                 string
223
        idMap                     map[string]any
224
        creationInfo              AnyCreationInfo
225
        relationshipMap           map[any][]*Relationship
226
        relationshipTypeMap       map[string]RelationshipType
227
        inverseRelationshipMap    map[string]RelationshipType
228
        lifecycleMap              map[string]LifecycleScopeType
229
        hashAlgorithmMap          map[string]HashAlgorithm
230
        annotationTypeMap         map[string]AnnotationType
231
        contentIdentifierTypeMap  map[string]ContentIdentifierType
232
        externalIdentifierTypeMap map[string]ExternalIdentifierType
233
        externalRefTypeMap        map[string]ExternalRefType
234
        primaryPurposeMap         map[string]SoftwarePurpose
235
        emailExtractor            *regexp.Regexp
236
}
237

238
func (c *documentConverter) addRelationship(r *Relationship) {
70✔
239
        rels := c.relationshipMap[r.From]
70✔
240
        for _, existing := range rels {
111✔
241
                if existing.Type == r.Type {
65✔
242
                        if r.Comment != existing.Comment {
24✔
NEW
243
                                continue
×
244
                        }
245
                        existing.To = appendUnique(existing.To, r.To...)
24✔
246
                        return
24✔
247
                }
248
        }
249
        c.relationshipMap[r.From] = append(c.relationshipMap[r.From], r)
46✔
250
}
251

252
func (c *documentConverter) convert23relationship(rel *v2_3.Relationship) AnyRelationship {
6✔
253
        if rel == nil {
6✔
NEW
254
                return nil
×
NEW
255
        }
×
256
        from, _ := c.idMap[string(rel.RefA.ElementRefID)].(AnyElement)
6✔
257
        to, _ := c.idMap[string(rel.RefB.ElementRefID)].(AnyElement)
6✔
258
        if from == nil || to == nil {
8✔
259
                c.logDropped(rel)
2✔
260
                return nil
2✔
261
        }
2✔
262

263
        typ, invert := c.convert23relationshipType(rel.Relationship)
4✔
264
        if invert {
4✔
NEW
265
                to, from = from, to
×
NEW
266
        }
×
267

268
        // SPDX 3 direct document elements are in RootElement list, not relationships
269
        if from == c.sbom {
6✔
270
                // TODO are there special cases depending on type?
2✔
271
                c.sbom.RootElements = append(c.sbom.RootElements, to)
2✔
272
                return nil
2✔
273
        }
2✔
274

275
        r := &Relationship{
2✔
276
                Comment: rel.RelationshipComment,
2✔
277
                From:    from,
2✔
278
                Type:    typ,
2✔
279
                To:      ElementList{to},
2✔
280
        }
2✔
281
        c.addRelationship(r)
2✔
282
        return r
2✔
283
}
284

285
func (c *documentConverter) convert23relationshipType(typ string) (RelationshipType, bool) {
4✔
286
        typ = strings.ToUpper(typ)
4✔
287
        out, ok := c.relationshipTypeMap[typ]
4✔
288
        if ok {
8✔
289
                return out, false
4✔
290
        }
4✔
NEW
291
        out, ok = c.inverseRelationshipMap[typ]
×
NEW
292
        if ok {
×
NEW
293
                return out, true
×
NEW
294
        }
×
NEW
295
        return RelationshipType{}, false
×
296
}
297

298
func (c *documentConverter) convert23creationInfo(info *v2_3.CreationInfo) AnyCreationInfo {
3✔
299
        if info == nil || len(info.Creators) == 0 {
3✔
NEW
300
                return nil
×
NEW
301
        }
×
302
        ci := &CreationInfo{
3✔
303
                Comment:      info.CreatorComment,
3✔
304
                Created:      c.convert23time(info.Created),
3✔
305
                CreatedBy:    list[AgentList](c.convert23creator, info.Creators...),
3✔
306
                CreatedUsing: list[ToolList](c.convert23tool, info.Creators...),
3✔
307
                SpecVersion:  Version, // specVersion is always the current version
3✔
308
        }
3✔
309

3✔
310
        // update circular references, which will be set to nil by default
3✔
311
        for _, a := range ci.CreatedBy.Agents() {
6✔
312
                a.SetCreationInfo(ci)
3✔
313
        }
3✔
314
        for _, a := range ci.CreatedUsing.Tools() {
3✔
NEW
315
                a.SetCreationInfo(ci)
×
NEW
316
        }
×
317

318
        return ci
3✔
319
}
320

321
func (c *documentConverter) convert23tool(creator common.Creator) AnyTool {
6✔
322
        if strings.ToLower(creator.CreatorType) != "tool" {
9✔
323
                return nil
3✔
324
        }
3✔
325

326
        if creator.Creator != "" {
6✔
327
                c.logDropped(creator)
3✔
328
                return nil
3✔
329
        }
3✔
330

NEW
331
        return &Tool{
×
NEW
332
                CreationInfo: c.creationInfo,
×
NEW
333
                Name:         creator.Creator,
×
NEW
334
        }
×
335
}
336

337
func (c *documentConverter) convert23creator(creator common.Creator) AnyAgent {
6✔
338
        return c.convert23agent(creator.CreatorType, creator.Creator)
6✔
339
}
6✔
340

341
func (c *documentConverter) convert23originator(creator *common.Originator) AnyAgent {
3✔
342
        if creator == nil {
3✔
NEW
343
                return nil
×
NEW
344
        }
×
345
        return c.convert23agent(creator.OriginatorType, creator.Originator)
3✔
346
}
347

348
func (c *documentConverter) convert23supplier(creator *common.Supplier) AnyAgent {
5✔
349
        if creator == nil {
5✔
NEW
350
                return nil
×
NEW
351
        }
×
352
        return c.convert23agent(creator.SupplierType, creator.Supplier)
5✔
353
}
354

355
func (c *documentConverter) convert23annotator(creator *common.Annotator) AnyAgent {
11✔
356
        return c.convert23agent(creator.AnnotatorType, creator.Annotator)
11✔
357
}
11✔
358

359
func (c *documentConverter) convert23contributors(contributors ...string) AgentList {
8✔
360
        var agents AgentList
8✔
361
        for _, contributor := range contributors {
20✔
362
                parts := strings.Split(contributor, ":")
12✔
363
                if len(parts) > 1 && strings.EqualFold(parts[0], "person") {
12✔
NEW
364
                        contributor = parts[1]
×
NEW
365
                }
×
366
                agent := c.convert23agent("person", contributor)
12✔
367
                if agent != nil {
24✔
368
                        agents = append(agents, agent)
12✔
369
                }
12✔
370
        }
371
        return agents
8✔
372
}
373

374
func (c *documentConverter) convert23agent(typ, name string) AnyAgent {
37✔
375
        name = strings.TrimSpace(name)
37✔
376
        if name == "" {
37✔
NEW
377
                return nil
×
NEW
378
        }
×
379
        emailValue := ""
37✔
380
        match := c.emailExtractor.FindStringSubmatch(name)
37✔
381
        if len(match) > 2 {
69✔
382
                name = strings.TrimSpace(match[1])
32✔
383
                emailValue = strings.TrimSpace(match[2])
32✔
384
        }
32✔
385
        var out AnyAgent
37✔
386
        switch strings.ToLower(typ) {
37✔
387
        case "person":
29✔
388
                out = &Person{
29✔
389
                        CreationInfo: c.creationInfo,
29✔
390
                        Name:         name,
29✔
391
                }
29✔
392
        case "organization", "org":
3✔
393
                out = &Organization{
3✔
394
                        CreationInfo: c.creationInfo,
3✔
395
                        Name:         name,
3✔
396
                }
3✔
397
        case "tool": // handled elsewhere
5✔
NEW
398
        default:
×
NEW
399
                c.logDropped(fmt.Sprintf("unknown agent type: %v with value: %v", typ, name))
×
400
        }
401
        if emailValue != "" {
69✔
402
                out.SetExternalIdentifiers(externalIdentifierListEmail(emailValue))
32✔
403
        }
32✔
404
        return out
37✔
405
}
406

407
func (c *documentConverter) convert23file(f *v2_3.File) AnyFile {
8✔
408
        if f == nil {
8✔
NEW
409
                return nil
×
NEW
410
        }
×
411

412
        out := &File{
8✔
413
                ID:               string(f.FileSPDXIdentifier),
8✔
414
                Comment:          f.FileComment,
8✔
415
                Name:             f.FileName,
8✔
416
                Description:      f.FileNotice,
8✔
417
                ExternalRefs:     nil,
8✔
418
                Summary:          "",
8✔
419
                VerifiedUsing:    list[IntegrityMethodList](c.convert23checksum, f.Checksums...),
8✔
420
                StandardNames:    nil,
8✔
421
                BuiltTime:        time.Time{},
8✔
422
                ReleaseTime:      time.Time{},
8✔
423
                SupportLevels:    nil,
8✔
424
                SuppliedBy:       nil,
8✔
425
                OriginatedBy:     c.convert23contributors(f.FileContributors...),
8✔
426
                ValidUntilTime:   time.Time{},
8✔
427
                AttributionTexts: f.FileAttributionTexts,
8✔
428
                CopyrightText:    f.FileCopyrightText,
8✔
429
                ContentType:      "",
8✔
430
                Kind:             FileKindType_File,
8✔
431
        }
8✔
432

8✔
433
        for _, s := range f.Snippets {
14✔
434
                v3 := c.convert23snippet(*s)
6✔
435
                c.addRelationship(&Relationship{
6✔
436
                        Type: RelationshipType_Contains,
6✔
437
                        From: out,
6✔
438
                        To:   ElementList{v3},
6✔
439
                })
6✔
440
        }
6✔
441

442
        for _, a := range f.Annotations {
14✔
443
                v3 := c.convert23annotation(&a)
6✔
444
                c.addRelationship(&Relationship{
6✔
445
                        Type: RelationshipType_Describes,
6✔
446
                        From: v3,
6✔
447
                        To:   ElementList{out},
6✔
448
                })
6✔
449
        }
6✔
450

451
        c.idMap[string(f.FileSPDXIdentifier)] = out
8✔
452
        return out
8✔
453
}
454

455
func (c *documentConverter) convert23package(pkg *v2_3.Package) AnyPackage {
5✔
456
        if pkg == nil {
5✔
NEW
457
                return nil
×
NEW
458
        }
×
459

460
        verificationCodes := list[IntegrityMethodList](c.convert23packageVerificationCode, pkg.PackageVerificationCode)
5✔
461

5✔
462
        for _, checksum := range pkg.PackageChecksums {
13✔
463
                ck := c.convert23checksum(checksum)
8✔
464
                if ck != nil {
16✔
465
                        verificationCodes = append(verificationCodes, ck)
8✔
466
                }
8✔
467
        }
468

469
        id := string(pkg.PackageSPDXIdentifier)
5✔
470
        out := &Package{
5✔
471
                ID:                  id,
5✔
472
                Name:                pkg.PackageName,
5✔
473
                Summary:             pkg.PackageSummary,
5✔
474
                Comment:             pkg.PackageComment,
5✔
475
                Description:         pkg.PackageDescription,
5✔
476
                ExternalIdentifiers: list[ExternalIdentifierList](c.convert23externalIdentifier, pkg.PackageExternalReferences...),
5✔
477
                VerifiedUsing:       verificationCodes,
5✔
478
                BuiltTime:           c.convert23time(pkg.BuiltDate),
5✔
479
                OriginatedBy:        list[AgentList](c.convert23originator, pkg.PackageOriginator),
5✔
480
                ReleaseTime:         c.convert23time(pkg.ReleaseDate),
5✔
481
                SuppliedBy:          c.convert23supplier(pkg.PackageSupplier),
5✔
482
                ValidUntilTime:      c.convert23time(pkg.ValidUntilDate),
5✔
483
                AttributionTexts:    pkg.PackageAttributionTexts,
5✔
484
                CopyrightText:       pkg.PackageCopyrightText,
5✔
485
                PrimaryPurpose:      c.convert23purpose(pkg.PrimaryPackagePurpose),
5✔
486
                Version:             pkg.PackageVersion,
5✔
487
                DownloadLocation:    c.convert23uri(pkg.PackageDownloadLocation),
5✔
488
                HomePage:            c.convert23uri(pkg.PackageHomePage),
5✔
489
                PackageURL:          c.convert23packageUrl(pkg.PackageExternalReferences),
5✔
490
                SourceInfo:          pkg.PackageSourceInfo,
5✔
491
        }
5✔
492

5✔
493
        // move the first valid PURL to the PackageURL field
5✔
494
        for _, ident := range out.ExternalIdentifiers.ExternalIdentifiers() {
11✔
495
                if ident.GetType() == ExternalIdentifierType_PackageURL {
12✔
496
                        if ident.GetComment() != "" {
9✔
497
                                continue
3✔
498
                        }
499
                        purl := URI(ident.GetIdentifier())
3✔
500
                        if purl.Validate() == nil {
6✔
501
                                out.PackageURL = purl
3✔
502
                                out.ExternalIdentifiers = slices.DeleteFunc(out.ExternalIdentifiers, func(identifier AnyExternalIdentifier) bool {
12✔
503
                                        return identifier.GetIdentifier() == string(purl)
9✔
504
                                })
9✔
505
                                break
3✔
506
                        }
507
                }
508
        }
509

510
        c.idMap[id] = out
5✔
511

5✔
512
        if pkg.PackageLicenseComments != "" {
8✔
513
                if out.Comment == "" {
3✔
NEW
514
                        out.Comment = pkg.PackageLicenseComments
×
515
                } else {
3✔
516
                        // this appears to be the behavior from the Java tools:
3✔
517
                        // https://github.com/spdx/Spdx-Java-Library/blob/e3640e27a423a5562c52bcc4075cce9ac35f433a/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java#L1233
3✔
518
                        out.Comment += ";" + pkg.PackageLicenseComments
3✔
519
                }
3✔
520
        }
521

522
        for _, l := range pkg.PackageLicenseInfoFromFiles {
11✔
523
                d := c.convert23licenseString(l)
6✔
524
                if d != nil {
12✔
525
                        c.addRelationship(&Relationship{
6✔
526
                                Type: RelationshipType_HasConcludedLicense,
6✔
527
                                From: out,
6✔
528
                                To:   ElementList{d},
6✔
529
                        })
6✔
530
                }
6✔
531
        }
532
        d := c.convert23licenseString(pkg.PackageLicenseDeclared)
5✔
533
        if d != nil {
10✔
534
                c.addRelationship(&Relationship{
5✔
535
                        Type: RelationshipType_HasDeclaredLicense,
5✔
536
                        From: out,
5✔
537
                        To:   ElementList{d},
5✔
538
                })
5✔
539
        }
5✔
540

541
        d = c.convert23licenseString(pkg.PackageLicenseConcluded)
5✔
542
        if d != nil {
10✔
543
                c.addRelationship(&Relationship{
5✔
544
                        Type: RelationshipType_HasConcludedLicense,
5✔
545
                        From: out,
5✔
546
                        To:   ElementList{d},
5✔
547
                })
5✔
548
        }
5✔
549

550
        for _, f := range pkg.Files {
8✔
551
                v3file := c.convert23file(f)
3✔
552
                if v3file == nil {
3✔
NEW
553
                        continue
×
554
                }
555
                c.addRelationship(&Relationship{
3✔
556
                        Type: RelationshipType_Contains,
3✔
557
                        From: out,
3✔
558
                        To:   ElementList{v3file},
3✔
559
                })
3✔
560
        }
561

562
        return out
5✔
563
}
564

565
func (c *documentConverter) convert23time(date string) time.Time {
29✔
566
        if date == "" {
35✔
567
                return time.Time{}
6✔
568
        }
6✔
569
        t, err := time.Parse(time.RFC3339, date)
23✔
570
        if err != nil {
23✔
NEW
571
                c.logDropped(err)
×
NEW
572
        }
×
573
        return t
23✔
574
}
575

576
func (c *documentConverter) convert23uri(uri string) ld.URI {
13✔
577
        out := ld.URI(uri)
13✔
578
        if out.Validate() == nil {
26✔
579
                return out
13✔
580
        }
13✔
NEW
581
        c.logDropped(out)
×
NEW
582
        return ""
×
583
}
584

585
func (c *documentConverter) logDropped(value any) {
5✔
586
        if value == nil || !internal.Debug {
10✔
587
                return
5✔
588
        }
5✔
NEW
589
        _, _ = fmt.Fprintf(os.Stderr, "dropped: %v", value)
×
590
}
591

592
func (c *documentConverter) convert23packageUrl(references []*v2_3.PackageExternalReference) ld.URI {
5✔
593
        for _, ref := range references {
8✔
594
                if ref != nil && ref.RefType == common.TypePackageManagerPURL {
6✔
595
                        return c.convert23uri(ref.Locator)
3✔
596
                }
3✔
597
        }
598
        return ""
2✔
599
}
600

601
func (c *documentConverter) convert23purpose(purpose string) SoftwarePurpose {
5✔
602
        return c.primaryPurposeMap[strings.ToLower(purpose)]
5✔
603
}
5✔
604

605
func (c *documentConverter) convert23packageVerificationCode(v *common.PackageVerificationCode) AnyIntegrityMethod {
3✔
606
        if v == nil || v.Value == "" {
3✔
NEW
607
                c.logDropped(v)
×
NEW
608
                return nil
×
NEW
609
        }
×
610
        return &PackageVerificationCode{
3✔
611
                Algorithm:     HashAlgorithm_Sha1,
3✔
612
                HashValue:     v.Value,
3✔
613
                ExcludedFiles: v.ExcludedFiles,
3✔
614
        }
3✔
615
}
616

617
func (c *documentConverter) convert23externalIdentifier(r *v2_3.PackageExternalReference) AnyExternalIdentifier {
9✔
618
        if r == nil {
9✔
NEW
619
                return nil
×
NEW
620
        }
×
621
        typ, ok := c.externalIdentifierTypeMap[r.RefType]
9✔
622
        if !ok || r.Locator == "" {
9✔
NEW
623
                c.logDropped(r)
×
NEW
624
                return nil
×
NEW
625
        }
×
626
        return &ExternalIdentifier{
9✔
627
                Comment:    r.ExternalRefComment,
9✔
628
                Type:       typ,
9✔
629
                Identifier: r.Locator,
9✔
630
        }
9✔
631
}
632

633
func (c *documentConverter) convert23license(l *v2_3.OtherLicense) AnyLicenseInfo {
5✔
634
        if l == nil {
5✔
NEW
635
                return nil
×
NEW
636
        }
×
637

638
        var seeAlso []ld.URI
5✔
639
        for _, ref := range l.LicenseCrossReferences {
12✔
640
                seeAlso = append(seeAlso, ld.URI(ref))
7✔
641
        }
7✔
642

643
        out := &CustomLicense{
5✔
644
                Name:     l.LicenseName,
5✔
645
                Comment:  l.LicenseComment,
5✔
646
                SeeAlsos: seeAlso,
5✔
647
                Text:     l.ExtractedText,
5✔
648
        }
5✔
649

5✔
650
        c.idMap[l.LicenseIdentifier] = out
5✔
651

5✔
652
        return out
5✔
653
}
654

655
func (c *documentConverter) convert23licenseString(licenseString string) AnyLicenseInfo {
40✔
656
        if licenseString == "" {
40✔
NEW
657
                return nil
×
NEW
658
        }
×
659

660
        var out AnyLicenseInfo
40✔
661
        if strings.HasPrefix(licenseString, "LicenseRef-") {
40✔
NEW
662
                license, _ := c.idMap[licenseString].(AnyLicenseInfo)
×
NEW
663
                if license != nil {
×
NEW
664
                        return license
×
NEW
665
                }
×
NEW
666
                out = &CustomLicense{
×
NEW
667
                        Name: licenseString,
×
NEW
668
                        Text: licenseString,
×
NEW
669
                }
×
670
        } else {
40✔
671
                out = &ListedLicense{
40✔
672
                        Name: licenseString,
40✔
673
                }
40✔
674
        }
40✔
675

676
        return out
40✔
677
}
678

679
func (c *documentConverter) convert23externalDocumentRef(r v2_3.ExternalDocumentRef) AnyExternalMap {
5✔
680
        if r.DocumentRefID == "" || r.URI == "" {
5✔
NEW
681
                c.logDropped(r)
×
NEW
682
                return nil
×
NEW
683
        }
×
684
        return &ExternalMap{
5✔
685
                ExternalSpdxID: ld.URI(r.DocumentRefID),
5✔
686
                LocationHint:   ld.URI(r.URI),
5✔
687
                VerifiedUsing:  list[IntegrityMethodList](c.convert23checksum, r.Checksum),
5✔
688
        }
5✔
689
}
690

691
func (c *documentConverter) convert23checksum(checksum common.Checksum) AnyIntegrityMethod {
27✔
692
        if checksum.Value == "" {
27✔
NEW
693
                c.logDropped(checksum)
×
NEW
694
                return nil
×
NEW
695
        }
×
696
        return &Hash{
27✔
697
                Value:     checksum.Value,
27✔
698
                Algorithm: c.hashAlgorithmMap[string(checksum.Algorithm)],
27✔
699
        }
27✔
700
}
701

702
func (c *documentConverter) convert23annotation(a *v2_3.Annotation) AnyAnnotation {
11✔
703
        if a == nil {
11✔
NEW
704
                return nil
×
NEW
705
        }
×
706

707
        typ, ok := c.annotationTypeMap[strings.ToUpper(a.AnnotationType)]
11✔
708
        if !ok {
11✔
NEW
709
                c.logDropped(a)
×
NEW
710
                return nil
×
NEW
711
        }
×
712

713
        out := &Annotation{
11✔
714
                CreationInfo: &CreationInfo{
11✔
715
                        Created:   c.convert23time(a.AnnotationDate),
11✔
716
                        CreatedBy: list[AgentList](c.convert23annotator, &a.Annotator),
11✔
717
                },
11✔
718
                Type:      typ,
11✔
719
                Statement: a.AnnotationComment,
11✔
720
        }
11✔
721

11✔
722
        to, _ := c.idMap[string(a.AnnotationSPDXIdentifier.ElementRefID)].(AnyElement)
11✔
723
        if to != nil {
20✔
724
                c.addRelationship(&Relationship{
9✔
725
                        Type: RelationshipType_Describes,
9✔
726
                        From: out,
9✔
727
                        To:   ElementList{to},
9✔
728
                })
9✔
729
        }
9✔
730
        return out
11✔
731
}
732

733
func (c *documentConverter) convert23snippet(s v2_3.Snippet) AnyElement {
11✔
734
        snippetFile, _ := c.idMap[string(s.SnippetFromFileSPDXIdentifier)].(AnyFile)
11✔
735

11✔
736
        var licenses LicenseInfoList
11✔
737
        d := c.convert23licenseString(s.SnippetLicenseConcluded)
11✔
738
        d.SetComment(s.SnippetLicenseComments)
11✔
739
        licenses = append(licenses, d)
11✔
740

11✔
741
        for _, licenseInfo := range s.LicenseInfoInSnippet {
22✔
742
                d = c.convert23licenseString(licenseInfo)
11✔
743
                if d != nil {
22✔
744
                        d.SetComment(s.SnippetLicenseComments)
11✔
745
                        licenses = append(licenses, d)
11✔
746
                }
11✔
747
        }
748

749
        var allSnippets ElementList
11✔
750

11✔
751
        for _, r := range s.Ranges {
24✔
752
                // there are 2 spots that might hold references to the file, try to handle them both:
13✔
753
                f, _ := c.idMap[string(r.StartPointer.FileSPDXIdentifier)].(AnyFile)
13✔
754
                if f == nil {
26✔
755
                        f = snippetFile
13✔
756
                }
13✔
757
                if snippetFile == nil {
22✔
758
                        snippetFile = f
9✔
759
                }
9✔
760
                newSnippet := &Snippet{
13✔
761
                        ID:               string(s.SnippetSPDXIdentifier),
13✔
762
                        Comment:          s.SnippetComment,
13✔
763
                        Name:             s.SnippetName,
13✔
764
                        CopyrightText:    s.SnippetCopyrightText,
13✔
765
                        AttributionTexts: s.SnippetAttributionTexts,
13✔
766
                        FromFile:         f,
13✔
767
                        LineRange: &PositiveIntegerRange{
13✔
768
                                BeginIntegerRange: ld.PositiveInt(r.StartPointer.LineNumber),
13✔
769
                                EndIntegerRange:   ld.PositiveInt(r.EndPointer.LineNumber),
13✔
770
                        },
13✔
771
                        ByteRange: &PositiveIntegerRange{
13✔
772
                                BeginIntegerRange: ld.PositiveInt(r.StartPointer.Offset),
13✔
773
                                EndIntegerRange:   ld.PositiveInt(r.EndPointer.Offset),
13✔
774
                        },
13✔
775
                }
13✔
776

13✔
777
                for _, l := range licenses {
39✔
778
                        c.addRelationship(&Relationship{
26✔
779
                                Type: RelationshipType_HasConcludedLicense,
26✔
780
                                From: newSnippet,
26✔
781
                                To:   ElementList{l},
26✔
782
                        })
26✔
783
                }
26✔
784

785
                allSnippets = append(allSnippets, newSnippet)
13✔
786
        }
787

788
        var out AnyElement
11✔
789
        switch len(allSnippets) {
11✔
NEW
790
        case 0:
×
791
        case 1:
9✔
792
                out = allSnippets[0]
9✔
793
        default:
2✔
794
                out = &Bundle{
2✔
795
                        Elements: allSnippets,
2✔
796
                }
2✔
797
        }
798

799
        c.idMap[string(s.SnippetSPDXIdentifier)] = out
11✔
800

11✔
801
        if snippetFile != nil {
13✔
802
                c.addRelationship(&Relationship{
2✔
803
                        Type: RelationshipType_Describes,
2✔
804
                        From: out,
2✔
805
                        To:   ElementList{snippetFile},
2✔
806
                })
2✔
807
        }
2✔
808

809
        return out
11✔
810
}
811

812
func appendUnique[T comparable](existing []T, adding ...T) []T {
24✔
813
        for _, add := range adding {
48✔
814
                if isNil(add) || slices.Contains(existing, add) {
24✔
NEW
815
                        continue
×
816
                }
817
                existing = append(existing, add)
24✔
818
        }
819
        return existing
24✔
820
}
821

822
func list[ListType ~[]To, From, To any](convertFunc func(From) To, values ...From) ListType {
47✔
823
        var out ListType
47✔
824
        for _, v := range values {
112✔
825
                if isNil(v) {
69✔
826
                        continue
4✔
827
                }
828
                o := convertFunc(v)
61✔
829
                if isNil(o) {
72✔
830
                        continue
11✔
831
                }
832
                out = append(out, o)
50✔
833
        }
834
        return out
47✔
835
}
836

837
func isNil(o any) bool {
176✔
838
        v := reflect.ValueOf(o)
176✔
839
        return !v.IsValid() || v.IsZero()
176✔
840
}
176✔
841

842
func externalIdentifierListEmail(emailAddr string) ExternalIdentifierList {
52✔
843
        return ExternalIdentifierList{
52✔
844
                &ExternalIdentifier{
52✔
845
                        Type:       ExternalIdentifierType_Email,
52✔
846
                        Identifier: emailAddr,
52✔
847
                },
52✔
848
        }
52✔
849
}
52✔
850

851
func duplicateLower[T any](m map[string]T) map[string]T {
100✔
852
        for k, v := range m {
1,353✔
853
                m[strings.ToLower(k)] = v
1,253✔
854
        }
1,253✔
855
        return m
100✔
856
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc