github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/cataloger/nix/parse_nix_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*[.a-zA-Z-][.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  	outputHash string
    30  	name       string
    31  	version    string
    32  	output     string
    33  }
    34  
    35  func (p nixStorePath) isValidPackage() bool {
    36  	return p.name != "" && p.version != ""
    37  }
    38  
    39  func findParentNixStorePath(source string) string {
    40  	source = strings.TrimRight(source, "/")
    41  	indicator := "nix/store/"
    42  	start := strings.Index(source, indicator)
    43  	if start == -1 {
    44  		return ""
    45  	}
    46  
    47  	startOfHash := start + len(indicator)
    48  	nextField := strings.Index(source[startOfHash:], "/")
    49  	if nextField == -1 {
    50  		return ""
    51  	}
    52  	startOfSubPath := startOfHash + nextField
    53  
    54  	return source[0:startOfSubPath]
    55  }
    56  
    57  func parseNixStorePath(source string) *nixStorePath {
    58  	if strings.HasSuffix(source, ".drv") {
    59  		// ignore derivations
    60  		return nil
    61  	}
    62  
    63  	source = path.Base(source)
    64  
    65  	versionStartIdx, versionIsh, prerelease := findVersionIsh(source)
    66  	if versionStartIdx == -1 {
    67  		return nil
    68  	}
    69  
    70  	hashName := strings.TrimSuffix(source[0:versionStartIdx], "-")
    71  	hashNameFields := strings.Split(hashName, "-")
    72  	if len(hashNameFields) < 2 {
    73  		return nil
    74  	}
    75  	hash, name := hashNameFields[0], strings.Join(hashNameFields[1:], "-")
    76  
    77  	prereleaseFields := strings.Split(prerelease, "-")
    78  	lastPrereleaseField := prereleaseFields[len(prereleaseFields)-1]
    79  
    80  	var version = versionIsh
    81  	var output string
    82  	if !hasNumeric(lastPrereleaseField) {
    83  		// this last prerelease field is probably a nix output
    84  		version = strings.TrimSuffix(versionIsh, fmt.Sprintf("-%s", lastPrereleaseField))
    85  		output = lastPrereleaseField
    86  	}
    87  
    88  	return &nixStorePath{
    89  		outputHash: hash,
    90  		name:       name,
    91  		version:    version,
    92  		output:     output,
    93  	}
    94  }
    95  
    96  func hasNumeric(s string) bool {
    97  	return numericPattern.MatchString(s)
    98  }
    99  
   100  func findVersionIsh(input string) (int, string, string) {
   101  	// we want to return the index of the start of the "version" group (the first capture group).
   102  	// note that the match indices are in the form of [start, end, start, end, ...]. Also note that the
   103  	// capture group for version in both regexes are the same index, but if the regexes are changed
   104  	// this code will start to fail.
   105  	versionGroup := 1
   106  
   107  	match := unstableVersion.FindAllStringSubmatchIndex(input, -1)
   108  	if len(match) > 0 && len(match[0]) > 0 {
   109  		return match[0][versionGroup*2], input[match[0][versionGroup*2]:match[0][(versionGroup*2)+1]], ""
   110  	}
   111  
   112  	match = rightMostVersionIshPattern.FindAllStringSubmatchIndex(input, -1)
   113  	if len(match) == 0 || len(match[0]) == 0 {
   114  		return -1, "", ""
   115  	}
   116  
   117  	var version string
   118  	versionStart, versionStop := match[0][versionGroup*2], match[0][(versionGroup*2)+1]
   119  	if versionStart != -1 || versionStop != -1 {
   120  		version = input[versionStart:versionStop]
   121  	}
   122  
   123  	prereleaseGroup := 7
   124  
   125  	var prerelease string
   126  	prereleaseStart, prereleaseStop := match[0][prereleaseGroup*2], match[0][(prereleaseGroup*2)+1]
   127  	if prereleaseStart != -1 && prereleaseStop != -1 {
   128  		prerelease = input[prereleaseStart:prereleaseStop]
   129  	}
   130  
   131  	return versionStart,
   132  		version,
   133  		prerelease
   134  }