1use crate::TextRange;
2use eyre::Context as _;
3use std::{
4 collections::BTreeMap,
5 fs::File,
6 io::{BufRead as _, BufReader},
7 path::{Path, PathBuf},
8};
9use tracing::error;
10
11#[derive(Debug)]
12pub struct PackageRange {
13 pub range: TextRange,
14 pub name_range: TextRange,
15 pub version_range: TextRange,
16}
17
18#[derive(Debug)]
19pub struct Lockfile {
20 pub lockfile_path: PathBuf,
21 pub dependencies: BTreeMap<String, PackageRange>,
22}
23
24impl Lockfile {
25 #[must_use]
26 #[inline]
27 pub fn dependency_range(&self, name: &str) -> TextRange {
28 self.dependencies.get(name).map_or_else(
29 || {
30 error!("failed to find a corresponding text range for dependency '{}'. This is probably a bug in `cargo-sonar`, if you can please report an issue at https://gitlab.com/woshilapin/cargo-sonar/-/issues, we'd be happy to try and fix it.", name);
31 TextRange::new((1, 0), (1, 0))
32 },
33 |ranges| ranges.range,
34 )
35 }
36}
37
38#[derive(Debug)]
39enum StateParser {
40 Package {
42 start_line: usize,
43 },
44 Name {
46 start_line: usize,
47 package_name: String,
48 name_range: TextRange,
49 },
50 Version {
52 start_line: usize,
53 package_name: String,
54 name_range: TextRange,
55 version_range: TextRange,
56 },
57 End,
59}
60
61impl TryFrom<&Path> for Lockfile {
62 type Error = eyre::Error;
63
64 #[inline]
65 fn try_from(lockfile_path: &Path) -> Result<Self, Self::Error> {
66 let lockfile_path = lockfile_path
67 .canonicalize()
68 .with_context(|| format!("failed to canonicalize '{}", lockfile_path.display()))?;
69 let mut dependencies = BTreeMap::new();
70 let file = File::open(&lockfile_path)
71 .with_context(|| format!("failed to open '{}", lockfile_path.display()))?;
72 let reader = BufReader::new(file);
73 let mut state = StateParser::End;
74 for (line_num, line) in reader
75 .lines()
76 .chain(std::iter::once(Ok(String::new())))
78 .enumerate()
79 {
80 let line_num = line_num.saturating_add(1);
81 let line = line?;
82 let create_range = |line: &str| {
83 let start_column = line.find('"').unwrap_or(0);
84 let end_column = line
85 .rfind('"')
86 .unwrap_or_else(|| line.len().saturating_sub(1));
87 TextRange::new((line_num, start_column), (line_num, end_column))
88 };
89 match state {
90 StateParser::End if line == "[[package]]" => {
91 state = StateParser::Package {
92 start_line: line_num,
93 };
94 }
95 StateParser::Package { start_line } if line.starts_with("name = ") => {
96 let name_range = create_range(&line);
97 state = StateParser::Name {
98 start_line,
99 package_name: line.replace("name = ", "").replace('"', ""),
100 name_range,
101 };
102 }
103 StateParser::Name {
104 start_line,
105 package_name,
106 name_range,
107 } if line.starts_with("version = ") => {
108 let version_range = create_range(&line);
109 state = StateParser::Version {
110 start_line,
111 package_name,
112 name_range,
113 version_range,
114 };
115 }
116 StateParser::Version {
117 start_line,
118 package_name,
119 name_range,
120 version_range,
121 } if line.is_empty() => {
122 let range = TextRange::new((start_line, 0), (line_num, 0));
123 let package_range = PackageRange {
124 range,
125 name_range,
126 version_range,
127 };
128 dependencies.insert(package_name, package_range);
129 state = StateParser::End;
130 }
131 s => state = s,
132 }
133 }
134 let lockfile = Self {
135 lockfile_path,
136 dependencies,
137 };
138 Ok(lockfile)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use crate::Lockfile;
145 use std::io::Write as _;
146 use test_log::test;
147
148 #[test]
149 fn cargo_lock_parsing() {
150 let lock = r#"
151version = 3
152
153[[package]]
154name = "addr2line"
155version = "0.20.0"
156source = "registry+https://github.com/rust-lang/crates.io-index"
157checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
158dependencies = [
159"gimli",
160]
161 "#;
162 let mut cargo_lock = tempfile::NamedTempFile::new().unwrap();
163 write!(cargo_lock, "{}", lock).unwrap();
164
165 let lockfile = Lockfile::try_from(cargo_lock.path()).unwrap();
166 let text_range = lockfile.dependency_range("addr2line");
167 assert_eq!(text_range.start.line, 4);
168 assert_eq!(text_range.start.column, 0);
169 assert_eq!(text_range.end.line, 13);
170 assert_eq!(text_range.end.column, 0);
171 }
172}