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 }