From 7c8aad9e1bdabfd41277c04d73e9f497bc3fbca4 Mon Sep 17 00:00:00 2001 From: Alan Pope Date: Wed, 9 Apr 2025 14:54:35 +0100 Subject: [PATCH] testing --- syft/pkg/cataloger/nix/cataloger.go | 119 ++++++++- syft/pkg/cataloger/nix/dependencies.go | 207 +++++++++++++++ syft/pkg/cataloger/nix/derivation.go | 136 ++++++++++ syft/pkg/cataloger/nix/package.go | 64 +++++ .../pkg/cataloger/nix/parse_nix_store_path.go | 235 ++++++++++-------- 5 files changed, 648 insertions(+), 113 deletions(-) create mode 100644 syft/pkg/cataloger/nix/dependencies.go create mode 100644 syft/pkg/cataloger/nix/derivation.go diff --git a/syft/pkg/cataloger/nix/cataloger.go b/syft/pkg/cataloger/nix/cataloger.go index 2b20f9e31..110381387 100644 --- a/syft/pkg/cataloger/nix/cataloger.go +++ b/syft/pkg/cataloger/nix/cataloger.go @@ -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 { diff --git a/syft/pkg/cataloger/nix/dependencies.go b/syft/pkg/cataloger/nix/dependencies.go new file mode 100644 index 000000000..b8480ef12 --- /dev/null +++ b/syft/pkg/cataloger/nix/dependencies.go @@ -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 +} diff --git a/syft/pkg/cataloger/nix/derivation.go b/syft/pkg/cataloger/nix/derivation.go new file mode 100644 index 000000000..6a161c775 --- /dev/null +++ b/syft/pkg/cataloger/nix/derivation.go @@ -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 "" +} diff --git a/syft/pkg/cataloger/nix/package.go b/syft/pkg/cataloger/nix/package.go index 56f8acfa9..a1646a8c8 100644 --- a/syft/pkg/cataloger/nix/package.go +++ b/syft/pkg/cataloger/nix/package.go @@ -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 + } +} diff --git a/syft/pkg/cataloger/nix/parse_nix_store_path.go b/syft/pkg/cataloger/nix/parse_nix_store_path.go index dc70e989d..26f1cc4ca 100644 --- a/syft/pkg/cataloger/nix/parse_nix_store_path.go +++ b/syft/pkg/cataloger/nix/parse_nix_store_path.go @@ -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(?P[0-9][a-zA-Z0-9]*)(\.(?P[0-9][a-zA-Z0-9]*))?(\.(?P0|[1-9][a-zA-Z0-9]*)){0,3}(?:-(?P\d*[.a-zA-Z-][.0-9a-zA-Z-]*)*)?(?:\+(?P[.0-9a-zA-Z-]+(?:\.[.0-9a-zA-Z-]+)*))?)`) - - unstableVersion = regexp.MustCompile(`-(?Punstable-\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 != "" }