dmg/
lib.rs

1// Copyright 2017 dmg Developers
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! Simple attaching/detaching of macOS disk images.
9//!
10//! # Example
11//!
12//! Attach a disk image until dropped:
13//!
14//! ```rust
15//! use dmg::Attach;
16//!
17//! let info = Attach::new("Test.dmg").with().expect("could not attach");
18//! println!("Mounted at {:?}", info.mount_point);
19//! // Detched when 'info' dropped
20//! ```
21//!
22//! If you prefer to handle detaching yourself simply use [`attach()`](struct.Attach.html#method.attach):
23//!
24//! ```rust
25//! use dmg::Attach;
26//!
27//! let info = Attach::new("Test.dmg").attach().expect("could not attach");
28//! println!("Device node {:?}", info.device);
29//! info.detach().expect("could not detach"); // There is also .force_detach()
30//! ```
31//!
32//! If you know the device node or mount point, you can detach it like this too:
33//!
34//! ```rust,no_run
35//! use dmg;
36//! dmg::detach("/Volumes/Test", false).expect("could not detach"); // Do not force detach
37//! ```
38//!
39//! For more examples see [`src/tests.rs`][1] and [`src/bin/demo.rs`][2]
40//!
41//!
42//! [1]: https://github.com/mgoszcz2/dmg/blob/master/src/tests.rs
43//! [2]: https://github.com/mgoszcz2/dmg/blob/master/src/bin/demo.rs
44
45use 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
66/// Builder to attach a disk image.
67pub struct Attach {
68    image: PathBuf,
69    mount: Mount,
70    hidden: bool,
71    force_readonly: bool,
72}
73
74/// Data associated with an attached disk image.
75#[derive(Debug)]
76pub struct Info {
77    /// Path at which the disk image is mounted.
78    pub mount_point: PathBuf,
79
80    /// Device node path for this disk image.
81    pub device: PathBuf,
82}
83
84/// Convinience handle for detaching an attached disk image.
85///
86/// Created with [`attach()`](struct.Attach.html#method.attach)
87#[derive(Debug)]
88pub struct Handle(Info);
89
90/// An attached disk image handle that detaches it when dropped.
91///
92/// Created with [`with()`](struct.Attach.html#method.with)
93#[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        /// Access the [`Info`](struct.Info.html) struct associated with this handle.
108        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    /// Detach the image, ignoring any open files.
122    pub fn force_detach(self) -> io::Result<()> {
123        detach(&self.device, true)
124    }
125
126    /// Detach the image.
127    pub fn detach(self) -> io::Result<()> {
128        detach(&self.device, false)
129    }
130}
131
132/// Detach the disk image on drop
133impl 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    /// Creates a new attach builder for the given disk image.
161    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    /// Mount in a random folder inside the temporary directory.
178    ///
179    /// Equivalent to `mount_random(std::env::temp_dir())`
180    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            // This is not as informative as I wish it would be
221            // .. but neither is hdiutil
222            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                        // If we don't have this something has gonne _really_ wrong
233                        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    /// Attach the disk image
243    pub fn attach(self) -> io::Result<Handle> {
244        self.attach_info().map(Handle)
245    }
246
247    /// Attach the disk image, detaching when dropped
248    pub fn with(self) -> io::Result<With> {
249        self.attach_info().map(With)
250    }
251}
252
253/// Detach an image using a path.
254///
255/// The path can be either a device node path or a mount point.
256pub 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}