]> git.lizzy.rs Git - rust.git/blob - src/config/license.rs
Release 1.4.0
[rust.git] / src / config / license.rs
1 use std::fmt;
2 use std::fs::File;
3 use std::io;
4 use std::io::Read;
5
6 use regex;
7 use regex::Regex;
8
9 #[derive(Debug)]
10 pub(crate) enum LicenseError {
11     IO(io::Error),
12     Regex(regex::Error),
13     Parse(String),
14 }
15
16 impl fmt::Display for LicenseError {
17     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18         match *self {
19             LicenseError::IO(ref err) => err.fmt(f),
20             LicenseError::Regex(ref err) => err.fmt(f),
21             LicenseError::Parse(ref err) => write!(f, "parsing failed, {}", err),
22         }
23     }
24 }
25
26 impl From<io::Error> for LicenseError {
27     fn from(err: io::Error) -> LicenseError {
28         LicenseError::IO(err)
29     }
30 }
31
32 impl From<regex::Error> for LicenseError {
33     fn from(err: regex::Error) -> LicenseError {
34         LicenseError::Regex(err)
35     }
36 }
37
38 // the template is parsed using a state machine
39 enum ParsingState {
40     Lit,
41     LitEsc,
42     // the u32 keeps track of brace nesting
43     Re(u32),
44     ReEsc(u32),
45     Abort(String),
46 }
47
48 use self::ParsingState::*;
49
50 pub(crate) struct TemplateParser {
51     parsed: String,
52     buffer: String,
53     state: ParsingState,
54     linum: u32,
55     open_brace_line: u32,
56 }
57
58 impl TemplateParser {
59     fn new() -> Self {
60         Self {
61             parsed: "^".to_owned(),
62             buffer: String::new(),
63             state: Lit,
64             linum: 1,
65             // keeps track of last line on which a regex placeholder was started
66             open_brace_line: 0,
67         }
68     }
69
70     /// Converts a license template into a string which can be turned into a regex.
71     ///
72     /// The license template could use regex syntax directly, but that would require a lot of manual
73     /// escaping, which is inconvenient. It is therefore literal by default, with optional regex
74     /// subparts delimited by `{` and `}`. Additionally:
75     ///
76     /// - to insert literal `{`, `}` or `\`, escape it with `\`
77     /// - an empty regex placeholder (`{}`) is shorthand for `{.*?}`
78     ///
79     /// This function parses this input format and builds a properly escaped *string* representation
80     /// of the equivalent regular expression. It **does not** however guarantee that the returned
81     /// string is a syntactically valid regular expression.
82     ///
83     /// # Examples
84     ///
85     /// ```text
86     /// assert_eq!(
87     ///     TemplateParser::parse(
88     ///         r"
89     /// // Copyright {\d+} The \} Rust \\ Project \{ Developers. See the {([A-Z]+)}
90     /// // file at the top-level directory of this distribution and at
91     /// // {}.
92     /// //
93     /// // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
94     /// // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
95     /// // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
96     /// // option. This file may not be copied, modified, or distributed
97     /// // except according to those terms.
98     /// "
99     ///     ).unwrap(),
100     ///     r"^
101     /// // Copyright \d+ The \} Rust \\ Project \{ Developers\. See the ([A-Z]+)
102     /// // file at the top\-level directory of this distribution and at
103     /// // .*?\.
104     /// //
105     /// // Licensed under the Apache License, Version 2\.0 <LICENSE\-APACHE or
106     /// // http://www\.apache\.org/licenses/LICENSE\-2\.0> or the MIT license
107     /// // <LICENSE\-MIT or http://opensource\.org/licenses/MIT>, at your
108     /// // option\. This file may not be copied, modified, or distributed
109     /// // except according to those terms\.
110     /// "
111     /// );
112     /// ```
113     pub(crate) fn parse(template: &str) -> Result<String, LicenseError> {
114         let mut parser = Self::new();
115         for chr in template.chars() {
116             if chr == '\n' {
117                 parser.linum += 1;
118             }
119             parser.state = match parser.state {
120                 Lit => parser.trans_from_lit(chr),
121                 LitEsc => parser.trans_from_litesc(chr),
122                 Re(brace_nesting) => parser.trans_from_re(chr, brace_nesting),
123                 ReEsc(brace_nesting) => parser.trans_from_reesc(chr, brace_nesting),
124                 Abort(msg) => return Err(LicenseError::Parse(msg)),
125             };
126         }
127         // check if we've ended parsing in a valid state
128         match parser.state {
129             Abort(msg) => return Err(LicenseError::Parse(msg)),
130             Re(_) | ReEsc(_) => {
131                 return Err(LicenseError::Parse(format!(
132                     "escape or balance opening brace on l. {}",
133                     parser.open_brace_line
134                 )));
135             }
136             LitEsc => {
137                 return Err(LicenseError::Parse(format!(
138                     "incomplete escape sequence on l. {}",
139                     parser.linum
140                 )));
141             }
142             _ => (),
143         }
144         parser.parsed.push_str(&regex::escape(&parser.buffer));
145
146         Ok(parser.parsed)
147     }
148
149     fn trans_from_lit(&mut self, chr: char) -> ParsingState {
150         match chr {
151             '{' => {
152                 self.parsed.push_str(&regex::escape(&self.buffer));
153                 self.buffer.clear();
154                 self.open_brace_line = self.linum;
155                 Re(1)
156             }
157             '}' => Abort(format!(
158                 "escape or balance closing brace on l. {}",
159                 self.linum
160             )),
161             '\\' => LitEsc,
162             _ => {
163                 self.buffer.push(chr);
164                 Lit
165             }
166         }
167     }
168
169     fn trans_from_litesc(&mut self, chr: char) -> ParsingState {
170         self.buffer.push(chr);
171         Lit
172     }
173
174     fn trans_from_re(&mut self, chr: char, brace_nesting: u32) -> ParsingState {
175         match chr {
176             '{' => {
177                 self.buffer.push(chr);
178                 Re(brace_nesting + 1)
179             }
180             '}' => {
181                 match brace_nesting {
182                     1 => {
183                         // default regex for empty placeholder {}
184                         if self.buffer.is_empty() {
185                             self.parsed.push_str(".*?");
186                         } else {
187                             self.parsed.push_str(&self.buffer);
188                         }
189                         self.buffer.clear();
190                         Lit
191                     }
192                     _ => {
193                         self.buffer.push(chr);
194                         Re(brace_nesting - 1)
195                     }
196                 }
197             }
198             '\\' => {
199                 self.buffer.push(chr);
200                 ReEsc(brace_nesting)
201             }
202             _ => {
203                 self.buffer.push(chr);
204                 Re(brace_nesting)
205             }
206         }
207     }
208
209     fn trans_from_reesc(&mut self, chr: char, brace_nesting: u32) -> ParsingState {
210         self.buffer.push(chr);
211         Re(brace_nesting)
212     }
213 }
214
215 pub(crate) fn load_and_compile_template(path: &str) -> Result<Regex, LicenseError> {
216     let mut lt_file = File::open(&path)?;
217     let mut lt_str = String::new();
218     lt_file.read_to_string(&mut lt_str)?;
219     let lt_parsed = TemplateParser::parse(&lt_str)?;
220     Ok(Regex::new(&lt_parsed)?)
221 }
222
223 #[cfg(test)]
224 mod test {
225     use super::TemplateParser;
226
227     #[test]
228     fn test_parse_license_template() {
229         assert_eq!(
230             TemplateParser::parse("literal (.*)").unwrap(),
231             r"^literal \(\.\*\)"
232         );
233         assert_eq!(
234             TemplateParser::parse(r"escaping \}").unwrap(),
235             r"^escaping \}"
236         );
237         assert!(TemplateParser::parse("unbalanced } without escape").is_err());
238         assert_eq!(
239             TemplateParser::parse(r"{\d+} place{-?}holder{s?}").unwrap(),
240             r"^\d+ place-?holders?"
241         );
242         assert_eq!(TemplateParser::parse("default {}").unwrap(), "^default .*?");
243         assert_eq!(
244             TemplateParser::parse(r"unbalanced nested braces {\{{3}}").unwrap(),
245             r"^unbalanced nested braces \{{3}"
246         );
247         assert_eq!(
248             &TemplateParser::parse("parsing error }")
249                 .unwrap_err()
250                 .to_string(),
251             "parsing failed, escape or balance closing brace on l. 1"
252         );
253         assert_eq!(
254             &TemplateParser::parse("parsing error {\nsecond line")
255                 .unwrap_err()
256                 .to_string(),
257             "parsing failed, escape or balance opening brace on l. 1"
258         );
259         assert_eq!(
260             &TemplateParser::parse(r"parsing error \")
261                 .unwrap_err()
262                 .to_string(),
263             "parsing failed, incomplete escape sequence on l. 1"
264         );
265     }
266 }