mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 00:13:15 +01:00
testing
This commit is contained in:
parent
987ba83674
commit
7c8aad9e1b
@ -6,6 +6,9 @@ package nix
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
|
||||
@ -28,6 +31,30 @@ func (c *storeCataloger) Name() string {
|
||||
return catalogerName
|
||||
}
|
||||
|
||||
// Find the parent Nix store path for a given path
|
||||
func findParentNixStorePath(path string) string {
|
||||
// Handle standard /nix/store/ paths
|
||||
parts := strings.Split(path, "/")
|
||||
for i := 0; i < len(parts)-2; i++ {
|
||||
if parts[i] == "nix" && parts[i+1] == "store" && i+2 < len(parts) {
|
||||
// Check if it matches the hash-name pattern
|
||||
storeItem := parts[i+2]
|
||||
if matched, _ := regexp.MatchString(`^[a-z0-9]{32}-.*`, storeItem); matched {
|
||||
return filepath.Join("/", filepath.Join(parts[:i+3]...))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle short-form Nix paths (without the /nix/store/ prefix)
|
||||
// These would be paths like /[hash]-[package-name]/...
|
||||
shortPathPattern := regexp.MustCompile(`^/([a-z0-9]{32}-[^/]+)`)
|
||||
if matches := shortPathPattern.FindStringSubmatch(path); len(matches) == 2 {
|
||||
return "/" + matches[1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *storeCataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
// we want to search for only directories, which isn't possible via the stereoscope API, so we need to apply the glob manually on all returned paths
|
||||
var pkgs []pkg.Package
|
||||
@ -37,9 +64,23 @@ func (c *storeCataloger) Catalog(ctx context.Context, resolver file.Resolver) ([
|
||||
for location := range resolver.AllLocations(ctx) {
|
||||
matchesStorePath, err := doublestar.Match("**/nix/store/*", location.RealPath)
|
||||
if err != nil {
|
||||
log.Debugf("Error matching path %s: %v", location.RealPath, err)
|
||||
return nil, nil, fmt.Errorf("failed to match nix store path: %w", err)
|
||||
}
|
||||
if !matchesStorePath {
|
||||
// Check if this is a "short-form" Nix store path (without the /nix/store/ prefix)
|
||||
// This pattern matches paths like /[hash]-[package-name]/...
|
||||
shortPathPattern := regexp.MustCompile(`^/([a-z0-9]{32})-([^/]+)`)
|
||||
if shortPathPattern.MatchString(location.RealPath) {
|
||||
matchesStorePath = true
|
||||
log.Debugf("Matched short-form Nix path: %s", location.RealPath)
|
||||
}
|
||||
}
|
||||
|
||||
if !matchesStorePath {
|
||||
log.Debugf("Not a Nix store path: %s", location.RealPath)
|
||||
continue
|
||||
}
|
||||
parentStorePath := findParentNixStorePath(location.RealPath)
|
||||
if parentStorePath != "" {
|
||||
if _, ok := filesByPath[parentStorePath]; !ok {
|
||||
@ -50,17 +91,24 @@ func (c *storeCataloger) Catalog(ctx context.Context, resolver file.Resolver) ([
|
||||
}
|
||||
|
||||
if !matchesStorePath {
|
||||
log.Debugf("Not a Nix store path: %s", location.RealPath)
|
||||
continue
|
||||
}
|
||||
|
||||
storePath := parseNixStorePath(location.RealPath)
|
||||
|
||||
if storePath == nil || !storePath.isValidPackage() {
|
||||
if storePath == nil {
|
||||
log.Debugf("Failed to parse Nix path: %s", location.RealPath)
|
||||
continue
|
||||
}
|
||||
|
||||
p := newNixStorePackage(*storePath, location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
||||
pkgs = append(pkgs, p)
|
||||
// Only create packages for non-derivation/source paths
|
||||
if storePath.shouldIncludeAsPackage() {
|
||||
p := newNixStorePackage(*storePath, location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
||||
pkgs = append(pkgs, p)
|
||||
} else {
|
||||
log.Debugf("Skipping non-package path: %s (type: %s)", location.RealPath, storePath.pathType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// add file sets to packages
|
||||
@ -80,7 +128,64 @@ func (c *storeCataloger) Catalog(ctx context.Context, resolver file.Resolver) ([
|
||||
appendFiles(p, files.ToSlice()...)
|
||||
}
|
||||
|
||||
return pkgs, nil, nil
|
||||
// After all packages are created, enrich them with derivation data
|
||||
for i := range pkgs {
|
||||
p := &pkgs[i]
|
||||
metadata, ok := p.Metadata.(pkg.NixStoreEntry)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find and parse the derivation file
|
||||
derivPath := findDeriverPath(metadata.Path)
|
||||
if derivPath != "" {
|
||||
if deriv, err := ParseDerivation(derivPath); err == nil {
|
||||
// Enrich package metadata with derivation data
|
||||
if deriv.PName != "" {
|
||||
p.Name = deriv.PName
|
||||
}
|
||||
if deriv.Version != "" {
|
||||
p.Version = deriv.Version
|
||||
}
|
||||
|
||||
// Add license, homepage, etc. to metadata
|
||||
metadata.License = deriv.License
|
||||
metadata.Homepage = deriv.Homepage
|
||||
metadata.Description = deriv.Description
|
||||
metadata.DeriverPath = derivPath
|
||||
|
||||
// Update the package metadata
|
||||
p.Metadata = metadata
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find and add relationships between packages
|
||||
relationships, err := findDependencies(pkgs)
|
||||
if err != nil {
|
||||
log.Warnf("failed to resolve nix dependencies: %v", err)
|
||||
}
|
||||
// Deduplicate packages based on name and version
|
||||
dedupedPkgs := deduplicateNixPackages(pkgs)
|
||||
pkgs = dedupedPkgs
|
||||
return pkgs, relationships, nil
|
||||
}
|
||||
|
||||
// ensureNixStorePrefix makes sure the path has the /nix/store/ prefix
|
||||
func ensureNixStorePrefix(path string) string {
|
||||
// Check if the path already has the /nix/store/ prefix
|
||||
if strings.Contains(path, "/nix/store/") {
|
||||
return path
|
||||
}
|
||||
|
||||
// Check if this is a short-form path starting with /[hash]-[name]
|
||||
shortPathPattern := regexp.MustCompile(`^/([a-z0-9]{32}-[^/]+)(.*)$`)
|
||||
if matches := shortPathPattern.FindStringSubmatch(path); len(matches) >= 3 {
|
||||
// Convert short form path to full path with /nix/store/ prefix
|
||||
return "/nix/store/" + matches[1] + matches[2]
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func appendFiles(p *pkg.Package, location ...file.Location) {
|
||||
@ -91,7 +196,9 @@ func appendFiles(p *pkg.Package, location ...file.Location) {
|
||||
}
|
||||
|
||||
for _, l := range location {
|
||||
metadata.Files = append(metadata.Files, l.RealPath)
|
||||
// Normalize the path to ensure it has the /nix/store/ prefix
|
||||
normalizedPath := ensureNixStorePrefix(l.RealPath)
|
||||
metadata.Files = append(metadata.Files, normalizedPath)
|
||||
}
|
||||
|
||||
if metadata.Files == nil {
|
||||
|
||||
207
syft/pkg/cataloger/nix/dependencies.go
Normal file
207
syft/pkg/cataloger/nix/dependencies.go
Normal file
@ -0,0 +1,207 @@
|
||||
package nix
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// findDependencies attempts to find relationships between Nix packages
|
||||
// without using external commands
|
||||
func findDependencies(pkgs []pkg.Package) ([]artifact.Relationship, error) {
|
||||
var relationships []artifact.Relationship
|
||||
packageByPath := make(map[string]*pkg.Package)
|
||||
packageByStorePath := make(map[string]*pkg.Package)
|
||||
|
||||
// Index packages by store path and path for quick lookups
|
||||
for i := range pkgs {
|
||||
p := &pkgs[i]
|
||||
metadata, ok := p.Metadata.(pkg.NixStoreEntry)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
packageByStorePath[metadata.Path] = p
|
||||
|
||||
// Also index by the package files
|
||||
for _, filePath := range metadata.Files {
|
||||
packageByPath[filePath] = p
|
||||
}
|
||||
}
|
||||
|
||||
// For each package, try to find references to other packages
|
||||
for i := range pkgs {
|
||||
p := &pkgs[i]
|
||||
metadata, ok := p.Metadata.(pkg.NixStoreEntry)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Scan for references using the derivation file if available
|
||||
if metadata.DeriverPath != "" {
|
||||
refs, err := findReferencesInDerivation(metadata.DeriverPath, packageByStorePath)
|
||||
if err == nil {
|
||||
relationships = append(relationships, refs...)
|
||||
}
|
||||
}
|
||||
|
||||
// Scan for references within the package files
|
||||
if len(metadata.Files) > 0 {
|
||||
refs, err := findReferencesInFiles(metadata.Files, p, packageByStorePath)
|
||||
if err == nil {
|
||||
relationships = append(relationships, refs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
// findReferencesInDerivation tries to extract dependencies from a derivation file
|
||||
func findReferencesInDerivation(drvPath string, packageByPath map[string]*pkg.Package) ([]artifact.Relationship, error) {
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
// Read the derivation file contents
|
||||
content, err := os.ReadFile(drvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Look for store paths in the derivation file
|
||||
storePathPattern := regexp.MustCompile(`/nix/store/[a-z0-9]{32}-[^"'\s]+`)
|
||||
matches := storePathPattern.FindAllString(string(content), -1)
|
||||
|
||||
// Unique matches
|
||||
uniquePaths := make(map[string]struct{})
|
||||
for _, match := range matches {
|
||||
uniquePaths[match] = struct{}{}
|
||||
}
|
||||
|
||||
// Create relationships for found dependencies
|
||||
for path := range uniquePaths {
|
||||
depPkg, exists := packageByPath[path]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip self-references
|
||||
fromMeta, ok := depPkg.Metadata.(pkg.NixStoreEntry)
|
||||
if !ok || fromMeta.Path == drvPath {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the dependency relationship
|
||||
pkg, exists := packageByPath[drvPath]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
relationships = append(relationships, artifact.Relationship{
|
||||
From: *pkg,
|
||||
To: *depPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
Data: map[string]string{"dependencyType": "buildtime"},
|
||||
})
|
||||
}
|
||||
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
// findReferencesInFiles looks for references to other packages within file contents
|
||||
func findReferencesInFiles(files []string, fromPkg *pkg.Package, packageByPath map[string]*pkg.Package) ([]artifact.Relationship, error) {
|
||||
var relationships []artifact.Relationship
|
||||
uniqueRefs := make(map[string]struct{})
|
||||
|
||||
// Limit search to a reasonable number of files to avoid performance issues
|
||||
maxFiles := 10
|
||||
fileCount := 0
|
||||
|
||||
for _, file := range files {
|
||||
if fileCount >= maxFiles {
|
||||
break
|
||||
}
|
||||
|
||||
// Skip files that are too large
|
||||
info, err := os.Stat(file)
|
||||
if err != nil || info.Size() > 1024*1024 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip directories
|
||||
if info.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Look for binary files that might have references
|
||||
if isBinary(file) {
|
||||
fileCount++
|
||||
refs, err := findReferencesInBinary(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ref := range refs {
|
||||
uniqueRefs[ref] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create relationships for found dependencies
|
||||
for path := range uniqueRefs {
|
||||
depPkg, exists := packageByPath[path]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip self-references
|
||||
fromMeta, ok := fromPkg.Metadata.(pkg.NixStoreEntry)
|
||||
if !ok || fromMeta.Path == path {
|
||||
continue
|
||||
}
|
||||
|
||||
relationships = append(relationships, artifact.Relationship{
|
||||
From: *fromPkg,
|
||||
To: *depPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
Data: map[string]string{"dependencyType": "runtime"},
|
||||
})
|
||||
}
|
||||
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// isBinary returns true if the file appears to be a binary
|
||||
func isBinary(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
return ext == ".so" || ext == ".dylib" || ext == ".a" ||
|
||||
strings.HasSuffix(path, ".so.1") ||
|
||||
strings.Contains(path, ".so.")
|
||||
}
|
||||
|
||||
// findReferencesInBinary extracts Nix store references from binary files
|
||||
func findReferencesInBinary(path string) ([]string, error) {
|
||||
var references []string
|
||||
|
||||
// Use string search instead of executing nix-store
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Search for store paths in the binary
|
||||
storePathPattern := regexp.MustCompile(`/nix/store/[a-z0-9]{32}-[^\\:\*\?"<>\|\x00-\x1F]+`)
|
||||
matches := storePathPattern.FindAll(data, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
references = append(references, string(match))
|
||||
}
|
||||
|
||||
return references, nil
|
||||
}
|
||||
136
syft/pkg/cataloger/nix/derivation.go
Normal file
136
syft/pkg/cataloger/nix/derivation.go
Normal file
@ -0,0 +1,136 @@
|
||||
package nix
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// NixDerivation represents a parsed Nix derivation file
|
||||
type NixDerivation struct {
|
||||
Path string
|
||||
Name string
|
||||
PName string
|
||||
Version string
|
||||
System string
|
||||
Builder string
|
||||
Args []string
|
||||
EnvVars map[string]string
|
||||
Outputs map[string]string
|
||||
InputDrvs map[string][]string
|
||||
InputSrcs []string
|
||||
License string
|
||||
Homepage string
|
||||
Description string
|
||||
OutputHash string
|
||||
}
|
||||
|
||||
// ParseDerivation attempts to parse a Nix derivation file without external commands
|
||||
func ParseDerivation(path string) (*NixDerivation, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
derivation := &NixDerivation{
|
||||
Path: path,
|
||||
EnvVars: make(map[string]string),
|
||||
Outputs: make(map[string]string),
|
||||
InputDrvs: make(map[string][]string),
|
||||
}
|
||||
|
||||
// Simple parser for .drv files
|
||||
scanner := bufio.NewScanner(file)
|
||||
inEnvVars := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// Process environment variables section
|
||||
if strings.Contains(line, "envVars = {") {
|
||||
inEnvVars = true
|
||||
continue
|
||||
} else if inEnvVars && strings.Contains(line, "};") {
|
||||
inEnvVars = false
|
||||
continue
|
||||
} else if inEnvVars && strings.Contains(line, " = ") {
|
||||
// Parse environment variables
|
||||
parts := strings.SplitN(line, " = ", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.Trim(parts[0], " \"")
|
||||
value := strings.Trim(parts[1], " \";")
|
||||
derivation.EnvVars[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract package information from environment variables
|
||||
derivation.Name = derivation.EnvVars["name"]
|
||||
derivation.PName = derivation.EnvVars["pname"]
|
||||
derivation.Version = derivation.EnvVars["version"]
|
||||
derivation.System = derivation.EnvVars["system"]
|
||||
derivation.License = derivation.EnvVars["license"]
|
||||
derivation.Homepage = derivation.EnvVars["homepage"]
|
||||
derivation.Description = derivation.EnvVars["description"]
|
||||
|
||||
// If pname is not set but name follows pname-version pattern, extract it
|
||||
if derivation.PName == "" && derivation.Name != "" {
|
||||
re := regexp.MustCompile(`^(.*?)-([0-9].*)$`)
|
||||
if matches := re.FindStringSubmatch(derivation.Name); len(matches) == 3 {
|
||||
derivation.PName = matches[1]
|
||||
if derivation.Version == "" {
|
||||
derivation.Version = matches[2]
|
||||
}
|
||||
} else {
|
||||
derivation.PName = derivation.Name
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a patched package with security fixes
|
||||
for key := range derivation.EnvVars {
|
||||
if strings.HasPrefix(key, "patches") {
|
||||
value := derivation.EnvVars[key]
|
||||
if strings.Contains(value, "CVE-") {
|
||||
log.Debugf("Found security patch in %s: %s", derivation.Name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return derivation, nil
|
||||
}
|
||||
|
||||
// findDeriverPath attempts to find the derivation file for a store path
|
||||
func findDeriverPath(storePath string) string {
|
||||
// If the path is already a derivation, return it
|
||||
if strings.HasSuffix(storePath, ".drv") {
|
||||
return storePath
|
||||
}
|
||||
|
||||
// Look for a .drv file with a similar name
|
||||
drvPathGuess := storePath + ".drv"
|
||||
if _, err := os.Stat(drvPathGuess); err == nil {
|
||||
return drvPathGuess
|
||||
}
|
||||
|
||||
// Fall back strategy using string manipulation
|
||||
// This is an approximation without running nix-store commands
|
||||
parts := strings.Split(storePath, "-")
|
||||
if len(parts) >= 2 {
|
||||
// Try to find the derivation file using the hash part
|
||||
hash := strings.Split(filepath.Base(storePath), "-")[0]
|
||||
if matched, _ := regexp.MatchString(`^[a-z0-9]{32}$`, hash); matched {
|
||||
drvDir := filepath.Join("/nix/store", hash+"-*.drv")
|
||||
matches, err := filepath.Glob(drvDir)
|
||||
if err == nil && len(matches) > 0 {
|
||||
return matches[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@ -56,3 +56,67 @@ func packageURL(storePath nixStorePath) string {
|
||||
"")
|
||||
return pURL.ToString()
|
||||
}
|
||||
|
||||
// deduplicateNixPackages combines information from packages with the same name and version
|
||||
func deduplicateNixPackages(pkgs []pkg.Package) []pkg.Package {
|
||||
if len(pkgs) == 0 {
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// Use map to group by name+version
|
||||
packageMap := make(map[string]*pkg.Package)
|
||||
|
||||
for i := range pkgs {
|
||||
p := &pkgs[i]
|
||||
key := p.Name + ":" + p.Version
|
||||
|
||||
existing, exists := packageMap[key]
|
||||
if !exists {
|
||||
// First time seeing this name+version
|
||||
packageMap[key] = p
|
||||
continue
|
||||
}
|
||||
|
||||
// Merge information from this package into the existing one
|
||||
mergePackageInfo(existing, p)
|
||||
}
|
||||
|
||||
// Convert map back to slice
|
||||
result := make([]pkg.Package, 0, len(packageMap))
|
||||
for _, p := range packageMap {
|
||||
result = append(result, *p)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// mergePackageInfo combines information from src into dest
|
||||
func mergePackageInfo(dest, src *pkg.Package) {
|
||||
// Merge locations
|
||||
for _, loc := range src.Locations.ToSlice() {
|
||||
dest.Locations.Add(loc)
|
||||
}
|
||||
|
||||
// Merge metadata if both have NixStoreEntry type
|
||||
destMeta, destOk := dest.Metadata.(pkg.NixStoreEntry)
|
||||
srcMeta, srcOk := src.Metadata.(pkg.NixStoreEntry)
|
||||
|
||||
if destOk && srcOk {
|
||||
// Combine files lists
|
||||
destMeta.Files = append(destMeta.Files, srcMeta.Files...)
|
||||
|
||||
// Prefer non-empty fields from src
|
||||
if destMeta.License == "" && srcMeta.License != "" {
|
||||
destMeta.License = srcMeta.License
|
||||
}
|
||||
if destMeta.Homepage == "" && srcMeta.Homepage != "" {
|
||||
destMeta.Homepage = srcMeta.Homepage
|
||||
}
|
||||
if destMeta.Description == "" && srcMeta.Description != "" {
|
||||
destMeta.Description = srcMeta.Description
|
||||
}
|
||||
|
||||
// Update the metadata
|
||||
dest.Metadata = destMeta
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,134 +1,155 @@
|
||||
package nix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
numericPattern = regexp.MustCompile(`\d`)
|
||||
|
||||
// attempts to find the right-most example of something that appears to be a version (semver or otherwise)
|
||||
// example input: h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin
|
||||
// example output:
|
||||
// version: "2.34-210"
|
||||
// major: "2"
|
||||
// minor: "34"
|
||||
// patch: "210"
|
||||
// (there are other capture groups, but they can be ignored)
|
||||
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-]+)*))?)`)
|
||||
|
||||
unstableVersion = regexp.MustCompile(`-(?P<version>unstable-\d{4}-\d{2}-\d{2})$`)
|
||||
)
|
||||
|
||||
// checkout the package naming conventions here: https://nixos.org/manual/nixpkgs/stable/#sec-package-naming
|
||||
|
||||
type nixStorePath struct {
|
||||
path string
|
||||
hash string
|
||||
outputHash string
|
||||
name string
|
||||
version string
|
||||
output string
|
||||
// New field to indicate the type of Nix path
|
||||
pathType string // "package", "derivation", "source", or "other"
|
||||
}
|
||||
|
||||
func (p nixStorePath) isValidPackage() bool {
|
||||
return p.name != "" && p.version != ""
|
||||
}
|
||||
|
||||
func findParentNixStorePath(source string) string {
|
||||
source = strings.TrimRight(source, "/")
|
||||
indicator := "nix/store/"
|
||||
start := strings.Index(source, indicator)
|
||||
if start == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
startOfHash := start + len(indicator)
|
||||
nextField := strings.Index(source[startOfHash:], "/")
|
||||
if nextField == -1 {
|
||||
return ""
|
||||
}
|
||||
startOfSubPath := startOfHash + nextField
|
||||
|
||||
return source[0:startOfSubPath]
|
||||
}
|
||||
|
||||
func parseNixStorePath(source string) *nixStorePath {
|
||||
if strings.HasSuffix(source, ".drv") {
|
||||
// ignore derivations
|
||||
// parseNixStorePath extracts package information from a Nix store path.
|
||||
// This is a more permissive implementation that accepts more formats.
|
||||
func parseNixStorePath(path string) *nixStorePath {
|
||||
// Extract the store path component - handle both standard and short forms
|
||||
// Standard form: /nix/store/[hash]-[name]
|
||||
// Short form: /[hash]-[name]
|
||||
storePathPattern := regexp.MustCompile(`(^|.*?)(?:/nix/store/|/)([a-z0-9]{32})-(.+?)(/.*)?$`)
|
||||
matches := storePathPattern.FindStringSubmatch(path)
|
||||
if len(matches) < 4 {
|
||||
return nil
|
||||
}
|
||||
|
||||
source = path.Base(source)
|
||||
// Extract hash and full package name
|
||||
hash := matches[2]
|
||||
fullName := matches[3]
|
||||
|
||||
versionStartIdx, versionIsh, prerelease := findVersionIsh(source)
|
||||
if versionStartIdx == -1 {
|
||||
return nil
|
||||
// Extract path to first / after the hash-name pattern
|
||||
basePath := "/nix/store/" + hash + "-" + fullName
|
||||
if len(matches) > 4 && matches[4] != "" {
|
||||
// We have a file within a store path
|
||||
basePath = "/nix/store/" + hash + "-" + fullName
|
||||
}
|
||||
|
||||
hashName := strings.TrimSuffix(source[0:versionStartIdx], "-")
|
||||
hashNameFields := strings.Split(hashName, "-")
|
||||
if len(hashNameFields) < 2 {
|
||||
return nil
|
||||
}
|
||||
hash, name := hashNameFields[0], strings.Join(hashNameFields[1:], "-")
|
||||
|
||||
prereleaseFields := strings.Split(prerelease, "-")
|
||||
lastPrereleaseField := prereleaseFields[len(prereleaseFields)-1]
|
||||
|
||||
var version = versionIsh
|
||||
var output string
|
||||
if !hasNumeric(lastPrereleaseField) {
|
||||
// this last prerelease field is probably a nix output
|
||||
version = strings.TrimSuffix(versionIsh, fmt.Sprintf("-%s", lastPrereleaseField))
|
||||
output = lastPrereleaseField
|
||||
}
|
||||
|
||||
return &nixStorePath{
|
||||
// Create the basic result
|
||||
result := &nixStorePath{
|
||||
path: basePath,
|
||||
hash: hash,
|
||||
outputHash: hash,
|
||||
name: name,
|
||||
version: version,
|
||||
output: output,
|
||||
}
|
||||
|
||||
// Determine the path type based on extension or patterns
|
||||
if strings.HasSuffix(fullName, ".drv") {
|
||||
result.pathType = "derivation"
|
||||
// Remove .drv suffix for name parsing
|
||||
fullName = strings.TrimSuffix(fullName, ".drv")
|
||||
} else if strings.Contains(fullName, ".tar.") ||
|
||||
strings.HasSuffix(fullName, ".tgz") ||
|
||||
strings.HasSuffix(fullName, ".tar.gz") ||
|
||||
strings.HasSuffix(fullName, ".tar.bz2") ||
|
||||
strings.HasSuffix(fullName, ".tar.xz") ||
|
||||
strings.HasSuffix(fullName, ".zip") ||
|
||||
strings.HasSuffix(fullName, ".patch") {
|
||||
result.pathType = "source"
|
||||
} else {
|
||||
result.pathType = "package"
|
||||
}
|
||||
|
||||
// Try to parse the output from the path (if present)
|
||||
if outputParts := strings.Split(fullName, "-"); len(outputParts) > 1 {
|
||||
lastPart := outputParts[len(outputParts)-1]
|
||||
// Common Nix output names
|
||||
outputs := map[string]bool{
|
||||
"bin": true, "dev": true, "lib": true, "man": true,
|
||||
"doc": true, "info": true, "out": true,
|
||||
}
|
||||
if outputs[lastPart] {
|
||||
result.output = lastPart
|
||||
fullName = strings.Join(outputParts[:len(outputParts)-1], "-")
|
||||
}
|
||||
}
|
||||
|
||||
// For non-drv files, try different version extraction patterns
|
||||
versionPatterns := []*regexp.Regexp{
|
||||
// Standard version: name-1.2.3
|
||||
regexp.MustCompile(`^(.+)-([0-9][0-9.]+(?:-[0-9]+)?)$`),
|
||||
|
||||
// Unstable version: name-unstable-2022-05-15
|
||||
regexp.MustCompile(`^(.+)-unstable-([0-9]{4}-[0-9]{2}-[0-9]{2})$`),
|
||||
|
||||
// Version with prefix: name-v1.2.3
|
||||
regexp.MustCompile(`^(.+)-(v[0-9][0-9.]+)$`),
|
||||
|
||||
// Year-based versions: name-2022.05
|
||||
regexp.MustCompile(`^(.+)-([0-9]{4}(?:\.[0-9]+)*)$`),
|
||||
|
||||
// Version with suffix: name-1.2.3-suffix
|
||||
regexp.MustCompile(`^(.+)-([0-9][0-9.]+(?:-[a-z0-9]+)*)$`),
|
||||
}
|
||||
|
||||
// Try each pattern to extract name and version
|
||||
for _, pattern := range versionPatterns {
|
||||
if nameVerMatches := pattern.FindStringSubmatch(fullName); len(nameVerMatches) == 3 {
|
||||
result.name = nameVerMatches[1]
|
||||
result.version = nameVerMatches[2]
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// If no version pattern matched, consider the entire string the package name
|
||||
result.name = fullName
|
||||
result.version = ""
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func hasNumeric(s string) bool {
|
||||
return numericPattern.MatchString(s)
|
||||
// isLikelyOutput checks if a string is likely to be a Nix output name
|
||||
func isLikelyOutput(s string) bool {
|
||||
commonOutputs := map[string]bool{
|
||||
"bin": true, "lib": true, "dev": true, "out": true,
|
||||
"doc": true, "man": true, "info": true, "devdoc": true,
|
||||
}
|
||||
return commonOutputs[s]
|
||||
}
|
||||
|
||||
func findVersionIsh(input string) (int, string, string) {
|
||||
// we want to return the index of the start of the "version" group (the first capture group).
|
||||
// note that the match indices are in the form of [start, end, start, end, ...]. Also note that the
|
||||
// capture group for version in both regexes are the same index, but if the regexes are changed
|
||||
// this code will start to fail.
|
||||
versionGroup := 1
|
||||
|
||||
match := unstableVersion.FindAllStringSubmatchIndex(input, -1)
|
||||
if len(match) > 0 && len(match[0]) > 0 {
|
||||
return match[0][versionGroup*2], input[match[0][versionGroup*2]:match[0][(versionGroup*2)+1]], ""
|
||||
}
|
||||
|
||||
match = rightMostVersionIshPattern.FindAllStringSubmatchIndex(input, -1)
|
||||
if len(match) == 0 || len(match[0]) == 0 {
|
||||
return -1, "", ""
|
||||
}
|
||||
|
||||
var version string
|
||||
versionStart, versionStop := match[0][versionGroup*2], match[0][(versionGroup*2)+1]
|
||||
if versionStart != -1 || versionStop != -1 {
|
||||
version = input[versionStart:versionStop]
|
||||
}
|
||||
|
||||
prereleaseGroup := 7
|
||||
|
||||
var prerelease string
|
||||
prereleaseStart, prereleaseStop := match[0][prereleaseGroup*2], match[0][(prereleaseGroup*2)+1]
|
||||
if prereleaseStart != -1 && prereleaseStop != -1 {
|
||||
prerelease = input[prereleaseStart:prereleaseStop]
|
||||
}
|
||||
|
||||
return versionStart,
|
||||
version,
|
||||
prerelease
|
||||
// isLikelyVersion checks if a string looks like a version number
|
||||
func isLikelyVersion(s string) bool {
|
||||
// Simple check for common version patterns
|
||||
versionPattern := regexp.MustCompile(`^v?\d+(\.\d+)*(-\w+)*$`)
|
||||
return versionPattern.MatchString(s)
|
||||
}
|
||||
|
||||
// shouldIncludeAsPackage determines if this store path should be included as a package
|
||||
func (n *nixStorePath) shouldIncludeAsPackage() bool {
|
||||
// Only include actual packages, not derivations or source files
|
||||
if n.pathType == "derivation" || n.pathType == "source" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Skip paths that look like source archives even if not explicitly marked
|
||||
if strings.HasSuffix(n.path, ".tar.gz") ||
|
||||
strings.HasSuffix(n.path, ".tgz") ||
|
||||
strings.HasSuffix(n.path, ".tar.bz2") ||
|
||||
strings.HasSuffix(n.path, ".tar.xz") ||
|
||||
strings.HasSuffix(n.path, ".zip") ||
|
||||
strings.HasSuffix(n.path, ".drv") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Make sure it has a name (at minimum)
|
||||
return n.name != ""
|
||||
}
|
||||
|
||||
// isValidPackage should be much more permissive
|
||||
func (n *nixStorePath) isValidPackage() bool {
|
||||
// Any path with a hash and name is considered valid
|
||||
return n.hash != "" && n.name != ""
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user