github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/format/encoders_collection.go (about)

     1  package format
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"regexp"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/scylladb/go-set/strset"
    11  
    12  	"github.com/anchore/syft/internal/log"
    13  	"github.com/anchore/syft/syft/sbom"
    14  )
    15  
    16  type EncoderCollection struct {
    17  	encoders []sbom.FormatEncoder
    18  }
    19  
    20  func NewEncoderCollection(encoders ...sbom.FormatEncoder) *EncoderCollection {
    21  	return &EncoderCollection{
    22  		encoders: encoders,
    23  	}
    24  }
    25  
    26  // IDs returns all format IDs represented in the collection.
    27  func (e EncoderCollection) IDs() []sbom.FormatID {
    28  	idSet := strset.New()
    29  	for _, f := range e.encoders {
    30  		idSet.Add(string(f.ID()))
    31  	}
    32  
    33  	idList := idSet.List()
    34  	sort.Strings(idList)
    35  
    36  	var ids []sbom.FormatID
    37  	for _, id := range idList {
    38  		ids = append(ids, sbom.FormatID(id))
    39  	}
    40  
    41  	return ids
    42  }
    43  
    44  // NameVersions returns all formats that are supported by the collection as a list of "name@version" strings.
    45  func (e EncoderCollection) NameVersions() []string {
    46  	set := strset.New()
    47  	for _, f := range e.encoders {
    48  		if f.Version() == sbom.AnyVersion {
    49  			set.Add(string(f.ID()))
    50  		} else {
    51  			set.Add(fmt.Sprintf("%s@%s", f.ID(), f.Version()))
    52  		}
    53  	}
    54  
    55  	list := set.List()
    56  	sort.Strings(list)
    57  
    58  	return list
    59  }
    60  
    61  // Aliases returns all format aliases represented in the collection (where an ID would be "spdx-tag-value" the alias would be "spdx").
    62  func (e EncoderCollection) Aliases() []string {
    63  	aliases := strset.New()
    64  	for _, f := range e.encoders {
    65  		aliases.Add(f.Aliases()...)
    66  	}
    67  	lst := aliases.List()
    68  	sort.Strings(lst)
    69  	return lst
    70  }
    71  
    72  // Get returns the contained encoder for a given format name and version.
    73  func (e EncoderCollection) Get(name string, version string) sbom.FormatEncoder {
    74  	log.WithFields("name", name, "version", version).Trace("looking for matching encoder")
    75  
    76  	name = cleanFormatName(name)
    77  	var mostRecentFormat sbom.FormatEncoder
    78  
    79  	for _, f := range e.encoders {
    80  		log.WithFields("name", f.ID(), "version", f.Version(), "aliases", f.Aliases()).Trace("considering format")
    81  		names := []string{string(f.ID())}
    82  		names = append(names, f.Aliases()...)
    83  		for _, n := range names {
    84  			if cleanFormatName(n) == name && versionMatches(f.Version(), version) {
    85  				if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
    86  					mostRecentFormat = f
    87  				}
    88  			}
    89  		}
    90  	}
    91  
    92  	if mostRecentFormat != nil {
    93  		log.WithFields("name", mostRecentFormat.ID(), "version", mostRecentFormat.Version()).Trace("found matching encoder")
    94  	} else {
    95  		log.WithFields("search-name", name, "search-version", version).Trace("no matching encoder found")
    96  	}
    97  
    98  	return mostRecentFormat
    99  }
   100  
   101  // GetByString accepts a name@version string, such as:
   102  //   - json
   103  //   - spdx-json@2.1
   104  //   - cdx@1.5
   105  func (e EncoderCollection) GetByString(s string) sbom.FormatEncoder {
   106  	parts := strings.SplitN(s, "@", 2)
   107  	version := sbom.AnyVersion
   108  	if len(parts) > 1 {
   109  		version = parts[1]
   110  	}
   111  	return e.Get(parts[0], version)
   112  }
   113  
   114  func versionMatches(version string, match string) bool {
   115  	if version == sbom.AnyVersion || match == sbom.AnyVersion {
   116  		return true
   117  	}
   118  
   119  	match = strings.ReplaceAll(match, ".", "\\.")
   120  	match = strings.ReplaceAll(match, "*", ".*")
   121  	match = fmt.Sprintf("^%s(\\..*)*$", match)
   122  	matcher, err := regexp.Compile(match)
   123  	if err != nil {
   124  		return false
   125  	}
   126  	return matcher.MatchString(version)
   127  }
   128  
   129  func cleanFormatName(name string) string {
   130  	r := strings.NewReplacer("-", "", "_", "")
   131  	return strings.ToLower(r.Replace(name))
   132  }
   133  
   134  // Encode takes all SBOM elements and a format option and encodes an SBOM document.
   135  func Encode(s sbom.SBOM, f sbom.FormatEncoder) ([]byte, error) {
   136  	buff := bytes.Buffer{}
   137  
   138  	if err := f.Encode(&buff, s); err != nil {
   139  		return nil, fmt.Errorf("unable to encode sbom: %w", err)
   140  	}
   141  
   142  	return buff.Bytes(), nil
   143  }