Skip to main content

package_parser/pkgs/ruby/
gemfile.rs

1use 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}