diff --git a/syft/pkg/catalog.go b/syft/pkg/catalog.go index 6445f98a84ea..e038f23986d4 100644 --- a/syft/pkg/catalog.go +++ b/syft/pkg/catalog.go @@ -26,6 +26,15 @@ loopNewIDs: } } +func (s *orderedIDSet) delete(id artifact.ID) { + for i, existingID := range s.slice { + if existingID == id { + s.slice = append(s.slice[:i], s.slice[i+1:]...) + return + } + } +} + // Catalog represents a collection of Packages. type Catalog struct { byID map[artifact.ID]Package @@ -92,30 +101,32 @@ func (c *Catalog) Packages(ids []artifact.ID) (result []Package) { return result } -// Add a package to the Catalog. -func (c *Catalog) Add(p Package) { +// Add n packages to the catalog. +func (c *Catalog) Add(pkgs ...Package) { c.lock.Lock() defer c.lock.Unlock() - id := p.ID() - if id == "" { - log.Warnf("found package with empty ID while adding to the catalog: %+v", p) - p.SetID() - id = p.ID() - } + for _, p := range pkgs { + id := p.ID() + if id == "" { + log.Warnf("found package with empty ID while adding to the catalog: %+v", p) + p.SetID() + id = p.ID() + } - if existing, exists := c.byID[id]; exists { - // there is already a package with this fingerprint merge the existing record with the new one - if err := existing.merge(p); err != nil { - log.Warnf("failed to merge packages: %+v", err) - } else { - c.byID[id] = existing - c.addPathsToIndex(p) + if existing, exists := c.byID[id]; exists { + // there is already a package with this fingerprint merge the existing record with the new one + if err := existing.merge(p); err != nil { + log.Warnf("failed to merge packages: %+v", err) + } else { + c.byID[id] = existing + c.addPathsToIndex(p) + } + return } - return - } - c.addToIndex(p) + c.addToIndex(p) + } } func (c *Catalog) addToIndex(p Package) { @@ -157,6 +168,59 @@ func (c *Catalog) addPathToIndex(id artifact.ID, path string) { c.idsByPath[path] = pathIndex } +func (c *Catalog) Delete(ids ...artifact.ID) { + c.lock.Lock() + defer c.lock.Unlock() + + for _, id := range ids { + p, exists := c.byID[id] + if !exists { + return + } + + delete(c.byID, id) + c.deleteNameFromIndex(p) + c.deleteTypeFromIndex(p) + c.deletePathsFromIndex(p) + } +} + +func (c *Catalog) deleteNameFromIndex(p Package) { + nameIndex := c.idsByName[p.Name] + nameIndex.delete(p.id) + c.idsByName[p.Name] = nameIndex +} + +func (c *Catalog) deleteTypeFromIndex(p Package) { + typeIndex := c.idsByType[p.Type] + typeIndex.delete(p.id) + c.idsByType[p.Type] = typeIndex +} + +func (c *Catalog) deletePathsFromIndex(p Package) { + observedPaths := internal.NewStringSet() + for _, l := range p.Locations.ToSlice() { + if l.RealPath != "" && !observedPaths.Contains(l.RealPath) { + c.deletePathFromIndex(p.id, l.RealPath) + observedPaths.Add(l.RealPath) + } + if l.VirtualPath != "" && l.RealPath != l.VirtualPath && !observedPaths.Contains(l.VirtualPath) { + c.deletePathFromIndex(p.id, l.VirtualPath) + observedPaths.Add(l.VirtualPath) + } + } +} + +func (c *Catalog) deletePathFromIndex(id artifact.ID, path string) { + pathIndex := c.idsByPath[path] + pathIndex.delete(id) + if len(pathIndex.slice) == 0 { + delete(c.idsByPath, path) + } else { + c.idsByPath[path] = pathIndex + } +} + // Enumerate all packages for the given type(s), enumerating all packages if no type is specified. func (c *Catalog) Enumerate(types ...Type) <-chan Package { channel := make(chan Package) diff --git a/syft/pkg/catalog_test.go b/syft/pkg/catalog_test.go index 09b6a973ec22..046938cebaa3 100644 --- a/syft/pkg/catalog_test.go +++ b/syft/pkg/catalog_test.go @@ -16,6 +16,148 @@ type expectedIndexes struct { byPath map[string]*strset.Set } +func TestCatalogDeleteRemovesPackages(t *testing.T) { + tests := []struct { + name string + pkgs []Package + deleteIDs []artifact.ID + expectedIndexes expectedIndexes + }{ + { + name: "delete one package", + pkgs: []Package{ + { + id: "pkg:deb/debian/1", + Name: "debian", + Version: "1", + Type: DebPkg, + Locations: source.NewLocationSet( + source.NewVirtualLocation("/c/path", "/another/path1"), + ), + }, + { + id: "pkg:deb/debian/2", + Name: "debian", + Version: "2", + Type: DebPkg, + Locations: source.NewLocationSet( + source.NewVirtualLocation("/d/path", "/another/path2"), + ), + }, + }, + deleteIDs: []artifact.ID{ + artifact.ID("pkg:deb/debian/1"), + }, + expectedIndexes: expectedIndexes{ + byType: map[Type]*strset.Set{ + DebPkg: strset.New("pkg:deb/debian/2"), + }, + byPath: map[string]*strset.Set{ + "/d/path": strset.New("pkg:deb/debian/2"), + "/another/path2": strset.New("pkg:deb/debian/2"), + }, + }, + }, + { + name: "delete multiple packages", + pkgs: []Package{ + { + id: "pkg:deb/debian/1", + Name: "debian", + Version: "1", + Type: DebPkg, + Locations: source.NewLocationSet( + source.NewVirtualLocation("/c/path", "/another/path1"), + ), + }, + { + id: "pkg:deb/debian/2", + Name: "debian", + Version: "2", + Type: DebPkg, + Locations: source.NewLocationSet( + source.NewVirtualLocation("/d/path", "/another/path2"), + ), + }, + { + id: "pkg:deb/debian/3", + Name: "debian", + Version: "3", + Type: DebPkg, + Locations: source.NewLocationSet( + source.NewVirtualLocation("/e/path", "/another/path3"), + ), + }, + }, + deleteIDs: []artifact.ID{ + artifact.ID("pkg:deb/debian/1"), + artifact.ID("pkg:deb/debian/3"), + }, + expectedIndexes: expectedIndexes{ + byType: map[Type]*strset.Set{ + DebPkg: strset.New("pkg:deb/debian/2"), + }, + byPath: map[string]*strset.Set{ + "/d/path": strset.New("pkg:deb/debian/2"), + "/another/path2": strset.New("pkg:deb/debian/2"), + }, + }, + }, + { + name: "delete non-existent package", + pkgs: []Package{ + { + id: artifact.ID("pkg:deb/debian/1"), + Name: "debian", + Version: "1", + Type: DebPkg, + Locations: source.NewLocationSet( + source.NewVirtualLocation("/c/path", "/another/path1"), + ), + }, + { + id: artifact.ID("pkg:deb/debian/2"), + Name: "debian", + Version: "2", + Type: DebPkg, + Locations: source.NewLocationSet( + source.NewVirtualLocation("/d/path", "/another/path2"), + ), + }, + }, + deleteIDs: []artifact.ID{ + artifact.ID("pkg:deb/debian/3"), + }, + expectedIndexes: expectedIndexes{ + byType: map[Type]*strset.Set{ + DebPkg: strset.New("pkg:deb/debian/1", "pkg:deb/debian/2"), + }, + byPath: map[string]*strset.Set{ + "/c/path": strset.New("pkg:deb/debian/1"), + "/another/path1": strset.New("pkg:deb/debian/1"), + "/d/path": strset.New("pkg:deb/debian/2"), + "/another/path2": strset.New("pkg:deb/debian/2"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + c := NewCatalog() + for _, p := range test.pkgs { + c.Add(p) + } + + for _, id := range test.deleteIDs { + c.Delete(id) + } + + assertIndexes(t, c, test.expectedIndexes) + }) + } +} + func TestCatalogAddPopulatesIndex(t *testing.T) { var pkgs = []Package{