github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/buildtools/bundler/lockfile.go (about)

     1  package bundler
     2  
     3  import (
     4  	"regexp"
     5  	"strings"
     6  
     7  	"github.com/apex/log"
     8  	"github.com/pkg/errors"
     9  
    10  	"github.com/fossas/fossa-cli/files"
    11  )
    12  
    13  type Lockfile struct {
    14  	Git          []Section
    15  	Path         []Section
    16  	Gem          []Section
    17  	Dependencies []Requirement
    18  }
    19  
    20  type Section struct {
    21  	Type     string
    22  	Remote   string
    23  	Revision string
    24  	Ref      string
    25  	Tag      string
    26  	Branch   string
    27  	Specs    []Spec
    28  }
    29  
    30  type Spec struct {
    31  	Name         string
    32  	Version      string
    33  	Dependencies []Requirement
    34  }
    35  
    36  type Requirement struct {
    37  	Name    string
    38  	Pinned  bool
    39  	Version VersionSpecifier
    40  }
    41  
    42  func (r *Requirement) String() string {
    43  	s := r.Name
    44  	if r.Pinned {
    45  		s += "!"
    46  	}
    47  	if r.Version != "" {
    48  		s += " (" + string(r.Version) + ")"
    49  	}
    50  	return s
    51  }
    52  
    53  // ^(leading whitespace)(name)(optional: exclamation mark or (space + (version specifier) within parentheses (optional: exclamation mark)))$
    54  var requirementsRegex = regexp.MustCompile(`^( *?)(\S+?)(?:\!?|( \((.*?)\)\!?)?)$`)
    55  
    56  // TODO: actually parse these. We ignore them right now, so I haven't bothered
    57  // implementing parsing logic.
    58  type VersionSpecifier string
    59  
    60  func FromLockfile(filename string) (Lockfile, error) {
    61  	contents, err := files.Read(filename)
    62  	if err != nil {
    63  		return Lockfile{}, errors.Wrap(err, "could not read Gemfile.lock")
    64  	}
    65  
    66  	var lockfile Lockfile
    67  	sections := strings.Split(string(contents), "\n\n")
    68  	for _, section := range sections {
    69  		lines := strings.Split(strings.TrimSpace(section), "\n")
    70  		header := lines[0]
    71  
    72  		switch header {
    73  		case "GIT":
    74  			lockfile.Git = append(lockfile.Git, ParseSpecSection(section))
    75  		case "PATH":
    76  			lockfile.Path = append(lockfile.Path, ParseSpecSection(section))
    77  		case "GEM":
    78  			lockfile.Gem = append(lockfile.Gem, ParseSpecSection(section))
    79  		case "DEPENDENCIES":
    80  			lockfile.Dependencies = ParseDependenciesSection(section)
    81  		default:
    82  			continue
    83  		}
    84  	}
    85  
    86  	return lockfile, nil
    87  }
    88  
    89  func ParseSpecSection(section string) Section {
    90  	lines := strings.Split(strings.TrimSpace(section), "\n")
    91  	header := lines[0]
    92  
    93  	remoteRegex := regexp.MustCompile("\n  remote: (.*?)\n")
    94  	revisionRegex := regexp.MustCompile("\n  revision: (.*?)\n")
    95  	refRegex := regexp.MustCompile("\n  ref: (.*?)\n")
    96  	tagRegex := regexp.MustCompile("\n  tag: (.*?)\n")
    97  	branchRegex := regexp.MustCompile("\n  branch: (.*?)\n")
    98  	specs := regexp.MustCompile("(?s)\n  specs:\n(.*?)$")
    99  
   100  	return Section{
   101  		Type:     header,
   102  		Remote:   fromMaybe(remoteRegex.FindStringSubmatch(section), 1),
   103  		Revision: fromMaybe(revisionRegex.FindStringSubmatch(section), 1),
   104  		Ref:      fromMaybe(refRegex.FindStringSubmatch(section), 1),
   105  		Tag:      fromMaybe(tagRegex.FindStringSubmatch(section), 1),
   106  		Branch:   fromMaybe(branchRegex.FindStringSubmatch(section), 1),
   107  		Specs:    ParseSpecs(specs.FindStringSubmatch(section)),
   108  	}
   109  }
   110  
   111  func fromMaybe(s []string, i int) string {
   112  	if len(s) > i {
   113  		return s[i]
   114  	}
   115  	return ""
   116  }
   117  
   118  func ParseSpecs(spec []string) []Spec {
   119  	var specs []Spec
   120  	if len(spec) < 2 {
   121  		log.Debug("No specs found in Gemfile")
   122  		return specs
   123  	}
   124  
   125  	s := spec[1]
   126  	lines := strings.Split(s, "\n")
   127  
   128  	var curr Spec
   129  	for i, line := range lines {
   130  		matches := requirementsRegex.FindStringSubmatch(line)
   131  
   132  		spaces := matches[1]
   133  		name := matches[2]
   134  		version := ""
   135  		if len(matches) == 5 {
   136  			version = matches[4]
   137  		}
   138  		log.WithFields(log.Fields{
   139  			"spaces":  spaces,
   140  			"name":    name,
   141  			"version": version,
   142  		}).Debug("parsing spec line")
   143  
   144  		switch len(spaces) {
   145  		// This is a spec.
   146  		case 4:
   147  			// Push the previous spec. Don't push the initial spec (which is zero).
   148  			if i != 0 {
   149  				specs = append(specs, curr)
   150  			}
   151  			curr = Spec{
   152  				Name:         name,
   153  				Version:      version,
   154  				Dependencies: []Requirement{},
   155  			}
   156  		// This is a requirement.
   157  		case 6:
   158  			curr.Dependencies = append(curr.Dependencies, Requirement{
   159  				Name:    name,
   160  				Version: VersionSpecifier(version),
   161  				Pinned:  strings.HasSuffix(name, "!"),
   162  			})
   163  		default:
   164  			// TODO: this should return an error instead of panicking.
   165  			log.Fatal("Malformed lockfile")
   166  		}
   167  	}
   168  	// Push the last spec.
   169  	log.WithField("spec", curr).Debug("push last spec")
   170  	specs = append(specs, curr)
   171  
   172  	return specs
   173  }
   174  
   175  func ParseDependenciesSection(section string) []Requirement {
   176  	lines := strings.Split(strings.TrimSpace(section), "\n")
   177  
   178  	// Ignore header.
   179  	var requirements []Requirement
   180  	for _, line := range lines[1:] {
   181  		matches := requirementsRegex.FindStringSubmatch(line)
   182  		spaces := matches[1]
   183  		name := matches[2]
   184  		version := ""
   185  		if len(matches) == 5 {
   186  			version = matches[4]
   187  		}
   188  		log.WithFields(log.Fields{
   189  			"spaces":  spaces,
   190  			"name":    name,
   191  			"version": version,
   192  		}).Debug("parsing dependency line")
   193  		requirements = append(requirements, Requirement{
   194  			Name:    strings.TrimSuffix(name, "!"),
   195  			Pinned:  strings.HasSuffix(name, "!"),
   196  			Version: VersionSpecifier(version),
   197  		})
   198  	}
   199  
   200  	return requirements
   201  }