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