#[cfg(target_os = "linux")]
use crate::transport::linux::SgTransport;
use crate::{
error::Error,
protocol::{caps::identity::Identity, cdbs::Inquiry, model::Model},
transport::{Data, Status, Transport, usb::UsbTransport},
};
use nusb::MaybeFuture;
#[cfg(any(target_os = "linux", target_os = "windows"))]
use std::path::PathBuf;
use std::{fmt, str::FromStr, time::Duration};
use tracing::debug;
const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Attach {
Usb {
bus: String,
ports: Vec<u8>,
},
#[cfg(target_os = "linux")]
Sg(PathBuf), #[cfg(target_os = "windows")]
Scanner(PathBuf), #[cfg(target_os = "macos")]
ScsiTask(u64),
}
impl fmt::Display for Attach {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Attach::Usb { bus, ports } => {
let chain: Vec<_> = ports.iter().map(u8::to_string).collect();
write!(f, "usb:{bus}-{}", chain.join("."))
}
#[cfg(target_os = "linux")]
Attach::Sg(p) => write!(f, "{}", p.display()),
#[cfg(target_os = "windows")]
Attach::Scanner(p) => write!(f, "{}", p.display()),
#[cfg(target_os = "macos")]
Attach::ScsiTask(id) => write!(f, "scsi:{id:#x}"),
}
}
}
#[derive(Debug, Clone)]
pub struct Device {
pub attach: Attach,
pub identity: Option<Identity>,
pub model: Option<Model>,
pub opened: bool,
}
impl Device {
pub fn name(&self) -> String {
if let Some(id) = &self.identity {
return format!("{} {}", id.vendor, id.product);
}
let why = match self.opened {
true => "no answer",
false => "in use",
};
match self.model {
Some(model) => format!("{} ({why})", model.name()),
None => format!("({why})"),
}
}
pub fn open(&self) -> Result<Box<dyn Transport>, Error> {
let io = |e: std::io::Error| Error::Transport(e.into());
match &self.attach {
Attach::Usb { bus, ports } => {
let info = usb_devices()
.into_iter()
.find(|d| d.bus_id() == bus && d.port_chain() == ports)
.ok_or(Error::NotFound)?;
Ok(Box::new(UsbTransport::open(info).map_err(io)?))
}
#[cfg(target_os = "linux")]
Attach::Sg(path) => Ok(Box::new(SgTransport::open(path).map_err(io)?)),
#[cfg(target_os = "windows")]
Attach::Scanner(path) => Ok(Box::new(
crate::transport::windows::ScsiScanDevice::open(path).map_err(io)?,
)),
#[cfg(target_os = "macos")]
Attach::ScsiTask(id) => Ok(Box::new(
crate::transport::darwin::ScsiTaskTransport::open(*id).map_err(io)?,
)),
}
}
}
impl fmt::Display for Device {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:<20} {}", self.name(), self.attach)
}
}
pub fn list() -> Vec<Device> {
let mut found: Vec<Device> = usb_devices()
.into_iter()
.map(|info| {
let attach = Attach::Usb {
bus: info.bus_id().to_string(),
ports: info.port_chain().to_vec(),
};
let model = Model::from_usb(info.vendor_id(), info.product_id());
let (identity, opened) = match UsbTransport::open(info) {
Ok(mut transport) => (probe(&mut transport), true),
Err(e) => {
debug!(%e, "could not open a unit to ask who it is");
(None, false)
}
};
Device {
attach,
model,
identity,
opened,
}
})
.collect();
found.extend(scsi_devices());
found
}
fn usb_devices() -> Vec<nusb::DeviceInfo> {
let all = match nusb::list_devices().wait() {
Ok(all) => all,
Err(e) => {
debug!(%e, "could not enumerate USB");
return Vec::new();
}
};
all.filter(|dev| Model::from_usb(dev.vendor_id(), dev.product_id()).is_some())
.collect()
}
fn probe(transport: &mut dyn Transport) -> Option<Identity> {
let cmd = Inquiry::standard();
let mut buf = vec![0u8; cmd.allocation_length()];
let completion = transport
.execute(&cmd.cdb(), Data::In(&mut buf), PROBE_TIMEOUT)
.ok()?;
if completion.status != Status::Good {
return None;
}
buf.truncate(completion.transferred);
Identity::parse(&buf).ok().filter(Identity::is_scanner)
}
#[cfg(target_os = "linux")]
fn scsi_devices() -> Vec<Device> {
use crate::transport::linux::SgTransport;
let Ok(entries) = std::fs::read_dir("/dev") else {
return Vec::new();
};
entries
.flatten()
.map(|e| e.path())
.filter(|path| {
path.file_name().and_then(|n| n.to_str()).is_some_and(|n| {
n.starts_with("sg") && n.len() > 2 && n[2..].bytes().all(|b| b.is_ascii_digit())
})
})
.filter_map(|path| {
let mut transport = SgTransport::open(&path).ok()?;
let identity = probe(&mut transport)?;
Some(Device {
attach: Attach::Sg(path),
model: identity.model(),
identity: Some(identity),
opened: true,
})
})
.collect()
}
#[cfg(target_os = "windows")]
fn scsi_devices() -> Vec<Device> {
use crate::transport::windows::ScsiScanDevice;
(0..)
.map(|n| PathBuf::from(format!(r"\\.\Scanner{n}")))
.map_while(|path| {
let mut transport = ScsiScanDevice::open(&path).ok()?;
Some((path, probe(&mut transport)))
})
.filter_map(|(path, identity)| {
let identity = identity?;
Some(Device {
attach: Attach::Scanner(path),
model: identity.model(),
identity: Some(identity),
opened: true,
})
})
.collect()
}
#[cfg(target_os = "macos")]
fn scsi_devices() -> Vec<Device> {
use crate::transport::darwin::ScsiTaskTransport;
ScsiTaskTransport::entry_ids()
.into_iter()
.filter_map(|id| {
let mut transport = ScsiTaskTransport::open(id).ok()?;
let identity = probe(&mut transport)?;
Some(Device {
attach: Attach::ScsiTask(id),
model: identity.model(),
identity: Some(identity),
opened: true,
})
})
.collect()
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
fn scsi_devices() -> Vec<Device> {
Vec::new()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Selector {
Only,
Location(String),
}
impl FromStr for Selector {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
Ok(if s.is_empty() {
Selector::Only
} else {
Selector::Location(s.to_string())
})
}
}
impl Selector {
pub fn resolve<'a>(&self, devices: &'a [Device]) -> Result<&'a Device, SelectError> {
let matches: Vec<&Device> = match self {
Selector::Only => devices.iter().collect(),
Selector::Location(loc) => devices
.iter()
.filter(|d| d.attach.to_string().eq_ignore_ascii_case(loc))
.collect(),
};
match matches.as_slice() {
[one] => Ok(one),
[] => Err(SelectError::NotFound),
many => Err(SelectError::Ambiguous(
many.iter().map(|d| d.attach.to_string()).collect(),
)),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SelectError {
#[error("no scanner matched")]
NotFound,
#[error("more than one scanner matched: {}", .0.join(", "))]
Ambiguous(Vec<String>),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::caps::identity::SCANNER;
fn usb(bus: &str, ports: &[u8], product: &str) -> Device {
Device {
attach: Attach::Usb {
bus: bus.into(),
ports: ports.to_vec(),
},
identity: Some(Identity {
qualifier: 0,
device_type: SCANNER,
removable: true,
ansi_version: 2,
vendor: "Nikon".into(),
product: product.into(),
revision: "1.00".into(),
}),
model: Model::from_product(product),
opened: true,
}
}
fn attached() -> Vec<Device> {
vec![
usb("1", &[3, 2], "LS-5000 ED"),
usb("1", &[4], "LS-5000 ED"),
usb("2", &[1], "LS-9000 ED"),
]
}
#[test]
fn identical_models_are_told_apart_by_port() {
let devices = attached();
assert_eq!(devices[0].attach.to_string(), "usb:1-3.2");
let picked = "usb:1-4".parse::<Selector>().unwrap().resolve(&devices);
assert_eq!(picked.unwrap().attach, devices[1].attach);
}
#[test]
fn no_selector_needs_exactly_one_scanner() {
assert!(Selector::Only.resolve(&attached()[..1]).is_ok());
assert!(matches!(
Selector::Only.resolve(&attached()),
Err(SelectError::Ambiguous(_))
));
assert!(matches!(
Selector::Only.resolve(&[]),
Err(SelectError::NotFound)
));
assert!(matches!(
"usb:9-9".parse::<Selector>().unwrap().resolve(&attached()),
Err(SelectError::NotFound)
));
}
}