bmputil 1.1.0

Black Magic Probe companion utility
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
// SPDX-FileCopyrightText: 2022-2025 1BitSquared <info@1bitsquared.com>
// SPDX-FileContributor: Written by Mikaela Szekely <mikaela.szekely@qyriad.me>
// SPDX-FileContributor: Modified by Rachel Mant <git@dragonmux.network>

use std::fmt::{self, Display};
#[cfg(any(target_os = "linux", target_os = "android"))]
use std::path::PathBuf;

use color_eyre::eyre::Result;
use nusb::DeviceInfo;
use thiserror::Error;

/// Simple newtype struct for some clarity in function arguments and whatnot.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Vid(pub u16);

/// Simple newtype struct for some clarity in function arguments and whatnot.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Pid(pub u16);

/// Simple newtype struct for some clarity in function arguments and whatnot.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct InterfaceClass(pub u8);
impl InterfaceClass
{
	/// bInterfaceClass field in DFU-class interface descriptors.
	///
	/// \[[USB DFU Device Class Spec § 4.2.1, Table 4.1](https://usb.org/sites/default/files/DFU_1.1.pdf#page=12)
	/// and [§ 4.2.3, Table 4.4](https://usb.org/sites/default/files/DFU_1.1.pdf#page=15)\]
	pub const APPLICATION_SPECIFIC: Self = Self(0xfe);
}

/// Simple newtype struct for some clarity in function arguments and whatnot.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct InterfaceSubClass(pub u8);
impl InterfaceSubClass
{
	/// bInterfaceSubClass field in DFU-class interface descriptors.
	///
	/// \[[USB DFU Device Class Spec § 4.2.1, Table 4.1](https://usb.org/sites/default/files/DFU_1.1.pdf#page=12)
	/// and [§ 4.2.3, Table 4.4](https://usb.org/sites/default/files/DFU_1.1.pdf#page=15)\]
	pub const DFU: Self = Self(0x01);
}

/// Simple newtype struct for some clarity in function arguments and whatnot.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct InterfaceProtocol(pub u8);
impl InterfaceProtocol
{
	/// bInterfaceProtocol field in DFU-class interface descriptors while in DFU mode.
	///
	/// \[[USB DFU Device Class Spec § 4.2.1, Table 4.1](https://usb.org/sites/default/files/DFU_1.1.pdf#page=12)
	/// and [§ 4.2.3, Table 4.4](https://usb.org/sites/default/files/DFU_1.1.pdf#page=15)\]
	#[allow(dead_code)] // XXX
	pub const DFU_DFU_MODE: Self = Self(0x02);
	/// bInterfaceProtocol field in DFU-class interface descriptors while in runtime mode.
	///
	/// \[[USB DFU Device Class Spec § 4.2.1, Table 4.1](https://usb.org/sites/default/files/DFU_1.1.pdf#page=12)
	/// and [§ 4.2.3, Table 4.4](https://usb.org/sites/default/files/DFU_1.1.pdf#page=15)\]
	#[allow(dead_code)] // XXX
	pub const DFU_RUNTIME_MODE: Self = Self(0x01);
}

/// Enum of request numbers for DFU class requests.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[allow(dead_code)]
pub enum DfuRequest
{
	Detach = 0,
	Dnload = 1,
	Upload = 2,
	GetStatus = 3,
	ClrStatus = 4,
	GetState = 5,
	Abort = 6,
}

/// Enum representing the two "modes" a DFU-class device can be in.
///
/// Runtime mode is the normal operation mode, in which a device does the things it's made for and
/// exposes all the necessary descriptors to do so.
/// DFU mode is limited operating mode used for firmware upgrade purposes *only*. Devices switch
/// into this mode at the host's request.
/// \[[USB DFU Device Class Spec § 4.1](https://usb.org/sites/default/files/DFU_1.1.pdf#page=11)
/// and [§ 4.2](https://usb.org/sites/default/files/DFU_1.1.pdf#page=14)\].
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum DfuOperatingMode
{
	Runtime,
	FirmwareUpgrade,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GenericDescriptorRef<'a>
{
	pub raw: &'a [u8],
}

impl<'a> GenericDescriptorRef<'a>
{
	/// Returns the first descriptor found.
	///
	/// Panics if `bytes.len()` < `bytes[0]`.
	pub fn single_from_bytes(bytes: &'a [u8]) -> Self
	{
		let length = bytes[0] as usize;

		Self {
			raw: &bytes[0..length],
		}
	}

	/// Panics if any descriptors have an invalid size.
	pub fn multiple_from_bytes(bytes: &'a [u8]) -> Vec<Self>
	{
		let mut v: Vec<Self> = Vec::new();

		let mut current_bytes = &bytes[0..];

		loop {
			let descriptor = Self::single_from_bytes(current_bytes);
			let parsed_count = descriptor.length_usize();
			let remaining = current_bytes.len() - parsed_count;
			v.push(descriptor);
			if remaining == 0 {
				break;
			} else if remaining > 2 {
				current_bytes = &current_bytes[parsed_count..];
			} else {
				panic!("Descriptor seems to have an invalid size of {}!", remaining);
			}
		}

		v
	}

	#[allow(dead_code)] // XXX
	pub fn length(&self) -> u8
	{
		self.raw[0]
	}

	pub fn length_usize(&self) -> usize
	{
		self.raw[0] as usize
	}

	pub fn descriptor_type(&self) -> u8
	{
		self.raw[1]
	}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Error)]
pub enum DescriptorConvertError
{
	#[error(
		"bLength field ({provided_length}) in provided data does not match the correct value({correct_length}) for \
		 this descriptor type"
	)]
	LengthFieldMismatch
	{
		provided_length: u8, correct_length: u8
	},

	#[error(
		"bDescriptorType field ({provided_type}) in provided data does not match the correctvalue ({correct_type}) \
		 for this descriptor type"
	)]
	DescriptorTypeMismatch
	{
		provided_type: u8, correct_type: u8
	},
}

