Skip to content

Commit 1a1dee6

Browse files
authored
Merge pull request #57 from puerco/document-query
Make the document queryable (and ensure unique names)
2 parents 58c15e1 + 91d8425 commit 1a1dee6

File tree

8 files changed

+244
-3
lines changed

8 files changed

+244
-3
lines changed

pkg/spdx/builder.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ func (builder *defaultDocBuilderImpl) GenerateDoc(
191191
if err != nil {
192192
return nil, errors.Wrap(err, "generating package from directory")
193193
}
194+
doc.ensureUniqueElementID(pkg)
194195

195196
if err := doc.AddPackage(pkg); err != nil {
196197
return nil, errors.Wrap(err, "adding directory package to document")
@@ -204,18 +205,22 @@ func (builder *defaultDocBuilderImpl) GenerateDoc(
204205
if err != nil {
205206
return nil, errors.Wrapf(err, "generating SPDX package from image ref %s", i)
206207
}
208+
doc.ensureUniqueElementID(p)
209+
doc.ensureUniquePeerIDs(p.GetRelationships())
207210
if err := doc.AddPackage(p); err != nil {
208211
return nil, errors.Wrap(err, "adding package to document")
209212
}
210213
}
211214

212215
// Process OCI image archives
213216
for _, tb := range genopts.Tarballs {
214-
logrus.Infof("Processing tarball %s", tb)
217+
logrus.Infof("Processing image archive %s", tb)
215218
p, err := spdx.PackageFromImageTarball(tb)
216219
if err != nil {
217220
return nil, errors.Wrap(err, "generating tarball package")
218221
}
222+
doc.ensureUniqueElementID(p)
223+
doc.ensureUniquePeerIDs(p.GetRelationships())
219224
if err := doc.AddPackage(p); err != nil {
220225
return nil, errors.Wrap(err, "adding package to document")
221226
}
@@ -228,6 +233,8 @@ func (builder *defaultDocBuilderImpl) GenerateDoc(
228233
if err != nil {
229234
return nil, errors.Wrap(err, "creating spdx package from archive")
230235
}
236+
doc.ensureUniqueElementID(p)
237+
doc.ensureUniquePeerIDs(p.GetRelationships())
231238
if err := doc.AddPackage(p); err != nil {
232239
return nil, errors.Wrap(err, "adding package to document")
233240
}
@@ -240,6 +247,7 @@ func (builder *defaultDocBuilderImpl) GenerateDoc(
240247
if err != nil {
241248
return nil, errors.Wrap(err, "adding file")
242249
}
250+
doc.ensureUniqueElementID(f)
243251
if err := doc.AddFile(f); err != nil {
244252
return nil, errors.Wrap(err, "adding file to document")
245253
}

pkg/spdx/document.go

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,13 @@ func (d *Document) AddPackage(pkg *Package) error {
175175

176176
if pkg.SPDXID() == "" {
177177
pkg.BuildID(pkg.Name)
178+
d.ensureUniqueElementID(pkg)
178179
}
179180
if pkg.SPDXID() == "" {
180-
return errors.New("package id is needed to add a new package")
181+
return errors.New("package ID is needed to add a new package")
181182
}
182183
if _, ok := d.Packages[pkg.SPDXID()]; ok {
183-
return errors.New("a package named " + pkg.SPDXID() + " already exists in the document")
184+
return errors.Errorf("a package with ID %s already exists in the document", pkg.SPDXID())
184185
}
185186

186187
d.Packages[pkg.SPDXID()] = pkg
@@ -278,6 +279,7 @@ func (d *Document) AddFile(file *File) error {
278279
}
279280
file.ID = "SPDXRef-File-" + fmt.Sprintf("%x", h.Sum(nil))
280281
}
282+
d.ensureUniqueElementID(file)
281283
d.Files[file.ID] = file
282284
return nil
283285
}
@@ -398,3 +400,75 @@ func (d *Document) WriteProvenanceStatement(opts *ProvenanceOptions, path string
398400
"writing sbom as provenance statement",
399401
)
400402
}
403+
404+
// ensureUniquePackageID takes a string and checks if
405+
// there is another string with the same name in the document.
406+
// If there is one, it will append a digit until a unique name
407+
// is found.
408+
func (d *Document) ensureUniqueElementID(o Object) {
409+
newID := o.SPDXID()
410+
i := 0
411+
for {
412+
// Check if there us already an element with the same ID
413+
if el := d.GetElementByID(newID); el == nil {
414+
if o.SPDXID() != newID {
415+
logrus.Infof(
416+
"Element name changed from %s to %s to ensure it is unique",
417+
o.SPDXID(), newID,
418+
)
419+
}
420+
o.SetSPDXID(newID)
421+
break
422+
}
423+
i++
424+
newID = fmt.Sprintf("%s-%04d", o.SPDXID(), i)
425+
}
426+
}
427+
428+
// ensureUniquePeerIDs gets a relationship collection and ensures all peers
429+
// have unique IDs
430+
func (d *Document) ensureUniquePeerIDs(rels *[]*Relationship) {
431+
// First, ensure peer names are unique among themselves
432+
seen := map[string]struct{}{}
433+
for _, rel := range *rels {
434+
if rel.Peer == nil || rel.Peer.SPDXID() == "" {
435+
continue
436+
}
437+
testName := rel.Peer.SPDXID()
438+
i := 0
439+
for {
440+
if _, ok := seen[testName]; !ok {
441+
rel.Peer.SetSPDXID(testName)
442+
seen[testName] = struct{}{}
443+
break
444+
}
445+
i++
446+
testName = fmt.Sprintf("%s-%04d", rel.Peer.SPDXID(), i)
447+
}
448+
}
449+
450+
// And then check against the document
451+
for _, rel := range *rels {
452+
if rel.Peer == nil {
453+
continue
454+
}
455+
d.ensureUniqueElementID(rel.Peer)
456+
}
457+
}
458+
459+
// GetPackageByID queries the packages to search for a specific entity by name
460+
// note that this method returns a copy of the entity if found.
461+
func (d *Document) GetElementByID(id string) Object {
462+
seen := map[string]struct{}{}
463+
for _, p := range d.Packages {
464+
if sub := recursiveSearch(id, p, &seen); sub != nil {
465+
return sub
466+
}
467+
}
468+
for _, f := range d.Files {
469+
if sub := recursiveSearch(id, f, &seen); sub != nil {
470+
return sub
471+
}
472+
}
473+
return nil
474+
}

pkg/spdx/document_unit_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,54 @@ func compareSubjects(t *testing.T, statement1, statement2 *provenance.Statement)
118118
}
119119
}
120120
}
121+
122+
func TestEnsureUniqueElementID(t *testing.T) {
123+
doc := NewDocument()
124+
name := "same-name"
125+
for i := 0; i < 3; i++ {
126+
subp := NewPackage()
127+
subp.SetSPDXID(name)
128+
129+
// Passing the subpackages through nsureUniquePackageID
130+
// should rename the last two, but not the first one
131+
doc.ensureUniqueElementID(subp)
132+
require.NoError(t, doc.AddPackage(subp))
133+
134+
if i == 0 {
135+
require.Equal(t, name, subp.SPDXID())
136+
} else {
137+
require.NotEqual(t, name, subp.SPDXID())
138+
}
139+
}
140+
}
141+
142+
func TestEnsureUniquePeerIDs(t *testing.T) {
143+
doc := NewDocument()
144+
name := "same-name"
145+
146+
// Add one node with the name
147+
namePkg := NewPackage()
148+
namePkg.SetSPDXID(name)
149+
150+
// Build a package with 3 peers all with the same name
151+
p := NewPackage()
152+
p.SetSPDXID("parentNode")
153+
for i := 0; i < 3; i++ {
154+
subp := NewPackage()
155+
subp.SetSPDXID(name)
156+
require.NoError(t, p.AddPackage(subp))
157+
}
158+
159+
// Pass the package with its peers through ensureUniquePeerIDs
160+
doc.ensureUniquePeerIDs(p.GetRelationships())
161+
162+
// Now, they should all be different
163+
seenNames := map[string]struct{}{}
164+
rels := p.GetRelationships()
165+
for _, rel := range *rels {
166+
logrus.Info("Checking " + rel.Peer.SPDXID())
167+
_, ok := seenNames[rel.Peer.SPDXID()]
168+
require.False(t, ok, rel.Peer.SPDXID()+" should be unique")
169+
seenNames[rel.Peer.SPDXID()] = struct{}{}
170+
}
171+
}

