1#[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#[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
46pub fn detect_os_from_device<'a, F: Into<FilesystemType<'a>>>(device: &Path, fs: F) -> Option<OS> {
52 TempDir::new("distinst").ok().and_then(|tempdir| {
54 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
63pub 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
72pub 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
85pub 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
96pub fn detect_windows(base: &Path) -> Option<OS> {
98 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 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
182pub(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}