| // Copyright 2018 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! This module is used to get Linux System Base (LSB) release information on Chrome OS systems, as |
| //! usually located in `/etc/lsb-release`. |
| |
| use std::collections::BTreeMap; |
| use std::error::Error; |
| use std::fmt::{self, Display, Formatter}; |
| use std::fs::File; |
| use std::io::{self, Read}; |
| use std::path::Path; |
| use std::result::Result; |
| use std::str::FromStr; |
| |
| const CHROMEOS_RELEASE_TRACK_KEY: &str = "CHROMEOS_RELEASE_TRACK"; |
| const LSB_RELEASE_FILE_PATH: &str = "/etc/lsb-release"; |
| |
| /// An error generated while gathering release information. |
| #[derive(Debug)] |
| pub enum LsbReleaseError { |
| FileOpenError(io::Error), |
| FileReadError(io::Error), |
| ParseError { row: usize, message: &'static str }, |
| } |
| |
| impl Display for LsbReleaseError { |
| fn fmt(&self, f: &mut Formatter) -> fmt::Result { |
| use self::LsbReleaseError::*; |
| |
| match self { |
| FileOpenError(e) => write!(f, "error opening release from file: {}", e), |
| FileReadError(e) => write!(f, "error reading release from file: {}", e), |
| ParseError { row, message } => write!(f, "parse error at row {}: {}", row, message), |
| } |
| } |
| } |
| |
| impl Error for LsbReleaseError {} |
| |
| /// A result from gathering resource information. |
| pub type LsbReleaseResult<T> = Result<T, LsbReleaseError>; |
| |
| /// Release information typically gathered from the environment or from `/etc/lsb-release`. |
| #[derive(Debug)] |
| pub struct LsbRelease { |
| info: BTreeMap<String, String>, |
| } |
| |
| impl LsbRelease { |
| /// Constructs release information from the Linux standard base release path: `/etc/lsb- |
| /// release`. |
| pub fn gather() -> LsbReleaseResult<LsbRelease> { |
| Self::from_path(LSB_RELEASE_FILE_PATH) |
| } |
| |
| /// Constructs release information from the file contents at `path`. |
| pub fn from_path<P: AsRef<Path>>(path: P) -> LsbReleaseResult<LsbRelease> { |
| let mut release_file = File::open(path).map_err(LsbReleaseError::FileOpenError)?; |
| let mut release_str = String::new(); |
| release_file |
| .read_to_string(&mut release_str) |
| .map_err(LsbReleaseError::FileReadError)?; |
| release_str.parse::<LsbRelease>() |
| } |
| |
| /// Gets arbitrary release information, or none if unavailable. |
| pub fn get<K: AsRef<str>>(&self, k: K) -> Option<&str> { |
| self.info.get(k.as_ref()).map(|s| s.as_str()) |
| } |
| |
| /// Gets the type of release channel this release information corresponds to, or none if this |
| /// information was not indicated. |
| pub fn release_channel(&self) -> Option<ReleaseChannel> { |
| self.get(CHROMEOS_RELEASE_TRACK_KEY).map(|c| c.into()) |
| } |
| } |
| |
| impl FromStr for LsbRelease { |
| type Err = LsbReleaseError; |
| |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| let mut info = BTreeMap::new(); |
| for (row, line) in s.lines().enumerate() { |
| let line_trimmed = line.trim(); |
| if line_trimmed.is_empty() { |
| continue; |
| } |
| // Attempt to match exactly two parts of the line, before and after the equals sign. |
| let mut line_parts = line_trimmed.splitn(2, '='); |
| match (line_parts.next(), line_parts.next()) { |
| (Some(key), Some(value)) => { |
| // If the `key` was already present, insert returns `Some(value)`. |
| if info.insert(key.to_owned(), value.to_owned()).is_some() { |
| return Err(LsbReleaseError::ParseError { |
| row, |
| message: "duplicate key in row", |
| }); |
| } |
| } |
| _ => { |
| return Err(LsbReleaseError::ParseError { |
| row, |
| message: "missing '=' in row", |
| }); |
| } |
| } |
| } |
| Ok(LsbRelease { info }) |
| } |
| } |
| |
| /// A channel of OS releases. Channels are distinguished by their relative stability and frequency |
| /// of release. |
| #[derive(PartialEq, Eq, Debug)] |
| pub enum ReleaseChannel<'a> { |
| Stable, |
| Beta, |
| Dev, |
| Canary, |
| /// Typically indicates that this release does not correspond to an official release channel. An |
| /// example of this would be `testimage-channel`. |
| Other(&'a str), |
| } |
| |
| impl<'a> From<&'a str> for ReleaseChannel<'a> { |
| fn from(s: &str) -> ReleaseChannel { |
| use self::ReleaseChannel::*; |
| match s { |
| "stable-channel" => Stable, |
| "beta-channel" => Beta, |
| "dev-channel" => Dev, |
| "canary-channel" => Canary, |
| _ => Other(s), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| const LSB_RELEASE: &str = r#" |
| CHROMEOS_RELEASE_APPID={495DCB07-E19A-4D7D-99B9-4710011A65B1} |
| CHROMEOS_BOARD_APPID={495DCB07-E19A-4D7D-99B9-4710011A65B1} |
| CHROMEOS_CANARY_APPID={90F229CE-83E2-4FAF-8479-E368A34938B1} |
| DEVICETYPE=CHROMEBOOK |
| CHROMEOS_RELEASE_BUILDER_PATH=nami-paladin/R73-11438.0.0-rc1 |
| CHROMEOS_RELEASE_BOARD=nami |
| CHROMEOS_RELEASE_BRANCH_NUMBER=0 |
| CHROMEOS_RELEASE_TRACK=testimage-channel |
| CHROMEOS_RELEASE_DESCRIPTION=11438.0.0-rc1 (Continuous Builder - Builder: N/A) nami |
| CHROMEOS_RELEASE_NAME=Chromium OS |
| CHROMEOS_AUSERVER=http://swarm-cros-457.c.chromeos-bot.internal:8080/update |
| CHROMEOS_ARC_VERSION=5193302 |
| CHROMEOS_ARC_ANDROID_SDK_VERSION=25 |
| GOOGLE_RELEASE=11438.0.0-rc1 |
| CHROMEOS_DEVSERVER=http://swarm-cros-457.c.chromeos-bot.internal:8080 |
| CHROMEOS_RELEASE_BUILD_NUMBER=11438 |
| CHROMEOS_RELEASE_CHROME_MILESTONE=73 |
| CHROMEOS_RELEASE_PATCH_NUMBER=0-rc1 |
| CHROMEOS_RELEASE_BUILD_TYPE=Continuous Builder - Builder: N/A |
| CHROMEOS_RELEASE_UNIBUILD=1 |
| CHROMEOS_RELEASE_VERSION=11438.0.0-rc1"#; |
| |
| #[test] |
| fn parse() { |
| let lsb_release = LSB_RELEASE.parse::<LsbRelease>().unwrap(); |
| assert_eq!( |
| lsb_release.get("CHROMEOS_RELEASE_APPID"), |
| Some("{495DCB07-E19A-4D7D-99B9-4710011A65B1}") |
| ); |
| assert_eq!(lsb_release.get("DEVICETYPE"), Some("CHROMEBOOK")); |
| assert_eq!( |
| lsb_release.get("CHROMEOS_RELEASE_VERSION"), |
| Some("11438.0.0-rc1") |
| ); |
| assert_eq!(lsb_release.info.len(), LSB_RELEASE.lines().count() - 1); |
| } |
| |
| #[test] |
| fn invalid_parse() { |
| assert!("SOMETHING_SOMETHING".parse::<LsbRelease>().is_err()); |
| assert!("A=1\nA=2".parse::<LsbRelease>().is_err()); |
| } |
| |
| #[test] |
| fn release_channel() { |
| let lsb_release = "CHROMEOS_RELEASE_TRACK=testimage-channel" |
| .parse::<LsbRelease>() |
| .unwrap(); |
| assert_eq!( |
| lsb_release.release_channel(), |
| Some(ReleaseChannel::Other("testimage-channel")) |
| ); |
| let lsb_release = "CHROMEOS_RELEASE_TRACK=canary-channel" |
| .parse::<LsbRelease>() |
| .unwrap(); |
| assert_eq!(lsb_release.release_channel(), Some(ReleaseChannel::Canary)); |
| let lsb_release = "CHROMEOS_RELEASE_TRACK=dev-channel" |
| .parse::<LsbRelease>() |
| .unwrap(); |
| assert_eq!(lsb_release.release_channel(), Some(ReleaseChannel::Dev)); |
| let lsb_release = "CHROMEOS_RELEASE_TRACK=beta-channel" |
| .parse::<LsbRelease>() |
| .unwrap(); |
| assert_eq!(lsb_release.release_channel(), Some(ReleaseChannel::Beta)); |
| let lsb_release = "CHROMEOS_RELEASE_TRACK=stable-channel" |
| .parse::<LsbRelease>() |
| .unwrap(); |
| assert_eq!(lsb_release.release_channel(), Some(ReleaseChannel::Stable)); |
| } |
| } |