1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
use super::DISK_DIR;

use async_std::{fs, path::Path};
use futures::stream::Stream;
use std::{
    io,
    pin::Pin,
    task::{Context, Poll},
};

#[derive(Debug, Error)]
pub enum Error {
    #[error("failed to open /dev/disk/by-path/ directory for reading")]
    Open(#[source] io::Error),
    #[error("directory iteration error")]
    Iteration(#[source] io::Error),
    #[error("device path lacks a file name")]
    DeviceWithoutFileName,
    #[error("device path is not UTF-8 valid")]
    DevicePathNotUtf8,
}

/// A stream which iterates on USB device paths to find USB storage disks in the system.
///
/// # Example
///
/// ```
/// use usb_disk_probe::stream::UsbDiskProbe;
/// 
/// use futures::stream::StreamExt;
/// 
/// fn main() {
///     futures::executor::block_on(async move {
///         let mut stream = UsbDiskProbe::new().await.unwrap();
///         while let Some(device_result) = stream.next().await {
///             let device = device_result.unwrap();
///             println!("{}", device.display());
///         }
///     });
/// }
/// ```
pub struct UsbDiskProbe(fs::ReadDir);

impl UsbDiskProbe {
    pub async fn new() -> Result<Self, Error> {
        fs::read_dir(DISK_DIR)
            .await
            .map(|dir| Self(dir))
            .map_err(Error::Open)
    }
}

impl Stream for UsbDiskProbe {
    type Item = Result<Box<Path>, Error>;

    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
        loop {
            match unsafe { Pin::new_unchecked(&mut self.0) }.poll_next(cx) {
                Poll::Pending => return Poll::Pending,
                Poll::Ready(Some(res)) => match filter_device(res) {
                    value @ Some(_) => return Poll::Ready(value),
                    None => continue,
                },
                Poll::Ready(None) => return Poll::Ready(None),
            }
        }
    }
}

/// Filter USB devices which are not USB devices
fn filter_device(entry: io::Result<fs::DirEntry>) -> Option<Result<Box<Path>, Error>> {
    transpose(entry.map_err(Error::Iteration), |entry| {
        let path = entry.path();

        let result = transpose(
            path.file_name().ok_or(Error::DeviceWithoutFileName),
            |filename_os| {
                transpose(
                    filename_os.to_str().ok_or(Error::DevicePathNotUtf8),
                    |filename_str| {
                        if is_usb(filename_str) {
                            Some(Ok(()))
                        } else {
                            None
                        }
                    },
                )
            },
        );

        match result {
            Some(Ok(())) => Some(Ok(path.into_boxed_path())),
            Some(Err(why)) => Some(Err(why)),
            None => None,
        }
    })
}

/// Checks if a device is a USB device
fn is_usb(filename: &str) -> bool {
    filename.starts_with("pci-") && filename.contains("-usb-") && filename.ends_with("-0:0:0:0")
}

/// Converts an `Err(E)` to a `Some(Err(E))`, and maps the `Ok(T)` to an `Option<Result<X, E>>`
#[inline]
fn transpose<T, E, X, F: FnOnce(T) -> Option<Result<X, E>>>(
    input: Result<T, E>,
    func: F,
) -> Option<Result<X, E>> {
    match input {
        Ok(value) => func(value),
        Err(why) => Some(Err(why)),
    }
}