Skip to main content

mtp_mount/
hints.rs

1//! Remedies for the failures people actually hit when opening a device.
2//!
3//! The same wording backs both the `--help` troubleshooting section and the
4//! error message printed when opening the device fails, so the two can't drift.
5
6/// Another process holds the USB interface.
7pub const BUSY_HINT: &str = "\
8Another program already claimed the USB interface.
9
10On Linux that's almost always gvfs: run `gio mount -l` to find the device,
11then `gio mount -u <mount-uri>` to release it. To stop gvfs from grabbing
12devices at all: `systemctl --user mask gvfs-mtp-volume-monitor`.
13
14On macOS it's `ptpcamerad`: stop it with `sudo killall ptpcamerad`, then
15reconnect the device (launchd starts it again on the next connect).";
16
17/// The OS refused access to the USB device node.
18pub const PERMISSION_HINT: &str = "\
19The OS denied access to the USB device.
20
21On Linux this is a missing udev rule: your user has no write access to
22/dev/bus/usb/*. Add yourself to the `plugdev` group, or install a udev rule
23for the device's vendor ID.
24See: https://github.com/vdavid/mtp-mount#requirements";
25
26/// Pick the remedy for a device-open failure, if there is a specific one.
27pub fn open_failure_hint(e: &mtp_rs::Error) -> Option<&'static str> {
28    if e.is_exclusive_access() {
29        Some(BUSY_HINT)
30    } else if e.is_permission_denied() {
31        Some(PERMISSION_HINT)
32    } else {
33        None
34    }
35}
36
37/// Indent every non-empty line, for embedding a hint in the `--help` sections.
38pub fn indent(text: &str, prefix: &str) -> String {
39    text.lines()
40        .map(|line| {
41            if line.is_empty() {
42                String::new()
43            } else {
44                format!("{prefix}{line}")
45            }
46        })
47        .collect::<Vec<_>>()
48        .join("\n")
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn busy_gets_the_release_the_device_remedy() {
57        // What mtp-rs maps a USB `EBUSY` / `kIOReturnExclusiveAccess` to when
58        // another process (gvfs, ptpcamerad) holds the interface.
59        let e = mtp_rs::Error::ExclusiveAccess;
60        assert!(e.is_exclusive_access(), "precondition: {e}");
61        let hint = open_failure_hint(&e).expect("busy needs a hint");
62        assert!(hint.contains("gio mount -l"));
63        assert!(hint.contains("gvfs-mtp-volume-monitor"));
64        assert!(hint.contains("ptpcamerad"));
65    }
66
67    #[test]
68    fn permission_denied_points_at_udev() {
69        let e = mtp_rs::Error::PermissionDenied;
70        assert!(e.is_permission_denied(), "precondition: {e}");
71        let hint = open_failure_hint(&e).expect("permission denied needs a hint");
72        assert!(hint.contains("udev"));
73    }
74
75    #[test]
76    fn other_failures_get_no_hint() {
77        let e = mtp_rs::Error::NotFound;
78        assert!(!e.is_exclusive_access());
79        assert!(!e.is_permission_denied());
80        assert!(open_failure_hint(&e).is_none());
81    }
82
83    #[test]
84    fn indent_prefixes_content_lines_only() {
85        assert_eq!(indent("a\n\nb", "  "), "  a\n\n  b");
86    }
87}