1 #![forbid(unsafe_code)]
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 extern crate ini;
7 extern crate regex;
8 extern crate semver;
9 
10 use crate::platform::ini_path;
11 use ini::Ini;
12 use regex::Regex;
13 use std::default::Default;
14 use std::error;
15 use std::fmt::{self, Display, Formatter};
16 use std::path::Path;
17 use std::process::{Command, Stdio};
18 use std::str::{self, FromStr};
19 
20 /// Details about the version of a Firefox build.
21 #[derive(Clone, Default)]
22 pub struct AppVersion {
23     /// Unique date-based id for a build
24     pub build_id: Option<String>,
25     /// Channel name
26     pub code_name: Option<String>,
27     /// Version number e.g. 55.0a1
28     pub version_string: Option<String>,
29     /// Url of the respoistory from which the build was made
30     pub source_repository: Option<String>,
31     /// Commit ID of the build
32     pub source_stamp: Option<String>,
33 }
34 
35 impl AppVersion {
new() -> AppVersion36     pub fn new() -> AppVersion {
37         Default::default()
38     }
39 
update_from_application_ini(&mut self, ini_file: &Ini)40     fn update_from_application_ini(&mut self, ini_file: &Ini) {
41         if let Some(section) = ini_file.section(Some("App")) {
42             if let Some(build_id) = section.get("BuildID") {
43                 self.build_id = Some(build_id.clone());
44             }
45             if let Some(code_name) = section.get("CodeName") {
46                 self.code_name = Some(code_name.clone());
47             }
48             if let Some(version) = section.get("Version") {
49                 self.version_string = Some(version.clone());
50             }
51             if let Some(source_repository) = section.get("SourceRepository") {
52                 self.source_repository = Some(source_repository.clone());
53             }
54             if let Some(source_stamp) = section.get("SourceStamp") {
55                 self.source_stamp = Some(source_stamp.clone());
56             }
57         }
58     }
59 
update_from_platform_ini(&mut self, ini_file: &Ini)60     fn update_from_platform_ini(&mut self, ini_file: &Ini) {
61         if let Some(section) = ini_file.section(Some("Build")) {
62             if let Some(build_id) = section.get("BuildID") {
63                 self.build_id = Some(build_id.clone());
64             }
65             if let Some(version) = section.get("Milestone") {
66                 self.version_string = Some(version.clone());
67             }
68             if let Some(source_repository) = section.get("SourceRepository") {
69                 self.source_repository = Some(source_repository.clone());
70             }
71             if let Some(source_stamp) = section.get("SourceStamp") {
72                 self.source_stamp = Some(source_stamp.clone());
73             }
74         }
75     }
76 
version(&self) -> Option<Version>77     pub fn version(&self) -> Option<Version> {
78         self.version_string
79             .as_ref()
80             .and_then(|x| Version::from_str(&*x).ok())
81     }
82 }
83 
84 #[derive(Default, Clone)]
85 /// Version number information
86 pub struct Version {
87     /// Major version number (e.g. 55 in 55.0)
88     pub major: u64,
89     /// Minor version number (e.g. 1 in 55.1)
90     pub minor: u64,
91     /// Patch version number (e.g. 2 in 55.1.2)
92     pub patch: u64,
93     /// Prerelase information (e.g. Some(("a", 1)) in 55.0a1)
94     pub pre: Option<(String, u64)>,
95     /// Is build an ESR build
96     pub esr: bool,
97 }
98 
99 impl Version {
to_semver(&self) -> semver::Version100     fn to_semver(&self) -> semver::Version {
101         // The way the semver crate handles prereleases isn't what we want here
102         // This should be fixed in the long term by implementing our own comparison
103         // operators, but for now just act as if prerelease metadata was missing,
104         // otherwise it is almost impossible to use this with nightly
105         semver::Version {
106             major: self.major,
107             minor: self.minor,
108             patch: self.patch,
109             pre: vec![],
110             build: vec![],
111         }
112     }
113 
matches(&self, version_req: &str) -> VersionResult<bool>114     pub fn matches(&self, version_req: &str) -> VersionResult<bool> {
115         let req = semver::VersionReq::parse(version_req)?;
116         Ok(req.matches(&self.to_semver()))
117     }
118 }
119 
120 impl FromStr for Version {
121     type Err = Error;
122 
from_str(version_string: &str) -> VersionResult<Version>123     fn from_str(version_string: &str) -> VersionResult<Version> {
124         let mut version: Version = Default::default();
125         let version_re = Regex::new(r"^(?P<major>[[:digit:]]+)\.(?P<minor>[[:digit:]]+)(?:\.(?P<patch>[[:digit:]]+))?(?:(?P<esr>esr)|(?P<pre0>[a-z]+)(?P<pre1>[[:digit:]]*))?$").unwrap();
126         if let Some(captures) = version_re.captures(version_string) {
127             match captures
128                 .name("major")
129                 .and_then(|x| u64::from_str(x.as_str()).ok())
130             {
131                 Some(x) => version.major = x,
132                 None => return Err(Error::VersionError("No major version number found".into())),
133             }
134             match captures
135                 .name("minor")
136                 .and_then(|x| u64::from_str(x.as_str()).ok())
137             {
138                 Some(x) => version.minor = x,
139                 None => return Err(Error::VersionError("No minor version number found".into())),
140             }
141             if let Some(x) = captures
142                 .name("patch")
143                 .and_then(|x| u64::from_str(x.as_str()).ok())
144             {
145                 version.patch = x
146             }
147             if captures.name("esr").is_some() {
148                 version.esr = true;
149             }
150             if let Some(pre_0) = captures.name("pre0").map(|x| x.as_str().to_string()) {
151                 if captures.name("pre1").is_some() {
152                     if let Some(pre_1) = captures
153                         .name("pre1")
154                         .and_then(|x| u64::from_str(x.as_str()).ok())
155                     {
156                         version.pre = Some((pre_0, pre_1))
157                     } else {
158                         return Err(Error::VersionError(
159                             "Failed to convert prelease number to u64".into(),
160                         ));
161                     }
162                 } else {
163                     return Err(Error::VersionError(
164                         "Failed to convert prelease number to u64".into(),
165                     ));
166                 }
167             }
168         } else {
169             return Err(Error::VersionError(format!(
170                 "Failed to parse {} as version string",
171                 version_string
172             )));
173         }
174         Ok(version)
175     }
176 }
177 
178 impl Display for Version {
fmt(&self, f: &mut Formatter) -> fmt::Result179     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
180         match self.patch {
181             0 => write!(f, "{}.{}", self.major, self.minor)?,
182             _ => write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?,
183         }
184         if self.esr {
185             write!(f, "esr")?;
186         }
187         if let Some(ref pre) = self.pre {
188             write!(f, "{}{}", pre.0, pre.1)?;
189         };
190         Ok(())
191     }
192 }
193 
194 /// Determine the version of Firefox using associated metadata files.
195 ///
196 /// Given the path to a Firefox binary, read the associated application.ini
197 /// and platform.ini files to extract information about the version of Firefox
198 /// at that path.
firefox_version(binary: &Path) -> VersionResult<AppVersion>199 pub fn firefox_version(binary: &Path) -> VersionResult<AppVersion> {
200     let mut version = AppVersion::new();
201     let mut updated = false;
202 
203     if let Some(dir) = ini_path(binary) {
204         let mut application_ini = dir.clone();
205         application_ini.push("application.ini");
206 
207         if Path::exists(&application_ini) {
208             let ini_file = Ini::load_from_file(application_ini).ok();
209             if let Some(ini) = ini_file {
210                 updated = true;
211                 version.update_from_application_ini(&ini);
212             }
213         }
214 
215         let mut platform_ini = dir;
216         platform_ini.push("platform.ini");
217 
218         if Path::exists(&platform_ini) {
219             let ini_file = Ini::load_from_file(platform_ini).ok();
220             if let Some(ini) = ini_file {
221                 updated = true;
222                 version.update_from_platform_ini(&ini);
223             }
224         }
225 
226         if !updated {
227             return Err(Error::MetadataError(
228                 "Neither platform.ini nor application.ini found".into(),
229             ));
230         }
231     } else {
232         return Err(Error::MetadataError("Invalid binary path".into()));
233     }
234     Ok(version)
235 }
236 
237 /// Determine the version of Firefox by executing the binary.
238 ///
239 /// Given the path to a Firefox binary, run firefox --version and extract the
240 /// version string from the output
firefox_binary_version(binary: &Path) -> VersionResult<Version>241 pub fn firefox_binary_version(binary: &Path) -> VersionResult<Version> {
242     let output = Command::new(binary)
243         .args(&["--version"])
244         .stdout(Stdio::piped())
245         .spawn()
246         .and_then(|child| child.wait_with_output())
247         .ok();
248 
249     if let Some(x) = output {
250         let output_str = str::from_utf8(&*x.stdout)
251             .map_err(|_| Error::VersionError("Couldn't parse version output as UTF8".into()))?;
252         parse_binary_version(&output_str)
253     } else {
254         Err(Error::VersionError("Running binary failed".into()))
255     }
256 }
257 
parse_binary_version(version_str: &str) -> VersionResult<Version>258 fn parse_binary_version(version_str: &str) -> VersionResult<Version> {
259     let version_regexp = Regex::new(r#"Mozilla Firefox[[:space:]]+(?P<version>.+)"#)
260         .expect("Error parsing version regexp");
261 
262     let version_match = version_regexp
263         .captures(version_str)
264         .and_then(|captures| captures.name("version"))
265         .ok_or_else(|| Error::VersionError("--version output didn't match expectations".into()))?;
266 
267     Version::from_str(version_match.as_str())
268 }
269 
270 #[derive(Clone, Debug)]
271 pub enum Error {
272     /// Error parsing a version string
273     VersionError(String),
274     /// Error reading application metadata
275     MetadataError(String),
276     /// Error processing a string as a semver comparator
277     SemVerError(semver::ReqParseError),
278 }
279 
280 impl Display for Error {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result281     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282         match *self {
283             Error::VersionError(ref x) => {
284                 "VersionError: ".fmt(f)?;
285                 x.fmt(f)
286             }
287             Error::MetadataError(ref x) => {
288                 "MetadataError: ".fmt(f)?;
289                 x.fmt(f)
290             }
291             Error::SemVerError(ref e) => {
292                 "SemVerError: ".fmt(f)?;
293                 e.fmt(f)
294             }
295         }
296     }
297 }
298 
299 impl From<semver::ReqParseError> for Error {
from(err: semver::ReqParseError) -> Error300     fn from(err: semver::ReqParseError) -> Error {
301         Error::SemVerError(err)
302     }
303 }
304 
305 impl error::Error for Error {
cause(&self) -> Option<&dyn error::Error>306     fn cause(&self) -> Option<&dyn error::Error> {
307         match *self {
308             Error::SemVerError(ref e) => Some(e),
309             Error::VersionError(_) | Error::MetadataError(_) => None,
310         }
311     }
312 }
313 
314 pub type VersionResult<T> = Result<T, Error>;
315 
316 #[cfg(target_os = "macos")]
317 mod platform {
318     use std::path::{Path, PathBuf};
319 
ini_path(binary: &Path) -> Option<PathBuf>320     pub fn ini_path(binary: &Path) -> Option<PathBuf> {
321         binary
322             .canonicalize()
323             .ok()
324             .as_ref()
325             .and_then(|dir| dir.parent())
326             .and_then(|dir| dir.parent())
327             .map(|dir| dir.join("Resources"))
328     }
329 }
330 
331 #[cfg(not(target_os = "macos"))]
332 mod platform {
333     use std::path::{Path, PathBuf};
334 
ini_path(binary: &Path) -> Option<PathBuf>335     pub fn ini_path(binary: &Path) -> Option<PathBuf> {
336         binary
337             .canonicalize()
338             .ok()
339             .as_ref()
340             .and_then(|dir| dir.parent())
341             .map(|dir| dir.to_path_buf())
342     }
343 }
344 
345 #[cfg(test)]
346 mod test {
347     use super::{parse_binary_version, Version};
348     use std::str::FromStr;
349 
parse_version(input: &str) -> String350     fn parse_version(input: &str) -> String {
351         Version::from_str(input).unwrap().to_string()
352     }
353 
compare(version: &str, comparison: &str) -> bool354     fn compare(version: &str, comparison: &str) -> bool {
355         let v = Version::from_str(version).unwrap();
356         v.matches(comparison).unwrap()
357     }
358 
359     #[test]
test_parser()360     fn test_parser() {
361         assert!(parse_version("50.0a1") == "50.0a1");
362         assert!(parse_version("50.0.1a1") == "50.0.1a1");
363         assert!(parse_version("50.0.0") == "50.0");
364         assert!(parse_version("78.0.11esr") == "78.0.11esr");
365     }
366 
367     #[test]
test_matches()368     fn test_matches() {
369         assert!(compare("50.0", "=50"));
370         assert!(compare("50.1", "=50"));
371         assert!(compare("50.1", "=50.1"));
372         assert!(compare("50.1.1", "=50.1"));
373         assert!(compare("50.0.0", "=50.0.0"));
374         assert!(compare("51.0.0", ">50"));
375         assert!(compare("49.0", "<50"));
376         assert!(compare("50.0", "<50.1"));
377         assert!(compare("50.0.0", "<50.0.1"));
378         assert!(!compare("50.1.0", ">50"));
379         assert!(!compare("50.1.0", "<50"));
380         assert!(compare("50.1.0", ">=50,<51"));
381         assert!(compare("50.0a1", ">49.0"));
382         assert!(compare("50.0a2", "=50"));
383         assert!(compare("78.1.0esr", ">=78"));
384         assert!(compare("78.1.0esr", "<79"));
385         assert!(compare("78.1.11esr", "<79"));
386         // This is the weird one
387         assert!(!compare("50.0a2", ">50.0"));
388     }
389 
390     #[test]
test_binary_parser()391     fn test_binary_parser() {
392         assert!(
393             parse_binary_version("Mozilla Firefox 50.0a1")
394                 .unwrap()
395                 .to_string()
396                 == "50.0a1"
397         );
398         assert!(
399             parse_binary_version("Mozilla Firefox 50.0.1a1")
400                 .unwrap()
401                 .to_string()
402                 == "50.0.1a1"
403         );
404         assert!(
405             parse_binary_version("Mozilla Firefox 50.0.0")
406                 .unwrap()
407                 .to_string()
408                 == "50.0"
409         );
410         assert!(
411             parse_binary_version("Mozilla Firefox 78.0.11esr")
412                 .unwrap()
413                 .to_string()
414                 == "78.0.11esr"
415         );
416         assert!(
417             parse_binary_version("Mozilla Firefox 78.0esr")
418                 .unwrap()
419                 .to_string()
420                 == "78.0esr"
421         );
422         assert!(
423             parse_binary_version("Mozilla Firefox 78.0")
424                 .unwrap()
425                 .to_string()
426                 == "78.0"
427         );
428     }
429 }
430