os_detect/
lib.rs

1//! Provides the means for for detecting the existence of an OS from an unmounted device, or path.
2//!
3//! ```rust,no_run
4//! extern crate os_detect;
5//!
6//! use os_detect::detect_os_from_device;
7//! use std::path::Path;
8//!
9//! pub fn main() {
10//!     let device_path = &Path::new("/dev/sda3");
11//!     let fs = "ext4";
12//!     if let Some(os) = detect_os_from_device(device_path, fs) {
13//!         println!("{:#?}", os);
14//!     }
15//! }
16//! ```
17
18#[macro_use]
19extern crate log;
20extern crate os_release;
21extern crate partition_identity;
22extern crate sys_mount;
23extern crate tempdir;
24
25use std::fs::File;
26use std::io::{self, BufRead, BufReader};
27use std::path::Path;
28use tempdir::TempDir;
29use os_release::OsRelease;
30use std::path::PathBuf;
31use partition_identity::PartitionID;
32use sys_mount::*;
33
34/// Describes the OS found on a partition.
35#[derive(Debug, Clone)]
36pub enum OS {
37    Windows(String),
38    Linux {
39        info: OsRelease,
40        partitions: Vec<PartitionID>,
41        targets: Vec<PathBuf>,
42    },
43    MacOs(String)
44}
45
46/// Mounts the partition to a temporary directory and checks for the existence of an
47/// installed operating system.
48///
49/// If the installed operating system is Linux, it will also report back the location
50/// of the home partition.
51pub fn detect_os_from_device<'a, F: Into<FilesystemType<'a>>>(device: &Path, fs: F) -> Option<OS> {
52    // Create a temporary directoy where we will mount the FS.
53    TempDir::new("distinst").ok().and_then(|tempdir| {
54        // Mount the FS to the temporary directory
55        let base = tempdir.path();
56        Mount::new(device, base, fs, MountFlags::empty(), None)
57            .map(|m| m.into_unmount_drop(UnmountFlags::DETACH))
58            .ok()
59            .and_then(|_mount| detect_os_from_path(base))
60    })
61}
62
63/// Detects the existence of an OS at a defined path.
64///
65/// This function is called by `detect_os_from_device`, after having temporarily mounted it.
66pub fn detect_os_from_path(base: &Path) -> Option<OS> {
67    detect_linux(base)
68        .or_else(|| detect_windows(base))
69        .or_else(|| detect_macos(base))
70}
71
72/// Detect if Linux is installed at the given path.
73pub fn detect_linux(base: &Path) -> Option<OS> {
74    let path = base.join("etc/os-release");
75    if path.exists() {
76        if let Ok(info) = OsRelease::new_from(path) {
77            let (partitions, targets) = find_linux_parts(base);
78            return Some(OS::Linux { info, partitions, targets });
79        }
80    }
81
82    None
83}
84
85/// Detect if Mac OS is installed at the given path.
86pub fn detect_macos(base: &Path) -> Option<OS> {
87    open(base.join("etc/os-release"))
88        .ok()
89        .and_then(|file| {
90            parse_plist(BufReader::new(file))
91                .or_else(|| Some("Mac OS (Unknown)".into()))
92                .map(OS::MacOs)
93        })
94}
95
96/// Detect if Windows is installed at the given path.
97pub fn detect_windows(base: &Path) -> Option<OS> {
98    // TODO: More advanced version-specific detection is possible.
99    base.join("Windows/System32/ntoskrnl.exe")
100        .exists()
101        .map(|| OS::Windows("Windows".into()))
102}
103
104fn find_linux_parts(base: &Path) -> (Vec<PartitionID>, Vec<PathBuf>) {
105    let mut partitions = Vec::new();
106    let mut targets = Vec::new();
107
108    if let Ok(fstab) = open(base.join("etc/fstab")) {
109        for entry in BufReader::new(fstab).lines() {
110            if let Ok(entry) = entry {
111                let entry = entry.trim();
112                if entry.starts_with('#') || entry.is_empty() {
113                    continue;
114                }
115
116                let mut fields = entry.split_whitespace();
117                let source = fields.next();
118                let target = fields.next();
119
120                if let Some(target) = target {
121                    if let Some(Ok(path)) = source.map(|s| s.parse::<PartitionID>()) {
122                        partitions.push(path);
123                        targets.push(PathBuf::from(String::from(target)));
124                    }
125                }
126            }
127        }
128    }
129
130    (partitions, targets)
131}
132
133fn parse_plist<R: BufRead>(file: R) -> Option<String> {
134    // The plist is an XML file, but we don't need complex XML parsing for this.
135    let mut product_name: Option<String> = None;
136    let mut version: Option<String> = None;
137    let mut flags = 0;
138
139    for entry in file.lines().flat_map(|line| line) {
140        let entry = entry.trim();
141        match flags {
142            0 => match entry {
143                "<key>ProductUserVisibleVersion</key>" => flags = 1,
144                "<key>ProductName</key>" => flags = 2,
145                _ => (),
146            },
147            1 => {
148                if entry.len() < 10 {
149                    return None;
150                }
151                version = Some(entry[8..entry.len() - 9].into());
152                flags = 0;
153            }
154            2 => {
155                if entry.len() < 10 {
156                    return None;
157                }
158                product_name = Some(entry[8..entry.len() - 9].into());
159                flags = 0;
160            }
161            _ => unreachable!(),
162        }
163        if product_name.is_some() && version.is_some() {
164            break;
165        }
166    }
167
168    if let (Some(name), Some(version)) = (product_name, version) {
169        Some(format!("{} ({})", name, version))
170    } else {
171        None
172    }
173}
174
175fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
176    File::open(&path).map_err(|why| io::Error::new(
177        io::ErrorKind::Other,
178        format!("unable to open file at {:?}: {}", path.as_ref(), why)
179    ))
180}
181
182/// Adds a new map method for boolean types.
183pub(crate) trait BoolExt {
184    fn map<T, F: Fn() -> T>(&self, action: F) -> Option<T>;
185}
186
187impl BoolExt for bool {
188    fn map<T, F: Fn() -> T>(&self, action: F) -> Option<T> {
189        if *self {
190            Some(action())
191        } else {
192            None
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use std::io::Cursor;
201
202    const MAC_PLIST: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
203<!DOCTYPE plist PUBLIC "Apple Stuff">
204<plist version="1.0">
205<dict>
206    <key>ProductBuildVersion</key>
207    <string>10C540</string>
208    <key>ProductName</key>
209    <string>Mac OS X</string>
210    <key>ProductUserVisibleVersion</key>
211    <string>10.6.2</string>
212    <key>ProductVersion</key>
213    <string>10.6.2</string>
214</dict>
215</plist>"#;
216
217    #[test]
218    fn mac_plist_parsing() {
219        assert_eq!(
220            parse_plist(Cursor::new(MAC_PLIST)),
221            Some("Mac OS X (10.6.2)".into())
222        );
223    }
224}