package_parser/pkgs/ruby/
gemfile.rs1use std::collections::{BTreeMap, HashSet};
2use std::fs::File;
3use std::io::{self, BufRead};
4use std::path::Path;
5use std::vec;
6
7use anyhow::Result as AnyhowResult;
8use lazy_static::lazy_static;
9use packageurl::PackageUrl;
10use regex::Regex;
11
12use crate::error::SourcePkgError;
13use crate::pkgs::common::model::{DependentPackage, Package, PackageManifest};
14
15lazy_static! {
16 static ref GEMFILE_REGEXES: BTreeMap<&'static str, Regex> = {
17 let mut m = BTreeMap::new();
18 m.insert(
19 "source",
20 Regex::new(r"source:[ ]?(?P<source>[a-zA-Z:/\.-]+)").unwrap(),
21 );
22 m.insert(
23 "git",
24 Regex::new(r"git:[ ]?(?P<git>[a-zA-Z:/\.-]+)").unwrap(),
25 );
26 m.insert(
27 "platform",
28 Regex::new(r"platform:[ ]?(?P<platform>[a-zA-Z:/\.-]+)").unwrap(),
29 );
30 m.insert(
31 "path",
32 Regex::new(r"path:[ ]?(?P<path>[a-zA-Z:/\.-]+)").unwrap(),
33 );
34 m.insert(
35 "branch",
36 Regex::new(r"branch:[ ]?(?P<branch>[a-zA-Z:/\.-]+)").unwrap(),
37 );
38 m.insert(
39 "autorequire",
40 Regex::new(r"require:[ ]?(?P<autorequire>[a-zA-Z:/\.-]+)").unwrap(),
41 );
42 m.insert(
43 "group",
44 Regex::new(r"group:[ ]?(?P<group>[a-zA-Z:/\.-]+)").unwrap(),
45 );
46 m.insert(
47 "name",
48 Regex::new(r"(?P<name>[a-zA-Z]+[\.0-9a-zA-Z _-]*)").unwrap(),
49 );
50 m.insert(
51 "requirement",
52 Regex::new(r"(?P<requirement>([>|<|=|~>|\d]+[ ]*[0-9\.\w]+[ ,]*)+)").unwrap(),
53 );
54 m
55 };
56 static ref GROUP_BLOCK_REGEX: Regex =
57 Regex::new(r"group[ ]?:[ ]?(?P<groupblock>.*?) do").unwrap();
58 static ref GEMSPEC_ADD_DVTDEP_REGEX: Regex =
59 Regex::new(r".*add_development_dependency(?P<line>.*)").unwrap();
60 static ref GEMSPEC_ADD_RUNDEP_REGEX: Regex =
61 Regex::new(r".*add_runtime_dependency(?P<line>.*)").unwrap();
62 static ref GEMSPEC_ADD_DEP_REGEX: Regex = Regex::new(r".*dependency(?P<line>.*)").unwrap();
63}
64
65pub struct GemfileInner {
66 current_group: String,
67 dependencies: Vec<DependentPackage>,
68 dependency_keys: HashSet<(String, String)>,
69}
70
71fn preprocess(line: &str) -> &str {
72 if let Some(index) = line.find('#') {
73 &line[..index]
74 } else {
75 line
76 }
77 .trim()
78}
79
80fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
81where
82 P: AsRef<Path>,
83{
84 let file = File::open(filename)?;
85 Ok(io::BufReader::new(file).lines())
86}
87
88impl GemfileInner {
89 pub fn new() -> Self {
90 Self {
91 current_group: "".into(),
92 dependencies: vec![],
93 dependency_keys: HashSet::new(),
94 }
95 }
96
97 fn parse_line(&mut self, line: &str) {
98 let mut column_list = vec![];
99 let columns = line.split(',');
100 for column in columns {
101 let stripped_column = column.replace('\'', "");
102 let stripped_column = stripped_column.replace('"', "");
103 let stripped_column = stripped_column.replace("%q<", "");
104 let stripped_column = stripped_column.replace('(', "");
105 let stripped_column = stripped_column.replace(')', "");
106 let stripped_column = stripped_column.replace('[', "");
107 let stripped_column = stripped_column.replace(']', "");
108 let stripped_column = stripped_column.trim().to_string();
109 column_list.push(stripped_column)
110 }
111
112 let mut dep = DependentPackage {
113 is_resolved: false,
114 ..Default::default()
115 };
116
117 for column in column_list {
118 for (criteria, criteria_regex) in GEMFILE_REGEXES.iter() {
119 if let Some(captures) = criteria_regex.captures(&column) {
120 let criteria_value = captures[criteria.to_owned()].to_owned();
121 let criteria = criteria.to_string();
122
123 println!("{} {}", &criteria, &criteria_value);
124 if criteria == "requirement" {
125 dep.requirement = criteria_value;
126 } else if criteria == "group" {
127 dep.scope = criteria_value;
128 } else if criteria == "name" {
129 dep.purl = PackageUrl::new("gem", criteria_value)
130 .expect("purl arguments are invalid")
131 .to_string();
132 }
133 break;
134 }
135 }
136 }
137
138 if !dep.purl.is_empty()
139 && !self
140 .dependency_keys
141 .contains(&(dep.purl.clone(), dep.requirement.clone()))
142 {
143 self.dependency_keys
144 .insert((dep.purl.clone(), dep.requirement.clone()));
145 self.dependencies.push(dep);
146 }
147 }
148
149 fn parse_gemspec(&mut self, contents: Vec<String>) {
150 for line in contents {
151 let line = preprocess(&line);
152 let mut matched = None;
153 if let Some(captures) = GEMSPEC_ADD_DVTDEP_REGEX.captures(line) {
154 self.current_group = "development".into();
155 matched = Some(captures["line"].to_owned());
156 } else if let Some(captures) = GEMSPEC_ADD_RUNDEP_REGEX.captures(line) {
157 self.current_group = "runtime".into();
158 matched = Some(captures["line"].to_owned());
159 } else if let Some(captures) = GEMSPEC_ADD_DEP_REGEX.captures(line) {
160 self.current_group = "dependency".into();
161 matched = Some(captures["line"].to_owned());
162 }
163
164 if let Some(line) = matched {
165 self.parse_line(&line);
166 }
167 }
168 }
169
170 pub fn parse_gemfile(&mut self, path: impl AsRef<Path>) -> AnyhowResult<()> {
171 let mut contents = vec![];
172 if let Ok(lines) = read_lines(path) {
173 lines.for_each(|line| {
174 if let Ok(ip) = line {
175 contents.push(ip);
176 }
177 });
178 }
179
180 let bk_contents = contents.clone();
181 for line in contents {
182 let line = preprocess(&line);
183 if line.is_empty() || line.starts_with("source") {
184 continue;
185 } else if line.starts_with("group") {
186 if let Some(captures) = GROUP_BLOCK_REGEX.captures(line) {
187 self.current_group = captures["groupblock"].into();
188 }
189 } else if line.starts_with("end") {
190 self.current_group = "runtime".into();
191 } else if line.starts_with("gemspec") {
192 self.parse_gemspec(bk_contents.clone());
193 } else if line.starts_with("gem ") {
194 let line = &line[3..];
195 self.parse_line(line);
196 }
197 }
198
199 Ok(())
200 }
201}
202
203pub struct Gemfile {}
204
205impl Gemfile {
206 pub fn new() -> Self {
207 Self {}
208 }
209}
210
211#[async_trait::async_trait]
212impl PackageManifest for Gemfile {
213 fn get_name(&self) -> String {
214 "gemfile".to_string()
215 }
216
217 async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
218 let lock_path = path.with_file_name("Gemfile.lock");
219 if lock_path.exists() {
220 log::info!("parsing Gemfile.lock");
221
222 match super::gemfilelock::parse_file(&lock_path) {
223 Ok(package) => return Ok(package),
224 Err(err) => {
225 log::warn!("failed to parse Gemfile.lock: {}", err);
226 }
227 }
228 }
229
230 let mut parser = GemfileInner::new();
231 parser.parse_gemfile(path)?;
232
233 let package = Package {
234 dependencies: parser.dependencies,
235 ..Default::default()
236 };
237
238 Ok(package)
239 }
240
241 fn file_name_patterns(&self) -> &'static [&'static str] {
242 &["Gemfile"]
243 }
244}
245
246#[cfg(test)]
247mod test {
248 use super::*;
249
250 #[test]
251 fn test_gemfile_lock() {
252 let filepath = Path::new(concat!(
253 env!("CARGO_MANIFEST_DIR"),
254 "/testdata/gemfile/Gemfile"
255 ));
256
257 let mut parser = GemfileInner::new();
258 parser.parse_gemfile(filepath).unwrap();
259 println!("{:?}", parser.dependencies);
260 }
261}