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

spdx / tools-golang / 18361478302

09 Oct 2025 12:13AM UTC coverage: 86.111% (-9.4%) from 95.483%
18361478302

Pull #247

github

kzantow
chore: bump go-ld

Signed-off-by: Keith Zantow <kzantow@gmail.com>
Pull Request #247: feat: prototype 3.0 model

713 of 825 new or added lines in 7 files covered. (86.42%)

3 existing lines in 1 file now uncovered.

8196 of 9518 relevant lines covered (86.11%)

19.06 hits per line

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

90.1
/spdx/v3/v3_0_1/convert.go
1
package v3_0_1
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
        // Java tools prefix all these values with: http://spdx.org/rdf/references/
10✔
90
        // I'm assuming this is because they are converted to RDF when imported, which we are not doing
10✔
91
        const referencePrefix = ""
10✔
92

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

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

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

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

267
        typ, invert := c.convert23relationshipType(rel.Relationship)
4✔
268
        if invert {
4✔
NEW
269
                to, from = from, to
×
NEW
270
        }
×
271

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

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

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

302
func (c *documentConverter) convert23creationInfo(info *v2_3.CreationInfo) AnyCreationInfo {
3✔
303
        if info == nil || len(info.Creators) == 0 {
3✔
NEW
304
                return nil
×
NEW
305
        }
×
306
        ci := &CreationInfo{
3✔
307
                Comment:      info.CreatorComment,
3✔
308
                Created:      c.convert23time(info.Created),
3✔
309
                CreatedBy:    list[AgentList](c.convert23creator, info.Creators...),
3✔
310
                CreatedUsing: list[ToolList](c.convert23tool, info.Creators...),
3✔
311
                // TODO should this be set?
3✔
312
                // SpecVersion:  info.LicenseListVersion,
3✔
313
        }
3✔
314

3✔
315
        // update circular references, which will be set to nil by default
3✔
316
        for _, a := range ci.CreatedBy.Agents() {
6✔
317
                a.SetCreationInfo(ci)
3✔
318
        }
3✔
319
        for _, a := range ci.CreatedUsing.Tools() {
3✔
NEW
320
                a.SetCreationInfo(ci)
×
NEW
321
        }
×
322

323
        return ci
3✔
324
}
325

326
func (c *documentConverter) convert23tool(creator common.Creator) AnyTool {
6✔
327
        if strings.ToLower(creator.CreatorType) != "tool" {
9✔
328
                return nil
3✔
329
        }
3✔
330

331
        if creator.Creator != "" {
6✔
332
                c.logDropped(creator)
3✔
333
                return nil
3✔
334
        }
3✔
335

NEW
336
        return &Tool{
×
NEW
337
                CreationInfo: c.creationInfo,
×
NEW
338
                Name:         creator.Creator,
×
NEW
339
        }
×
340
}
341

342
func (c *documentConverter) convert23creator(creator common.Creator) AnyAgent {
6✔
343
        return c.convert23agent(creator.CreatorType, creator.Creator)
6✔
344
}
6✔
345

346
func (c *documentConverter) convert23originator(creator *common.Originator) AnyAgent {
3✔
347
        return c.convert23agent(creator.OriginatorType, creator.Originator)
3✔
348
}
3✔
349

350
func (c *documentConverter) convert23supplier(creator *common.Supplier) AnyAgent {
5✔
351
        return c.convert23agent(creator.SupplierType, creator.Supplier)
5✔
352
}
5✔
353

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

561
        return out
5✔
562
}
563

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

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

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

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

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

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

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

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

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

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

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

5✔
651
        return out
5✔
652
}
653

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

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

675
        return out
40✔
676
}
677

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

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

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

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

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

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

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

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

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

748
        var allSnippets ElementList
11✔
749

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

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

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

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

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

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

808
        return out
11✔
809
}
810

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

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

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

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

850
func duplicateLower[T any](m map[string]T) map[string]T {
100✔
851
        for k, v := range m {
1,362✔
852
                m[strings.ToLower(k)] = v
1,262✔
853
        }
1,262✔
854
        return m
100✔
855
}
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