#[macro_use]
extern crate log;
extern crate os_release;
extern crate partition_identity;
extern crate sys_mount;
extern crate tempdir;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::Path;
use tempdir::TempDir;
use os_release::OsRelease;
use std::path::PathBuf;
use partition_identity::PartitionID;
use sys_mount::*;
#[derive(Debug, Clone)]
pub enum OS {
Windows(String),
Linux {
info: OsRelease,
partitions: Vec<PartitionID>,
targets: Vec<PathBuf>,
},
MacOs(String)
}
pub fn detect_os_from_device<'a, F: Into<FilesystemType<'a>>>(device: &Path, fs: F) -> Option<OS> {
TempDir::new("distinst").ok().and_then(|tempdir| {
let base = tempdir.path();
Mount::new(device, base, fs, MountFlags::empty(), None)
.map(|m| m.into_unmount_drop(UnmountFlags::DETACH))
.ok()
.and_then(|_mount| detect_os_from_path(base))
})
}
pub fn detect_os_from_path(base: &Path) -> Option<OS> {
detect_linux(base)
.or_else(|| detect_windows(base))
.or_else(|| detect_macos(base))
}
pub fn detect_linux(base: &Path) -> Option<OS> {
let path = base.join("etc/os-release");
if path.exists() {
if let Ok(info) = OsRelease::new_from(path) {
let (partitions, targets) = find_linux_parts(base);
return Some(OS::Linux { info, partitions, targets });
}
}
None
}
pub fn detect_macos(base: &Path) -> Option<OS> {
open(base.join("etc/os-release"))
.ok()
.and_then(|file| {
parse_plist(BufReader::new(file))
.or_else(|| Some("Mac OS (Unknown)".into()))
.map(OS::MacOs)
})
}
pub fn detect_windows(base: &Path) -> Option<OS> {
base.join("Windows/System32/ntoskrnl.exe")
.exists()
.map(|| OS::Windows("Windows".into()))
}
fn find_linux_parts(base: &Path) -> (Vec<PartitionID>, Vec<PathBuf>) {
let mut partitions = Vec::new();
let mut targets = Vec::new();
if let Ok(fstab) = open(base.join("etc/fstab")) {
for entry in BufReader::new(fstab).lines() {
if let Ok(entry) = entry {
let entry = entry.trim();
if entry.starts_with('#') || entry.is_empty() {
continue;
}
let mut fields = entry.split_whitespace();
let source = fields.next();
let target = fields.next();
if let Some(target) = target {
if let Some(Ok(path)) = source.map(|s| s.parse::<PartitionID>()) {
partitions.push(path);
targets.push(PathBuf::from(String::from(target)));
}
}
}
}
}
(partitions, targets)
}
fn parse_plist<R: BufRead>(file: R) -> Option<String> {
let mut product_name: Option<String> = None;
let mut version: Option<String> = None;
let mut flags = 0;
for entry in file.lines().flat_map(|line| line) {
let entry = entry.trim();
match flags {
0 => match entry {
"<key>ProductUserVisibleVersion</key>" => flags = 1,
"<key>ProductName</key>" => flags = 2,
_ => (),
},
1 => {
if entry.len() < 10 {
return None;
}
version = Some(entry[8..entry.len() - 9].into());
flags = 0;
}
2 => {
if entry.len() < 10 {
return None;
}
product_name = Some(entry[8..entry.len() - 9].into());
flags = 0;
}
_ => unreachable!(),
}
if product_name.is_some() && version.is_some() {
break;
}
}
if let (Some(name), Some(version)) = (product_name, version) {
Some(format!("{} ({})", name, version))
} else {
None
}
}
fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
File::open(&path).map_err(|why| io::Error::new(
io::ErrorKind::Other,
format!("unable to open file at {:?}: {}", path.as_ref(), why)
))
}
pub(crate) trait BoolExt {
fn map<T, F: Fn() -> T>(&self, action: F) -> Option<T>;
}
impl BoolExt for bool {
fn map<T, F: Fn() -> T>(&self, action: F) -> Option<T> {
if *self {
Some(action())
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
const MAC_PLIST: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "Apple Stuff">
<plist version="1.0">
<dict>
<key>ProductBuildVersion</key>
<string>10C540</string>
<key>ProductName</key>
<string>Mac OS X</string>
<key>ProductUserVisibleVersion</key>
<string>10.6.2</string>
<key>ProductVersion</key>
<string>10.6.2</string>
</dict>
</plist>"#;
#[test]
fn mac_plist_parsing() {
assert_eq!(
parse_plist(Cursor::new(MAC_PLIST)),
Some("Mac OS X (10.6.2)".into())
);
}
}