pkg/spdx/file.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,13 @@ func getFileContentType(path string) (string, error) {
190190
contentType := http.DetectContentType(buffer)
191191
return contentType, nil
192192
}
193+
194+
// GetElementByID search the file and its peers looking for the
195+
// specified SPDX id. If found, the function returns a copy of
196+
// the object identified by the SPDX-ID provided
197+
func (f *File) GetElementByID(id string) Object {
198+
if f.SPDXID() == id {
199+
return f
200+
}
201+
return recursiveSearch(id, f, &map[string]struct{}{})
202+
}

pkg/spdx/object.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
// objects. Currently this includes files and packages.
4141
type Object interface {
4242
SPDXID() string
43+
SetSPDXID(string)
4344
ReadSourceFile(string) error
4445
Render() (string, error)
4546
BuildID(seeds ...string)
@@ -48,6 +49,7 @@ type Object interface {
4849
GetRelationships() *[]*Relationship
4950
ToProvenanceSubject() *intoto.Subject
5051
getProvenanceSubjects(opts *ProvenanceOptions, seen *map[string]struct{}) []intoto.Subject
52+
GetElementByID(string) Object
5153
}
5254

5355
type Entity struct {
@@ -77,6 +79,11 @@ func (e *Entity) SPDXID() string {
7779
return e.ID
7880
}
7981

82+
// SPDXID returns the SPDX reference string for the object
83+
func (e *Entity) SetSPDXID(id string) {
84+
e.ID = id
85+
}
86+
8087
// BuildID sets the file ID, optionally from a series of strings
8188
func (e *Entity) BuildID(seeds ...string) {
8289
if len(seeds) <= 1 {
@@ -269,3 +276,6 @@ mloop:
269276
}
270277
return ret
271278
}
279+
280+
// GetElementByID nil function to be overridden by package and file
281+
func (e *Entity) GetElementByID(string) Object { return nil }

pkg/spdx/package.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,8 @@ func (p *Package) Draw(builder *strings.Builder, o *DrawingOptions, depth int, s
388388
}
389389
}
390390

391+
// ReadSourceFile reads a file from the filesystem and assigns its properties
392+
// to the package metadata
391393
func (p *Package) ReadSourceFile(path string) error {
392394
if err := p.Entity.ReadSourceFile(path); err != nil {
393395
return err
@@ -397,3 +399,12 @@ func (p *Package) ReadSourceFile(path string) error {
397399
}
398400
return nil
399401
}
402+
403+
// GetElementByID search the package and its peers looking for the specified SPDX
404+
// id. If found, the function returns a copy of the object
405+
func (p *Package) GetElementByID(id string) Object {
406+
if p.SPDXID() == id {
407+
return p
408+
}
409+
return recursiveSearch(id, p, &map[string]struct{}{})
410+
}

pkg/spdx/spdx.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,30 @@ func Banner() string {
246246
}
247247
return string(d)
248248
}
249+
250+
// recursiveSearch is a function that recursively searches an object's peers
251+
// to find the specified SPDX ID. If found, returns a copy of the object.
252+
// nolint:gocritic // seen is a pointer recursively populated
253+
func recursiveSearch(id string, o Object, seen *map[string]struct{}) Object {
254+
if o.SPDXID() == id {
255+
return o
256+
}
257+
for _, rel := range *o.GetRelationships() {
258+
if rel.Peer == nil {
259+
continue
260+
}
261+
262+
if _, ok := (*seen)[o.SPDXID()]; ok {
263+
continue
264+
}
265+
266+
if rel.Peer.SPDXID() == id {
267+
return rel.Peer
268+
}
269+
270+
if peerObject := recursiveSearch(id, rel.Peer, seen); peerObject != nil {
271+
return peerObject
272+
}
273+
}
274+
return nil
275+
}

pkg/spdx/spdx_unit_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,3 +402,53 @@ func TestIgnorePatterns(t *testing.T) {
402402
require.NoError(t, err)
403403
require.Len(t, p, 4)
404404
}
405+
406+
func TestRecursiveSearch(t *testing.T) {
407+
p := NewPackage()
408+
p.SetSPDXID("p-top")
409+
410+
// Lets nest 3 packages
411+
packages := []*Package{}
412+
for i := 0; i < 3; i++ {
413+
subp := NewPackage()
414+
subp.SetSPDXID(fmt.Sprintf("subpackage-%d", i))
415+
packages = append(packages, subp)
416+
}
417+
for i, sp := range packages {
418+
if i > 0 {
419+
require.NoError(t, packages[i-1].AddPackage(sp))
420+
}
421+
}
422+
require.NoError(t, p.AddPackage(packages[0]))
423+
424+
// This functions searches 3 packages with the same prefix:
425+
checkSubPackages := func(p *Package, radix string) {
426+
for i := 0; i < 3; i++ {
427+
require.NotNil(t, p.GetElementByID(fmt.Sprintf("%s-%d", radix, i)),
428+
fmt.Sprintf("searching for %s", radix),
429+
)
430+
}
431+
}
432+
433+
checkSubPackages(p, "subpackage")
434+
// Non existent packages should not return an element
435+
require.Nil(t, p.GetElementByID("subpackage-10000000"))
436+
437+
// Now bifurcating the document structure adding dependencies should
438+
// not alter those conditions.
439+
440+
// Lets add 3 dependencies to one of the nested packages
441+
for i := 0; i < 3; i++ {
442+
subp := NewPackage()
443+
subp.SetSPDXID(fmt.Sprintf("dep-%d", i))
444+
require.NoError(t, p.GetElementByID("subpackage-1").(*Package).AddPackage(subp))
445+
}
446+
447+
// Same tests should still pass:
448+
checkSubPackages(p, "subpackage")
449+
// Non existent packages should not return an element
450+
require.Nil(t, p.GetElementByID("subpackage-10000000"))
451+
452+
// But also dependencies should be found
453+
checkSubPackages(p, "dep")
454+
}

0 commit comments

Comments
 (0)