Skip to main content

package_parser/pkgs/ruby/
gemfilelock.rs

1use nom::{
2    branch::alt,
3    bytes::complete::{tag, take, take_until},
4    character::complete::{line_ending, none_of, space0},
5    combinator::recognize,
6    error::VerboseError,
7    multi::{many1, many_till},
8    sequence::{delimited, tuple},
9    IResult,
10};
11use packageurl::PackageUrl;
12
13use crate::error::SourcePkgError;
14use crate::pkgs::common::model::{DependentPackage, Package};
15
16use std::fs::File;
17use std::io::Read;
18use std::path::Path;
19
20type NomResult<T, U> = IResult<T, U, VerboseError<T>>;
21
22fn take_till_line_end(input: &str) -> NomResult<&str, &str> {
23    recognize(tuple((
24        alt((take_until("\n"), take_until("\r\n"))),
25        take(1usize),
26    )))(input)
27}
28
29fn take_till_blank_line(input: &str) -> NomResult<&str, &str> {
30    recognize(alt((take_until("\n\n"), take_until("\r\n\r\n"))))(input)
31}
32
33fn gem_header(input: &str) -> NomResult<&str, &str> {
34    let (input, _) = recognize(take_until("GEM"))(input)?;
35    recognize(tuple((tag("GEM"), line_ending)))(input)
36}
37
38fn specs(input: &str) -> NomResult<&str, &str> {
39    let (input, _consumed) = recognize(many_till(
40        take_till_line_end,
41        recognize(tuple((space0, tag("specs:"), line_ending))),
42    ))(input)?;
43
44    take_till_blank_line(input)
45}
46
47fn package_name(input: &str) -> NomResult<&str, &str> {
48    let (input, _) = recognize(space0)(input)?;
49    recognize(take_until(" "))(input)
50}
51
52fn package_version(input: &str) -> NomResult<&str, &str> {
53    let (input, _) = space0(input)?;
54    delimited(tag("("), recognize(many1(none_of(" \t()"))), tag(")"))(input)
55}
56
57fn package(input: &str) -> Option<(String, String)> {
58    let (input, name) = package_name(input).ok()?;
59    let (_, version) = package_version(input).ok()?;
60
61    Some((name.to_string(), version.to_string()))
62}
63
64fn parse(input: &str) -> Result<Vec<(String, String)>, SourcePkgError> {
65    let (input, _) =
66        gem_header(input).map_err(|e| SourcePkgError::GenericsError2(e.to_string()))?;
67
68    let (_, consumed) = specs(input).map_err(|e| SourcePkgError::GenericsError2(e.to_string()))?;
69
70    let pkgs = consumed.lines().filter_map(package).collect::<Vec<_>>();
71
72    Ok(pkgs)
73}
74
75// NOTICE:
76// DO NOT parse the 'unlocked' gemfile file
77// because some version requirements are unable to resolve
78// which maybe cause the false positive result
79
80pub fn parse_file(path: impl AsRef<Path>) -> Result<Package, SourcePkgError> {
81    let mut fs = File::open(path)?;
82    let mut content = String::new();
83    fs.read_to_string(&mut content)?;
84
85    let pkgs = parse(&content)?;
86
87    let mut dependencies = vec![];
88
89    for (name, version) in pkgs {
90        let dependency = DependentPackage {
91            purl: PackageUrl::new("gem", name).unwrap().to_string(),
92            requirement: version,
93            is_resolved: true,
94            ..Default::default()
95        };
96
97        dependencies.push(dependency);
98    }
99
100    let package = Package {
101        dependencies,
102        ..Default::default()
103    };
104
105    Ok(package)
106}
107
108#[cfg(test)]
109mod test {
110    use super::*;
111
112    #[tokio::test]
113    async fn test_gemfile_lock() {
114        let filepath = Path::new(concat!(
115            env!("CARGO_MANIFEST_DIR"),
116            "/testdata/gemfile_lock/as_deps/Gemfile.lock"
117        ));
118
119        let p = parse_file(filepath).unwrap();
120        println!("{:?}", p);
121    }
122}