tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }