Bug 1430152 - Move Rust port of mozversion to central. r?ahal draft
authorAndreas Tolfsen <ato@sny.no>
Fri, 12 Jan 2018 16:08:07 +0000
changeset 721583 7e2e0484a193cc4823e0e7f73174469d93ce2917
parent 721495 4e429d313fd2e0f9202271ee8f3fb798817ec3e7
child 721584 4047a16519ad6e76db2fa6e0717f3b20ee85ebc0
push id95877
push userbmo:ato@sny.no
push dateWed, 17 Jan 2018 13:53:34 +0000
reviewersahal
bugs1430152
milestone59.0a1
Bug 1430152 - Move Rust port of mozversion to central. r?ahal This moves the Rust crate mozversion into central from GitHub. The old repository will be graveyarded: https://github.com/jgraham/mozversion The git history is not considered important, hence this does not overlay that onto central like we did for testing/geckodriver and testing/webdriver. MozReview-Commit-ID: HeBggGmGsg6
.hgignore
testing/mozbase/rust/mozversion/.gitignore
testing/mozbase/rust/mozversion/Cargo.toml
testing/mozbase/rust/mozversion/src/lib.rs
--- a/.hgignore
+++ b/.hgignore
@@ -106,16 +106,20 @@ GPATH
 # Android Gradle artifacts.
 ^mobile/android/gradle/.gradle
 
 # XCode project cruft
 ^[^/]*\.xcodeproj/
 ^embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/project.xcworkspace/xcuserdata
 ^embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/xcuserdata
 
+# Rust port of mozbase are Rust libraries
+^testing/mozbase/rust/.*/target
+^testing/mozbase/rust/.*/Cargo.lock
+
 # Ignore mozharness execution files
 ^testing/mozharness/.tox/
 ^testing/mozharness/build/
 ^testing/mozharness/logs/
 ^testing/mozharness/.coverage
 ^testing/mozharness/nosetests.xml
 
 # Ignore tox generated dir
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/rust/mozversion/.gitignore
@@ -0,0 +1,2 @@
+target
+Cargo.lock
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/rust/mozversion/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "mozversion"
+version = "0.1.2"
+authors = ["James Graham <james@hoppipolla.co.uk>"]
+description = "Utility for accessing Firefox version metadata"
+keywords = ["mozilla", "firefox"]
+repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/rust/mozversion"
+license = "MPL-2.0"
+
+[dependencies]
+regex = "0.2"
+rust-ini = "0.10.0"
+semver = "0.6"
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/rust/mozversion/src/lib.rs
@@ -0,0 +1,319 @@
+extern crate ini;
+extern crate regex;
+extern crate semver;
+
+use ini::Ini;
+use regex::Regex;
+use platform::ini_path;
+use std::default::Default;
+use std::error;
+use std::fmt::{self, Display, Formatter};
+use std::path::Path;
+use std::str::FromStr;
+
+/// Details about the version of a Firefox build.
+#[derive(Clone, Default)]
+pub struct AppVersion {
+    /// Unique date-based id for a build
+    pub build_id: Option<String>,
+    /// Channel name
+    pub code_name: Option<String>,
+    /// Version number e.g. 55.0a1
+    pub version_string: Option<String>,
+    /// Url of the respoistory from which the build was made
+    pub source_repository: Option<String>,
+    /// Commit ID of the build
+    pub source_stamp: Option<String>,
+}
+
+impl AppVersion {
+    pub fn new() -> AppVersion {
+        Default::default()
+    }
+
+    fn update_from_application_ini(&mut self, ini_file: &Ini) {
+        if let Some(section) = ini_file.section(Some("App")) {
+            if let Some(build_id) = section.get("BuildID") {
+                self.build_id = Some(build_id.clone());
+            }
+            if let Some(code_name) = section.get("CodeName") {
+                self.code_name = Some(code_name.clone());
+            }
+            if let Some(version) = section.get("Version") {
+                self.version_string = Some(version.clone());
+            }
+            if let Some(source_repository) = section.get("SourceRepository") {
+                self.source_repository = Some(source_repository.clone());
+            }
+            if let Some(source_stamp) = section.get("SourceStamp") {
+                self.source_stamp = Some(source_stamp.clone());
+            }
+        }
+    }
+
+    fn update_from_platform_ini(&mut self, ini_file: &Ini) {
+        if let Some(section) = ini_file.section(Some("Build")) {
+            if let Some(build_id) = section.get("BuildID") {
+                self.build_id = Some(build_id.clone());
+            }
+            if let Some(version) = section.get("Milestone") {
+                self.version_string = Some(version.clone());
+            }
+            if let Some(source_repository) = section.get("SourceRepository") {
+                self.source_repository = Some(source_repository.clone());
+            }
+            if let Some(source_stamp) = section.get("SourceStamp") {
+                self.source_stamp = Some(source_stamp.clone());
+            }
+        }
+    }
+
+    pub fn version(&self) -> Option<Version> {
+        self.version_string
+            .as_ref()
+            .and_then(|x| Version::from_str(&*x).ok())
+    }
+}
+
+#[derive(Default, Clone)]
+/// Version number information
+pub struct Version {
+    /// Major version number (e.g. 55 in 55.0)
+    pub major: u64,
+    /// Minor version number (e.g. 1 in 55.1)
+    pub minor: u64,
+    /// Patch version number (e.g. 2 in 55.1.2)
+    pub patch: u64,
+    /// Prerelase information (e.g. Some(("a", 1)) in 55.0a1)
+    pub pre: Option<(String, u64)>,
+}
+
+impl Version {
+    pub fn from_str(version_string: &str) -> Result<Version, Error> {
+        let mut version: Version = Default::default();
+        let version_re = Regex::new(r"^(?P<major>\d+)\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?(?:(?P<pre0>[a-z]+)(?P<pre1>\d*))?$").unwrap();
+        if let Some(captures) = version_re.captures(version_string) {
+            match captures.name("major")
+                .and_then(|x| u64::from_str(x.as_str()).ok()) {
+                    Some(x) => version.major = x,
+                    None => return Err(Error::VersionError("No major version number found".into()))
+                }
+            match captures.name("minor")
+                .and_then(|x| u64::from_str(x.as_str()).ok()) {
+                    Some(x) => version.minor = x,
+                    None => return Err(Error::VersionError("No minor version number found".into()))
+                }
+            match captures.name("patch")
+                .and_then(|x| u64::from_str(x.as_str()).ok()) {
+                    Some(x) => version.patch = x,
+                    None => {}
+                }
+            if let Some(pre_0) = captures.name("pre0").map(|x| x.as_str().to_string()) {
+                if captures.name("pre1").is_some() {
+                    if let Some(pre_1) = captures.name("pre1")
+                        .and_then(|x| u64::from_str(x.as_str()).ok()) {
+                            version.pre = Some((pre_0, pre_1))
+                        } else {
+                            return Err(Error::VersionError("Failed to convert prelease number to u64".into()));
+                        }
+                } else {
+                    return Err(Error::VersionError("Failed to convert prelease number to u64".into()));
+                }
+            }
+        } else {
+            return Err(Error::VersionError("Failed to parse input as version string".into()))
+        }
+        Ok(version)
+    }
+
+    fn to_semver(&self) -> semver::Version {
+        // The way the semver crate handles prereleases isn't what we want here
+        // This should be fixed in the long term by implementing our own comparison
+        // operators, but for now just act as if prerelease metadata was missing, otherwise
+        // it is almost impossible to use this with nightly
+        semver::Version {
+            major: self.major,
+            minor: self.minor,
+            patch: self.patch,
+            pre: vec![],
+            build: vec![],
+        }
+    }
+
+    pub fn matches(&self, version_req: &str) -> Result<bool, Error> {
+        let req = try!(semver::VersionReq::parse(version_req));
+        Ok(req.matches(&self.to_semver()))
+    }
+}
+
+impl Display for Version {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        match self.patch {
+            0 => try!(write!(f, "{}.{}", self.major, self.minor)),
+            _ => try!(write!(f, "{}.{}.{}", self.major, self.minor, self.patch))
+        }
+        if let Some(ref pre) = self.pre {
+            try!(write!(f, "{}{}", pre.0, pre.1));
+        };
+        Ok(())
+    }
+}
+
+/// Determine the version of Firefox given the path to a binary.
+///
+/// Given the path to a Firefox binary, read the associated application.ini
+/// and platform.ini files to extract information about the version of Firefox
+/// at that path.
+pub fn firefox_version(binary: &Path) -> Result<AppVersion, Error>  {
+    let mut version = AppVersion::new();
+    let mut updated = false;
+
+    if let Some(dir) = ini_path(binary) {
+        let mut application_ini = dir.clone();
+        application_ini.push("application.ini");
+
+        if Path::exists(&application_ini) {
+            let ini_file = Ini::load_from_file(application_ini).ok();
+            if let Some(ini) = ini_file {
+                updated = true;
+                version.update_from_application_ini(&ini);
+            }
+        }
+
+        let mut platform_ini = dir.clone();
+        platform_ini.push("platform.ini");
+
+        if Path::exists(&platform_ini) {
+            let ini_file = Ini::load_from_file(platform_ini).ok();
+            if let Some(ini) = ini_file {
+                updated = true;
+                version.update_from_platform_ini(&ini);
+            }
+        }
+
+        if !updated {
+            return Err(Error::MetadataError("Neither platform.ini nor application.ini found".into()))
+        }
+    } else {
+        return Err(Error::MetadataError("Invalid binary path".into()))
+    }
+    Ok(version)
+}
+
+#[derive(Debug)]
+pub enum Error {
+    /// Error parsing a version string
+    VersionError(String),
+    /// Error reading application metadata
+    MetadataError(String),
+    /// Error processing a string as a semver comparator
+    SemVerError(semver::ReqParseError)
+}
+
+impl Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            &Error::VersionError(ref x) => {
+                try!("VersionError: ".fmt(f));
+                x.fmt(f)
+            },
+            &Error::MetadataError(ref x) => {
+                try!("MetadataError: ".fmt(f));
+                x.fmt(f)
+            },
+            &Error::SemVerError(ref e) => {
+                try!("SemVerError: ".fmt(f));
+                e.fmt(f)
+            }
+        }
+    }
+}
+
+impl From<semver::ReqParseError> for Error {
+    fn from(err: semver::ReqParseError) -> Error {
+        Error::SemVerError(err)
+    }
+}
+
+impl error::Error for Error {
+    fn description(&self) -> &str {
+        match self {
+            &Error::VersionError(ref x) => &*x,
+            &Error::MetadataError(ref x) => &*x,
+            &Error::SemVerError(ref e) => e.description(),
+        }
+    }
+
+    fn cause(&self) -> Option<&error::Error> {
+        match self {
+            &Error::SemVerError(ref e) => Some(e),
+            _ => None,
+        }
+    }
+}
+
+#[cfg(target_os = "macos")]
+mod platform {
+    use std::path::{Path, PathBuf};
+
+    pub fn ini_path(binary: &Path) -> Option<PathBuf> {
+        binary.canonicalize().ok()
+            .as_ref()
+            .and_then(|dir| dir.parent())
+            .and_then(|dir| dir.parent())
+            .map(|dir| dir.join("Resources"))
+    }
+}
+
+#[cfg(not(target_os = "macos"))]
+mod platform {
+    use std::path::{Path, PathBuf};
+
+    pub fn ini_path(binary: &Path) -> Option<PathBuf> {
+        binary.canonicalize().ok()
+            .as_ref()
+            .and_then(|dir| dir.parent())
+            .map(|dir| dir.to_path_buf())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::{Version};
+
+    fn parse_version(input: &str) -> String {
+        Version::from_str(input).unwrap().to_string()
+    }
+
+    fn compare(version: &str, comparison: &str) -> bool {
+        let v = Version::from_str(version).unwrap();
+        v.matches(comparison).unwrap()
+    }
+
+    #[test]
+    fn test_parser() {
+        assert!(parse_version("50.0a1") == "50.0a1");
+        assert!(parse_version("50.0.1a1") == "50.0.1a1");
+        assert!(parse_version("50.0.0") == "50.0");
+    }
+
+    #[test]
+    fn test_matches() {
+        assert!(compare("50.0", "=50"));
+        assert!(compare("50.1", "=50"));
+        assert!(compare("50.1", "=50.1"));
+        assert!(compare("50.1.1", "=50.1"));
+        assert!(compare("50.0.0", "=50.0.0"));
+        assert!(compare("51.0.0", ">50"));
+        assert!(compare("49.0", "<50"));
+        assert!(compare("50.0", "<50.1"));
+        assert!(compare("50.0.0", "<50.0.1"));
+        assert!(!compare("50.1.0", ">50"));
+        assert!(!compare("50.1.0", "<50"));
+        assert!(compare("50.1.0", ">=50,<51"));
+        assert!(compare("50.0a1", ">49.0"));
+        assert!(compare("50.0a2", "=50"));
+        //This is the weird one
+        assert!(!compare("50.0a2", ">50.0"));
+    }
+}