github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/cataloger/ruby/parse_gemspec.go (about)

     1  package ruby
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"fmt"
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/mitchellh/mapstructure"
    11  
    12  	"github.com/anchore/syft/internal"
    13  	"github.com/anchore/syft/syft/artifact"
    14  	"github.com/anchore/syft/syft/file"
    15  	"github.com/anchore/syft/syft/pkg"
    16  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    17  )
    18  
    19  var _ generic.Parser = parseGemFileLockEntries
    20  
    21  type postProcessor func(string) []string
    22  
    23  type gemData struct {
    24  	Licenses        []string `mapstructure:"licenses" json:"licenses,omitempty"`
    25  	pkg.GemMetadata `mapstructure:",squash" json:",inline"`
    26  }
    27  
    28  // match example:      Al\u003Ex   --->   003E
    29  var unicodePattern = regexp.MustCompile(`\\u(?P<unicode>[0-9A-F]{4})`)
    30  
    31  var patterns = map[string]*regexp.Regexp{
    32  	// match example:       name = "railties".freeze   --->   railties
    33  	"name": regexp.MustCompile(`.*\.name\s*=\s*["']{1}(?P<name>.*)["']{1} *`),
    34  
    35  	// match example:       version = "1.0.4".freeze   --->   1.0.4
    36  	"version": regexp.MustCompile(`.*\.version\s*=\s*["']{1}(?P<version>.*)["']{1} *`),
    37  
    38  	// match example:
    39  	// homepage = "https://github.com/anchore/syft".freeze   --->   https://github.com/anchore/syft
    40  	"homepage": regexp.MustCompile(`.*\.homepage\s*=\s*["']{1}(?P<homepage>.*)["']{1} *`),
    41  
    42  	// match example:       files = ["exe/bundle".freeze, "exe/bundler".freeze]    --->    "exe/bundle".freeze, "exe/bundler".freeze
    43  	"files": regexp.MustCompile(`.*\.files\s*=\s*\[(?P<files>.*)] *`),
    44  
    45  	// match example:       authors = ["Andr\u00E9 Arko".freeze, "Samuel Giddins".freeze, "Colby Swandale".freeze,
    46  	//								   "Hiroshi Shibata".freeze, "David Rodr\u00EDguez".freeze, "Grey Baker".freeze...]
    47  	"authors": regexp.MustCompile(`.*\.authors\s*=\s*\[(?P<authors>.*)] *`),
    48  
    49  	// match example:	    licenses = ["MIT".freeze]   ----> "MIT".freeze
    50  	"licenses": regexp.MustCompile(`.*\.licenses\s*=\s*\[(?P<licenses>.*)] *`),
    51  }
    52  
    53  var postProcessors = map[string]postProcessor{
    54  	"files":    processList,
    55  	"authors":  processList,
    56  	"licenses": processList,
    57  }
    58  
    59  func processList(s string) []string {
    60  	var results []string
    61  	for _, item := range strings.Split(s, ",") {
    62  		results = append(results, strings.Trim(item, "\" "))
    63  	}
    64  	return results
    65  }
    66  
    67  func parseGemSpecEntries(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    68  	var pkgs []pkg.Package
    69  	var fields = make(map[string]interface{})
    70  	scanner := bufio.NewScanner(reader)
    71  
    72  	for scanner.Scan() {
    73  		line := scanner.Text()
    74  
    75  		sanitizedLine := strings.TrimSpace(line)
    76  		sanitizedLine = strings.ReplaceAll(sanitizedLine, ".freeze", "")
    77  		sanitizedLine = renderUtf8(sanitizedLine)
    78  
    79  		if sanitizedLine == "" {
    80  			continue
    81  		}
    82  
    83  		for field, pattern := range patterns {
    84  			matchMap := internal.MatchNamedCaptureGroups(pattern, sanitizedLine)
    85  			if value := matchMap[field]; value != "" {
    86  				if pp := postProcessors[field]; pp != nil {
    87  					fields[field] = pp(value)
    88  				} else {
    89  					fields[field] = value
    90  				}
    91  				// TODO: know that a line could actually match on multiple patterns, this is unlikely though
    92  				break
    93  			}
    94  		}
    95  	}
    96  
    97  	if fields["name"] != "" && fields["version"] != "" {
    98  		var metadata gemData
    99  		if err := mapstructure.Decode(fields, &metadata); err != nil {
   100  			return nil, nil, fmt.Errorf("unable to decode gem metadata: %w", err)
   101  		}
   102  
   103  		pkgs = append(
   104  			pkgs,
   105  			newGemspecPackage(
   106  				metadata,
   107  				reader.Location,
   108  			),
   109  		)
   110  	}
   111  
   112  	return pkgs, nil, nil
   113  }
   114  
   115  // renderUtf8 takes any string escaped string sub-sections from the ruby string and replaces those sections with the UTF8 runes.
   116  func renderUtf8(s string) string {
   117  	fullReplacement := unicodePattern.ReplaceAllStringFunc(s, func(unicodeSection string) string {
   118  		var replacement string
   119  		// note: the json parser already has support for interpreting hex-representations of unicode escaped strings as unicode runes.
   120  		// we can do this ourselves with strconv.Atoi, or leverage the existing json package.
   121  		if err := json.Unmarshal([]byte(`"`+unicodeSection+`"`), &replacement); err != nil {
   122  			return unicodeSection
   123  		}
   124  		return replacement
   125  	})
   126  	return fullReplacement
   127  }