/// Structure of the DFU-class functional descriptor.
///
/// Unfortunately, as this structure contains `u16`s at uneven offsets, making this struct
/// `repr(packed)` would allow you to easily create unaligned references, and thus this
/// struct does not match the memory layout of the data sent over the USB bus. Sadface indeed.
#[allow(non_snake_case)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct DfuFunctionalDescriptor
{
	pub bLength: u8,         // Should be 0x09.
	pub bDescriptorType: u8, // Should be 0x21.
	pub bmAttributes: u8,
	pub wDetachTimeOut: u16,
	pub wTransferSize: u16,
	pub bcdDFUVersion: u16,
}

impl DfuFunctionalDescriptor
{
	pub const LENGTH: u8 = 0x09;
	pub const TYPE: u8 = 0x21;

	/// Constructs a [DfuFunctionalDescriptor] from a byte slice, via per-field copy.
	pub fn copy_from_bytes(bytes: &[u8; 0x09]) -> Result<Self, DescriptorConvertError>
	{
		if bytes[0] != Self::LENGTH {
			return Err(DescriptorConvertError::LengthFieldMismatch {
				provided_length: bytes[0],
				correct_length: Self::LENGTH,
			});
		}

		if bytes[1] != Self::TYPE {
			return Err(DescriptorConvertError::DescriptorTypeMismatch {
				provided_type: bytes[1],
				correct_type: Self::TYPE,
			});
		}

		Ok(Self {
			bLength: bytes[0],
			bDescriptorType: bytes[1],
			bmAttributes: bytes[2],
			wDetachTimeOut: u16::from_le_bytes(bytes[3..=4].try_into().unwrap()),
			wTransferSize: u16::from_le_bytes(bytes[5..=6].try_into().unwrap()),
			bcdDFUVersion: u16::from_le_bytes(bytes[7..=8].try_into().unwrap()),
		})
	}
}

// Abstraction of an arbitrary nusb device's location on the host system
#[derive(Debug, Eq, Clone)]
pub struct PortId
{
	bus_number: u8,
	#[cfg(any(target_os = "linux", target_os = "android"))]
	path: PathBuf,
	#[cfg(target_os = "windows")]
	port_number: u32,
	#[cfg(target_os = "macos")]
	location: u32,
}

impl PortId
{
	pub fn new(device: &DeviceInfo) -> Self
	{
		Self {
			bus_number: device.bus_number(),
			#[cfg(any(target_os = "linux", target_os = "android"))]
			path: device.sysfs_path().to_path_buf(),
			#[cfg(target_os = "windows")]
			port_number: device.port_number(),
			#[cfg(target_os = "macos")]
			location: device.location_id(),
		}
	}
}

impl PartialEq for PortId
{
	#[cfg(any(target_os = "linux", target_os = "android"))]
	fn eq(&self, other: &Self) -> bool
	{
		self.bus_number == other.bus_number && self.path == other.path
	}

	#[cfg(target_os = "windows")]
	fn eq(&self, other: &Self) -> bool
	{
		self.bus_number == other.bus_number && self.port_number == other.port_number
	}

	#[cfg(target_os = "macos")]
	fn eq(&self, other: &Self) -> bool
	{
		self.bus_number == other.bus_number && self.location == other.location
	}
}

impl Display for PortId
{
	#[cfg(any(target_os = "linux", target_os = "android"))]
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
	{
		let port = self.path.file_name().map_or_else(
			|| Ok("Invalid PortId (bad path)".into()),
			|name| name.to_os_string().into_string(),
		);

		match port {
			Ok(port) => write!(f, "{}", port),
			Err(_) => Err(fmt::Error),
		}
	}

	#[cfg(target_os = "windows")]
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
	{
		write!(f, "{}-{}", self.bus_number, self.port_number)
	}

	#[cfg(target_os = "macos")]
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
	{
		write!(f, "{}-{}", self.bus_number, self.location)
	}
}