Skip to main content

libscsi/
lib.rs

1mod transport;
2mod types;
3
4pub use types::{
5    Cdb, Direction, MAX_CDB_LEN, MAX_SENSE_LEN, OpenOpts, ScsiCommand, ScsiResult, ScsiStatus,
6    Sense,
7};
8
9/// Library-level error type.
10#[derive(Debug)]
11pub enum Error {
12    Io(std::io::Error),
13    InvalidParameter(&'static str),
14    Internal(&'static str),
15}
16
17impl std::fmt::Display for Error {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        match self {
20            Error::Io(e) => write!(f, "I/O error: {e}"),
21            Error::InvalidParameter(msg) => write!(f, "invalid parameter: {msg}"),
22            Error::Internal(msg) => write!(f, "internal error: {msg}"),
23        }
24    }
25}
26
27impl std::error::Error for Error {
28    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
29        match self {
30            Error::Io(e) => Some(e),
31            _ => None,
32        }
33    }
34}
35
36impl From<std::io::Error> for Error {
37    fn from(e: std::io::Error) -> Self {
38        Error::Io(e)
39    }
40}
41
42/// An open handle to a SCSI device.
43///
44/// Create one with [`ScsiDevice::open`], then call [`ScsiDevice::execute`] to
45/// issue commands.  The handle is closed automatically when dropped.
46pub struct ScsiDevice(transport::Device);
47
48impl ScsiDevice {
49    /// Open a SCSI device by path.
50    ///
51    /// | Platform | Example paths                            |
52    /// |----------|------------------------------------------|
53    /// | Windows  | `\\\\.\\PhysicalDrive0`, `\\\\.\\Scsi0:` |
54    /// | Linux    | `/dev/sg0`, `/dev/sda`                   |
55    /// | macOS    | `/dev/disk0`                             |
56    ///
57    /// Pass `OpenOpts::default()` for shared (non-exclusive) access.
58    pub fn open(path: &std::path::Path, opts: &OpenOpts) -> Result<Self, Error> {
59        transport::Device::open(path, opts).map(ScsiDevice)
60    }
61
62    /// Issue a SCSI command and wait for it to complete.
63    ///
64    /// For `Direction::In` pre-fill `cmd.data` with `vec![0u8; <expected bytes>]`.
65    /// On success `ScsiResult::data` is already truncated to the number of bytes
66    /// the device actually returned — no separate length field is needed.
67    pub fn execute(&mut self, cmd: ScsiCommand) -> Result<ScsiResult, Error> {
68        self.0.execute(cmd)
69    }
70}
71
72// ── Test helpers ────────────────────────────────────────────────────────────
73
74#[cfg(test)]
75impl ScsiDevice {
76    #[cfg(target_os = "windows")]
77    fn new_test() -> std::io::Result<Self> {
78        transport::Device::new_test().map(ScsiDevice)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    // ── Error type ──────────────────────────────────────────────────────────
87
88    #[test]
89    fn error_display_invalid_parameter() {
90        let e = Error::InvalidParameter("something went wrong");
91        assert_eq!(e.to_string(), "invalid parameter: something went wrong");
92    }
93
94    #[test]
95    fn error_display_io() {
96        let e = Error::Io(std::io::Error::from(std::io::ErrorKind::NotFound));
97        assert!(e.to_string().starts_with("I/O error:"));
98    }
99
100    #[test]
101    fn error_source_io_is_some() {
102        use std::error::Error as _;
103        let e = Error::Io(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
104        assert!(e.source().is_some());
105    }
106
107    #[test]
108    fn error_source_invalid_parameter_is_none() {
109        use std::error::Error as _;
110        let e = Error::InvalidParameter("x");
111        assert!(e.source().is_none());
112    }
113
114    // ── ScsiDevice::open ────────────────────────────────────────────────────
115
116    #[test]
117    fn open_nonexistent_path_returns_io_error() {
118        let result = ScsiDevice::open(
119            std::path::Path::new(r"\\.\LibScsiNonexistentDevice999"),
120            &OpenOpts::default(),
121        );
122        assert!(matches!(result, Err(Error::Io(_))));
123    }
124
125    // ── ScsiDevice::execute — reaches IOCTL layer ───────────────────────────
126
127    /// A valid command against a non-SCSI handle must fail at the IOCTL level.
128    #[cfg(target_os = "windows")]
129    #[test]
130    fn execute_valid_cdb_reaches_ioctl() {
131        let mut dev = ScsiDevice::new_test().expect("temp file");
132        let cmd = ScsiCommand {
133            cdb: Cdb::new([0u8; 6]).unwrap(),
134            direction: Direction::None,
135            data: vec![],
136            timeout_secs: None,
137        };
138        assert!(matches!(dev.execute(cmd), Err(Error::Io(_))));
139    }
140}