diff options
Diffstat (limited to 'src/device/mod.rs')
-rw-r--r-- | src/device/mod.rs | 785 |
1 files changed, 785 insertions, 0 deletions
diff --git a/src/device/mod.rs b/src/device/mod.rs new file mode 100644 index 0000000..c84faa1 --- /dev/null +++ b/src/device/mod.rs @@ -0,0 +1,785 @@ +// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org> +// SPDX-License-Identifier: MIT + +mod pro; +mod storage; +mod wrapper; + +use std::convert::{TryFrom, TryInto}; +use std::ffi; +use std::fmt; +use std::str; + +use crate::auth::Authenticate; +use crate::config::{Config, RawConfig}; +use crate::error::{CommunicationError, Error, LibraryError}; +use crate::otp::GenerateOtp; +use crate::pws::GetPasswordSafe; +use crate::util::{ + get_command_result, get_cstring, owned_str_from_ptr, result_or_error, run_with_string, +}; + +pub use pro::Pro; +pub use storage::{ + OperationStatus, SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode, + VolumeStatus, +}; +pub use wrapper::DeviceWrapper; + +/// Available Nitrokey models. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Model { + /// The Nitrokey Storage. + Storage, + /// The Nitrokey Pro. + Pro, +} + +impl fmt::Display for Model { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Model::Pro => "Pro", + Model::Storage => "Storage", + }) + } +} + +impl From<Model> for nitrokey_sys::NK_device_model { + fn from(model: Model) -> Self { + match model { + Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE, + Model::Pro => nitrokey_sys::NK_device_model_NK_PRO, + } + } +} + +impl TryFrom<nitrokey_sys::NK_device_model> for Model { + type Error = Error; + + fn try_from(model: nitrokey_sys::NK_device_model) -> Result<Self, Error> { + match model { + nitrokey_sys::NK_device_model_NK_DISCONNECTED => { + Err(CommunicationError::NotConnected.into()) + } + nitrokey_sys::NK_device_model_NK_PRO => Ok(Model::Pro), + nitrokey_sys::NK_device_model_NK_STORAGE => Ok(Model::Storage), + _ => Err(Error::UnsupportedModelError), + } + } +} + +/// Serial number of a Nitrokey device. +/// +/// The serial number can be formatted as a string using the [`ToString`][] trait, and it can be +/// parsed from a string using the [`FromStr`][] trait. It can also be represented as a 32-bit +/// unsigned integer using [`as_u32`][]. This integer is the ID of the smartcard of the Nitrokey +/// device. +/// +/// Neither the format of the string representation nor the integer representation are guaranteed +/// to stay the same for new firmware versions. +/// +/// [`as_u32`]: #method.as_u32 +/// [`FromStr`]: #impl-FromStr +/// [`ToString`]: #impl-ToString +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SerialNumber { + value: u32, +} + +impl SerialNumber { + /// Creates an emtpty serial number. + /// + /// This function can be used to create a placeholder value or to compare a `SerialNumber` + /// instance with an empty serial number. + pub fn empty() -> Self { + SerialNumber::new(0) + } + + fn new(value: u32) -> Self { + SerialNumber { value } + } + + /// Returns the integer reprensentation of this serial number. + /// + /// This integer currently is the ID of the smartcard of the Nitrokey device. Upcoming + /// firmware versions might change the meaning of this representation, or add additional + /// components to the serial number. + // To provide a stable API even if the internal representation of SerialNumber changes, we want + // to borrow SerialNumber instead of copying it even if it might be less efficient. + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn as_u32(&self) -> u32 { + self.value + } +} + +impl fmt::Display for SerialNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#010x}", self.value) + } +} + +impl str::FromStr for SerialNumber { + type Err = Error; + + /// Try to parse a serial number from a hex string. + /// + /// The input string must be a valid hex string. Optionally, it can include a `0x` prefix. + /// + /// # Errors + /// + /// - [`InvalidHexString`][] if the given string is not a valid hex string + /// + /// # Example + /// + /// ```no_run + /// use std::convert::TryFrom; + /// use nitrokey::{DeviceInfo, Error, SerialNumber}; + /// + /// fn find_device(serial_number: &str) -> Result<Option<DeviceInfo>, Error> { + /// let serial_number: SerialNumber = serial_number.parse()?; + /// Ok(nitrokey::list_devices()? + /// .into_iter() + /// .filter(|device| device.serial_number == Some(serial_number)) + /// .next()) + /// } + /// + /// ``` + /// + /// [`InvalidHexString`]: enum.LibraryError.html#variant.InvalidHexString + fn from_str(s: &str) -> Result<SerialNumber, Error> { + // ignore leading 0x + let hex_string = if s.starts_with("0x") { + s.split_at(2).1 + } else { + s + }; + + u32::from_str_radix(hex_string, 16) + .map(SerialNumber::new) + .map_err(|_| LibraryError::InvalidHexString.into()) + } +} + +/// Connection information for a Nitrokey device. +#[derive(Clone, Debug, PartialEq)] +pub struct DeviceInfo { + /// The model of the Nitrokey device, or `None` if the model is not supported by this crate. + pub model: Option<Model>, + /// The USB device path. + pub path: String, + /// The serial number of the device, or `None` if the device does not expose its serial number. + pub serial_number: Option<SerialNumber>, +} + +impl TryFrom<&nitrokey_sys::NK_device_info> for DeviceInfo { + type Error = Error; + + fn try_from(device_info: &nitrokey_sys::NK_device_info) -> Result<DeviceInfo, Error> { + let model_result = device_info.model.try_into(); + let model_option = model_result.map(Some).or_else(|err| match err { + Error::UnsupportedModelError => Ok(None), + _ => Err(err), + })?; + let serial_number = unsafe { ffi::CStr::from_ptr(device_info.serial_number) } + .to_str() + .map_err(Error::from)?; + Ok(DeviceInfo { + model: model_option, + path: owned_str_from_ptr(device_info.path)?, + serial_number: get_hidapi_serial_number(serial_number), + }) + } +} + +impl fmt::Display for DeviceInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.model { + Some(model) => write!(f, "Nitrokey {}", model)?, + None => write!(f, "Unsupported Nitrokey model")?, + } + write!(f, " at {} with ", self.path)?; + match self.serial_number { + Some(serial_number) => write!(f, "serial no. {}", serial_number), + None => write!(f, "an unknown serial number"), + } + } +} + +/// Parses a serial number returned by hidapi. +/// +/// If the serial number is all zero, this function returns `None`. Otherwise, it uses the last +/// eight characters. If these are all zero, the first eight characters are used instead. The +/// selected substring is parse as a hex string and its integer value is returned from the +/// function. If the string cannot be parsed, this function returns `None`. +/// +/// The reason for this behavior is that the Nitrokey Storage does not report its serial number at +/// all (all zero value), while the Nitrokey Pro with firmware 0.9 or later writes its serial +/// number to the last eight characters. Nitrokey Pro devices with firmware 0.8 or earlier wrote +/// their serial number to the first eight characters. +fn get_hidapi_serial_number(serial_number: &str) -> Option<SerialNumber> { + let len = serial_number.len(); + if len < 8 { + // The serial number in the USB descriptor has 12 bytes, we need at least four + return None; + } + + let mut iter = serial_number.char_indices().rev(); + if let Some((i, _)) = iter.find(|(_, c)| *c != '0') { + let substr = if len - i < 8 { + // The last eight characters contain at least one non-zero character --> use them + serial_number.split_at(len - 8).1 + } else { + // The last eight characters are all zero --> use the first eight + serial_number.split_at(8).0 + }; + substr.parse().ok() + } else { + // The serial number is all zero + None + } +} + +/// A firmware version for a Nitrokey device. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FirmwareVersion { + /// The major firmware version, e. g. 0 in v0.40. + pub major: u8, + /// The minor firmware version, e. g. 40 in v0.40. + pub minor: u8, +} + +impl fmt::Display for FirmwareVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "v{}.{}", self.major, self.minor) + } +} + +/// The status information common to all Nitrokey devices. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Status { + /// The firmware version of the device. + pub firmware_version: FirmwareVersion, + /// The serial number of the device. + pub serial_number: SerialNumber, + /// The configuration of the device. + pub config: Config, +} + +impl From<nitrokey_sys::NK_status> for Status { + fn from(status: nitrokey_sys::NK_status) -> Self { + Self { + firmware_version: FirmwareVersion { + major: status.firmware_version_major, + minor: status.firmware_version_minor, + }, + serial_number: SerialNumber::new(status.serial_number_smart_card), + config: RawConfig::from(&status).into(), + } + } +} + +/// A Nitrokey device. +/// +/// This trait provides the commands that can be executed without authentication and that are +/// present on all supported Nitrokey devices. +pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt::Debug { + /// Returns the [`Manager`][] instance that has been used to connect to this device. + /// + /// # Example + /// + /// ``` + /// use nitrokey::{Device, DeviceWrapper}; + /// + /// fn do_something(device: DeviceWrapper) { + /// // reconnect to any device + /// let manager = device.into_manager(); + /// let device = manager.connect(); + /// // do something with the device + /// // ... + /// } + /// + /// match nitrokey::take()?.connect() { + /// Ok(device) => do_something(device), + /// Err(err) => println!("Could not connect to a Nitrokey: {}", err), + /// } + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + fn into_manager(self) -> &'a mut crate::Manager; + + /// Returns the model of the connected Nitrokey device. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let device = manager.connect()?; + /// println!("Connected to a Nitrokey {}", device.get_model()); + /// # Ok(()) + /// # } + fn get_model(&self) -> Model; + + /// Returns the status of the Nitrokey device. + /// + /// This methods returns the status information common to all Nitrokey devices as a + /// [`Status`][] struct. Some models may provide more information, for example + /// [`get_storage_status`][] returns the [`StorageStatus`][] struct. + /// + /// # Errors + /// + /// - [`NotConnected`][] if the Nitrokey device has been disconnected + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// + /// let mut manager = nitrokey::take()?; + /// let device = manager.connect()?; + /// let status = device.get_status()?; + /// println!("Firmware version: {}", status.firmware_version); + /// println!("Serial number: {}", status.serial_number); + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + /// + /// [`get_storage_status`]: struct.Storage.html#method.get_storage_status + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + /// [`Status`]: struct.Status.html + /// [`StorageStatus`]: struct.StorageStatus.html + fn get_status(&self) -> Result<Status, Error>; + + /// Returns the serial number of the Nitrokey device. + /// + /// For display purpuses, the serial number should be formatted as an 8-digit hex string. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let device = manager.connect()?; + /// match device.get_serial_number() { + /// Ok(number) => println!("serial no: {}", number), + /// Err(err) => eprintln!("Could not get serial number: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + fn get_serial_number(&self) -> Result<SerialNumber, Error> { + run_with_string(unsafe { nitrokey_sys::NK_device_serial_number() }, |s| { + s.parse() + }) + } + + /// Returns the number of remaining authentication attempts for the user. The total number of + /// available attempts is three. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let device = manager.connect()?; + /// let count = device.get_user_retry_count(); + /// match device.get_user_retry_count() { + /// Ok(count) => println!("{} remaining authentication attempts (user)", count), + /// Err(err) => eprintln!("Could not get user retry count: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + fn get_user_retry_count(&self) -> Result<u8, Error> { + result_or_error(unsafe { nitrokey_sys::NK_get_user_retry_count() }) + } + + /// Returns the number of remaining authentication attempts for the admin. The total number of + /// available attempts is three. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let device = manager.connect()?; + /// let count = device.get_admin_retry_count(); + /// match device.get_admin_retry_count() { + /// Ok(count) => println!("{} remaining authentication attempts (admin)", count), + /// Err(err) => eprintln!("Could not get admin retry count: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + fn get_admin_retry_count(&self) -> Result<u8, Error> { + result_or_error(unsafe { nitrokey_sys::NK_get_admin_retry_count() }) + } + + /// Returns the firmware version. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let device = manager.connect()?; + /// match device.get_firmware_version() { + /// Ok(version) => println!("Firmware version: {}", version), + /// Err(err) => eprintln!("Could not access firmware version: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + fn get_firmware_version(&self) -> Result<FirmwareVersion, Error> { + let major = result_or_error(unsafe { nitrokey_sys::NK_get_major_firmware_version() })?; + let minor = result_or_error(unsafe { nitrokey_sys::NK_get_minor_firmware_version() })?; + Ok(FirmwareVersion { major, minor }) + } + + /// Returns the current configuration of the Nitrokey device. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let device = manager.connect()?; + /// let config = device.get_config()?; + /// println!("numlock binding: {:?}", config.numlock); + /// println!("capslock binding: {:?}", config.capslock); + /// println!("scrollock binding: {:?}", config.scrollock); + /// println!("require password for OTP: {:?}", config.user_password); + /// # Ok(()) + /// # } + /// ``` + fn get_config(&self) -> Result<Config, Error> { + let mut raw_status = nitrokey_sys::NK_status { + firmware_version_major: 0, + firmware_version_minor: 0, + serial_number_smart_card: 0, + config_numlock: 0, + config_capslock: 0, + config_scrolllock: 0, + otp_user_password: false, + }; + get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?; + Ok(RawConfig::from(&raw_status).into()) + } + + /// Changes the administrator PIN. + /// + /// # Errors + /// + /// - [`InvalidString`][] if one of the provided passwords contains a null byte + /// - [`WrongPassword`][] if the current admin password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let mut device = manager.connect()?; + /// match device.change_admin_pin("12345678", "12345679") { + /// Ok(()) => println!("Updated admin PIN."), + /// Err(err) => eprintln!("Failed to update admin PIN: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn change_admin_pin(&mut self, current: &str, new: &str) -> Result<(), Error> { + let current_string = get_cstring(current)?; + let new_string = get_cstring(new)?; + get_command_result(unsafe { + nitrokey_sys::NK_change_admin_PIN(current_string.as_ptr(), new_string.as_ptr()) + }) + } + + /// Changes the user PIN. + /// + /// # Errors + /// + /// - [`InvalidString`][] if one of the provided passwords contains a null byte + /// - [`WrongPassword`][] if the current user password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let mut device = manager.connect()?; + /// match device.change_user_pin("123456", "123457") { + /// Ok(()) => println!("Updated admin PIN."), + /// Err(err) => eprintln!("Failed to update admin PIN: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn change_user_pin(&mut self, current: &str, new: &str) -> Result<(), Error> { + let current_string = get_cstring(current)?; + let new_string = get_cstring(new)?; + get_command_result(unsafe { + nitrokey_sys::NK_change_user_PIN(current_string.as_ptr(), new_string.as_ptr()) + }) + } + + /// Unlocks the user PIN after three failed login attempts and sets it to the given value. + /// + /// # Errors + /// + /// - [`InvalidString`][] if one of the provided passwords contains a null byte + /// - [`WrongPassword`][] if the admin password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let mut device = manager.connect()?; + /// match device.unlock_user_pin("12345678", "123456") { + /// Ok(()) => println!("Unlocked user PIN."), + /// Err(err) => eprintln!("Failed to unlock user PIN: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn unlock_user_pin(&mut self, admin_pin: &str, user_pin: &str) -> Result<(), Error> { + let admin_pin_string = get_cstring(admin_pin)?; + let user_pin_string = get_cstring(user_pin)?; + get_command_result(unsafe { + nitrokey_sys::NK_unlock_user_password( + admin_pin_string.as_ptr(), + user_pin_string.as_ptr(), + ) + }) + } + + /// Locks the Nitrokey device. + /// + /// This disables the password store if it has been unlocked. On the Nitrokey Storage, this + /// also disables the volumes if they have been enabled. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let mut device = manager.connect()?; + /// match device.lock() { + /// Ok(()) => println!("Locked the Nitrokey device."), + /// Err(err) => eprintln!("Could not lock the Nitrokey device: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + fn lock(&mut self) -> Result<(), Error> { + get_command_result(unsafe { nitrokey_sys::NK_lock_device() }) + } + + /// Performs a factory reset on the Nitrokey device. + /// + /// This commands performs a factory reset on the smart card (like the factory reset via `gpg + /// --card-edit`) and then clears the flash memory (password safe, one-time passwords etc.). + /// After a factory reset, [`build_aes_key`][] has to be called before the password safe or the + /// encrypted volume can be used. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the provided password contains a null byte + /// - [`WrongPassword`][] if the admin password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let mut device = manager.connect()?; + /// match device.factory_reset("12345678") { + /// Ok(()) => println!("Performed a factory reset."), + /// Err(err) => eprintln!("Could not perform a factory reset: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`build_aes_key`]: #method.build_aes_key + fn factory_reset(&mut self, admin_pin: &str) -> Result<(), Error> { + let admin_pin_string = get_cstring(admin_pin)?; + get_command_result(unsafe { nitrokey_sys::NK_factory_reset(admin_pin_string.as_ptr()) }) + } + + /// Builds a new AES key on the Nitrokey. + /// + /// The AES key is used to encrypt the password safe and the encrypted volume. You may need + /// to call this method after a factory reset, either using [`factory_reset`][] or using `gpg + /// --card-edit`. You can also use it to destroy the data stored in the password safe or on + /// the encrypted volume. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the provided password contains a null byte + /// - [`WrongPassword`][] if the admin password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::Error; + /// + /// # fn try_main() -> Result<(), Error> { + /// let mut manager = nitrokey::take()?; + /// let mut device = manager.connect()?; + /// match device.build_aes_key("12345678") { + /// Ok(()) => println!("New AES keys have been built."), + /// Err(err) => eprintln!("Could not build new AES keys: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`factory_reset`]: #method.factory_reset + fn build_aes_key(&mut self, admin_pin: &str) -> Result<(), Error> { + let admin_pin_string = get_cstring(admin_pin)?; + get_command_result(unsafe { nitrokey_sys::NK_build_aes_key(admin_pin_string.as_ptr()) }) + } +} + +fn get_connected_model() -> Result<Model, Error> { + Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() }) +} + +pub(crate) fn create_device_wrapper( + manager: &mut crate::Manager, + model: Model, +) -> DeviceWrapper<'_> { + match model { + Model::Pro => Pro::new(manager).into(), + Model::Storage => Storage::new(manager).into(), + } +} + +pub(crate) fn get_connected_device( + manager: &mut crate::Manager, +) -> Result<DeviceWrapper<'_>, Error> { + Ok(create_device_wrapper(manager, get_connected_model()?)) +} + +pub(crate) fn connect_enum(model: Model) -> bool { + unsafe { nitrokey_sys::NK_login_enum(model.into()) == 1 } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::{get_hidapi_serial_number, LibraryError, SerialNumber}; + + #[test] + fn test_serial_number_display() { + fn assert_str(s: &str, n: u32) { + assert_eq!(s.to_owned(), SerialNumber::new(n).to_string()); + } + + assert_str("0x00000000", 0); + assert_str("0x00001000", 0x1000); + assert_str("0x12345678", 0x12345678); + } + + #[test] + fn test_serial_number_try_from() { + fn assert_ok(v: u32, s: &str) { + assert_eq!(SerialNumber::new(v), SerialNumber::from_str(s).unwrap()); + assert_eq!( + SerialNumber::new(v), + SerialNumber::from_str(format!("0x{}", s).as_ref()).unwrap() + ); + } + + fn assert_err(s: &str) { + match SerialNumber::from_str(s).unwrap_err() { + super::Error::LibraryError(LibraryError::InvalidHexString) => {} + err => assert!( + false, + "expected InvalidHexString error, got {} (input {})", + err, s + ), + } + } + + assert_ok(0x1234, "1234"); + assert_ok(0x1234, "01234"); + assert_ok(0x1234, "001234"); + assert_ok(0x1234, "0001234"); + + assert_ok(0, "0"); + assert_ok(0xdeadbeef, "deadbeef"); + + assert_err("deadpork"); + assert_err("blubb"); + assert_err(""); + } + + #[test] + fn test_get_hidapi_serial_number() { + fn assert_none(s: &str) { + assert_eq!(None, get_hidapi_serial_number(s)); + } + + fn assert_some(n: u32, s: &str) { + assert_eq!(Some(SerialNumber::new(n)), get_hidapi_serial_number(s)); + } + + assert_none(""); + assert_none("00000000000000000"); + assert_none("blubb"); + assert_none("1234"); + + assert_some(0x1234, "00001234"); + assert_some(0x1234, "000000001234"); + assert_some(0x1234, "100000001234"); + assert_some(0x12340000, "123400000000"); + assert_some(0x5678, "000000000000000000005678"); + assert_some(0x1234, "000012340000000000000000"); + assert_some(0xffff, "00000000000000000000FFFF"); + assert_some(0xffff, "00000000000000000000ffff"); + } +} |