github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/nix/parse_store_path.go (about)

     1  package nix
     2  
     3  import (
     4  	"fmt"
     5  	"path"
     6  	"regexp"
     7  	"strings"
     8  )
     9  
    10  var (
    11  	numericPattern = regexp.MustCompile(`\d`)
    12  
    13  	// attempts to find the right-most example of something that appears to be a version (semver or otherwise)
    14  	// example input: h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin
    15  	// example output:
    16  	//  version: "2.34-210"
    17  	//  major: "2"
    18  	//  minor: "34"
    19  	//  patch: "210"
    20  	// (there are other capture groups, but they can be ignored)
    21  	rightMostVersionIshPattern = regexp.MustCompile(`-(?P<version>(?P<major>[0-9][a-zA-Z0-9]*)(\.(?P<minor>[0-9][a-zA-Z0-9]*))?(\.(?P<patch>0|[1-9][a-zA-Z0-9]*)){0,3}(?:-(?P<prerelease>\d*[.0-9a-zA-Z-]*)*)?(?:\+(?P<metadata>[.0-9a-zA-Z-]+(?:\.[.0-9a-zA-Z-]+)*))?)`)
    22  
    23  	unstableVersion = regexp.MustCompile(`-(?P<version>unstable-\d{4}-\d{2}-\d{2})$`)
    24  )
    25  
    26  // checkout the package naming conventions here: https://nixos.org/manual/nixpkgs/stable/#sec-package-naming
    27  
    28  type nixStorePath struct {
    29  	StorePath  string
    30  	OutputHash string
    31  	Name       string
    32  	Version    string
    33  	Output     string
    34  }
    35  
    36  func (p nixStorePath) isValidPackage() bool {
    37  	return p.Name != "" && p.Version != ""
    38  }
    39  
    40  func findParentNixStorePath(source string) string {
    41  	source = strings.TrimRight(source, "/")
    42  	indicator := "nix/store/"
    43  	start := strings.Index(source, indicator)
    44  	if start == -1 {
    45  		return ""
    46  	}
    47  
    48  	startOfHash := start + len(indicator)
    49  	nextField := strings.Index(source[startOfHash:], "/")
    50  	if nextField == -1 {
    51  		return ""
    52  	}
    53  	startOfSubPath := startOfHash + nextField
    54  
    55  	return source[0:startOfSubPath]
    56  }
    57  
    58  func parseNixStorePath(og string) *nixStorePath {
    59  	if strings.HasSuffix(og, ".drv") {
    60  		// ignore derivations
    61  		return nil
    62  	}
    63  
    64  	source := path.Base(og)
    65  
    66  	versionStartIdx, versionIsh, prerelease := findVersionIsh(source)
    67  	if versionStartIdx == -1 {
    68  		return nil
    69  	}
    70  
    71  	hashName := strings.TrimSuffix(source[0:versionStartIdx], "-")
    72  	hashNameFields := strings.Split(hashName, "-")
    73  	if len(hashNameFields) < 2 {
    74  		return nil
    75  	}
    76  	hash, name := hashNameFields[0], strings.Join(hashNameFields[1:], "-")
    77  
    78  	prereleaseFields := strings.Split(prerelease, "-")
    79  	lastPrereleaseField := prereleaseFields[len(prereleaseFields)-1]
    80  
    81  	var version = versionIsh
    82  	var output string
    83  	if !hasNumeric(lastPrereleaseField) {
    84  		// this last prerelease field is probably a nix output
    85  		version = strings.TrimSuffix(versionIsh, fmt.Sprintf("-%s", lastPrereleaseField))
    86  		output = lastPrereleaseField
    87  	}
    88  
    89  	if og != "" && !strings.HasPrefix(og, "/") {
    90  		og = fmt.Sprintf("/%s", og)
    91  	}
    92  
    93  	return &nixStorePath{
    94  		StorePath:  og,
    95  		OutputHash: hash,
    96  		Name:       name,
    97  		Version:    version,
    98  		Output:     output,
    99  	}
   100  }
   101  
   102  func hasNumeric(s string) bool {
   103  	return numericPattern.MatchString(s)
   104  }
   105  
   106  func findVersionIsh(input string) (int, string, string) {
   107  	// we want to return the index of the start of the "version" group (the first capture group).
   108  	// note that the match indices are in the form of [start, end, start, end, ...]. Also note that the
   109  	// capture group for version in both regexes are the same index, but if the regexes are changed
   110  	// this code will start to fail.
   111  
   112  	// check for unstable version pattern first
   113  	if match := unstableVersion.FindStringSubmatch(input); match != nil {
   114  		indices := unstableVersion.FindStringSubmatchIndex(input)
   115  		versionStart := indices[2] // index of first capture group's start
   116  		version := match[1]        // first capture group is the version
   117  		return versionStart, version, ""
   118  	}
   119  
   120  	// try the regular version pattern
   121  	match := rightMostVersionIshPattern.FindStringSubmatch(input)
   122  	if match == nil {
   123  		return -1, "", ""
   124  	}
   125  
   126  	version := match[1] // capture group 1 is the version
   127  	indices := rightMostVersionIshPattern.FindStringSubmatchIndex(input)
   128  	versionStart := indices[2] // index of first capture group's start
   129  	prerelease := match[7]     // capture group 7 is the prerelease version
   130  
   131  	return versionStart, version, prerelease
   132  }