1use std::path::{Path, PathBuf};
46use std::process::{Command, Stdio};
47use std::io::{self, ErrorKind, Cursor};
48use std::ops::Deref;
49use std::env;
50
51use log::info;
52use plist::Value;
53
54#[cfg(test)]
55mod tests;
56
57static DISK_COMMAND: &str = "hdiutil";
58
59enum Mount {
60 Default,
61 Random(PathBuf),
62 Root(PathBuf),
63 Point(PathBuf)
64}
65
66pub struct Attach {
68 image: PathBuf,
69 mount: Mount,
70 hidden: bool,
71 force_readonly: bool,
72}
73
74#[derive(Debug)]
76pub struct Info {
77 pub mount_point: PathBuf,
79
80 pub device: PathBuf,
82}
83
84#[derive(Debug)]
88pub struct Handle(Info);
89
90#[derive(Debug)]
94pub struct With(Info);
95
96macro_rules! check {
97 ($opt:expr) => {
98 match $opt {
99 Some(res) => res,
100 None => return Err(io::Error::new(ErrorKind::InvalidData, "could not find property")),
101 }
102 }
103}
104
105macro_rules! deref_info {
106 ($name:ident) => {
107 impl Deref for $name {
109 type Target = Info;
110 fn deref(&self) -> &Info {
111 &self.0
112 }
113 }
114 }
115}
116
117deref_info!(Handle);
118deref_info!(With);
119
120impl Handle {
121 pub fn force_detach(self) -> io::Result<()> {
123 detach(&self.device, true)
124 }
125
126 pub fn detach(self) -> io::Result<()> {
128 detach(&self.device, false)
129 }
130}
131
132impl Drop for With {
134 fn drop(&mut self) {
135 detach(&self.device, false).expect("could not detach");
136 }
137}
138
139macro_rules! mount_fn {
140 ($doc:expr, $name:ident, $variant:ident) => {
141 #[doc=$doc]
142 pub fn $name<P: Into<PathBuf>>(mut self, path: P) -> Attach {
143 self.mount = Mount::$variant(path.into());
144 self
145 }
146 }
147}
148
149macro_rules! enable_fn {
150 ($doc:expr, $name:ident) => {
151 #[doc=$doc]
152 pub fn $name(mut self) -> Attach {
153 self.$name = true;
154 self
155 }
156 }
157}
158
159impl Attach {
160 pub fn new<P: Into<PathBuf>>(path: P) -> Attach {
162 Attach {
163 image: path.into(),
164 mount: Mount::Default,
165 hidden: false,
166 force_readonly: false,
167 }
168 }
169
170
171 mount_fn!("Mount volumes on subdirectories of path instead of under `/Volumes`.", mount_root, Root);
172 mount_fn!("Asuming only one volume, mount it at path instead of in `/Volumes`.", mount_point, Point);
173 mount_fn!("Mount under `path` with a random unique mount point directory name.", mount_random, Random);
174 enable_fn!("Render the volume invisible in applications like Finder.", hidden);
175 enable_fn!("Force the device to be read-only.", force_readonly);
176
177 pub fn mount_temp(self) -> Attach {
181 self.mount_random(env::temp_dir())
182 }
183
184 fn attach_info(self) -> io::Result<Info> {
185 let mut cmd = Command::new(DISK_COMMAND);
186 cmd.arg("attach");
187
188 match self.mount {
189 Mount::Default => {},
190 Mount::Random(ref path) => {
191 cmd.arg("-mountrandom");
192 cmd.arg(path);
193 },
194 Mount::Root(ref path) => {
195 cmd.arg("-mountroot");
196 cmd.arg(path);
197 },
198 Mount::Point(ref path) => {
199 cmd.arg("-mountpoint");
200 cmd.arg(path);
201 }
202 }
203
204 if self.force_readonly {
205 cmd.arg("-readonly");
206 }
207
208 if self.hidden {
209 cmd.arg("-nobrowse");
210 }
211
212 cmd.arg("-plist");
213 cmd.arg(&self.image);
214
215 info!("Attaching {:?}", cmd);
216 let output = cmd.output()?;
217 info!("Status {:?}", output.status);
218
219 if !output.status.success() {
220 return Err(io::Error::new(ErrorKind::Other, "hdiutil failed"));
223 }
224
225 if let Ok(plist) = Value::from_reader(Cursor::new(output.stdout)) {
226 let entities = check!(check!(check!(plist.as_dictionary()).get("system-entities")).as_array());
227 for entity in entities {
228 let properties = check!(entity.as_dictionary());
229 if let Some(mount_point) = properties.get("mount-point") {
230 return Ok(Info {
231 mount_point: PathBuf::from(check!(mount_point.as_string())),
232 device: PathBuf::from(check!(properties["dev-entry"].as_string())),
234 });
235 }
236 }
237 return Err(io::Error::new(ErrorKind::Other, "could not extract data"));
238 }
239 return Err(io::Error::new(ErrorKind::InvalidData, "could not parse plist"));
240 }
241
242 pub fn attach(self) -> io::Result<Handle> {
244 self.attach_info().map(Handle)
245 }
246
247 pub fn with(self) -> io::Result<With> {
249 self.attach_info().map(With)
250 }
251}
252
253pub fn detach<P: AsRef<Path>>(path: P, force: bool) -> io::Result<()> {
257 let mut cmd = Command::new(DISK_COMMAND);
258 cmd.stdout(Stdio::null());
259 cmd.stderr(Stdio::null());
260
261 cmd.arg("detach");
262 if force {
263 cmd.arg("-force");
264 }
265 cmd.arg(path.as_ref());
266
267 info!("Detaching (force: {:?}): {:?}", force, cmd);
268 let status = cmd.status()?;
269 info!("Status {:?}", status);
270
271 if status.success() {
272 Ok(())
273 } else {
274 Err(io::Error::new(ErrorKind::Other, "non-zero exit status for detach"))
275 }
276}