From e3720b8afb6471ee635ed58504cf36e93145539a Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Mon, 13 Oct 2025 23:36:52 +0100 Subject: [PATCH] chore: update README Signed-off-by: Nikolaos Karaolidis --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 20 ++- src/cli/backup/mod.rs | 29 ++-- src/cli/common/file.rs | 26 ++-- src/cli/common/film.rs | 13 +- src/cli/device/mod.rs | 39 +++-- src/hardware/mod.rs | 322 +++++++++++++++++++++++++++++++++-------- src/hardware/xt5.rs | 55 ------- src/log.rs | 4 +- src/main.rs | 12 +- src/usb/mod.rs | 30 ++-- 12 files changed, 354 insertions(+), 198 deletions(-) delete mode 100644 src/hardware/xt5.rs diff --git a/Cargo.lock b/Cargo.lock index 953add4..65b6a81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" name = "fujicli" version = "0.1.0" dependencies = [ + "anyhow", "clap", "libptp", "log", diff --git a/Cargo.toml b/Cargo.toml index bcddae2..9f56185 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ lto = true codegen-units = 1 [dependencies] +anyhow = "1.0.100" clap = { version = "4.5.48", features = ["derive", "wrap_help"] } libptp = "0.6.5" log = "0.4.28" diff --git a/README.md b/README.md index 5b0c2b9..3d02a39 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ # fujicli -A CLI that performs on-device image rendering similar to Fujifilm X RAW Studio \ No newline at end of file +A CLI to manage Fujifilm devices, simulations, backups, and rendering. + +## Status + +This tool has only been tested with the **Fujifilm X-T5**, as it is the sole camera I own. While the underlying PTP commands may be compatible with other models, **compatibility is not guaranteed**. + +**Use this software at your own risk.** I am not responsible for any damage, loss of data, or other adverse outcomes - physical or psychological - to your camera or equipment resulting from the use of this program. + +This project is currently under heavy development. Contributions are welcome. If you own a different Fujifilm camera, testing and reporting compatibility is highly appreciated. + +## Resources + +This project builds upon the following fantastic reverse-engineering efforts: + +* [fujihack](https://github.com/fujihack/fujihack) +* [fudge](https://github.com/petabyt/fudge) +* [libpict](https://github.com/petabyt/libpict) +* [fp](https://github.com/petabyt/fp) +* [libgphoto2](https://github.com/gphoto/libgphoto2) diff --git a/src/cli/backup/mod.rs b/src/cli/backup/mod.rs index ea1865d..741310c 100644 --- a/src/cli/backup/mod.rs +++ b/src/cli/backup/mod.rs @@ -1,5 +1,3 @@ -use std::error::Error; - use crate::usb; use super::common::file::{Input, Output}; @@ -22,35 +20,32 @@ pub enum BackupCmd { }, } -fn handle_export( - device_id: Option<&str>, - output: Output, -) -> Result<(), Box> { +fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> { let camera = usb::get_camera(device_id)?; + let mut ptp = camera.ptp_session()?; let mut writer = output.get_writer()?; - - todo!(); + let backup = camera.export_backup(&mut ptp)?; + writer.write_all(&backup)?; Ok(()) } -fn handle_import( - device_id: Option<&str>, - input: Input, -) -> Result<(), Box> { +fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> { let camera = usb::get_camera(device_id)?; + let mut ptp = camera.ptp_session()?; let mut reader = input.get_reader()?; - - todo!(); + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer)?; + camera.import_backup(&mut ptp, &buffer)?; Ok(()) } -pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), Box> { +pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> { match cmd { - BackupCmd::Export { output_file } => handle_export(device_id, output_file), - BackupCmd::Import { input_file } => handle_import(device_id, input_file), + BackupCmd::Export { output_file } => handle_export(device_id, &output_file), + BackupCmd::Import { input_file } => handle_import(device_id, &input_file), } } diff --git a/src/cli/common/file.rs b/src/cli/common/file.rs index b2f7e3b..3b11bb3 100644 --- a/src/cli/common/file.rs +++ b/src/cli/common/file.rs @@ -1,4 +1,4 @@ -use std::{error::Error, fs::File, io, path::PathBuf, str::FromStr}; +use std::{fs::File, io, path::PathBuf, str::FromStr}; #[derive(Debug, Clone)] pub enum Input { @@ -7,21 +7,21 @@ pub enum Input { } impl FromStr for Input { - type Err = Box; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { if s == "-" { - Ok(Input::Stdin) + Ok(Self::Stdin) } else { - Ok(Input::Path(PathBuf::from(s))) + Ok(Self::Path(PathBuf::from(s))) } } } impl Input { - pub fn get_reader(&self) -> Result, Box> { + pub fn get_reader(&self) -> Result, anyhow::Error> { match self { - Input::Stdin => Ok(Box::new(io::stdin())), - Input::Path(path) => Ok(Box::new(File::open(path)?)), + Self::Stdin => Ok(Box::new(io::stdin())), + Self::Path(path) => Ok(Box::new(File::open(path)?)), } } } @@ -33,21 +33,21 @@ pub enum Output { } impl FromStr for Output { - type Err = Box; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { if s == "-" { - Ok(Output::Stdout) + Ok(Self::Stdout) } else { - Ok(Output::Path(PathBuf::from(s))) + Ok(Self::Path(PathBuf::from(s))) } } } impl Output { - pub fn get_writer(&self) -> Result, Box> { + pub fn get_writer(&self) -> Result, anyhow::Error> { match self { - Output::Stdout => Ok(Box::new(io::stdout())), - Output::Path(path) => Ok(Box::new(File::create(path)?)), + Self::Stdout => Ok(Box::new(io::stdout())), + Self::Path(path) => Ok(Box::new(File::create(path)?)), } } } diff --git a/src/cli/common/film.rs b/src/cli/common/film.rs index de95caa..c266a73 100644 --- a/src/cli/common/film.rs +++ b/src/cli/common/film.rs @@ -1,5 +1,4 @@ -use std::error::Error; - +use anyhow::bail; use clap::Args; #[derive(Debug, Clone)] @@ -9,18 +8,18 @@ pub enum SimulationSelector { } impl std::str::FromStr for SimulationSelector { - type Err = Box; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { if let Ok(slot) = s.parse::() { - return Ok(SimulationSelector::Slot(slot)); + return Ok(Self::Slot(slot)); } if s.is_empty() { - Err("Simulation name cannot be empty".into()) - } else { - Ok(SimulationSelector::Name(s.to_string())) + bail!("Simulation name cannot be empty") } + + Ok(Self::Name(s.to_string())) } } diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs index 1868ace..2c9a0bc 100644 --- a/src/cli/device/mod.rs +++ b/src/cli/device/mod.rs @@ -1,14 +1,14 @@ -use std::{error::Error, fmt}; +use std::fmt; use clap::Subcommand; use serde::Serialize; use crate::{ - hardware::{CameraImpl, FujiUsbMode}, + hardware::{CameraImpl, UsbMode}, usb, }; -#[derive(Subcommand, Debug)] +#[derive(Subcommand, Debug, Clone, Copy)] pub enum DeviceCmd { /// List cameras #[command(alias = "l")] @@ -29,7 +29,7 @@ pub struct CameraItemRepr { impl From<&Box> for CameraItemRepr { fn from(camera: &Box) -> Self { - CameraItemRepr { + Self { id: camera.usb_id(), name: camera.id().name.to_string(), vendor_id: format!("0x{:04x}", camera.id().vendor), @@ -48,10 +48,10 @@ impl fmt::Display for CameraItemRepr { } } -fn handle_list(json: bool) -> Result<(), Box> { +fn handle_list(json: bool) -> Result<(), anyhow::Error> { let cameras: Vec = usb::get_connected_camers()? .iter() - .map(|d| d.into()) + .map(std::convert::Into::into) .collect(); if json { @@ -66,7 +66,7 @@ fn handle_list(json: bool) -> Result<(), Box> { println!("Connected cameras:"); for d in cameras { - println!("- {}", d); + println!("- {d}"); } Ok(()) @@ -81,7 +81,7 @@ pub struct CameraRepr { pub model: String, pub device_version: String, pub serial_number: String, - pub mode: FujiUsbMode, + pub mode: UsbMode, pub battery: u32, } @@ -103,19 +103,22 @@ impl fmt::Display for CameraRepr { } } -fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), Box> { - let mut camera = usb::get_camera(device_id)?; +fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> { + let camera = usb::get_camera(device_id)?; + let mut ptp = camera.ptp(); - let info = camera.get_info()?; - let mode = camera.get_fuji_usb_mode()?; - let battery = camera.get_fuji_battery_info()?; + let info = camera.get_info(&mut ptp)?; + + let mut ptp = camera.open_session(ptp)?; + let mode = camera.get_usb_mode(&mut ptp)?; + let battery = camera.get_battery_info(&mut ptp)?; let repr = CameraRepr { device: (&camera).into(), manufacturer: info.Manufacturer.clone(), model: info.Model.clone(), device_version: info.DeviceVersion.clone(), - serial_number: info.SerialNumber.clone(), + serial_number: info.SerialNumber, mode, battery, }; @@ -125,15 +128,11 @@ fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), Box, -) -> Result<(), Box> { +pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> { match cmd { DeviceCmd::List => handle_list(json), DeviceCmd::Info => handle_info(json, device_id), diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs index 85b88fb..1543df5 100644 --- a/src/hardware/mod.rs +++ b/src/hardware/mod.rs @@ -1,12 +1,15 @@ -use std::{error::Error, fmt, time::Duration}; +use std::{ + fmt, + ops::{Deref, DerefMut}, + time::Duration, +}; +use anyhow::bail; use libptp::{DeviceInfo, StandardCommandCode}; -use log::debug; +use log::{debug, error}; use rusb::{DeviceDescriptor, GlobalContext}; use serde::Serialize; -mod xt5; - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CameraId { pub name: &'static str, @@ -14,16 +17,16 @@ pub struct CameraId { pub product: u16, } +type CameraFactory = fn(rusb::Device) -> Result, anyhow::Error>; + pub struct SupportedCamera { pub id: CameraId, - pub factory: fn( - rusb::Device, - ) -> Result, Box>, + pub factory: CameraFactory, } pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera { - id: xt5::FUJIFILM_XT5, - factory: |d| xt5::FujifilmXT5::new_boxed(d), + id: FUJIFILM_XT5, + factory: |d| FujifilmXT5::new_boxed(&d), }]; impl SupportedCamera { @@ -41,43 +44,106 @@ pub enum DevicePropCode { FujiBatteryInfo2 = 0xD36B, } -#[derive(Debug, Clone, Copy, Serialize)] -pub enum FujiUsbMode { - RawConversion, // mode == 6 +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +pub enum UsbMode { + RawConversion, Unsupported, } -impl From for FujiUsbMode { +impl From for UsbMode { fn from(val: u32) -> Self { match val { - 6 => FujiUsbMode::RawConversion, - _ => FujiUsbMode::Unsupported, + 6 => Self::RawConversion, + _ => Self::Unsupported, } } } -impl fmt::Display for FujiUsbMode { +impl fmt::Display for UsbMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { - FujiUsbMode::RawConversion => "USB RAW CONV./BACKUP RESTORE", - FujiUsbMode::Unsupported => "Unsupported USB Mode", + Self::RawConversion => "USB RAW CONV./BACKUP RESTORE", + Self::Unsupported => "Unsupported USB Mode", }; - write!(f, "{}", s) + write!(f, "{s}") + } +} + +pub struct Ptp { + ptp: libptp::Camera, +} + +impl Deref for Ptp { + type Target = libptp::Camera; + + fn deref(&self) -> &Self::Target { + &self.ptp + } +} + +impl DerefMut for Ptp { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ptp + } +} + +impl From> for Ptp { + fn from(ptp: libptp::Camera) -> Self { + Self { ptp } + } +} + +type SessionCloseFn = + Box) -> Result<(), anyhow::Error>>; + +pub struct PtpSession { + ptp: libptp::Camera, + session_id: u32, + close_fn: Option, +} + +impl Deref for PtpSession { + type Target = libptp::Camera; + + fn deref(&self) -> &Self::Target { + &self.ptp + } +} + +impl DerefMut for PtpSession { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ptp + } +} + +impl Drop for PtpSession { + fn drop(&mut self) { + if let Some(close_fn) = self.close_fn.take() { + if let Err(e) = close_fn(self.session_id, &mut self.ptp) { + error!("Error closing session {}: {}", self.session_id, e); + } + } } } pub trait CameraImpl { fn id(&self) -> &'static CameraId; - fn usb_id(&self) -> String; + fn device(&self) -> &rusb::Device; - fn ptp(&self) -> libptp::Camera; + fn usb_id(&self) -> String { + let bus = self.device().bus_number(); + let address = self.device().address(); + format!("{bus}.{address}") + } - fn get_info(&self) -> Result> { + fn ptp(&self) -> Ptp; + + fn ptp_session(&self) -> Result; + + fn get_info(&self, ptp: &mut Ptp) -> Result { debug!("Sending GetDeviceInfo command"); - let response = - self.ptp() - .command(StandardCommandCode::GetDeviceInfo, &[], None, Some(TIMEOUT))?; + let response = ptp.command(StandardCommandCode::GetDeviceInfo, &[], None, Some(TIMEOUT))?; debug!("Received response with {} bytes", response.len()); let info = DeviceInfo::decode(&response)?; @@ -86,46 +152,47 @@ pub trait CameraImpl { fn next_session_id(&self) -> u32; - fn open_session(&self) -> Result<(), Box> { + fn open_session(&self, ptp: Ptp) -> Result { let session_id = self.next_session_id(); + let mut ptp = ptp.ptp; - debug!("Opening new session with id {}", session_id); - self.ptp().command( + debug!("Opening session with id {session_id}"); + ptp.command( StandardCommandCode::OpenSession, &[session_id], None, Some(TIMEOUT), )?; + debug!("Session {session_id} open"); - Ok(()) - } + let close_fn: Option = Some(Box::new(move |_, ptp| { + debug!("Closing session with id {session_id}"); + ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?; + debug!("Session {session_id} closed"); + Ok(()) + })); - fn close_session(&self) -> Result<(), Box> { - debug!("Closing session"); - self.ptp() - .command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?; - - Ok(()) + Ok(PtpSession { + ptp, + session_id, + close_fn, + }) } fn get_prop_value_raw( &self, + ptp: &mut PtpSession, prop: DevicePropCode, - ) -> Result, Box> { - self.open_session()?; + ) -> Result, anyhow::Error> { + debug!("Getting property {prop:?}"); - debug!("Getting property {:?}", prop); - - let response = self.ptp().command( + let response = ptp.command( StandardCommandCode::GetDevicePropValue, &[prop as u32], None, Some(TIMEOUT), - ); + )?; - self.close_session()?; - - let response = response?; debug!("Received response with {} bytes", response.len()); Ok(response) @@ -133,29 +200,30 @@ pub trait CameraImpl { fn get_prop_value_scalar( &self, + ptp: &mut PtpSession, prop: DevicePropCode, - ) -> Result> { - let data = self.get_prop_value_raw(prop)?; + ) -> Result { + let data = self.get_prop_value_raw(ptp, prop)?; match data.len() { - 1 => Ok(data[0] as u32), - 2 => Ok(u16::from_le_bytes([data[0], data[1]]) as u32), + 1 => Ok(u32::from(data[0])), + 2 => Ok(u32::from(u16::from_le_bytes([data[0], data[1]]))), 4 => Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])), - n => Err(format!("Cannot parse property {:?} as scalar: {} bytes", prop, n).into()), + n => bail!("Cannot parse property {prop:?} as scalar: {n} bytes"), } } - fn get_fuji_usb_mode(&self) -> Result> { - let result = self.get_prop_value_scalar(DevicePropCode::FujiUsbMode)?; + fn get_usb_mode(&self, ptp: &mut PtpSession) -> Result { + let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?; Ok(result.into()) } - fn get_fuji_battery_info(&self) -> Result> { - let data = self.get_prop_value_raw(DevicePropCode::FujiBatteryInfo2)?; - debug!("Raw battery data: {:?}", data); + fn get_battery_info(&self, ptp: &mut PtpSession) -> Result { + let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?; + debug!("Raw battery data: {data:?}"); if data.len() < 3 { - return Err("Battery info payload too short".into()); + bail!("Battery info payload too short"); } let utf16: Vec = data[1..] @@ -164,17 +232,153 @@ pub trait CameraImpl { .take_while(|&c| c != 0) .collect(); - debug!("Decoded UTF-16 units: {:?}", utf16); + debug!("Decoded UTF-16 units: {utf16:?}"); let utf8_string = String::from_utf16(&utf16)?; - debug!("Decoded UTF-16 string: {}", utf8_string); + debug!("Decoded UTF-16 string: {utf8_string}"); let percentage: u32 = utf8_string .split(',') .next() - .ok_or("Failed to parse battery percentage")? + .ok_or_else(|| anyhow::anyhow!("Failed to parse battery percentage"))? .parse()?; Ok(percentage) } + + fn export_backup(&self, ptp: &mut PtpSession) -> Result, anyhow::Error> { + const HANDLE: u32 = 0x0; + + debug!("Getting object info for backup"); + + let info = ptp.command( + StandardCommandCode::GetObjectInfo, + &[HANDLE], + None, + Some(TIMEOUT), + )?; + + debug!("Got object info, {} bytes", info.len()); + + debug!("Downloading backup object"); + + let object = ptp.command( + StandardCommandCode::GetObject, + &[HANDLE], + None, + Some(TIMEOUT), + )?; + + debug!("Downloaded backup object ({} bytes)", object.len()); + + Ok(object) + } + + fn import_backup(&self, ptp: &mut PtpSession, buffer: &[u8]) -> Result<(), anyhow::Error> { + todo!("This is currently broken"); + + debug!("Preparing ObjectInfo header for backup"); + + let mut obj_info = vec![0u8; 1088]; + let mut offset = 0; + + let padding0: u32 = 0x0; + let object_format: u16 = 0x5000; + let padding1: u16 = 0x0; + + obj_info[offset..offset + size_of::()].copy_from_slice(&padding0.to_le_bytes()); + offset += size_of::(); + obj_info[offset..offset + size_of::()].copy_from_slice(&object_format.to_le_bytes()); + offset += size_of::(); + obj_info[offset..offset + size_of::()].copy_from_slice(&padding1.to_le_bytes()); + offset += size_of::(); + obj_info[offset..offset + size_of::()] + .copy_from_slice(&u32::try_from(buffer.len())?.to_le_bytes()); + + let param0: u32 = 0x0; + let param1: u32 = 0x0; + + debug!("Sending ObjectInfo for backup"); + + ptp.command( + libptp::StandardCommandCode::SendObjectInfo, + &[param0, param1], + Some(&obj_info), + Some(TIMEOUT), + )?; + + debug!("Sending backup payload ({} bytes)", buffer.len()); + + ptp.command( + libptp::StandardCommandCode::SendObject, + &[], + Some(buffer), + Some(TIMEOUT), + )?; + + Ok(()) + } } + +macro_rules! default_camera_impl { + ( + $const_name:ident, + $struct_name:ident, + $vendor:expr, + $product:expr, + $display_name:expr + ) => { + pub const $const_name: CameraId = CameraId { + name: $display_name, + vendor: $vendor, + product: $product, + }; + + pub struct $struct_name { + device: rusb::Device, + session_counter: std::sync::atomic::AtomicU32, + } + + impl $struct_name { + pub fn new_boxed( + rusb_device: &rusb::Device, + ) -> Result, anyhow::Error> { + let session_counter = std::sync::atomic::AtomicU32::new(1); + + let handle = rusb_device.open()?; + let device = handle.device(); + + Ok(Box::new(Self { + device, + session_counter, + })) + } + } + + impl CameraImpl for $struct_name { + fn id(&self) -> &'static CameraId { + &$const_name + } + + fn device(&self) -> &rusb::Device { + &self.device + } + + fn ptp(&self) -> Ptp { + libptp::Camera::new(&self.device).unwrap().into() + } + + fn ptp_session(&self) -> Result { + let ptp = self.ptp(); + self.open_session(ptp) + } + + fn next_session_id(&self) -> u32 { + self.session_counter + .fetch_add(1, std::sync::atomic::Ordering::SeqCst) + } + } + }; +} + +default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5"); diff --git a/src/hardware/xt5.rs b/src/hardware/xt5.rs deleted file mode 100644 index 8e3b60f..0000000 --- a/src/hardware/xt5.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{ - error::Error, - sync::atomic::{AtomicU32, Ordering}, -}; - -use rusb::GlobalContext; - -use super::{CameraId, CameraImpl}; - -pub const FUJIFILM_XT5: CameraId = CameraId { - name: "FUJIFILM XT-5", - vendor: 0x04cb, - product: 0x02fc, -}; - -pub struct FujifilmXT5 { - device: rusb::Device, - session_counter: AtomicU32, -} - -impl FujifilmXT5 { - pub fn new_boxed( - rusb_device: rusb::Device, - ) -> Result, Box> { - let session_counter = AtomicU32::new(1); - - let handle = rusb_device.open()?; - let device = handle.device(); - - Ok(Box::new(Self { - session_counter, - device, - })) - } -} - -impl CameraImpl for FujifilmXT5 { - fn id(&self) -> &'static CameraId { - &FUJIFILM_XT5 - } - - fn usb_id(&self) -> String { - let bus = self.device.bus_number(); - let address = self.device.address(); - format!("{}.{}", bus, address) - } - - fn ptp(&self) -> libptp::Camera { - libptp::Camera::new(&self.device).unwrap() - } - - fn next_session_id(&self) -> u32 { - self.session_counter.fetch_add(1, Ordering::SeqCst) - } -} diff --git a/src/log.rs b/src/log.rs index 5adce50..396e3ac 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,5 +1,3 @@ -use std::error::Error; - use log::LevelFilter; use log4rs::{ Config, @@ -8,7 +6,7 @@ use log4rs::{ encode::pattern::PatternEncoder, }; -pub fn init(quiet: bool, verbose: bool) -> Result<(), Box> { +pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> { let level = if quiet { LevelFilter::Warn } else if verbose { diff --git a/src/main.rs b/src/main.rs index cfc5f1e..62e449c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ -use std::error::Error; +#![warn(clippy::all, clippy::pedantic, clippy::nursery)] +#![allow(clippy::missing_docs_in_private_items)] use clap::Parser; use cli::Commands; @@ -8,15 +9,16 @@ mod hardware; mod log; mod usb; -fn main() -> Result<(), Box> { +fn main() -> Result<(), anyhow::Error> { let cli = cli::Cli::parse(); log::init(cli.quiet, cli.verbose)?; + let device_id = cli.device.as_deref(); + match cli.command { - Commands::Device(device_cmd) => { - cli::device::handle(device_cmd, cli.json, cli.device.as_deref())? - } + Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?, + Commands::Backup(backup_cmd) => cli::backup::handle(backup_cmd, device_id)?, _ => todo!(), } diff --git a/src/usb/mod.rs b/src/usb/mod.rs index eb65505..51ea467 100644 --- a/src/usb/mod.rs +++ b/src/usb/mod.rs @@ -1,18 +1,16 @@ -use std::error::Error; +use anyhow::{anyhow, bail}; use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS}; -pub fn get_connected_camers() --> Result>, Box> { +pub fn get_connected_camers() -> Result>, anyhow::Error> { let mut connected_cameras = Vec::new(); for device in rusb::devices()?.iter() { - let descriptor = match device.device_descriptor() { - Ok(d) => d, - Err(_) => continue, + let Ok(descriptor) = device.device_descriptor() else { + continue; }; - for camera in SUPPORTED_CAMERAS.iter() { + for camera in SUPPORTED_CAMERAS { if camera.matches_descriptor(&descriptor) { let camera = (camera.factory)(device)?; connected_cameras.push(camera); @@ -24,12 +22,10 @@ pub fn get_connected_camers() Ok(connected_cameras) } -pub fn get_connected_camera_by_id( - id: &str, -) -> Result, Box> { +pub fn get_connected_camera_by_id(id: &str) -> Result, anyhow::Error> { let parts: Vec<&str> = id.split('.').collect(); if parts.len() != 2 { - return Err(format!("Invalid device id format: {}", id).into()); + bail!("Invalid device id format: {id}"); } let bus: u8 = parts[0].parse()?; @@ -39,28 +35,26 @@ pub fn get_connected_camera_by_id( if device.bus_number() == bus && device.address() == address { let descriptor = device.device_descriptor()?; - for camera in SUPPORTED_CAMERAS.iter() { + for camera in SUPPORTED_CAMERAS { if camera.matches_descriptor(&descriptor) { let camera = (camera.factory)(device)?; return Ok(camera); } } - return Err(format!("Device found at {} but is not supported", id).into()); + bail!("Device found at {id} but is not supported"); } } - Err(format!("No device found with id: {}", id).into()) + bail!("No device found with id: {id}"); } -pub fn get_camera( - device_id: Option<&str>, -) -> Result, Box> { +pub fn get_camera(device_id: Option<&str>) -> Result, anyhow::Error> { match device_id { Some(id) => get_connected_camera_by_id(id), None => get_connected_camers()? .into_iter() .next() - .ok_or_else(|| "No supported devices connected.".into()), + .ok_or_else(|| anyhow!("No supported devices connected.")), } }