lib.rs (13433B)
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::fmt::{self, Display, Formatter}; 15 use std::path::Path; 16 use std::process::{Command, Stdio}; 17 use std::str::{self, FromStr}; 18 use thiserror::Error; 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 { 36 pub fn new() -> AppVersion { 37 Default::default() 38 } 39 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 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 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 { 100 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: semver::Prerelease::EMPTY, 110 build: semver::BuildMetadata::EMPTY, 111 } 112 } 113 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 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 { 179 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. 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 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 258 fn parse_binary_version(version_str: &str) -> VersionResult<Version> { 259 let version_regexp = 260 Regex::new(r#"Firefox[[:space:]]+(?P<version>.+)"#).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, Error)] 271 pub enum Error { 272 /// Error parsing a version string 273 #[error("VersionError: {0}")] 274 VersionError(String), 275 /// Error reading application metadata 276 #[error("MetadataError: {0}")] 277 MetadataError(String), 278 /// Error processing a string as a semver comparator 279 #[error("SemVerError: {0}")] 280 SemVerError(String), 281 } 282 283 impl From<semver::Error> for Error { 284 fn from(err: semver::Error) -> Error { 285 Error::SemVerError(err.to_string()) 286 } 287 } 288 289 pub type VersionResult<T> = Result<T, Error>; 290 291 #[cfg(target_os = "macos")] 292 mod platform { 293 use std::path::{Path, PathBuf}; 294 295 pub fn ini_path(binary: &Path) -> Option<PathBuf> { 296 binary 297 .canonicalize() 298 .ok() 299 .as_ref() 300 .and_then(|dir| dir.parent()) 301 .and_then(|dir| dir.parent()) 302 .map(|dir| dir.join("Resources")) 303 } 304 } 305 306 #[cfg(not(target_os = "macos"))] 307 mod platform { 308 use std::path::{Path, PathBuf}; 309 310 pub fn ini_path(binary: &Path) -> Option<PathBuf> { 311 binary 312 .canonicalize() 313 .ok() 314 .as_ref() 315 .and_then(|dir| dir.parent()) 316 .map(|dir| dir.to_path_buf()) 317 } 318 } 319 320 #[cfg(test)] 321 mod test { 322 use super::{parse_binary_version, Version}; 323 use std::str::FromStr; 324 325 fn parse_version(input: &str) -> String { 326 Version::from_str(input).unwrap().to_string() 327 } 328 329 fn compare(version: &str, comparison: &str) -> bool { 330 let v = Version::from_str(version).unwrap(); 331 v.matches(comparison).unwrap() 332 } 333 334 #[test] 335 fn test_parser() { 336 assert!(parse_version("50.0a1") == "50.0a1"); 337 assert!(parse_version("50.0.1a1") == "50.0.1a1"); 338 assert!(parse_version("50.0.0") == "50.0"); 339 assert!(parse_version("78.0.11esr") == "78.0.11esr"); 340 } 341 342 #[test] 343 fn test_matches() { 344 assert!(compare("50.0", "=50")); 345 assert!(compare("50.1", "=50")); 346 assert!(compare("50.1", "=50.1")); 347 assert!(compare("50.1.1", "=50.1")); 348 assert!(compare("50.0.0", "=50.0.0")); 349 assert!(compare("51.0.0", ">50")); 350 assert!(compare("49.0", "<50")); 351 assert!(compare("50.0", "<50.1")); 352 assert!(compare("50.0.0", "<50.0.1")); 353 assert!(!compare("50.1.0", ">50")); 354 assert!(!compare("50.1.0", "<50")); 355 assert!(compare("50.1.0", ">=50,<51")); 356 assert!(compare("50.0a1", ">49.0")); 357 assert!(compare("50.0a2", "=50")); 358 assert!(compare("78.1.0esr", ">=78")); 359 assert!(compare("78.1.0esr", "<79")); 360 assert!(compare("78.1.11esr", "<79")); 361 // This is the weird one 362 assert!(!compare("50.0a2", ">50.0")); 363 } 364 365 #[test] 366 fn test_binary_parser() { 367 assert!( 368 parse_binary_version("Mozilla Firefox 50.0a1") 369 .unwrap() 370 .to_string() 371 == "50.0a1" 372 ); 373 assert!( 374 parse_binary_version("Mozilla Firefox 50.0.1a1") 375 .unwrap() 376 .to_string() 377 == "50.0.1a1" 378 ); 379 assert!( 380 parse_binary_version("Mozilla Firefox 50.0.0") 381 .unwrap() 382 .to_string() 383 == "50.0" 384 ); 385 assert!( 386 parse_binary_version("Mozilla Firefox 78.0.11esr") 387 .unwrap() 388 .to_string() 389 == "78.0.11esr" 390 ); 391 assert!( 392 parse_binary_version("Mozilla Firefox 78.0esr") 393 .unwrap() 394 .to_string() 395 == "78.0esr" 396 ); 397 assert!( 398 parse_binary_version("Mozilla Firefox 78.0") 399 .unwrap() 400 .to_string() 401 == "78.0" 402 ); 403 assert!( 404 parse_binary_version("Foo Firefox 113.0.2-1") 405 .unwrap() 406 .to_string() 407 == "113.0.2-1" 408 ); 409 } 410 }