Skip to main content

package_parser/pkgs/
spec.rs

1use lazy_static::lazy_static;
2use regex::Regex;
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5use std::path::Path;
6use std::vec;
7
8use crate::error::SourcePkgError;
9
10#[derive(Debug, Clone, Default)]
11pub struct SpecInfo {
12    pub name: Option<String>,
13    pub version: Option<String>,
14    pub license: Option<String>,
15    pub summary: Option<String>,
16    pub description: Option<String>,
17    pub homepage: Option<String>,
18    pub source: Option<String>,
19    pub emails: Vec<String>,
20    pub authors: Vec<String>,
21}
22
23/// Return line after comments and space.
24fn pre_process(line: &mut String) -> String {
25    let newline = line
26        .find('#')
27        .map(|i| line.split_off(i))
28        .unwrap_or_else(|| line.to_string());
29    newline.trim().to_string()
30}
31
32/// Return data after removing unnecessary special character
33fn get_stripped_data(data: &mut str) -> String {
34    let new_data = data
35        .replace(['\'', '\"', '{', '}', '[', ']'], "")
36        .replace("%q", "");
37
38    new_data.trim().to_string()
39}
40
41/// Return description from spec file.
42///   s.description  = <<-DESC
43///                     Nanopb is a small code-size Protocol Buffers implementation
44///                     in ansi C. It is especially suitable for use in
45///                     microcontrollers, but fits any memory restricted system.
46///                    DESC
47fn get_description(path: impl AsRef<Path>) -> Option<String> {
48    let location = path.as_ref();
49    let fs = match File::open(location) {
50        Ok(fs) => fs,
51        Err(_) => return None,
52    };
53
54    let mut description = String::new();
55    let mut enter_description_section = false;
56    for line in BufReader::new(fs).lines() {
57        let line = match line {
58            Ok(line) => line,
59            Err(_) => continue,
60        };
61
62        if line.contains(".description") {
63            enter_description_section = true;
64            continue;
65        }
66
67        if enter_description_section {
68            let line = line.trim().to_string();
69            if line.contains("DESC") {
70                break;
71            }
72            description.push_str(&line);
73        }
74    }
75    Some(description)
76}
77
78pub struct Spec;
79
80lazy_static! {
81    static ref PARSE_NAME: Regex = Regex::new(r".*\.name(\s*)=(?P<name>.*)").unwrap();
82    static ref PARSE_VERSION: Regex = Regex::new(r".*\.version(\s*)=(?P<version>.*)").unwrap();
83    static ref PARSE_LICENSE: Regex = Regex::new(r".*\.license(\s*)=(?P<license>.*)").unwrap();
84    static ref PARSE_SUMMARY: Regex = Regex::new(r".*\.summary(\s*)=(?P<summary>.*)").unwrap();
85    static ref PARSE_DESCRIPTION: Regex =
86        Regex::new(r".*\.description(\s*)=(?P<description>.*)").unwrap();
87    static ref PARSE_HOMEPAGE: Regex = Regex::new(r".*\.homepage(\s*)=(?P<homepage>.*)").unwrap();
88    static ref PARSE_SOURCE: Regex = Regex::new(r".*\.source(\s*)=(?P<source>.*)").unwrap();
89}
90
91impl Spec {
92    pub fn new() -> Self {
93        Self {}
94    }
95
96    pub fn parse_spec(&self, path: impl AsRef<Path>) -> Result<SpecInfo, SourcePkgError> {
97        let location = path.as_ref();
98        let fs = File::open(location).map_err(SourcePkgError::Io)?;
99
100        let mut spec_info = SpecInfo::default();
101        for line in BufReader::new(fs).lines() {
102            let mut line = line.unwrap_or_default();
103            let line = pre_process(&mut line);
104
105            if let Some(name) = self.parse_name(&line) {
106                spec_info.name = Some(name);
107            }
108
109            if let Some(version) = self.parse_version(&line) {
110                spec_info.version = Some(version);
111            }
112
113            if let Some(license) = self.parse_license(&line) {
114                spec_info.license = Some(license);
115            }
116
117            if let Some(summary) = self.parse_summary(&line) {
118                spec_info.summary = Some(summary)
119            }
120
121            if let Some(homepage) = self.parse_homepage(&line) {
122                spec_info.homepage = Some(homepage);
123            }
124
125            if let Some(description) = self.parse_description(&path, &line) {
126                spec_info.description = Some(description);
127            }
128
129            let emails = self.parse_email(&line);
130            if !emails.is_empty() {
131                spec_info.emails.extend(emails);
132            }
133        }
134
135        Ok(spec_info)
136    }
137
138    fn parse_name(&self, line: &str) -> Option<String> {
139        if let Some(captures) = PARSE_NAME.captures(line) {
140            let mut name = match captures.name("name") {
141                Some(name) => name.as_str().to_string(),
142                None => return None,
143            };
144            let name = get_stripped_data(&mut name);
145            return Some(name);
146        }
147
148        None
149    }
150
151    fn parse_version(&self, line: &str) -> Option<String> {
152        if let Some(captures) = PARSE_VERSION.captures(line) {
153            let mut version = match captures.name("version") {
154                Some(version) => version.as_str().to_string(),
155                None => return None,
156            };
157            let version = get_stripped_data(&mut version);
158            return Some(version);
159        }
160
161        None
162    }
163
164    fn parse_license(&self, line: &str) -> Option<String> {
165        if let Some(captures) = PARSE_LICENSE.captures(line) {
166            let mut license = match captures.name("license") {
167                Some(license) => license.as_str().to_string(),
168                None => return None,
169            };
170            let license = get_stripped_data(&mut license);
171            return Some(license);
172        }
173
174        None
175    }
176
177    fn parse_summary(&self, line: &str) -> Option<String> {
178        if let Some(captures) = PARSE_SUMMARY.captures(line) {
179            let mut summary = match captures.name("summary") {
180                Some(summary) => summary.as_str().to_string(),
181                None => return None,
182            };
183            let summary = get_stripped_data(&mut summary);
184            return Some(summary);
185        }
186
187        None
188    }
189
190    fn parse_homepage(&self, line: &str) -> Option<String> {
191        if let Some(captures) = PARSE_HOMEPAGE.captures(line) {
192            let mut homepage = match captures.name("homepage") {
193                Some(homepage) => homepage.as_str().to_string(),
194                None => return None,
195            };
196            let homepage = get_stripped_data(&mut homepage);
197            return Some(homepage);
198        }
199
200        None
201    }
202
203    #[allow(dead_code)]
204    fn parse_source(&self, line: &str) -> Option<String> {
205        lazy_static! {
206            static ref SOURCE_REGEX1: Regex = Regex::new(r"/*.*source.*?>").unwrap();
207            static ref SOURCE_REGEX2: Regex = Regex::new(r",.*").unwrap();
208        };
209
210        if let Some(captures) = PARSE_SOURCE.captures(line) {
211            let source = match captures.name("source") {
212                Some(source) => source.as_str().to_string(),
213                None => return None,
214            };
215
216            let source = SOURCE_REGEX1.replace_all(&source, "");
217            let mut stripped_source = SOURCE_REGEX2.replace_all(&source, "").to_string();
218            let stripped_source = get_stripped_data(&mut stripped_source);
219            return Some(stripped_source);
220        }
221
222        None
223    }
224
225    fn parse_description(&self, path: impl AsRef<Path>, line: &str) -> Option<String> {
226        if let Some(captures) = PARSE_DESCRIPTION.captures(line) {
227            let mut description = match captures.name("description") {
228                Some(description) => description.as_str().to_string(),
229                None => return None,
230            };
231
232            let location = path.as_ref();
233            let location = location.to_string_lossy();
234            if location.ends_with(".gemspec") {
235                // FIXME: description can be in single or multi-lines
236                // There are many different ways to write description.
237                let description = get_stripped_data(&mut description);
238                return Some(description);
239            } else {
240                return get_description(path);
241            }
242        }
243
244        None
245    }
246
247    fn parse_email(&self, line: &str) -> Vec<String> {
248        if line.contains(".email") {
249            let stripped_emails = line.rfind('=').map(|index| {
250                let email = &line[index + 1..line.len() - 1];
251                email.to_string()
252            });
253
254            match stripped_emails {
255                Some(mut email) => {
256                    let email = get_stripped_data(&mut email);
257                    let email = email.trim();
258                    let emails = email
259                        .split(',')
260                        .map(|email| email.to_string())
261                        .collect::<Vec<String>>();
262                    return emails;
263                }
264                None => return vec![],
265            }
266        }
267
268        vec![]
269    }
270
271    #[allow(dead_code)]
272    fn parse_author(&self, line: &str) -> Vec<String> {
273        lazy_static! {
274            static ref AUTHOR_REGEX1: Regex = Regex::new(r"/*.*author.*?=").unwrap();
275            static ref AUTHOR_REGEX2: Regex = Regex::new(r"(\s*=>\s*)").unwrap();
276        }
277
278        if line.contains(".author") {
279            let mut stripped_authors = AUTHOR_REGEX1.replace_all(line, "").to_string();
280            let stripped_authors = get_stripped_data(&mut stripped_authors);
281            let stripped_authors = AUTHOR_REGEX2
282                .replace_all(&stripped_authors, "=>")
283                .to_string();
284            let stripped_authors = stripped_authors.trim();
285            let stripped_authors = stripped_authors
286                .split(',')
287                .map(|author| author.to_string())
288                .collect::<Vec<String>>();
289
290            return stripped_authors;
291        }
292
293        vec![]
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use std::path::PathBuf;
301
302    #[test]
303    fn test_bower_json() {
304        let filepath = PathBuf::from(concat!(
305            env!("CARGO_MANIFEST_DIR"),
306            "/testdata/cocoapods/podspec/SwiftLib.podspec"
307        ));
308
309        let p = Spec::new();
310        let result = p.parse_spec(filepath).unwrap();
311        println!("{:#?}", result);
312    }
313}