github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/homebrew/parse_homebrew_formula.go (about)

     1  package homebrew
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"path"
     7  	"strings"
     8  
     9  	"github.com/anchore/syft/internal/log"
    10  	"github.com/anchore/syft/syft/artifact"
    11  	"github.com/anchore/syft/syft/file"
    12  	"github.com/anchore/syft/syft/pkg"
    13  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    14  )
    15  
    16  type parsedHomebrewData struct {
    17  	Tap      string
    18  	Name     string
    19  	Version  string
    20  	Desc     string
    21  	Homepage string
    22  	License  string
    23  }
    24  
    25  func parseHomebrewFormula(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    26  	pd, err := parseFormulaFile(reader)
    27  	if err != nil {
    28  		log.WithFields("path", reader.RealPath).Trace("failed to parse formula")
    29  		return nil, nil, err
    30  	}
    31  
    32  	if pd == nil {
    33  		return nil, nil, nil
    34  	}
    35  
    36  	return []pkg.Package{
    37  		newHomebrewPackage(
    38  			ctx,
    39  			resolver,
    40  			*pd,
    41  			reader.Location,
    42  		),
    43  	}, nil, nil
    44  }
    45  
    46  func parseFormulaFile(reader file.LocationReadCloser) (*parsedHomebrewData, error) {
    47  	pd := parsedHomebrewData{}
    48  
    49  	scanner := bufio.NewScanner(reader)
    50  	for scanner.Scan() {
    51  		line := strings.TrimSpace(scanner.Text())
    52  		if strings.Contains(line, "class ") && strings.Contains(line, " < Formula") {
    53  			// this is the start of the class declaration, ignore anything before this
    54  			pd = parsedHomebrewData{}
    55  			continue
    56  		}
    57  
    58  		switch {
    59  		case matchesVariable(line, "desc"):
    60  			pd.Desc = getQuotedValue(line)
    61  		case matchesVariable(line, "homepage"):
    62  			pd.Homepage = getQuotedValue(line)
    63  		case matchesVariable(line, "license"):
    64  			pd.License = getQuotedValue(line)
    65  		case matchesVariable(line, "name"):
    66  			pd.Name = getQuotedValue(line)
    67  		case matchesVariable(line, "version"):
    68  			pd.Version = getQuotedValue(line)
    69  		}
    70  	}
    71  
    72  	pd.Tap = getTapFromPath(reader.RealPath)
    73  
    74  	if err := scanner.Err(); err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	if pd.Name != "" && pd.Version != "" {
    79  		return &pd, nil
    80  	}
    81  
    82  	pd.Name, pd.Version = getNameAndVersionFromPath(reader.RealPath)
    83  
    84  	return &pd, nil
    85  }
    86  
    87  func matchesVariable(line, name string) bool {
    88  	// should return true if the line starts with "name<space>" or "name<tab>"
    89  	return strings.HasPrefix(line, name+" ") || strings.HasPrefix(line, name+"\t")
    90  }
    91  
    92  func getNameAndVersionFromPath(p string) (string, string) {
    93  	if p == "" {
    94  		return "", ""
    95  	}
    96  
    97  	pathParts := strings.Split(p, "/")
    98  
    99  	// extract from a formula path...
   100  	// e.g. /opt/homebrew/Cellar/foo/1.0.0/.brew/foo.rb
   101  	var name, ver string
   102  	for i := len(pathParts) - 1; i >= 0; i-- {
   103  		if pathParts[i] == ".brew" && i-2 >= 0 {
   104  			name = pathParts[i-2]
   105  			ver = pathParts[i-1]
   106  			break
   107  		}
   108  	}
   109  
   110  	if name == "" {
   111  		// get it from the filename
   112  		name = strings.TrimSuffix(path.Base(p), ".rb")
   113  	}
   114  
   115  	return name, ver
   116  }
   117  
   118  func getTapFromPath(path string) string {
   119  	// get testorg/sometap from opt/homebrew/Library/Taps/testorg/sometap/Formula/bar.rb
   120  	// key off of Library/Taps/ as the path just before the org/tap name
   121  
   122  	paths := strings.Split(path, "Library/Taps/")
   123  	if len(paths) < 2 {
   124  		return ""
   125  	}
   126  
   127  	paths = strings.Split(paths[1], "/")
   128  	if len(paths) < 2 {
   129  		return ""
   130  	}
   131  	return strings.Join(paths[0:2], "/")
   132  }
   133  
   134  func getQuotedValue(s string) string {
   135  	s = strings.TrimSpace(s)
   136  	if s == "" {
   137  		return ""
   138  	}
   139  
   140  	start := strings.Index(s, "\"")
   141  	if start == -1 {
   142  		return ""
   143  	}
   144  
   145  	end := strings.LastIndex(s, "\"")
   146  	if end == -1 || end <= start {
   147  		return ""
   148  	}
   149  
   150  	return s[start+1 : end]
   151  }