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 }