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 }