github.com/anchore/syft@v1.38.2/internal/relationship/index.go (about)

     1  package relationship
     2  
     3  import (
     4  	"slices"
     5  	"strings"
     6  
     7  	"github.com/anchore/syft/syft/artifact"
     8  	"github.com/anchore/syft/syft/file"
     9  )
    10  
    11  // Index indexes relationships, preventing duplicates
    12  type Index struct {
    13  	all    []*sortableRelationship
    14  	fromID map[artifact.ID]*mappedRelationships
    15  	toID   map[artifact.ID]*mappedRelationships
    16  }
    17  
    18  // NewIndex returns a new relationship Index
    19  func NewIndex(relationships ...artifact.Relationship) *Index {
    20  	out := Index{
    21  		fromID: make(map[artifact.ID]*mappedRelationships),
    22  		toID:   make(map[artifact.ID]*mappedRelationships),
    23  	}
    24  	out.Add(relationships...)
    25  	return &out
    26  }
    27  
    28  // Add adds all the given relationships to the index, without adding duplicates
    29  func (i *Index) Add(relationships ...artifact.Relationship) {
    30  	// store appropriate indexes for stable ordering to minimize ID() calls
    31  	for _, r := range relationships {
    32  		// prevent duplicates
    33  		if i.Contains(r) {
    34  			continue
    35  		}
    36  
    37  		fromID := r.From.ID()
    38  		toID := r.To.ID()
    39  
    40  		relationship := &sortableRelationship{
    41  			from:         fromID,
    42  			to:           toID,
    43  			relationship: r,
    44  		}
    45  
    46  		// add to all relationships
    47  		i.all = append(i.all, relationship)
    48  
    49  		// add from -> to mapping
    50  		mapped := i.fromID[fromID]
    51  		if mapped == nil {
    52  			mapped = &mappedRelationships{}
    53  			i.fromID[fromID] = mapped
    54  		}
    55  		mapped.add(toID, relationship)
    56  
    57  		// add to -> from mapping
    58  		mapped = i.toID[toID]
    59  		if mapped == nil {
    60  			mapped = &mappedRelationships{}
    61  			i.toID[toID] = mapped
    62  		}
    63  		mapped.add(fromID, relationship)
    64  	}
    65  }
    66  
    67  func (i *Index) Remove(id artifact.ID) {
    68  	delete(i.fromID, id)
    69  	delete(i.toID, id)
    70  
    71  	for idx := 0; idx < len(i.all); {
    72  		if i.all[idx].from == id || i.all[idx].to == id {
    73  			i.all = append(i.all[:idx], i.all[idx+1:]...)
    74  		} else {
    75  			idx++
    76  		}
    77  	}
    78  }
    79  
    80  func (i *Index) Replace(ogID artifact.ID, replacement artifact.Identifiable) {
    81  	for _, mapped := range fromMappedByID(i.fromID, ogID) {
    82  		// the stale relationship(i.e. if there's an elder ID in either side) should be discarded
    83  		if len(fromMappedByID(i.toID, mapped.relationship.To.ID())) == 0 {
    84  			continue
    85  		}
    86  		i.Add(artifact.Relationship{
    87  			From: replacement,
    88  			To:   mapped.relationship.To,
    89  			Type: mapped.relationship.Type,
    90  		})
    91  	}
    92  
    93  	for _, mapped := range fromMappedByID(i.toID, ogID) {
    94  		// same as the above
    95  		if len(fromMappedByID(i.fromID, mapped.relationship.To.ID())) == 0 {
    96  			continue
    97  		}
    98  		i.Add(artifact.Relationship{
    99  			From: mapped.relationship.From,
   100  			To:   replacement,
   101  			Type: mapped.relationship.Type,
   102  		})
   103  	}
   104  
   105  	i.Remove(ogID)
   106  }
   107  
   108  // From returns all relationships from the given identifiable, with specified types
   109  func (i *Index) From(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []artifact.Relationship {
   110  	return toSortedSlice(fromMapped(i.fromID, identifiable), types)
   111  }
   112  
   113  // To returns all relationships to the given identifiable, with specified types
   114  func (i *Index) To(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []artifact.Relationship {
   115  	return toSortedSlice(fromMapped(i.toID, identifiable), types)
   116  }
   117  
   118  // References returns all relationships that reference to or from the given identifiable
   119  func (i *Index) References(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []artifact.Relationship {
   120  	return toSortedSlice(append(fromMapped(i.fromID, identifiable), fromMapped(i.toID, identifiable)...), types)
   121  }
   122  
   123  // Coordinates returns all coordinates for the provided identifiable for provided relationship types
   124  // If no types are provided, all relationship types are considered.
   125  func (i *Index) Coordinates(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []file.Coordinates {
   126  	var coordinates []file.Coordinates
   127  	for _, relationship := range i.References(identifiable, types...) {
   128  		cords := extractCoordinates(relationship)
   129  		coordinates = append(coordinates, cords...)
   130  	}
   131  	return coordinates
   132  }
   133  
   134  // Contains indicates the relationship is present in this index
   135  func (i *Index) Contains(r artifact.Relationship) bool {
   136  	if mapped := i.fromID[r.From.ID()]; mapped != nil {
   137  		if ids := mapped.typeMap[r.Type]; ids != nil {
   138  			return ids[r.To.ID()] != nil
   139  		}
   140  	}
   141  	return false
   142  }
   143  
   144  // All returns a sorted set of relationships matching all types, or all relationships if no types specified
   145  func (i *Index) All(types ...artifact.RelationshipType) []artifact.Relationship {
   146  	return toSortedSlice(i.all, types)
   147  }
   148  
   149  func fromMapped(idMap map[artifact.ID]*mappedRelationships, identifiable artifact.Identifiable) []*sortableRelationship {
   150  	if identifiable == nil {
   151  		return nil
   152  	}
   153  	return fromMappedByID(idMap, identifiable.ID())
   154  }
   155  
   156  func fromMappedByID(idMap map[artifact.ID]*mappedRelationships, id artifact.ID) []*sortableRelationship {
   157  	if idMap == nil {
   158  		return nil
   159  	}
   160  	mapped := idMap[id]
   161  	if mapped == nil {
   162  		return nil
   163  	}
   164  	return mapped.allRelated
   165  }
   166  
   167  func toSortedSlice(relationships []*sortableRelationship, types []artifact.RelationshipType) []artifact.Relationship {
   168  	// always return sorted for SBOM stability
   169  	slices.SortFunc(relationships, sortFunc)
   170  	var out []artifact.Relationship
   171  	for _, r := range relationships {
   172  		if len(types) == 0 || slices.Contains(types, r.relationship.Type) {
   173  			out = append(out, r.relationship)
   174  		}
   175  	}
   176  	return out
   177  }
   178  
   179  func extractCoordinates(relationship artifact.Relationship) (results []file.Coordinates) {
   180  	if coordinates, exists := relationship.From.(file.Coordinates); exists {
   181  		results = append(results, coordinates)
   182  	}
   183  
   184  	if coordinates, exists := relationship.To.(file.Coordinates); exists {
   185  		results = append(results, coordinates)
   186  	}
   187  
   188  	return results
   189  }
   190  
   191  type mappedRelationships struct {
   192  	typeMap    map[artifact.RelationshipType]map[artifact.ID]*sortableRelationship
   193  	allRelated []*sortableRelationship
   194  }
   195  
   196  func (m *mappedRelationships) add(id artifact.ID, newRelationship *sortableRelationship) {
   197  	m.allRelated = append(m.allRelated, newRelationship)
   198  	if m.typeMap == nil {
   199  		m.typeMap = map[artifact.RelationshipType]map[artifact.ID]*sortableRelationship{}
   200  	}
   201  	typeMap := m.typeMap[newRelationship.relationship.Type]
   202  	if typeMap == nil {
   203  		typeMap = map[artifact.ID]*sortableRelationship{}
   204  		m.typeMap[newRelationship.relationship.Type] = typeMap
   205  	}
   206  	typeMap[id] = newRelationship
   207  }
   208  
   209  type sortableRelationship struct {
   210  	from         artifact.ID
   211  	to           artifact.ID
   212  	relationship artifact.Relationship
   213  }
   214  
   215  func sortFunc(a, b *sortableRelationship) int {
   216  	cmp := strings.Compare(string(a.relationship.Type), string(b.relationship.Type))
   217  	if cmp != 0 {
   218  		return cmp
   219  	}
   220  	cmp = strings.Compare(string(a.from), string(b.from))
   221  	if cmp != 0 {
   222  		return cmp
   223  	}
   224  	return strings.Compare(string(a.to), string(b.to))
   225  }