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

     1  package r
     2  
     3  import (
     4  	"bufio"
     5  	"io"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/anchore/syft/syft/artifact"
    10  	"github.com/anchore/syft/syft/file"
    11  	"github.com/anchore/syft/syft/pkg"
    12  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    13  )
    14  
    15  /* some examples of license strings found in DESCRIPTION files:
    16  find /usr/local/lib/R -name DESCRIPTION | xargs cat | grep 'License:' | sort | uniq
    17  License: GPL
    18  License: GPL (>= 2)
    19  License: GPL (>=2)
    20  License: GPL(>=2)
    21  License: GPL (>= 2) | file LICENCE
    22  License: GPL-2 | GPL-3
    23  License: GPL-3
    24  License: LGPL (>= 2)
    25  License: LGPL (>= 2.1)
    26  License: MIT + file LICENSE
    27  License: Part of R 4.3.0
    28  License: Unlimited
    29  */
    30  
    31  func parseDescriptionFile(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    32  	values := extractFieldsFromDescriptionFile(reader)
    33  	m := parseDataFromDescriptionMap(values)
    34  	p := newPackage(m, []file.Location{reader.Location}...)
    35  	if p.Name == "" || p.Version == "" {
    36  		return nil, nil, nil
    37  	}
    38  	return []pkg.Package{p}, nil, nil
    39  }
    40  
    41  type parseData struct {
    42  	Package string
    43  	Version string
    44  	License string
    45  	pkg.RDescriptionFileMetadata
    46  }
    47  
    48  func parseDataFromDescriptionMap(values map[string]string) parseData {
    49  	return parseData{
    50  		License: values["License"],
    51  		Package: values["Package"],
    52  		Version: values["Version"],
    53  		RDescriptionFileMetadata: pkg.RDescriptionFileMetadata{
    54  			Title:            values["Title"],
    55  			Description:      cleanMultiLineValue(values["Description"]),
    56  			Maintainer:       values["Maintainer"],
    57  			URL:              commaSeparatedList(values["URL"]),
    58  			Depends:          commaSeparatedList(values["Depends"]),
    59  			Imports:          commaSeparatedList(values["Imports"]),
    60  			Suggests:         commaSeparatedList(values["Suggests"]),
    61  			NeedsCompilation: yesNoToBool(values["NeedsCompilation"]),
    62  			Author:           values["Author"],
    63  			Repository:       values["Repository"],
    64  			Built:            values["Built"],
    65  		},
    66  	}
    67  }
    68  
    69  func yesNoToBool(s string) bool {
    70  	/*
    71  		$ docker run --rm -it rocker/r-ver bash
    72  		$ install2.r ggplot2 dplyr mlr3 caret # just some packages for a larger sample
    73  		$ find /usr/local/lib/R -name DESCRIPTION | xargs cat | grep 'NeedsCompilation:' | sort | uniq
    74  		NeedsCompilation: no
    75  		NeedsCompilation: yes
    76  		$ find /usr/local/lib/R -name DESCRIPTION | xargs cat | grep 'NeedsCompilation:' | wc -l
    77  		105
    78  	*/
    79  	return strings.EqualFold(s, "yes")
    80  }
    81  
    82  func commaSeparatedList(s string) []string {
    83  	var result []string
    84  	split := strings.Split(s, ",")
    85  	for _, piece := range split {
    86  		value := strings.TrimSpace(piece)
    87  		if value == "" {
    88  			continue
    89  		}
    90  		result = append(result, value)
    91  	}
    92  	return result
    93  }
    94  
    95  var space = regexp.MustCompile(`\s+`)
    96  
    97  func cleanMultiLineValue(s string) string {
    98  	return space.ReplaceAllString(s, " ")
    99  }
   100  
   101  func extractFieldsFromDescriptionFile(reader io.Reader) map[string]string {
   102  	result := make(map[string]string)
   103  	key := ""
   104  	var valueFragment strings.Builder
   105  	scanner := bufio.NewScanner(reader)
   106  
   107  	for scanner.Scan() {
   108  		line := scanner.Text()
   109  		// line is like Key: Value -> start capturing value; close out previous value
   110  		// line is like \t\t continued value -> append to existing value
   111  		if len(line) == 0 {
   112  			continue
   113  		}
   114  		if startsWithWhitespace(line) {
   115  			// we're continuing a value
   116  			if key == "" {
   117  				continue
   118  			}
   119  			valueFragment.WriteByte('\n')
   120  			valueFragment.WriteString(strings.TrimSpace(line))
   121  		} else {
   122  			if key != "" {
   123  				// capture previous value
   124  				result[key] = valueFragment.String()
   125  				key = ""
   126  				valueFragment = strings.Builder{}
   127  			}
   128  			parts := strings.SplitN(line, ":", 2)
   129  			if len(parts) != 2 {
   130  				continue
   131  			}
   132  			key = parts[0]
   133  			valueFragment.WriteString(strings.TrimSpace(parts[1]))
   134  		}
   135  	}
   136  	if key != "" {
   137  		result[key] = valueFragment.String()
   138  	}
   139  	return result
   140  }
   141  
   142  func startsWithWhitespace(s string) bool {
   143  	if s == "" {
   144  		return false
   145  	}
   146  	return s[0] == ' ' || s[0] == '\t'
   147  }