Skip to content

Commit 0d549ab

Browse files
authored
Minor improvements (#6)
1 parent 589d6bc commit 0d549ab

File tree

10 files changed

+256
-191
lines changed

10 files changed

+256
-191
lines changed

.goreleaser.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ builds:
2626
- darwin
2727
ldflags:
2828
- -s -w
29-
- -X github.com/boringbin/sbomlicense/internal/version.Version={{.Version}}
29+
- -X github.com/boringbin/sbomlicense/internal/version.version={{.Version}}
3030
- id: sbomlicensed
3131
binary: sbomlicensed
3232
main: ./cmd/sbomlicensed
@@ -38,7 +38,7 @@ builds:
3838
- darwin
3939
ldflags:
4040
- -s -w
41-
- -X github.com/boringbin/sbomlicense/internal/version.Version={{.Version}}
41+
- -X github.com/boringbin/sbomlicense/internal/version.version={{.Version}}
4242

4343
archives:
4444
# Separate archive for CLI tool

cmd/sbomlicense/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func run() int {
5151

5252
// Handle version flag
5353
if *showVersion {
54-
fmt.Fprintf(os.Stdout, "sbomlicense version %s\n", version.Version)
54+
fmt.Fprintf(os.Stdout, "sbomlicense version %s\n", version.Get())
5555
return exitSuccess
5656
}
5757

cmd/sbomlicense/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ func TestRun_Version(t *testing.T) {
244244
if !strings.Contains(output, "sbomlicense version") {
245245
t.Errorf("run() --version output = %q, want to contain 'sbomlicense version'", output)
246246
}
247-
if !strings.Contains(output, version.Version) {
248-
t.Errorf("run() --version output = %q, want to contain version %q", output, version.Version)
247+
if !strings.Contains(output, version.Get()) {
248+
t.Errorf("run() --version output = %q, want to contain version %q", output, version.Get())
249249
}
250250
}
251251

cmd/sbomlicensed/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func run() int {
109109
})
110110

111111
// Create server
112-
srv := server.NewServer(service, cacheInstance, logger, *parallel, *cacheTTL, version.Version)
112+
srv := server.NewServer(service, cacheInstance, logger, *parallel, *cacheTTL, version.Get())
113113

114114
// Create HTTP server
115115
httpServer := &http.Server{

internal/cache/bbolt.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,7 @@ func (c *BboltCache) Get(key string) (string, error) {
7373

7474
var entry bboltEntry
7575
if err := json.Unmarshal(data, &entry); err != nil {
76-
// Backward compatibility: treat as plain string value
77-
value = string(data)
78-
return nil //nolint:nilerr // Intentional: backward compatibility with non-TTL entries
76+
return err
7977
}
8078

8179
if entry.isExpired() {

internal/enricher/cyclonedx.go

Lines changed: 39 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"io"
8-
"log/slog"
9-
"sync"
107
"time"
118

129
"github.com/boringbin/sbomlicense/internal/cache"
@@ -60,6 +57,29 @@ type LicenseText struct {
6057
Content string `json:"content"`
6158
}
6259

60+
// GetPurl extracts the purl from the CycloneDX component.
61+
func (c *Component) GetPurl() (string, error) {
62+
return GetCycloneDXComponentPurl(c), nil
63+
}
64+
65+
// HasLicense returns true if the component already has license information.
66+
func (c *Component) HasLicense() bool {
67+
return HasComponentLicense(c)
68+
}
69+
70+
// SetLicense updates the component with the provided license string.
71+
// Adds license using Expression format (simpler and more common).
72+
func (c *Component) SetLicense(license string) {
73+
c.Licenses = append(c.Licenses, LicenseChoice{
74+
Expression: license,
75+
})
76+
}
77+
78+
// GetLogID returns the BOM reference for logging purposes.
79+
func (c *Component) GetLogID() string {
80+
return c.BOMRef
81+
}
82+
6383
// ParseCycloneDXFile parses the CycloneDX file into a CycloneDX BOM.
6484
func ParseCycloneDXFile(data []byte) (*BOM, error) {
6585
// Parse the JSON into a CycloneDX BOM
@@ -123,91 +143,30 @@ func NewCycloneDXEnricher(
123143
}
124144

125145
// Enrich enriches the CycloneDX SBOM with license information.
126-
//
127-
//nolint:gocognit // Complexity is inherent to parallel enrichment with worker pool pattern
128146
func (s *CycloneDXEnricher) Enrich(ctx context.Context, opts Options) ([]byte, error) {
129147
// Parse the SBOM file into a CycloneDX BOM
130148
bom, err := ParseCycloneDXFile(opts.SBOM)
131149
if err != nil {
132150
return nil, fmt.Errorf("failed to parse SBOM file: %w", err)
133151
}
134152

135-
if len(bom.Components) == 0 {
136-
// No components to enrich, return original SBOM
137-
return opts.SBOM, nil
138-
}
139-
140-
// Determine parallelism
141-
parallelism := opts.Parallelism
142-
if parallelism <= 0 {
143-
parallelism = 1
144-
}
145-
146-
// Use provided logger or create a no-op logger
147-
logger := opts.Logger
148-
if logger == nil {
149-
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
150-
}
151-
152-
// Create a channel for components to enrich
153-
type job struct {
154-
component *Component
155-
purl string
156-
}
157-
158-
jobs := make(chan job, len(bom.Components))
159-
var wg sync.WaitGroup
160-
161-
// Spawn workers
162-
for range parallelism {
163-
wg.Add(1)
164-
go func() {
165-
defer wg.Done()
166-
for j := range jobs {
167-
// Check if component already has licenses (in any format)
168-
hasLicense := HasComponentLicense(j.component)
169-
170-
if hasLicense {
171-
continue
172-
}
173-
174-
// Get the license from the service
175-
lic, licErr := provider.Get(ctx, provider.GetOptions{
176-
Purl: j.purl,
177-
Provider: s.provider,
178-
Cache: s.cache,
179-
CacheTTL: s.cacheTTL,
180-
})
181-
if licErr != nil {
182-
// Log error but continue processing other components
183-
logger.ErrorContext(ctx, "failed to get license for component",
184-
"purl", j.purl,
185-
"bom_ref", j.component.BOMRef,
186-
"error", licErr)
187-
continue
188-
}
189-
if lic != "" {
190-
// Add license using Expression format (simpler and more common)
191-
j.component.Licenses = append(j.component.Licenses, LicenseChoice{
192-
Expression: lic,
193-
})
194-
}
195-
}
196-
}()
197-
}
198-
199-
// Queue all components
153+
// Convert []Component to []*Component for interface satisfaction
154+
components := make([]*Component, len(bom.Components))
200155
for i := range bom.Components {
201-
component := &bom.Components[i]
202-
// Get the purl for the component if it exists
203-
purl := GetCycloneDXComponentPurl(component)
204-
205-
jobs <- job{component: component, purl: purl}
156+
components[i] = &bom.Components[i]
206157
}
207158

208-
// Close the channel and wait for workers to finish
209-
close(jobs)
210-
wg.Wait()
211-
212-
return json.Marshal(bom)
159+
// Enrich and marshal using common helper
160+
return enrichDocument(
161+
ctx,
162+
opts,
163+
bom,
164+
components,
165+
s.provider,
166+
s.cache,
167+
s.cacheTTL,
168+
func(b *BOM) ([]byte, error) {
169+
return json.Marshal(b)
170+
},
171+
)
213172
}

internal/enricher/spdx.go

Lines changed: 50 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"io"
8-
"log/slog"
9-
"sync"
107
"time"
118

129
"github.com/boringbin/sbomlicense/internal/cache"
@@ -47,6 +44,40 @@ type ExternalRef struct {
4744
ReferenceLocator string `json:"referenceLocator"`
4845
}
4946

47+
// GetPurl extracts the purl from the SPDX package's external references.
48+
func (p *Package) GetPurl() (string, error) {
49+
return GetSPDXPackagePurl(p)
50+
}
51+
52+
// HasLicense returns true if the package already has license information.
53+
// Checks both LicenseConcluded and LicenseDeclared fields.
54+
func (p *Package) HasLicense() bool {
55+
return (p.LicenseConcluded != "" &&
56+
p.LicenseConcluded != spdxLicenseNone &&
57+
p.LicenseConcluded != spdxLicenseNoAssertion) ||
58+
(p.LicenseDeclared != "" &&
59+
p.LicenseDeclared != spdxLicenseNone &&
60+
p.LicenseDeclared != spdxLicenseNoAssertion)
61+
}
62+
63+
// SetLicense updates the package with the provided license string.
64+
// Sets LicenseConcluded as the primary field and also updates LicenseDeclared if it's empty.
65+
func (p *Package) SetLicense(license string) {
66+
// Set LicenseConcluded (primary field)
67+
p.LicenseConcluded = license
68+
// Also set LicenseDeclared if it's empty
69+
if p.LicenseDeclared == "" ||
70+
p.LicenseDeclared == spdxLicenseNone ||
71+
p.LicenseDeclared == spdxLicenseNoAssertion {
72+
p.LicenseDeclared = license
73+
}
74+
}
75+
76+
// GetLogID returns the SPDX ID for logging purposes.
77+
func (p *Package) GetLogID() string {
78+
return p.SPDXID
79+
}
80+
5081
// UnwrapGitHubSBOM checks if the data is wrapped in GitHub's {"sbom": {...}} format and returns the unwrapped SPDX
5182
// data if so, or the original data otherwise.
5283
func UnwrapGitHubSBOM(data []byte) ([]byte, error) {
@@ -113,108 +144,30 @@ func NewSPDXEnricher(provider provider.Provider, cache cache.Cache, cacheTTL tim
113144
}
114145

115146
// Enrich enriches the SPDX SBOM with license information.
116-
//
117-
//nolint:gocognit // Complexity is inherent to parallel enrichment with worker pool pattern
118147
func (s *SPDXEnricher) Enrich(ctx context.Context, opts Options) ([]byte, error) {
119148
// Parse the SBOM file into an SPDX document
120149
doc, err := ParseSBOMFile(opts.SBOM)
121150
if err != nil {
122151
return nil, fmt.Errorf("failed to parse SBOM file: %w", err)
123152
}
124153

125-
if len(doc.Packages) == 0 {
126-
// No packages to enrich, return original SBOM
127-
return opts.SBOM, nil
128-
}
129-
130-
// Determine parallelism
131-
parallelism := opts.Parallelism
132-
if parallelism <= 0 {
133-
parallelism = 1
134-
}
135-
136-
// Use provided logger or create a no-op logger
137-
logger := opts.Logger
138-
if logger == nil {
139-
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
140-
}
141-
142-
// Create a channel for packages to enrich
143-
type job struct {
144-
pkg *Package
145-
purl string
146-
}
147-
148-
jobs := make(chan job, len(doc.Packages))
149-
var wg sync.WaitGroup
150-
151-
// Spawn workers
152-
for range parallelism {
153-
wg.Add(1)
154-
go func() {
155-
defer wg.Done()
156-
for j := range jobs {
157-
// Check if package already has a license
158-
// Try LicenseConcluded first, fallback to LicenseDeclared
159-
hasLicense := (j.pkg.LicenseConcluded != "" &&
160-
j.pkg.LicenseConcluded != spdxLicenseNone &&
161-
j.pkg.LicenseConcluded != spdxLicenseNoAssertion) ||
162-
(j.pkg.LicenseDeclared != "" &&
163-
j.pkg.LicenseDeclared != spdxLicenseNone &&
164-
j.pkg.LicenseDeclared != spdxLicenseNoAssertion)
165-
166-
if hasLicense {
167-
continue
168-
}
169-
170-
// Get the license from the service
171-
lic, licErr := provider.Get(ctx, provider.GetOptions{
172-
Purl: j.purl,
173-
Provider: s.provider,
174-
Cache: s.cache,
175-
CacheTTL: s.cacheTTL,
176-
})
177-
if licErr != nil {
178-
// Log error but continue processing other packages
179-
logger.ErrorContext(ctx, "failed to get license for package",
180-
"purl", j.purl,
181-
"spdx_id", j.pkg.SPDXID,
182-
"error", licErr)
183-
continue
184-
}
185-
if lic != "" {
186-
// Set LicenseConcluded (primary field)
187-
j.pkg.LicenseConcluded = lic
188-
// Also set LicenseDeclared if it's empty
189-
if j.pkg.LicenseDeclared == "" ||
190-
j.pkg.LicenseDeclared == spdxLicenseNone ||
191-
j.pkg.LicenseDeclared == spdxLicenseNoAssertion {
192-
j.pkg.LicenseDeclared = lic
193-
}
194-
}
195-
}
196-
}()
197-
}
198-
199-
// Queue all packages
154+
// Convert []Package to []*Package for interface satisfaction
155+
pkgs := make([]*Package, len(doc.Packages))
200156
for i := range doc.Packages {
201-
pkg := &doc.Packages[i]
202-
// Get the purl for the package if it exists
203-
purl, purlErr := GetSPDXPackagePurl(pkg)
204-
if purlErr != nil {
205-
// Log error but continue processing other packages
206-
logger.ErrorContext(ctx, "failed to get purl for package",
207-
"spdx_id", pkg.SPDXID,
208-
"error", purlErr)
209-
continue
210-
}
211-
212-
jobs <- job{pkg: pkg, purl: purl}
157+
pkgs[i] = &doc.Packages[i]
213158
}
214159

215-
// Close the channel and wait for workers to finish
216-
close(jobs)
217-
wg.Wait()
218-
219-
return json.Marshal(doc)
160+
// Enrich and marshal using common helper
161+
return enrichDocument(
162+
ctx,
163+
opts,
164+
doc,
165+
pkgs,
166+
s.provider,
167+
s.cache,
168+
s.cacheTTL,
169+
func(d *Document) ([]byte, error) {
170+
return json.Marshal(d)
171+
},
172+
)
220173
}

0 commit comments

Comments
 (0)