diff --git a/src/backend/cynthion.rs b/src/backend/cynthion.rs index b095760d..57eac1ea 100644 --- a/src/backend/cynthion.rs +++ b/src/backend/cynthion.rs @@ -1,3 +1,5 @@ +//! USB capture backend for Cynthion. + use std::cmp::Ordering; use std::collections::VecDeque; use std::time::Duration; diff --git a/src/backend/ice40usbtrace.rs b/src/backend/ice40usbtrace.rs index ddeed6ea..9e254e4e 100644 --- a/src/backend/ice40usbtrace.rs +++ b/src/backend/ice40usbtrace.rs @@ -1,3 +1,5 @@ +//! USB capture backend for iCE40-usbtrace. + use std::collections::VecDeque; use std::sync::mpsc; use std::time::Duration; diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 6dba179c..51fb9a42 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,3 +1,5 @@ +//! The backend API for USB capture devices. + use std::collections::BTreeMap; use std::sync::mpsc; use std::thread::{JoinHandle, spawn, sleep}; diff --git a/src/backend/transfer_queue.rs b/src/backend/transfer_queue.rs index ec81c25a..1ecb09e1 100644 --- a/src/backend/transfer_queue.rs +++ b/src/backend/transfer_queue.rs @@ -1,3 +1,5 @@ +//! Helper type for managing a queue of USB bulk transfers. + use std::sync::mpsc; use anyhow::{Context, Error}; diff --git a/src/build.rs b/src/build.rs index d8f91cb9..ca155645 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,3 +1,5 @@ +//! Code to be executed at build time. + fn main() { built::write_built_file().expect("Failed to acquire build-time information"); } diff --git a/src/capture.rs b/src/capture.rs index 59e01008..af12b666 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -1,3 +1,5 @@ +//! Capture database implementation for USB 2.0 + use std::cmp::min; use std::fmt::{Debug, Write}; use std::iter::once; @@ -7,14 +9,25 @@ use std::sync::atomic::Ordering::{Acquire, Release}; use std::sync::Arc; use std::mem::size_of; -use crate::id::{Id, HasLength}; -use crate::data_stream::{ - data_stream, data_stream_with_block_size, DataWriter, DataReader}; -use crate::compact_index::{compact_index, CompactWriter, CompactReader}; -use crate::rcu::SingleWriterRcu; -use crate::vec_map::{Key, VecMap}; -use crate::usb::{self, prelude::*, validate_packet}; -use crate::util::{fmt_count, fmt_size, titlecase}; +use crate::database::{ + CompactReader, + CompactWriter, + compact_index, + DataReader, + DataWriter, + data_stream, + data_stream_with_block_size, +}; +use crate::usb::{self, prelude::*}; +use crate::util::{ + id::Id, + rcu::SingleWriterRcu, + vec_map::{Key, VecMap}, + Bytes, + RangeLength, + fmt_count, + fmt_size, +}; use anyhow::{Context, Error, bail}; use arc_swap::{ArcSwap, ArcSwapOption}; @@ -215,88 +228,6 @@ pub type EndpointDataEvent = u64; pub type EndpointByteCount = u64; pub type DeviceVersion = u32; -#[derive(Clone, Debug)] -pub enum TrafficItem { - TransactionGroup(GroupId), - Transaction(Option, TransactionId), - Packet(Option, Option, PacketId), -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] -pub enum TrafficViewMode { - Hierarchical, - Transactions, - Packets, -} - -pub type DeviceViewMode = (); - -impl TrafficViewMode { - pub const fn display_name(&self) -> &'static str { - use TrafficViewMode::*; - match self { - Hierarchical => "Hierarchical", - Transactions => "Transactions", - Packets => "Packets", - } - } - - #[cfg(any(test, feature="record-ui-test"))] - pub const fn log_name(&self) -> &'static str { - use TrafficViewMode::*; - match self { - Hierarchical => "traffic-hierarchical", - Transactions => "traffic-transactions", - Packets => "traffic-packets", - } - } - - #[cfg(any(test, feature="record-ui-test"))] - pub fn from_log_name(log_name: &str) -> TrafficViewMode { - use TrafficViewMode::*; - match log_name { - "traffic-hierarchical" => Hierarchical, - "traffic-transactions" => Transactions, - "traffic-packets" => Packets, - _ => panic!("Unrecognised log name '{log_name}'") - } - } -} - -#[derive(Clone, Debug)] -pub struct DeviceItem { - pub device_id: DeviceId, - pub version: DeviceVersion, - pub content: DeviceItemContent, - pub indent: u8, -} - -#[derive(Clone, Debug)] -pub enum DeviceItemContent { - Device(Option), - DeviceDescriptor(Option), - DeviceDescriptorField(DeviceDescriptor, DeviceField), - Configuration(ConfigNum, ConfigDescriptor, Option), - ConfigurationDescriptor(ConfigDescriptor), - ConfigurationDescriptorField(ConfigDescriptor, ConfigField), - Function(ConfigNum, InterfaceAssociationDescriptor), - FunctionDescriptor(InterfaceAssociationDescriptor), - FunctionDescriptorField(InterfaceAssociationDescriptor, IfaceAssocField), - Interface(ConfigNum, InterfaceDescriptor), - InterfaceDescriptor(InterfaceDescriptor), - InterfaceDescriptorField(InterfaceDescriptor, InterfaceField), - Endpoint(ConfigNum, InterfaceKey, IfaceEpNum), - EndpointDescriptor(EndpointDescriptor), - EndpointDescriptorField(EndpointDescriptor, EndpointField), - HidDescriptor(HidDescriptor), - HidDescriptorField(HidDescriptor, HidField), - HidDescriptorList(HidDescriptor), - HidDescriptorEntry(HidDescriptor, HidField), - OtherDescriptor(Descriptor, Option), -} - -type IfaceEpNum = u8; - #[derive(Copy, Clone, Debug, Default, Pod, Zeroable)] #[repr(C)] pub struct Device { @@ -608,7 +539,7 @@ impl DeviceData { self.version.fetch_add(1, Release); } - fn version(&self) -> DeviceVersion { + pub fn version(&self) -> DeviceVersion { self.version.load(Acquire) } } @@ -662,10 +593,10 @@ impl Configuration { } impl Interface { - pub fn endpoint(&self, number: IfaceEpNum) + pub fn endpoint(&self, number: InterfaceEpNum) -> Result<&usb::Endpoint, Error> { - match self.endpoints.get(number as usize) { + match self.endpoints.get(number.0 as usize) { Some(ep) => Ok(ep), _ => bail!("Interface has no endpoint {number}") } @@ -773,12 +704,12 @@ impl Transaction { } } - fn description(&self, - capture: &mut CaptureReader, - endpoint: &Endpoint, - detail: bool) - -> Result - { + pub fn description( + &self, + capture: &mut CaptureReader, + endpoint: &Endpoint, + detail: bool + ) -> Result { use PID::*; use StartComplete::*; Ok(match (self.start_pid, &self.split) { @@ -860,68 +791,6 @@ pub enum GroupContent { Invalid, } -struct Bytes<'src> { - partial: bool, - bytes: &'src [u8], -} - -impl<'src> Bytes<'src> { - fn first(max: usize, bytes: &'src [u8]) -> Self { - if bytes.len() > max { - Bytes { - partial: true, - bytes: &bytes[0..max], - } - } else { - Bytes { - partial: false, - bytes, - } - } - } - - fn looks_like_ascii(&self) -> bool { - let mut num_printable = 0; - for &byte in self.bytes { - if byte == 0 || byte >= 0x80 { - // Outside ASCII range. - return false; - } - // Count printable and pseudo-printable characters. - let printable = match byte { - c if (0x20..0x7E).contains(&c) => true, // printable range - 0x09 => true, // tab - 0x0A => true, // new line - 0x0D => true, // carriage return - _ => false - }; - if printable { - num_printable += 1; - } - } - // If the string is at least half printable, treat as ASCII. - num_printable > 0 && num_printable >= self.bytes.len() / 2 - } -} - -impl std::fmt::Display for Bytes<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - if self.looks_like_ascii() { - write!(f, "'{}'", String::from_utf8( - self.bytes.iter() - .flat_map(|c| {std::ascii::escape_default(*c)}) - .collect::>()).unwrap())? - } else { - write!(f, "{:02X?}", self.bytes)? - }; - if self.partial { - write!(f, "...") - } else { - Ok(()) - } - } -} - impl CaptureShared { pub fn packet_endpoint(&self, pid: PID, packet: &[u8]) -> Result @@ -1030,7 +899,7 @@ impl CaptureReader { Ok(self.endpoint_readers.get_mut(endpoint_id).unwrap()) } - fn group_range(&mut self, entry: &GroupIndexEntry) + pub fn group_range(&mut self, entry: &GroupIndexEntry) -> Result, Error> { let endpoint_id = entry.endpoint_id(); @@ -1103,7 +972,7 @@ impl CaptureReader { Ok(transfer_bytes) } - fn endpoint_state(&mut self, group_id: GroupId) + pub fn endpoint_state(&mut self, group_id: GroupId) -> Result, Error> { let range = self.endpoint_state_index.target_range( @@ -1339,7 +1208,7 @@ impl CaptureReader { .clone()) } - fn group_extended( + pub fn group_extended( &mut self, endpoint_id: EndpointId, group_id: GroupId @@ -1356,12 +1225,8 @@ impl CaptureReader { }) } - fn completion(&self) -> CompletionStatus { - use CompletionStatus::*; - match self.shared.complete.load(Acquire) { - false => Ongoing, - true => Complete, - } + pub fn complete(&self) -> bool { + self.shared.complete.load(Acquire) } } @@ -1391,1017 +1256,6 @@ impl EndpointReader { } } -#[derive(Copy, Clone)] -pub enum CompletionStatus { - Complete, - Ongoing -} - -impl CompletionStatus { - pub fn is_complete(&self) -> bool { - use CompletionStatus::*; - match self { - Complete => true, - Ongoing => false, - } - } -} - -pub trait ItemSource { - fn item(&mut self, - parent: Option<&Item>, - view_mode: ViewMode, - index: u64) - -> Result; - fn item_update(&mut self, item: &Item) - -> Result, Error>; - fn child_item(&mut self, parent: &Item, index: u64) - -> Result; - fn item_children(&mut self, - parent: Option<&Item>, - view_mode: ViewMode) - -> Result<(CompletionStatus, u64), Error>; - fn description(&mut self, - item: &Item, - detail: bool) - -> Result; - fn connectors(&mut self, - view_mode: ViewMode, - item: &Item) - -> Result; - fn timestamp(&mut self, item: &Item) -> Result; -} - -impl ItemSource for CaptureReader { - fn item(&mut self, - parent: Option<&TrafficItem>, - view_mode: TrafficViewMode, - index: u64) - -> Result - { - use TrafficItem::*; - use TrafficViewMode::*; - match parent { - None => Ok(match view_mode { - Hierarchical => { - let item_id = TrafficItemId::from(index); - let group_id = self.item_index.get(item_id)?; - TransactionGroup(group_id) - }, - Transactions => - Transaction(None, TransactionId::from(index)), - Packets => - Packet(None, None, PacketId::from(index)), - }), - Some(item) => self.child_item(item, index) - } - } - - fn item_update(&mut self, _item: &TrafficItem) - -> Result, Error> - { - Ok(None) - } - - fn child_item(&mut self, parent: &TrafficItem, index: u64) - -> Result - { - use TrafficItem::*; - Ok(match parent { - TransactionGroup(group_id) => - Transaction(Some(*group_id), { - let entry = self.group_index.get(*group_id)?; - let endpoint_id = entry.endpoint_id(); - let ep_group_id = entry.group_id(); - let ep_traf = self.endpoint_traffic(endpoint_id)?; - let offset = ep_traf.group_index.get(ep_group_id)?; - ep_traf.transaction_ids.get(offset + index)? - }), - Transaction(group_id_opt, transaction_id) => - Packet(*group_id_opt, Some(*transaction_id), { - self.transaction_index.get(*transaction_id)? + index}), - Packet(..) => bail!("Packets have no child items") - }) - } - - fn item_children(&mut self, - parent: Option<&TrafficItem>, - view_mode: TrafficViewMode) - -> Result<(CompletionStatus, u64), Error> - { - use TrafficItem::*; - use TrafficViewMode::*; - use CompletionStatus::*; - Ok(match parent { - None => { - (self.completion(), match view_mode { - Hierarchical => self.item_index.len(), - Transactions => self.transaction_index.len(), - Packets => self.packet_index.len(), - }) - }, - Some(TransactionGroup(group_id)) => { - let entry = self.group_index.get(*group_id)?; - if !entry.is_start() { - return Ok((Complete, 0)); - } - let transaction_count = self.group_range(&entry)?.len(); - let ep_traf = self.endpoint_traffic(entry.endpoint_id())?; - if entry.group_id().value >= ep_traf.end_index.len() { - (Ongoing, transaction_count) - } else { - (Complete, transaction_count) - } - }, - Some(Transaction(_, transaction_id)) => { - let packet_count = self.transaction_index.target_range( - *transaction_id, self.packet_index.len())?.len(); - if transaction_id.value < self.transaction_index.len() - 1 { - (Complete, packet_count) - } else { - (Ongoing, packet_count) - } - }, - Some(Packet(..)) => (Complete, 0), - }) - } - - fn description(&mut self, item: &TrafficItem, detail: bool) - -> Result - { - use PID::*; - use TrafficItem::*; - use usb::StartComplete::*; - let mut s = String::new(); - Ok(match item { - Packet(.., packet_id) => { - let packet = self.packet(*packet_id)?; - let len = packet.len(); - let too_long = len > 1027; - if detail { - writeln!(s, "Packet #{} with {len} bytes", - packet_id.value + 1)?; - writeln!(s, "Timestamp: {} ns from capture start", - fmt_count(self.packet_time(*packet_id)?))?; - } - match validate_packet(&packet) { - Err(None) => { - write!(s, "Malformed 0-byte packet")?; - }, - Err(Some(pid)) => { - write!(s, "Malformed packet")?; - match pid { - RSVD if too_long => write!(s, - " (reserved PID, and too long)"), - Malformed if too_long => write!(s, - " (invalid PID, and too long)"), - RSVD => write!(s, - " (reserved PID)"), - Malformed => write!(s, - " (invalid PID)"), - pid if too_long => write!(s, - " (possibly {pid}, but too long)"), - pid => write!(s, - " (possibly {pid}, but {})", - match pid { - SOF|SETUP|IN|OUT|PING => { - if len != 3 { - "wrong length" - } else { - "bad CRC" - } - }, - SPLIT => { - if len != 4 { - "wrong length" - } else { - "bad CRC" - } - }, - DATA0|DATA1|DATA2|MDATA => { - if len < 3 { - "too short" - } else { - "bad CRC" - } - }, - ACK|NAK|NYET|STALL|ERR => "too long", - RSVD|Malformed => unreachable!(), - } - ), - }?; - if len == 1 { - write!(s, " of 1 byte") - } else { - write!(s, " of {len} bytes") - }?; - if detail { - write!(s, "\nHex bytes: {}", Bytes::first(1024, &packet)) - } else { - write!(s, ": {}", Bytes::first(100, &packet)) - }?; - }, - Ok(pid) => { - write!(s, "{pid} packet")?; - let fields = PacketFields::from_packet(&packet); - match &fields { - PacketFields::SOF(sof) => write!(s, - " with frame number {}, CRC {:02X}", - sof.frame_number(), - sof.crc()), - PacketFields::Token(token) => write!(s, - " on {}.{}, CRC {:02X}", - token.device_address(), - token.endpoint_number(), - token.crc()), - PacketFields::Data(data) if len <= 3 => write!(s, - " with CRC {:04X} and no data", - data.crc), - PacketFields::Data(data) => write!(s, - " with CRC {:04X} and {} data bytes", - data.crc, - len - 3), - PacketFields::Split(split) => write!(s, - " {} {} speed {} transaction on hub {} port {}", - match split.sc() { - Start => "starting", - Complete => "completing", - }, - format!("{:?}", split.speed()) - .to_lowercase(), - format!("{:?}", split.endpoint_type()) - .to_lowercase(), - split.hub_address(), - split.port()), - PacketFields::None => Ok(()), - }?; - if matches!(fields, PacketFields::Data(_)) && len > 3 { - let data = &packet[1 .. len - 2]; - if detail { - write!(s, concat!( - "\nHex bytes: [{:02X}, , {:02X}, {:02X}]", - "\nPayload: {}"), - packet[0], packet[len - 2], packet[len - 1], - Bytes::first(1024, data)) - } else { - write!(s, ": {}", Bytes::first(100, data)) - }?; - } else if detail { - write!(s, "\nHex bytes: {packet:02X?}")?; - } - } - } - s - }, - Transaction(group_id_opt, transaction_id) => { - let num_packets = self.packet_index.len(); - let packet_id_range = self.transaction_index.target_range( - *transaction_id, num_packets)?; - let start_packet_id = packet_id_range.start; - let start_packet = self.packet(start_packet_id)?; - let packet_count = packet_id_range.len(); - if detail { - writeln!(s, "Transaction #{} with {} {}", - transaction_id.value + 1, - packet_count, - if packet_count == 1 {"packet"} else {"packets"})?; - writeln!(s, "Timestamp: {} ns from capture start", - fmt_count(self.packet_time(start_packet_id)?))?; - write!(s, "Packets: #{}", packet_id_range.start + 1)?; - if packet_count > 1 { - write!(s, " to #{}", packet_id_range.end)?; - } - writeln!(s)?; - } - if let Ok(pid) = validate_packet(&start_packet) { - if pid == SPLIT && start_packet_id.value + 1 == num_packets { - // We can't know the endpoint yet. - let split = SplitFields::from_packet(&start_packet); - return Ok(format!( - "{} {} speed {} transaction on hub {} port {}", - match split.sc() { - Start => "Starting", - Complete => "Completing", - }, - format!("{:?}", split.speed()).to_lowercase(), - format!("{:?}", split.endpoint_type()).to_lowercase(), - split.hub_address(), - split.port())) - } - let endpoint_id = match group_id_opt { - Some(group_id) => { - let entry = self.group_index.get(*group_id)?; - entry.endpoint_id() - }, - None => match self.shared.packet_endpoint( - pid, &start_packet) - { - Ok(endpoint_id) => endpoint_id, - Err(_) => INVALID_EP_ID - } - }; - let endpoint = self.endpoints.get(endpoint_id)?; - let transaction = self.transaction(*transaction_id)?; - s += &transaction.description(self, &endpoint, detail)? - } else { - let packet_count = packet_id_range.len(); - write!(s, - "{} malformed {}", - packet_count, - if packet_count == 1 {"packet"} else {"packets"})?; - } - s - }, - TransactionGroup(group_id) => { - use GroupContent::*; - let group = self.group(*group_id)?; - if detail && group.is_start { - let ep_traf = - self.endpoint_traffic(group.endpoint_id)?; - let start_ep_transaction_id = group.range.start; - let start_transaction_id = - ep_traf.transaction_ids.get(start_ep_transaction_id)?; - let start_packet_id = - self.transaction_index.get(start_transaction_id)?; - if group.count == 1 { - writeln!(s, "Transaction group with 1 transaction")?; - } else { - writeln!(s, "Transaction group with {} transactions", - group.count)?; - } - writeln!(s, "Timestamp: {} ns from start of capture", - fmt_count(self.packet_time(start_packet_id)?))?; - writeln!(s, "First transaction #{}, first packet #{}", - start_transaction_id.value + 1, - start_packet_id.value + 1)?; - } - let endpoint = &group.endpoint; - let endpoint_type = group.endpoint_type; - let addr = group.endpoint.device_address(); - let count = group.count; - match (group.content, group.is_start) { - (Invalid, true) => write!(s, - "{count} invalid groups"), - (Invalid, false) => write!(s, - "End of invalid groups"), - (Framing, true) => write!(s, - "{count} SOF groups"), - (Framing, false) => write!(s, - "End of SOF groups"), - (Request(transfer), true) if detail => write!(s, - "Control transfer on device {addr}\n{}", - transfer.summary(true)), - (Request(transfer), true) => write!(s, - "{}", transfer.summary(false)), - (IncompleteRequest, true) => write!(s, - "Incomplete control transfer on device {addr}"), - (Request(_) | IncompleteRequest, false) => write!(s, - "End of control transfer on device {addr}"), - (Data(data_range), true) => { - let ep_traf = - self.endpoint_traffic(group.endpoint_id)?; - let length = - ep_traf.transfer_data_length(&data_range)?; - let length_string = fmt_size(length); - let max = if detail { 1024 } else { 100 }; - let display_length = min(length, max) as usize; - let transfer_bytes = self.transfer_bytes( - group.endpoint_id, &data_range, display_length)?; - let display_bytes = Bytes { - partial: length > display_length as u64, - bytes: &transfer_bytes, - }; - let ep_type_string = titlecase( - &format!("{endpoint_type}")); - write!(s, "{ep_type_string} transfer ")?; - write!(s, "of {length_string} ")?; - write!(s, "on endpoint {endpoint}")?; - if detail { - write!(s, "\nPayload: {display_bytes}") - } else { - write!(s, ": {display_bytes}") - } - }, - (Data(_), false) => write!(s, - "End of {endpoint_type} transfer on endpoint {endpoint}"), - (Polling(count), true) => write!(s, - "Polling {count} times for {endpoint_type} transfer on endpoint {endpoint}"), - (Polling(_count), false) => write!(s, - "End polling for {endpoint_type} transfer on endpoint {endpoint}"), - (Ambiguous(_data_range, count), true) => { - write!(s, "{count} ambiguous transactions on endpoint {endpoint}")?; - if detail { - write!(s, "\nThe result of these transactions is ambiguous because the endpoint type is not known.")?; - write!(s, "\nTry starting the capture before this device is enumerated, so that its descriptors are captured.")?; - } - Ok(()) - }, - (Ambiguous(..), false) => write!(s, - "End of ambiguous transactions."), - }?; - s - } - }) - } - - fn connectors(&mut self, view_mode: TrafficViewMode, item: &TrafficItem) - -> Result - { - use EndpointState::*; - use TrafficItem::*; - use TrafficViewMode::*; - if view_mode == Packets { - return Ok(String::from("")); - } - let last_packet = match item { - Packet(_, Some(transaction_id), packet_id) => { - let range = self.transaction_index.target_range( - *transaction_id, self.packet_index.len())?; - *packet_id == range.end - 1 - }, _ => false - }; - if view_mode == Transactions { - return Ok(String::from(match (item, last_packet) { - (TransactionGroup(_), _) => unreachable!(), - (Transaction(..), _) => "○", - (Packet(..), false) => "├──", - (Packet(..), true ) => "└──", - })); - } - let endpoint_count = self.endpoints.len() as usize; - let max_string_length = endpoint_count + " └──".len(); - let mut connectors = String::with_capacity(max_string_length); - let group_id = match item { - TransactionGroup(i) | - Transaction(Some(i), _) | - Packet(Some(i), ..) => *i, - _ => unreachable!() - }; - let entry = self.group_index.get(group_id)?; - let endpoint_id = entry.endpoint_id(); - let endpoint_state = self.endpoint_state(group_id)?; - let extended = self.group_extended(endpoint_id, group_id)?; - let ep_traf = self.endpoint_traffic(endpoint_id)?; - let last_transaction = match item { - Transaction(_, transaction_id) | - Packet(_, Some(transaction_id), _) => { - let range = ep_traf.group_index.target_range( - entry.group_id(), ep_traf.transaction_ids.len())?; - let last_transaction_id = - ep_traf.transaction_ids.get(range.end - 1)?; - *transaction_id == last_transaction_id - }, _ => false - }; - let last = last_transaction && !extended; - let mut thru = false; - for (i, &state) in endpoint_state.iter().enumerate() { - let state = EndpointState::from(state); - let active = state != Idle; - let on_endpoint = i == endpoint_id.value as usize; - thru |= match (item, state, on_endpoint) { - (TransactionGroup(..), Starting | Ending, _) => true, - (Transaction(..) | Packet(..), _, true) => on_endpoint, - _ => false, - }; - connectors.push(match item { - TransactionGroup(..) => { - match (state, thru) { - (Idle, false) => ' ', - (Idle, true ) => '─', - (Starting, _ ) => '○', - (Ongoing, false) => '│', - (Ongoing, true ) => '┼', - (Ending, _ ) => '└', - } - }, - Transaction(..) => { - match (on_endpoint, active, thru, last) { - (false, false, false, _ ) => ' ', - (false, false, true, _ ) => '─', - (false, true, false, _ ) => '│', - (false, true, true, _ ) => '┼', - (true, _, _, false) => '├', - (true, _, _, true ) => '└', - } - }, - Packet(..) => { - match (on_endpoint, active, last) { - (false, false, _ ) => ' ', - (false, true, _ ) => '│', - (true, _, false) => '│', - (true, _, true ) => ' ', - } - } - }); - }; - let state_length = endpoint_state.len(); - for _ in state_length..endpoint_count { - connectors.push(match item { - TransactionGroup(..) => '─', - Transaction(..) => '─', - Packet(..) => ' ', - }); - } - connectors.push_str( - match (item, last_packet) { - (TransactionGroup(_), _) if entry.is_start() => "─", - (TransactionGroup(_), _) => "──□ ", - (Transaction(..), _) => "───", - (Packet(..), false) => " ├──", - (Packet(..), true) => " └──", - } - ); - Ok(connectors) - } - - fn timestamp(&mut self, item: &TrafficItem) - -> Result - { - use TrafficItem::*; - let packet_id = match item { - TransactionGroup(group_id) => { - let entry = self.group_index.get(*group_id)?; - let ep_traf = self.endpoint_traffic(entry.endpoint_id())?; - let ep_transaction_id = - ep_traf.group_index.get(entry.group_id())?; - let transaction_id = - ep_traf.transaction_ids.get(ep_transaction_id)?; - self.transaction_index.get(transaction_id)? - }, - Transaction(.., transaction_id) => - self.transaction_index.get(*transaction_id)?, - Packet(.., packet_id) => *packet_id, - }; - self.packet_time(packet_id) - } -} - -impl ItemSource for CaptureReader { - fn item(&mut self, - parent: Option<&DeviceItem>, - _view_mode: DeviceViewMode, - index: u64) - -> Result - { - match parent { - None => { - let device_id = DeviceId::from(index + 1); - let data = self.device_data(device_id)?; - let descriptor = data.device_descriptor.load_full(); - Ok(DeviceItem { - device_id, - version: data.version(), - content: DeviceItemContent::Device( - descriptor.map(|arc| *arc) - ), - indent: 0, - }) - }, - Some(item) => self.child_item(item, index) - } - } - - fn item_update(&mut self, item: &DeviceItem) - -> Result, Error> - { - use DeviceItemContent::*; - let data = self.device_data(item.device_id)?; - if data.version() == item.version { - return Ok(None) - } - // These items may have changed because we saw a new descriptor. - Ok(match item.content { - Device(_) | - DeviceDescriptorField(..) | - ConfigurationDescriptorField(..) | - InterfaceDescriptorField(..) => Some( - DeviceItem { - device_id: item.device_id, - version: data.version(), - content: item.content.clone(), - indent: item.indent, - } - ), - _ => None, - }) - } - - fn child_item(&mut self, parent: &DeviceItem, index: u64) - -> Result - { - use DeviceItemContent::*; - let data = self.device_data(parent.device_id)?; - let content = match &parent.content { - Device(desc_opt) => match index { - 0 => DeviceDescriptor(*desc_opt), - n => { - let conf = ConfigNum(n.try_into()?); - let config = data.configuration(conf)?; - Configuration( - conf, - config.descriptor, - desc_opt.map(|desc| desc.device_class) - ) - } - }, - DeviceDescriptor(desc_opt) => match desc_opt { - Some(desc) => - DeviceDescriptorField(*desc, - DeviceField(index.try_into()?)), - None => bail!("Device descriptor fields not available") - }, - Configuration(conf, desc, class) => { - let config = data.configuration(*conf)?; - let other_count = config.other_descriptors.len(); - let func_count = config.functions.len(); - match index.try_into()? { - 0 => ConfigurationDescriptor(*desc), - n if n < 1 + other_count => - OtherDescriptor( - config - .other_descriptor(n - 1)? - .clone(), - *class), - n if n < 1 + other_count + func_count => - Function(*conf, config - .function(n - 1 - other_count)? - .descriptor), - n => Interface(*conf, config - .unassociated_interfaces() - .nth(n - 1 - other_count - func_count) - .context("Failed to find unassociated interface")? - .descriptor) - } - }, - ConfigurationDescriptor(desc) => - ConfigurationDescriptorField(*desc, - ConfigField(index.try_into()?)), - Function(conf, desc) => { - let config = data.configuration(*conf)?; - match index.try_into()? { - 0 => FunctionDescriptor(*desc), - n => match config.associated_interfaces(desc).nth(n - 1) { - Some(interface) => - Interface(*conf, interface.descriptor), - None => bail!( - "Function has no interface with index {n}") - } - } - }, - FunctionDescriptor(desc) => - FunctionDescriptorField(*desc, - IfaceAssocField(index.try_into()?)), - Interface(conf, if_desc) => { - let config = data.configuration(*conf)?; - let interface = config.interface(if_desc.key())?; - let desc_count = interface.other_descriptors.len(); - match index.try_into()? { - 0 => InterfaceDescriptor(*if_desc), - n if n < 1 + desc_count => { - let desc = interface.other_descriptor(n - 1)?.clone(); - if let Descriptor::Hid(hid_desc) = desc { - HidDescriptor(hid_desc) - } else { - OtherDescriptor(desc, - Some(interface.descriptor.interface_class)) - } - }, - n => { - let ep_num = (n - 1 - desc_count).try_into()?; - Endpoint(*conf, if_desc.key(), ep_num) - } - } - }, - Endpoint(conf, if_key, ep_num) => { - let config = data.configuration(*conf)?; - let interface = config.interface(*if_key)?; - let endpoint = interface.endpoint(*ep_num)?; - match index.try_into()? { - 0 => EndpointDescriptor(endpoint.descriptor), - n => OtherDescriptor( - endpoint.other_descriptors - .get(n - 1) - .context("Other endpoint descriptor not found")? - .clone(), - Some(interface.descriptor.interface_class) - ) - } - }, - InterfaceDescriptor(desc) => - InterfaceDescriptorField(*desc, - InterfaceField(index.try_into()?)), - EndpointDescriptor(desc) => - EndpointDescriptorField(*desc, - EndpointField(index.try_into()?)), - HidDescriptor(desc) => { - const N: usize = usb::HidDescriptor::NUM_FIELDS; - const LAST_FIELD: usize = N - 1; - match index.try_into()? { - 0..=LAST_FIELD => - HidDescriptorField(desc.clone(), - HidField(index.try_into()?)), - N => HidDescriptorList(desc.clone()), - _ => bail!("HID descriptor has no child with index {index}") - } - }, - HidDescriptorList(desc) => - HidDescriptorEntry(desc.clone(), - HidField(index.try_into()?)), - _ => bail!("This device item type cannot have children") - }; - Ok(DeviceItem { - device_id: parent.device_id, - version: data.version(), - content, - indent: parent.indent + 1, - }) - } - - fn item_children(&mut self, - parent: Option<&DeviceItem>, - _view_mode: DeviceViewMode) - -> Result<(CompletionStatus, u64), Error> - { - use DeviceItemContent::*; - use CompletionStatus::*; - let (completion, children) = match parent { - None => - (self.completion(), - self.devices.len().saturating_sub(1) as usize), - Some(item) => { - let data = self.device_data(item.device_id)?; - match &item.content { - Device(_) => { - let count = data.configurations.load().len(); - (Ongoing, if count == 0 { 1 } else { count }) - }, - DeviceDescriptor(_) => - match data.device_descriptor.load().as_ref() { - Some(_) => - (Ongoing, usb::DeviceDescriptor::NUM_FIELDS), - None => (Ongoing, 0), - }, - Configuration(conf, ..) => { - let config = data.configuration(*conf)?; - (Ongoing, - 1 + config.other_descriptors.len() - + config.functions.len() - + config.unassociated_interfaces().count()) - } - ConfigurationDescriptor(_) => - (Ongoing, usb::ConfigDescriptor::NUM_FIELDS), - Function(conf, desc) => { - let config = data.configuration(*conf)?; - let interfaces = config.associated_interfaces(desc); - (Complete, 1 + interfaces.count()) - } - FunctionDescriptor(_) => - (Complete, - usb::InterfaceAssociationDescriptor::NUM_FIELDS), - Interface(conf, desc) => { - let config = data.configuration(*conf)?; - let interface = config.interface(desc.key())?; - (Ongoing, - 1 + interface.endpoints.len() - + interface.other_descriptors.len()) - }, - Endpoint(conf, key, ep_num) => { - let config = data.configuration(*conf)?; - let interface = config.interface(*key)?; - let endpoint = interface.endpoint(*ep_num)?; - (Complete, 1 + endpoint.other_descriptors.len()) - } - InterfaceDescriptor(_) => - (Ongoing, usb::InterfaceDescriptor::NUM_FIELDS), - EndpointDescriptor(_) => - (Complete, usb::EndpointDescriptor::NUM_FIELDS), - HidDescriptor(_) => - (Complete, usb::HidDescriptor::NUM_FIELDS + 1), - HidDescriptorList(desc) => - (Complete, desc.available_descriptors.len()), - // Other types have no children. - _ => (Complete, 0), - } - } - }; - Ok((completion, children as u64)) - } - - fn description(&mut self, item: &DeviceItem, _detail: bool) - -> Result - { - use DeviceItemContent::*; - let data = self.device_data(item.device_id)?; - Ok(match &item.content { - Device(_) => { - let device = self.devices.get(item.device_id)?; - format!("Device {}: {}", device.address, data.description()) - }, - DeviceDescriptor(desc) => { - match desc { - Some(_) => "Device descriptor", - None => "No device descriptor" - }.to_string() - }, - DeviceDescriptorField(desc, field) => { - let strings = data.strings.load(); - desc.field_text(*field, strings.as_ref()) - }, - Configuration(conf, ..) => format!( - "Configuration {conf}"), - ConfigurationDescriptor(_) => - "Configuration descriptor".to_string(), - ConfigurationDescriptorField(desc, field) => { - let strings = data.strings.load(); - desc.field_text(*field, strings.as_ref()) - }, - Function(_conf, desc) => { - format!("Function {}: {}", - desc.function, - desc.function_class.name() - ) - }, - FunctionDescriptor(_) => - "Interface association descriptor".to_string(), - FunctionDescriptorField(desc, field) => desc.field_text(*field), - Interface(_conf, desc) => { - let num = desc.interface_number; - let class = desc.interface_class.name(); - match desc.alternate_setting { - InterfaceAlt(0) => format!( - "Interface {num}: {class}"), - InterfaceAlt(alt) => format!( - "Interface {num} alt {alt}: {class}"), - } - }, - InterfaceDescriptor(_) => - "Interface descriptor".to_string(), - InterfaceDescriptorField(desc, field) => { - let strings = data.strings.load(); - desc.field_text(*field, strings.as_ref()) - }, - Endpoint(conf, if_key, ep_num) => { - let config = data.configuration(*conf)?; - let interface = config.interface(*if_key)?; - let endpoint = interface.endpoint(*ep_num)?; - let desc = &endpoint.descriptor; - let addr = desc.endpoint_address; - let attrs = desc.attributes; - format!("Endpoint {} {} ({})", addr.number(), - addr.direction(), attrs.endpoint_type()) - }, - EndpointDescriptor(_) => - "Endpoint descriptor".to_string(), - EndpointDescriptorField(desc, field) => desc.field_text(*field), - HidDescriptor(_) => "HID descriptor".to_string(), - HidDescriptorField(desc, field) => desc.field_text(*field), - HidDescriptorList(_) => "Available descriptors".to_string(), - HidDescriptorEntry(desc, field) => { - let (desc_type, length) = - desc.available_descriptors - .get(field.0 as usize) - .context("Not enough entries in descriptor list")?; - format!("{}, {} bytes", - desc_type.description_with_class(ClassId::HID), length) - }, - OtherDescriptor(desc, class) => desc.description(*class), - }) - } - - fn connectors(&mut self, _view_mode: (), item: &DeviceItem) - -> Result - { - Ok(" ".repeat(item.indent as usize)) - } - - fn timestamp(&mut self, _item: &DeviceItem) - -> Result - { - unreachable!() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs::File; - use std::io::{BufReader, BufWriter, BufRead, Write}; - use std::path::PathBuf; - use crate::decoder::Decoder; - use crate::pcap::Loader; - - fn summarize_item( - cap: &mut CaptureReader, - item: &Item, - mode: ViewMode, - ) -> String - where CaptureReader: ItemSource, - ViewMode: Copy - { - let mut summary = format!("{} {}", - cap.connectors(mode, item).unwrap(), - cap.description(item, false).unwrap() - ); - let (_completion, num_children) = - cap.item_children(Some(item), mode).unwrap(); - let child_ids = 0..num_children; - for (n, child_summary) in child_ids - .map(|child_id| { - let child = cap.child_item(item, child_id).unwrap(); - summarize_item(cap, &child, mode) - }) - .dedup_with_count() - { - summary += "\n"; - if n > 1 { - summary += &format!("{} ({} times)", &child_summary, n); - } else { - summary += &child_summary; - } - } - summary - } - - fn write_item( - cap: &mut CaptureReader, - item: &Item, - mode: ViewMode, - writer: &mut dyn Write - ) - where CaptureReader: ItemSource, - ViewMode: Copy - { - let summary = summarize_item(cap, item, mode); - writer.write(summary.as_bytes()).unwrap(); - writer.write(b"\n").unwrap(); - } - - #[test] - fn test_captures() { - let test_dir = PathBuf::from("./tests/"); - let mut list_path = test_dir.clone(); - list_path.push("tests.txt"); - let list_file = File::open(list_path).unwrap(); - let mode = TrafficViewMode::Hierarchical; - for test_name in BufReader::new(list_file).lines() { - let mut test_path = test_dir.clone(); - test_path.push(test_name.unwrap()); - let mut cap_path = test_path.clone(); - let mut traf_ref_path = test_path.clone(); - let mut traf_out_path = test_path.clone(); - let mut dev_ref_path = test_path.clone(); - let mut dev_out_path = test_path.clone(); - cap_path.push("capture.pcap"); - traf_ref_path.push("reference.txt"); - traf_out_path.push("output.txt"); - dev_ref_path.push("devices-reference.txt"); - dev_out_path.push("devices-output.txt"); - { - let file = File::open(cap_path).unwrap(); - let mut loader = Loader::open(file).unwrap(); - let (writer, mut reader) = create_capture().unwrap(); - let mut decoder = Decoder::new(writer).unwrap(); - while let Some(result) = loader.next() { - let (packet, timestamp_ns) = result.unwrap(); - decoder - .handle_raw_packet(&packet.data, timestamp_ns) - .unwrap(); - } - decoder.finish().unwrap(); - let traf_out_file = File::create(traf_out_path.clone()).unwrap(); - let mut traf_out_writer = BufWriter::new(traf_out_file); - let num_items = reader.item_index.len(); - for item_id in 0 .. num_items { - let item = reader.item(None, mode, item_id).unwrap(); - write_item(&mut reader, &item, mode, &mut traf_out_writer); - } - let dev_out_file = File::create(dev_out_path.clone()).unwrap(); - let mut dev_out_writer = BufWriter::new(dev_out_file); - let num_devices = reader.devices.len() - 1; - for device_id in 0 .. num_devices { - let item = reader.item(None, (), device_id).unwrap(); - write_item(&mut reader, &item, (), &mut dev_out_writer); - } - } - for (ref_path, out_path) in [ - (traf_ref_path, traf_out_path), - (dev_ref_path, dev_out_path), - ] { - let ref_file = File::open(ref_path).unwrap(); - let out_file = File::open(out_path.clone()).unwrap(); - let ref_reader = BufReader::new(ref_file); - let out_reader = BufReader::new(out_file); - let mut out_lines = out_reader.lines(); - for line in ref_reader.lines() { - let expected = line.unwrap(); - let actual = out_lines.next().unwrap().unwrap(); - assert_eq!(actual, expected); - } - } - } - } -} - pub mod prelude { #[allow(unused_imports)] pub use super::{ diff --git a/src/cli.rs b/src/cli.rs index d8150953..95d887ca 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,3 +1,7 @@ +//! CLI wrapper program. +//! +//! Necessary on Windows, where the GUI binary cannot have console output. + use std::process::{Command, ExitCode}; #[cfg(unix)] diff --git a/src/compact_index.rs b/src/database/compact_index.rs similarity index 98% rename from src/compact_index.rs rename to src/database/compact_index.rs index 61033c63..03efd4d8 100644 --- a/src/compact_index.rs +++ b/src/database/compact_index.rs @@ -1,3 +1,7 @@ +//! Compressed index implementation. +//! +//! This is the basis of all indexes in the capture database. + use std::cmp::max; use std::fmt::Debug; use std::iter::{Peekable, once}; @@ -9,10 +13,11 @@ use std::sync::Arc; use anyhow::{Context, Error, anyhow, bail}; use itertools::{structs::Zip, multizip}; -use crate::data_stream::{data_stream, DataReader, DataWriter, DataIterator}; -use crate::id::Id; -use crate::index_stream::{index_stream, IndexReader, IndexWriter, IndexIterator}; -use crate::util::{fmt_count, fmt_size}; +use crate::database::{ + data_stream::{data_stream, DataReader, DataWriter, DataIterator}, + index_stream::{index_stream, IndexReader, IndexWriter, IndexIterator}, +}; +use crate::util::{id::Id, fmt_count, fmt_size}; type Offset = Id; type SegmentId = Id; diff --git a/src/data_stream.rs b/src/database/data_stream.rs similarity index 97% rename from src/data_stream.rs rename to src/database/data_stream.rs index 887e0d78..0d990358 100644 --- a/src/data_stream.rs +++ b/src/database/data_stream.rs @@ -1,3 +1,7 @@ +//! Typed data stream implementation. +//! +//! Stores streams of specific types, rather than raw bytes. + use std::marker::PhantomData; use std::mem::size_of; use std::ops::{Deref, Range}; @@ -5,8 +9,14 @@ use std::ops::{Deref, Range}; use anyhow::Error; use bytemuck::{bytes_of, cast_slice, from_bytes, Pod}; -use crate::id::Id; -use crate::stream::{stream, StreamReader, StreamWriter, Data, MIN_BLOCK}; +use crate::util::id::Id; +use crate::database::stream::{ + stream, + StreamReader, + StreamWriter, + Data, + MIN_BLOCK +}; use crate::util::{fmt_count, fmt_size}; /// Unique handle for append-only write access to a data stream. diff --git a/src/index_stream.rs b/src/database/index_stream.rs similarity index 97% rename from src/index_stream.rs rename to src/database/index_stream.rs index 9fa503aa..f7eab6de 100644 --- a/src/index_stream.rs +++ b/src/database/index_stream.rs @@ -1,13 +1,23 @@ +//! Basic index stream implementation. +//! +//! Stores non-compressed indexes. Used in the compressed index implementation. + use std::cmp::min; use std::marker::PhantomData; use std::ops::Range; use anyhow::Error; -use crate::data_stream::{data_stream, DataReader, DataWriter, DataIterator}; -use crate::id::Id; -use crate::stream::MIN_BLOCK; -use crate::util::{fmt_count, fmt_size}; +use crate::database::{ + stream::MIN_BLOCK, + data_stream::{ + data_stream, + DataReader, + DataWriter, + DataIterator + }, +}; +use crate::util::{id::Id, fmt_count, fmt_size}; /// Unique handle for append-only write access to an index. pub struct IndexWriter { diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 00000000..5e57ea31 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,18 @@ +//! Storage primitives for the capture database. + +mod stream; +mod data_stream; +mod index_stream; +mod compact_index; + +pub use data_stream::{ + DataReader, + DataWriter, + data_stream, + data_stream_with_block_size, +}; +pub use compact_index::{ + CompactReader, + CompactWriter, + compact_index, +}; diff --git a/src/stream.rs b/src/database/stream.rs similarity index 98% rename from src/stream.rs rename to src/database/stream.rs index 918526df..5856df8c 100644 --- a/src/stream.rs +++ b/src/database/stream.rs @@ -1,3 +1,10 @@ +//! Core stream implementation. +//! +//! This is the basis for all non-bounded data storage in Packetry. +//! +//! Stores a growing stream of bytes from a single writer, whilst providing +//! efficient random access by multiple readers to any part of the stream. + #![deny(unsafe_op_in_unsafe_fn)] use std::alloc::{GlobalAlloc, Layout, System}; diff --git a/src/decoder.rs b/src/decoder.rs index 6ea658e6..54832646 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -1,12 +1,18 @@ +//! USB 2.0 protocol decoder. +//! +//! Processes incoming packets and populates a capture database. + use std::sync::atomic::Ordering::Release; use std::sync::Arc; use anyhow::{Context, Error, bail}; use crate::capture::prelude::*; -use crate::rcu::SingleWriterRcu; use crate::usb::{self, prelude::*, validate_packet}; -use crate::vec_map::VecMap; +use crate::util::{ + rcu::SingleWriterRcu, + vec_map::VecMap, +}; struct EndpointData { device_id: DeviceId, diff --git a/src/fuzzer.rs b/src/fuzzer.rs index 3e89a727..b30e3132 100644 --- a/src/fuzzer.rs +++ b/src/fuzzer.rs @@ -9,17 +9,11 @@ extern crate bitfield; use libfuzzer_sys::{arbitrary::{Arbitrary, Unstructured}, fuzz_target}; mod capture; -mod compact_index; -mod data_stream; +mod database; mod decoder; -mod id; -mod index_stream; mod pcap; -mod rcu; -mod stream; mod usb; mod util; -mod vec_map; use capture::create_capture; use decoder::Decoder; diff --git a/src/item.rs b/src/item.rs new file mode 100644 index 00000000..30288db2 --- /dev/null +++ b/src/item.rs @@ -0,0 +1,1155 @@ +//! Items displayed in the UI tree views. +//! +//! Defines how items are fetched from the database and described with text. + +use std::cmp::min; +use std::fmt::Write; + +use anyhow::{Context, Error, bail}; + +use crate::capture::{ + CaptureReader, + DeviceId, + DeviceVersion, + EndpointState, + GroupContent, + GroupId, + Timestamp, + TrafficItemId, + TransactionId, + PacketId, + INVALID_EP_ID, +}; +use crate::usb::{self, prelude::*, validate_packet}; +use crate::util::{Bytes, RangeLength, fmt_count, fmt_size, titlecase}; + +pub trait ItemSource { + /// Fetch an item from the source by index, relative to either the root + /// of the item tree or to a parent item. + fn item( + &mut self, + parent: Option<&Item>, + view_mode: ViewMode, + index: u64, + ) -> Result; + + /// Count how many children this item has, and whether it is complete. + fn item_children( + &mut self, + parent: Option<&Item>, + view_mode: ViewMode, + ) -> Result<(CompletionStatus, u64), Error>; + + /// Fetch a child item by index, relative to a parent item. + fn child_item(&mut self, parent: &Item, index: u64) -> Result; + + /// Check whether a newer version of this item is available. + fn item_update(&mut self, item: &Item) -> Result, Error>; + + /// Generate a description for this item, either one line or with detail. + fn description( + &mut self, + item: &Item, + detail: bool, + ) -> Result; + + /// Generate connecting lines for this item. + fn connectors( + &mut self, + view_mode: ViewMode, + item: &Item, + ) -> Result; + + /// Get the timestamp of this item. + fn timestamp(&mut self, item: &Item) -> Result; +} + +#[derive(Copy, Clone)] +pub enum CompletionStatus { + Complete, + Ongoing +} + +impl CompletionStatus { + pub fn is_complete(&self) -> bool { + use CompletionStatus::*; + match self { + Complete => true, + Ongoing => false, + } + } +} + +#[derive(Clone, Debug)] +pub enum TrafficItem { + TransactionGroup(GroupId), + Transaction(Option, TransactionId), + Packet(Option, Option, PacketId), +} + +#[derive(Clone, Debug)] +pub struct DeviceItem { + pub device_id: DeviceId, + pub version: DeviceVersion, + pub content: DeviceItemContent, + pub indent: u8, +} + +#[derive(Clone, Debug)] +pub enum DeviceItemContent { + Device(Option), + DeviceDescriptor(Option), + DeviceDescriptorField(DeviceDescriptor, DeviceField), + Configuration(ConfigNum, ConfigDescriptor, Option), + ConfigurationDescriptor(ConfigDescriptor), + ConfigurationDescriptorField(ConfigDescriptor, ConfigField), + Function(ConfigNum, InterfaceAssociationDescriptor), + FunctionDescriptor(InterfaceAssociationDescriptor), + FunctionDescriptorField(InterfaceAssociationDescriptor, IfaceAssocField), + Interface(ConfigNum, InterfaceDescriptor), + InterfaceDescriptor(InterfaceDescriptor), + InterfaceDescriptorField(InterfaceDescriptor, InterfaceField), + Endpoint(ConfigNum, InterfaceKey, InterfaceEpNum), + EndpointDescriptor(EndpointDescriptor), + EndpointDescriptorField(EndpointDescriptor, EndpointField), + HidDescriptor(HidDescriptor), + HidDescriptorField(HidDescriptor, HidField), + HidDescriptorList(HidDescriptor), + HidDescriptorEntry(HidDescriptor, HidField), + OtherDescriptor(Descriptor, Option), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum TrafficViewMode { + Hierarchical, + Transactions, + Packets, +} + +impl TrafficViewMode { + pub const fn display_name(&self) -> &'static str { + use TrafficViewMode::*; + match self { + Hierarchical => "Hierarchical", + Transactions => "Transactions", + Packets => "Packets", + } + } + + #[cfg(any(test, feature="record-ui-test"))] + pub const fn log_name(&self) -> &'static str { + use TrafficViewMode::*; + match self { + Hierarchical => "traffic-hierarchical", + Transactions => "traffic-transactions", + Packets => "traffic-packets", + } + } + + #[cfg(test)] + pub fn from_log_name(log_name: &str) -> TrafficViewMode { + use TrafficViewMode::*; + match log_name { + "traffic-hierarchical" => Hierarchical, + "traffic-transactions" => Transactions, + "traffic-packets" => Packets, + _ => panic!("Unrecognised log name '{log_name}'") + } + } +} + +pub type DeviceViewMode = (); + +impl ItemSource for CaptureReader { + fn item( + &mut self, + parent: Option<&TrafficItem>, + view_mode: TrafficViewMode, + index: u64, + ) -> Result { + use TrafficItem::*; + use TrafficViewMode::*; + match parent { + None => Ok(match view_mode { + Hierarchical => { + let item_id = TrafficItemId::from(index); + let group_id = self.item_index.get(item_id)?; + TransactionGroup(group_id) + }, + Transactions => + Transaction(None, TransactionId::from(index)), + Packets => + Packet(None, None, PacketId::from(index)), + }), + Some(item) => self.child_item(item, index) + } + } + + fn item_update( + &mut self, + _item: &TrafficItem, + ) -> Result, Error> { + Ok(None) + } + + fn child_item( + &mut self, + parent: &TrafficItem, + index: u64, + ) -> Result { + use TrafficItem::*; + Ok(match parent { + TransactionGroup(group_id) => + Transaction(Some(*group_id), { + let entry = self.group_index.get(*group_id)?; + let endpoint_id = entry.endpoint_id(); + let ep_group_id = entry.group_id(); + let ep_traf = self.endpoint_traffic(endpoint_id)?; + let offset = ep_traf.group_index.get(ep_group_id)?; + ep_traf.transaction_ids.get(offset + index)? + }), + Transaction(group_id_opt, transaction_id) => + Packet(*group_id_opt, Some(*transaction_id), { + self.transaction_index.get(*transaction_id)? + index}), + Packet(..) => bail!("Packets have no child items") + }) + } + + fn item_children( + &mut self, + parent: Option<&TrafficItem>, + view_mode: TrafficViewMode, + ) -> Result<(CompletionStatus, u64), Error> { + use TrafficItem::*; + use TrafficViewMode::*; + use CompletionStatus::*; + Ok(match parent { + None => { + let completion = if self.complete() { + Complete + } else { + Ongoing + }; + (completion, match view_mode { + Hierarchical => self.item_index.len(), + Transactions => self.transaction_index.len(), + Packets => self.packet_index.len(), + }) + }, + Some(TransactionGroup(group_id)) => { + let entry = self.group_index.get(*group_id)?; + if !entry.is_start() { + return Ok((Complete, 0)); + } + let transaction_count = self.group_range(&entry)?.len(); + let ep_traf = self.endpoint_traffic(entry.endpoint_id())?; + if entry.group_id().value >= ep_traf.end_index.len() { + (Ongoing, transaction_count) + } else { + (Complete, transaction_count) + } + }, + Some(Transaction(_, transaction_id)) => { + let packet_count = self.transaction_index.target_range( + *transaction_id, self.packet_index.len())?.len(); + if transaction_id.value < self.transaction_index.len() - 1 { + (Complete, packet_count) + } else { + (Ongoing, packet_count) + } + }, + Some(Packet(..)) => (Complete, 0), + }) + } + + fn description( + &mut self, + item: &TrafficItem, + detail: bool, + ) -> Result { + use PID::*; + use TrafficItem::*; + use usb::StartComplete::*; + let mut s = String::new(); + Ok(match item { + Packet(.., packet_id) => { + let packet = self.packet(*packet_id)?; + let len = packet.len(); + let too_long = len > 1027; + if detail { + writeln!(s, "Packet #{} with {len} bytes", + packet_id.value + 1)?; + writeln!(s, "Timestamp: {} ns from capture start", + fmt_count(self.packet_time(*packet_id)?))?; + } + match validate_packet(&packet) { + Err(None) => { + write!(s, "Malformed 0-byte packet")?; + }, + Err(Some(pid)) => { + write!(s, "Malformed packet")?; + match pid { + RSVD if too_long => write!(s, + " (reserved PID, and too long)"), + Malformed if too_long => write!(s, + " (invalid PID, and too long)"), + RSVD => write!(s, + " (reserved PID)"), + Malformed => write!(s, + " (invalid PID)"), + pid if too_long => write!(s, + " (possibly {pid}, but too long)"), + pid => write!(s, + " (possibly {pid}, but {})", + match pid { + SOF|SETUP|IN|OUT|PING => { + if len != 3 { + "wrong length" + } else { + "bad CRC" + } + }, + SPLIT => { + if len != 4 { + "wrong length" + } else { + "bad CRC" + } + }, + DATA0|DATA1|DATA2|MDATA => { + if len < 3 { + "too short" + } else { + "bad CRC" + } + }, + ACK|NAK|NYET|STALL|ERR => "too long", + RSVD|Malformed => unreachable!(), + } + ), + }?; + if len == 1 { + write!(s, " of 1 byte") + } else { + write!(s, " of {len} bytes") + }?; + if detail { + write!(s, "\nHex bytes: {}", Bytes::first(1024, &packet)) + } else { + write!(s, ": {}", Bytes::first(100, &packet)) + }?; + }, + Ok(pid) => { + write!(s, "{pid} packet")?; + let fields = PacketFields::from_packet(&packet); + match &fields { + PacketFields::SOF(sof) => write!(s, + " with frame number {}, CRC {:02X}", + sof.frame_number(), + sof.crc()), + PacketFields::Token(token) => write!(s, + " on {}.{}, CRC {:02X}", + token.device_address(), + token.endpoint_number(), + token.crc()), + PacketFields::Data(data) if len <= 3 => write!(s, + " with CRC {:04X} and no data", + data.crc), + PacketFields::Data(data) => write!(s, + " with CRC {:04X} and {} data bytes", + data.crc, + len - 3), + PacketFields::Split(split) => write!(s, + " {} {} speed {} transaction on hub {} port {}", + match split.sc() { + Start => "starting", + Complete => "completing", + }, + format!("{:?}", split.speed()) + .to_lowercase(), + format!("{:?}", split.endpoint_type()) + .to_lowercase(), + split.hub_address(), + split.port()), + PacketFields::None => Ok(()), + }?; + if matches!(fields, PacketFields::Data(_)) && len > 3 { + let data = &packet[1 .. len - 2]; + if detail { + write!(s, concat!( + "\nHex bytes: [{:02X}, , {:02X}, {:02X}]", + "\nPayload: {}"), + packet[0], packet[len - 2], packet[len - 1], + Bytes::first(1024, data)) + } else { + write!(s, ": {}", Bytes::first(100, data)) + }?; + } else if detail { + write!(s, "\nHex bytes: {packet:02X?}")?; + } + } + } + s + }, + Transaction(group_id_opt, transaction_id) => { + let num_packets = self.packet_index.len(); + let packet_id_range = self.transaction_index.target_range( + *transaction_id, num_packets)?; + let start_packet_id = packet_id_range.start; + let start_packet = self.packet(start_packet_id)?; + let packet_count = packet_id_range.len(); + if detail { + writeln!(s, "Transaction #{} with {} {}", + transaction_id.value + 1, + packet_count, + if packet_count == 1 {"packet"} else {"packets"})?; + writeln!(s, "Timestamp: {} ns from capture start", + fmt_count(self.packet_time(start_packet_id)?))?; + write!(s, "Packets: #{}", packet_id_range.start + 1)?; + if packet_count > 1 { + write!(s, " to #{}", packet_id_range.end)?; + } + writeln!(s)?; + } + if let Ok(pid) = validate_packet(&start_packet) { + if pid == SPLIT && start_packet_id.value + 1 == num_packets { + // We can't know the endpoint yet. + let split = SplitFields::from_packet(&start_packet); + return Ok(format!( + "{} {} speed {} transaction on hub {} port {}", + match split.sc() { + Start => "Starting", + Complete => "Completing", + }, + format!("{:?}", split.speed()).to_lowercase(), + format!("{:?}", split.endpoint_type()).to_lowercase(), + split.hub_address(), + split.port())) + } + let endpoint_id = match group_id_opt { + Some(group_id) => { + let entry = self.group_index.get(*group_id)?; + entry.endpoint_id() + }, + None => match self.shared.packet_endpoint( + pid, &start_packet) + { + Ok(endpoint_id) => endpoint_id, + Err(_) => INVALID_EP_ID + } + }; + let endpoint = self.endpoints.get(endpoint_id)?; + let transaction = self.transaction(*transaction_id)?; + s += &transaction.description(self, &endpoint, detail)? + } else { + let packet_count = packet_id_range.len(); + write!(s, + "{} malformed {}", + packet_count, + if packet_count == 1 {"packet"} else {"packets"})?; + } + s + }, + TransactionGroup(group_id) => { + use GroupContent::*; + let group = self.group(*group_id)?; + if detail && group.is_start { + let ep_traf = + self.endpoint_traffic(group.endpoint_id)?; + let start_ep_transaction_id = group.range.start; + let start_transaction_id = + ep_traf.transaction_ids.get(start_ep_transaction_id)?; + let start_packet_id = + self.transaction_index.get(start_transaction_id)?; + if group.count == 1 { + writeln!(s, "Transaction group with 1 transaction")?; + } else { + writeln!(s, "Transaction group with {} transactions", + group.count)?; + } + writeln!(s, "Timestamp: {} ns from start of capture", + fmt_count(self.packet_time(start_packet_id)?))?; + writeln!(s, "First transaction #{}, first packet #{}", + start_transaction_id.value + 1, + start_packet_id.value + 1)?; + } + let endpoint = &group.endpoint; + let endpoint_type = group.endpoint_type; + let addr = group.endpoint.device_address(); + let count = group.count; + match (group.content, group.is_start) { + (Invalid, true) => write!(s, + "{count} invalid groups"), + (Invalid, false) => write!(s, + "End of invalid groups"), + (Framing, true) => write!(s, + "{count} SOF groups"), + (Framing, false) => write!(s, + "End of SOF groups"), + (Request(transfer), true) if detail => write!(s, + "Control transfer on device {addr}\n{}", + transfer.summary(true)), + (Request(transfer), true) => write!(s, + "{}", transfer.summary(false)), + (IncompleteRequest, true) => write!(s, + "Incomplete control transfer on device {addr}"), + (Request(_) | IncompleteRequest, false) => write!(s, + "End of control transfer on device {addr}"), + (Data(data_range), true) => { + let ep_traf = + self.endpoint_traffic(group.endpoint_id)?; + let length = + ep_traf.transfer_data_length(&data_range)?; + let length_string = fmt_size(length); + let max = if detail { 1024 } else { 100 }; + let display_length = min(length, max) as usize; + let transfer_bytes = self.transfer_bytes( + group.endpoint_id, &data_range, display_length)?; + let display_bytes = Bytes { + partial: length > display_length as u64, + bytes: &transfer_bytes, + }; + let ep_type_string = titlecase( + &format!("{endpoint_type}")); + write!(s, "{ep_type_string} transfer ")?; + write!(s, "of {length_string} ")?; + write!(s, "on endpoint {endpoint}")?; + if detail { + write!(s, "\nPayload: {display_bytes}") + } else { + write!(s, ": {display_bytes}") + } + }, + (Data(_), false) => write!(s, + "End of {endpoint_type} transfer on endpoint {endpoint}"), + (Polling(count), true) => write!(s, + "Polling {count} times for {endpoint_type} transfer on endpoint {endpoint}"), + (Polling(_count), false) => write!(s, + "End polling for {endpoint_type} transfer on endpoint {endpoint}"), + (Ambiguous(_data_range, count), true) => { + write!(s, "{count} ambiguous transactions on endpoint {endpoint}")?; + if detail { + write!(s, "\nThe result of these transactions is ambiguous because the endpoint type is not known.")?; + write!(s, "\nTry starting the capture before this device is enumerated, so that its descriptors are captured.")?; + } + Ok(()) + }, + (Ambiguous(..), false) => write!(s, + "End of ambiguous transactions."), + }?; + s + } + }) + } + + fn connectors( + &mut self, + view_mode: TrafficViewMode, + item: &TrafficItem, + ) -> Result { + use EndpointState::*; + use TrafficItem::*; + use TrafficViewMode::*; + if view_mode == Packets { + return Ok(String::from("")); + } + let last_packet = match item { + Packet(_, Some(transaction_id), packet_id) => { + let range = self.transaction_index.target_range( + *transaction_id, self.packet_index.len())?; + *packet_id == range.end - 1 + }, _ => false + }; + if view_mode == Transactions { + return Ok(String::from(match (item, last_packet) { + (TransactionGroup(_), _) => unreachable!(), + (Transaction(..), _) => "○", + (Packet(..), false) => "├──", + (Packet(..), true ) => "└──", + })); + } + let endpoint_count = self.endpoints.len() as usize; + let max_string_length = endpoint_count + " └──".len(); + let mut connectors = String::with_capacity(max_string_length); + let group_id = match item { + TransactionGroup(i) | + Transaction(Some(i), _) | + Packet(Some(i), ..) => *i, + _ => unreachable!() + }; + let entry = self.group_index.get(group_id)?; + let endpoint_id = entry.endpoint_id(); + let endpoint_state = self.endpoint_state(group_id)?; + let extended = self.group_extended(endpoint_id, group_id)?; + let ep_traf = self.endpoint_traffic(endpoint_id)?; + let last_transaction = match item { + Transaction(_, transaction_id) | + Packet(_, Some(transaction_id), _) => { + let range = ep_traf.group_index.target_range( + entry.group_id(), ep_traf.transaction_ids.len())?; + let last_transaction_id = + ep_traf.transaction_ids.get(range.end - 1)?; + *transaction_id == last_transaction_id + }, _ => false + }; + let last = last_transaction && !extended; + let mut thru = false; + for (i, &state) in endpoint_state.iter().enumerate() { + let state = EndpointState::from(state); + let active = state != Idle; + let on_endpoint = i == endpoint_id.value as usize; + thru |= match (item, state, on_endpoint) { + (TransactionGroup(..), Starting | Ending, _) => true, + (Transaction(..) | Packet(..), _, true) => on_endpoint, + _ => false, + }; + connectors.push(match item { + TransactionGroup(..) => { + match (state, thru) { + (Idle, false) => ' ', + (Idle, true ) => '─', + (Starting, _ ) => '○', + (Ongoing, false) => '│', + (Ongoing, true ) => '┼', + (Ending, _ ) => '└', + } + }, + Transaction(..) => { + match (on_endpoint, active, thru, last) { + (false, false, false, _ ) => ' ', + (false, false, true, _ ) => '─', + (false, true, false, _ ) => '│', + (false, true, true, _ ) => '┼', + (true, _, _, false) => '├', + (true, _, _, true ) => '└', + } + }, + Packet(..) => { + match (on_endpoint, active, last) { + (false, false, _ ) => ' ', + (false, true, _ ) => '│', + (true, _, false) => '│', + (true, _, true ) => ' ', + } + } + }); + }; + let state_length = endpoint_state.len(); + for _ in state_length..endpoint_count { + connectors.push(match item { + TransactionGroup(..) => '─', + Transaction(..) => '─', + Packet(..) => ' ', + }); + } + connectors.push_str( + match (item, last_packet) { + (TransactionGroup(_), _) if entry.is_start() => "─", + (TransactionGroup(_), _) => "──□ ", + (Transaction(..), _) => "───", + (Packet(..), false) => " ├──", + (Packet(..), true) => " └──", + } + ); + Ok(connectors) + } + + fn timestamp(&mut self, item: &TrafficItem) -> Result { + use TrafficItem::*; + let packet_id = match item { + TransactionGroup(group_id) => { + let entry = self.group_index.get(*group_id)?; + let ep_traf = self.endpoint_traffic(entry.endpoint_id())?; + let ep_transaction_id = + ep_traf.group_index.get(entry.group_id())?; + let transaction_id = + ep_traf.transaction_ids.get(ep_transaction_id)?; + self.transaction_index.get(transaction_id)? + }, + Transaction(.., transaction_id) => + self.transaction_index.get(*transaction_id)?, + Packet(.., packet_id) => *packet_id, + }; + self.packet_time(packet_id) + } +} + +impl ItemSource for CaptureReader { + fn item( + &mut self, + parent: Option<&DeviceItem>, + _view_mode: DeviceViewMode, + index: u64, + ) -> Result { + match parent { + None => { + let device_id = DeviceId::from(index + 1); + let data = self.device_data(device_id)?; + let descriptor = data.device_descriptor.load_full(); + Ok(DeviceItem { + device_id, + version: data.version(), + content: DeviceItemContent::Device( + descriptor.map(|arc| *arc) + ), + indent: 0, + }) + }, + Some(item) => self.child_item(item, index) + } + } + + fn item_update( + &mut self, + item: &DeviceItem, + ) -> Result, Error> { + use DeviceItemContent::*; + let data = self.device_data(item.device_id)?; + if data.version() == item.version { + return Ok(None) + } + // These items may have changed because we saw a new descriptor. + Ok(match item.content { + Device(_) | + DeviceDescriptorField(..) | + ConfigurationDescriptorField(..) | + InterfaceDescriptorField(..) => Some( + DeviceItem { + device_id: item.device_id, + version: data.version(), + content: item.content.clone(), + indent: item.indent, + } + ), + _ => None, + }) + } + + fn child_item( + &mut self, + parent: &DeviceItem, + index: u64, + ) -> Result { + use DeviceItemContent::*; + let data = self.device_data(parent.device_id)?; + let content = match &parent.content { + Device(desc_opt) => match index { + 0 => DeviceDescriptor(*desc_opt), + n => { + let conf = ConfigNum(n.try_into()?); + let config = data.configuration(conf)?; + Configuration( + conf, + config.descriptor, + desc_opt.map(|desc| desc.device_class) + ) + } + }, + DeviceDescriptor(desc_opt) => match desc_opt { + Some(desc) => + DeviceDescriptorField(*desc, + DeviceField(index.try_into()?)), + None => bail!("Device descriptor fields not available") + }, + Configuration(conf, desc, class) => { + let config = data.configuration(*conf)?; + let other_count = config.other_descriptors.len(); + let func_count = config.functions.len(); + match index.try_into()? { + 0 => ConfigurationDescriptor(*desc), + n if n < 1 + other_count => + OtherDescriptor( + config + .other_descriptor(n - 1)? + .clone(), + *class), + n if n < 1 + other_count + func_count => + Function(*conf, config + .function(n - 1 - other_count)? + .descriptor), + n => Interface(*conf, config + .unassociated_interfaces() + .nth(n - 1 - other_count - func_count) + .context("Failed to find unassociated interface")? + .descriptor) + } + }, + ConfigurationDescriptor(desc) => + ConfigurationDescriptorField(*desc, + ConfigField(index.try_into()?)), + Function(conf, desc) => { + let config = data.configuration(*conf)?; + match index.try_into()? { + 0 => FunctionDescriptor(*desc), + n => match config.associated_interfaces(desc).nth(n - 1) { + Some(interface) => + Interface(*conf, interface.descriptor), + None => bail!( + "Function has no interface with index {n}") + } + } + }, + FunctionDescriptor(desc) => + FunctionDescriptorField(*desc, + IfaceAssocField(index.try_into()?)), + Interface(conf, if_desc) => { + let config = data.configuration(*conf)?; + let interface = config.interface(if_desc.key())?; + let desc_count = interface.other_descriptors.len(); + match index.try_into()? { + 0 => InterfaceDescriptor(*if_desc), + n if n < 1 + desc_count => { + let desc = interface.other_descriptor(n - 1)?.clone(); + if let Descriptor::Hid(hid_desc) = desc { + HidDescriptor(hid_desc) + } else { + OtherDescriptor(desc, + Some(interface.descriptor.interface_class)) + } + }, + n => { + let ep_num = InterfaceEpNum( + (n - 1 - desc_count).try_into()?); + Endpoint(*conf, if_desc.key(), ep_num) + } + } + }, + Endpoint(conf, if_key, ep_num) => { + let config = data.configuration(*conf)?; + let interface = config.interface(*if_key)?; + let endpoint = interface.endpoint(*ep_num)?; + match index.try_into()? { + 0 => EndpointDescriptor(endpoint.descriptor), + n => OtherDescriptor( + endpoint.other_descriptors + .get(n - 1) + .context("Other endpoint descriptor not found")? + .clone(), + Some(interface.descriptor.interface_class) + ) + } + }, + InterfaceDescriptor(desc) => + InterfaceDescriptorField(*desc, + InterfaceField(index.try_into()?)), + EndpointDescriptor(desc) => + EndpointDescriptorField(*desc, + EndpointField(index.try_into()?)), + HidDescriptor(desc) => { + const N: usize = usb::HidDescriptor::NUM_FIELDS; + const LAST_FIELD: usize = N - 1; + match index.try_into()? { + 0..=LAST_FIELD => + HidDescriptorField(desc.clone(), + HidField(index.try_into()?)), + N => HidDescriptorList(desc.clone()), + _ => bail!("HID descriptor has no child with index {index}") + } + }, + HidDescriptorList(desc) => + HidDescriptorEntry(desc.clone(), + HidField(index.try_into()?)), + _ => bail!("This device item type cannot have children") + }; + Ok(DeviceItem { + device_id: parent.device_id, + version: data.version(), + content, + indent: parent.indent + 1, + }) + } + + fn item_children( + &mut self, + parent: Option<&DeviceItem>, + _view_mode: DeviceViewMode + ) -> Result<(CompletionStatus, u64), Error> { + use DeviceItemContent::*; + use CompletionStatus::*; + let (completion, children) = match parent { + None => { + let completion = if self.complete() { + Complete + } else { + Ongoing + }; + let children = self.devices.len().saturating_sub(1) as usize; + (completion, children) + }, + Some(item) => { + let data = self.device_data(item.device_id)?; + match &item.content { + Device(_) => { + let count = data.configurations.load().len(); + (Ongoing, if count == 0 { 1 } else { count }) + }, + DeviceDescriptor(_) => + match data.device_descriptor.load().as_ref() { + Some(_) => + (Ongoing, usb::DeviceDescriptor::NUM_FIELDS), + None => (Ongoing, 0), + }, + Configuration(conf, ..) => { + let config = data.configuration(*conf)?; + (Ongoing, + 1 + config.other_descriptors.len() + + config.functions.len() + + config.unassociated_interfaces().count()) + } + ConfigurationDescriptor(_) => + (Ongoing, usb::ConfigDescriptor::NUM_FIELDS), + Function(conf, desc) => { + let config = data.configuration(*conf)?; + let interfaces = config.associated_interfaces(desc); + (Complete, 1 + interfaces.count()) + } + FunctionDescriptor(_) => + (Complete, + usb::InterfaceAssociationDescriptor::NUM_FIELDS), + Interface(conf, desc) => { + let config = data.configuration(*conf)?; + let interface = config.interface(desc.key())?; + (Ongoing, + 1 + interface.endpoints.len() + + interface.other_descriptors.len()) + }, + Endpoint(conf, key, ep_num) => { + let config = data.configuration(*conf)?; + let interface = config.interface(*key)?; + let endpoint = interface.endpoint(*ep_num)?; + (Complete, 1 + endpoint.other_descriptors.len()) + } + InterfaceDescriptor(_) => + (Ongoing, usb::InterfaceDescriptor::NUM_FIELDS), + EndpointDescriptor(_) => + (Complete, usb::EndpointDescriptor::NUM_FIELDS), + HidDescriptor(_) => + (Complete, usb::HidDescriptor::NUM_FIELDS + 1), + HidDescriptorList(desc) => + (Complete, desc.available_descriptors.len()), + // Other types have no children. + _ => (Complete, 0), + } + } + }; + Ok((completion, children as u64)) + } + + fn description( + &mut self, + item: &DeviceItem, + _detail: bool + ) -> Result { + use DeviceItemContent::*; + let data = self.device_data(item.device_id)?; + Ok(match &item.content { + Device(_) => { + let device = self.devices.get(item.device_id)?; + format!("Device {}: {}", device.address, data.description()) + }, + DeviceDescriptor(desc) => { + match desc { + Some(_) => "Device descriptor", + None => "No device descriptor" + }.to_string() + }, + DeviceDescriptorField(desc, field) => { + let strings = data.strings.load(); + desc.field_text(*field, strings.as_ref()) + }, + Configuration(conf, ..) => format!( + "Configuration {conf}"), + ConfigurationDescriptor(_) => + "Configuration descriptor".to_string(), + ConfigurationDescriptorField(desc, field) => { + let strings = data.strings.load(); + desc.field_text(*field, strings.as_ref()) + }, + Function(_conf, desc) => { + format!("Function {}: {}", + desc.function, + desc.function_class.name() + ) + }, + FunctionDescriptor(_) => + "Interface association descriptor".to_string(), + FunctionDescriptorField(desc, field) => desc.field_text(*field), + Interface(_conf, desc) => { + let num = desc.interface_number; + let class = desc.interface_class.name(); + match desc.alternate_setting { + InterfaceAlt(0) => format!( + "Interface {num}: {class}"), + InterfaceAlt(alt) => format!( + "Interface {num} alt {alt}: {class}"), + } + }, + InterfaceDescriptor(_) => + "Interface descriptor".to_string(), + InterfaceDescriptorField(desc, field) => { + let strings = data.strings.load(); + desc.field_text(*field, strings.as_ref()) + }, + Endpoint(conf, if_key, ep_num) => { + let config = data.configuration(*conf)?; + let interface = config.interface(*if_key)?; + let endpoint = interface.endpoint(*ep_num)?; + let desc = &endpoint.descriptor; + let addr = desc.endpoint_address; + let attrs = desc.attributes; + format!("Endpoint {} {} ({})", addr.number(), + addr.direction(), attrs.endpoint_type()) + }, + EndpointDescriptor(_) => + "Endpoint descriptor".to_string(), + EndpointDescriptorField(desc, field) => desc.field_text(*field), + HidDescriptor(_) => "HID descriptor".to_string(), + HidDescriptorField(desc, field) => desc.field_text(*field), + HidDescriptorList(_) => "Available descriptors".to_string(), + HidDescriptorEntry(desc, field) => { + let (desc_type, length) = + desc.available_descriptors + .get(field.0 as usize) + .context("Not enough entries in descriptor list")?; + format!("{}, {} bytes", + desc_type.description_with_class(ClassId::HID), length) + }, + OtherDescriptor(desc, class) => desc.description(*class), + }) + } + + fn connectors( + &mut self, + _view_mode: (), + item: &DeviceItem + ) -> Result { + Ok(" ".repeat(item.indent as usize)) + } + + fn timestamp(&mut self, _item: &DeviceItem) -> Result { + unreachable!() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::{BufReader, BufWriter, BufRead, Write}; + use std::path::PathBuf; + use itertools::Itertools; + use crate::capture::create_capture; + use crate::decoder::Decoder; + use crate::pcap::Loader; + + fn summarize_item( + cap: &mut CaptureReader, + item: &Item, + mode: ViewMode, + ) -> String + where CaptureReader: ItemSource, + ViewMode: Copy + { + let mut summary = format!("{} {}", + cap.connectors(mode, item).unwrap(), + cap.description(item, false).unwrap() + ); + let (_completion, num_children) = + cap.item_children(Some(item), mode).unwrap(); + let child_ids = 0..num_children; + for (n, child_summary) in child_ids + .map(|child_id| { + let child = cap.child_item(item, child_id).unwrap(); + summarize_item(cap, &child, mode) + }) + .dedup_with_count() + { + summary += "\n"; + if n > 1 { + summary += &format!("{} ({} times)", &child_summary, n); + } else { + summary += &child_summary; + } + } + summary + } + + fn write_item( + cap: &mut CaptureReader, + item: &Item, + mode: ViewMode, + writer: &mut dyn Write + ) + where CaptureReader: ItemSource, + ViewMode: Copy + { + let summary = summarize_item(cap, item, mode); + writer.write(summary.as_bytes()).unwrap(); + writer.write(b"\n").unwrap(); + } + + #[test] + fn test_captures() { + let test_dir = PathBuf::from("./tests/"); + let mut list_path = test_dir.clone(); + list_path.push("tests.txt"); + let list_file = File::open(list_path).unwrap(); + let mode = TrafficViewMode::Hierarchical; + for test_name in BufReader::new(list_file).lines() { + let mut test_path = test_dir.clone(); + test_path.push(test_name.unwrap()); + let mut cap_path = test_path.clone(); + let mut traf_ref_path = test_path.clone(); + let mut traf_out_path = test_path.clone(); + let mut dev_ref_path = test_path.clone(); + let mut dev_out_path = test_path.clone(); + cap_path.push("capture.pcap"); + traf_ref_path.push("reference.txt"); + traf_out_path.push("output.txt"); + dev_ref_path.push("devices-reference.txt"); + dev_out_path.push("devices-output.txt"); + { + let file = File::open(cap_path).unwrap(); + let mut loader = Loader::open(file).unwrap(); + let (writer, mut reader) = create_capture().unwrap(); + let mut decoder = Decoder::new(writer).unwrap(); + while let Some(result) = loader.next() { + let (packet, timestamp_ns) = result.unwrap(); + decoder + .handle_raw_packet(&packet.data, timestamp_ns) + .unwrap(); + } + decoder.finish().unwrap(); + let traf_out_file = File::create(traf_out_path.clone()).unwrap(); + let mut traf_out_writer = BufWriter::new(traf_out_file); + let num_items = reader.item_index.len(); + for item_id in 0 .. num_items { + let item = reader.item(None, mode, item_id).unwrap(); + write_item(&mut reader, &item, mode, &mut traf_out_writer); + } + let dev_out_file = File::create(dev_out_path.clone()).unwrap(); + let mut dev_out_writer = BufWriter::new(dev_out_file); + let num_devices = reader.devices.len() - 1; + for device_id in 0 .. num_devices { + let item = reader.item(None, (), device_id).unwrap(); + write_item(&mut reader, &item, (), &mut dev_out_writer); + } + } + for (ref_path, out_path) in [ + (traf_ref_path, traf_out_path), + (dev_ref_path, dev_out_path), + ] { + let ref_file = File::open(ref_path).unwrap(); + let out_file = File::open(out_path.clone()).unwrap(); + let ref_reader = BufReader::new(ref_file); + let out_reader = BufReader::new(out_file); + let mut out_lines = out_reader.lines(); + for line in ref_reader.lines() { + let expected = line.unwrap(); + let actual = out_lines.next().unwrap().unwrap(); + assert_eq!(actual, expected); + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index a7008a1a..61f91182 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +//! The main Packetry binary. + // On Windows, produce a GUI app rather than a console one. #![windows_subsystem = "windows"] @@ -20,35 +22,21 @@ pub mod built { // Declare all modules used. mod backend; mod capture; -mod compact_index; -mod data_stream; +mod database; mod decoder; -mod id; -mod index_stream; -mod item_widget; -mod model; +mod item; mod pcap; -mod rcu; -mod row_data; -mod stream; -mod test_cynthion; -mod tree_list_model; +mod testing; mod ui; mod usb; mod util; -mod vec_map; mod version; -// Declare optional modules. -#[cfg(any(test, feature="record-ui-test"))] -mod record_ui; -#[cfg(test)] -mod test_replay; - use gtk::prelude::*; use gtk::gio::ApplicationFlags; use gtk::glib::{self, OptionArg, OptionFlags}; +use testing::test_cynthion; use ui::{ activate, display_error, @@ -75,7 +63,7 @@ fn main() { version_info(have_argument("--dependencies"))); } else if have_argument("--test-cynthion") { let save_captures = have_argument("--save-captures"); - test_cynthion::run_test(save_captures); + test_cynthion(save_captures); } else { if gtk::init().is_err() { eprintln!("Failed to initialize GTK"); diff --git a/src/pcap.rs b/src/pcap.rs index c24b8f29..befe8372 100644 --- a/src/pcap.rs +++ b/src/pcap.rs @@ -1,3 +1,5 @@ +//! Code for loading and saving pcap files. + use std::borrow::Cow; use std::io::{BufReader, BufWriter, Read, Write}; use std::mem::size_of; diff --git a/src/test_cynthion.rs b/src/testing.rs similarity index 98% rename from src/test_cynthion.rs rename to src/testing.rs index de085e0c..606ff280 100644 --- a/src/test_cynthion.rs +++ b/src/testing.rs @@ -1,3 +1,5 @@ +//! Hardware-in-the loop test using a Cynthion USB analyzer. + use crate::backend::{BackendHandle, Speed}; use crate::backend::cynthion::{CynthionDevice, CynthionHandle, VID_PID}; use crate::capture::{ @@ -23,7 +25,7 @@ use std::time::{Duration, Instant}; const US: Duration = Duration::from_micros(1); const MS: Duration = Duration::from_millis(1); -pub fn run_test(save_captures: bool) { +pub fn test_cynthion(save_captures: bool) { for (name, speed, ep_addr, length, sof) in [ ("HS", Speed::High, 0x81, 4096, Some((124*US, 126*US))), ("FS", Speed::Full, 0x82, 512, Some((995*US, 1005*US))), diff --git a/src/item_widget.rs b/src/ui/item_widget.rs similarity index 97% rename from src/item_widget.rs rename to src/ui/item_widget.rs index 5c74881f..92353e6f 100644 --- a/src/item_widget.rs +++ b/src/ui/item_widget.rs @@ -1,4 +1,6 @@ -//! GObject subclass for our custom widget. +//! GObject subclass for the widget we use to display an item. +//! +//! Wraps a GTK box which contains further widgets. use std::cell::RefMut; use gtk::{ diff --git a/src/ui.rs b/src/ui/mod.rs similarity index 99% rename from src/ui.rs rename to src/ui/mod.rs index 2f5c11b3..a1f97c3a 100644 --- a/src/ui.rs +++ b/src/ui/mod.rs @@ -1,3 +1,5 @@ +//! The Packetry user interface. + use std::cell::RefCell; use std::collections::BTreeMap; use std::io::Write; @@ -79,6 +81,8 @@ use crate::capture::{ EndpointDataEvent, Group, GroupContent, +}; +use crate::item::{ ItemSource, TrafficItem, TrafficViewMode::{self,*}, @@ -87,22 +91,33 @@ use crate::capture::{ DeviceViewMode, }; use crate::decoder::Decoder; -use crate::item_widget::ItemWidget; use crate::pcap::{Loader, Writer}; -use crate::model::{GenericModel, TrafficModel, DeviceModel}; -use crate::row_data::{ - GenericRowData, - ToGenericRowData, - TrafficRowData, - DeviceRowData}; use crate::usb::{Descriptor, PacketFields, validate_packet}; use crate::util::{fmt_count, fmt_size}; use crate::version::{version, version_info}; +pub mod item_widget; +pub mod model; +pub mod row_data; +pub mod tree_list_model; +#[cfg(any(test, feature="record-ui-test"))] +pub mod record_ui; +#[cfg(test)] +mod test_replay; + +use item_widget::ItemWidget; +use model::{GenericModel, TrafficModel, DeviceModel}; +use row_data::{ + GenericRowData, + ToGenericRowData, + TrafficRowData, + DeviceRowData, +}; + #[cfg(any(test, feature="record-ui-test"))] use { std::rc::Rc, - crate::record_ui::Recording, + record_ui::Recording, }; const TRAFFIC_MODES: [TrafficViewMode; 3] = @@ -1409,7 +1424,7 @@ fn save_data( } fn show_about() -> Result<(), Error> { - const LICENSE: &str = include_str!("../LICENSE"); + const LICENSE: &str = include_str!("../../LICENSE"); let about = AboutDialog::builder() .program_name("Packetry") .version(format!("Version: {}", version())) diff --git a/src/model.rs b/src/ui/model.rs similarity index 93% rename from src/model.rs rename to src/ui/model.rs index c8687aa2..3f2a551d 100644 --- a/src/model.rs +++ b/src/ui/model.rs @@ -11,14 +11,9 @@ use gtk::{gio, glib}; use anyhow::Error; -use crate::capture::{ - CaptureReader, - TrafficItem, - TrafficViewMode, - DeviceItem, - DeviceViewMode, -}; -use crate::tree_list_model::{TreeListModel, ItemNodeRc}; +use crate::capture::CaptureReader; +use crate::item::{TrafficItem, TrafficViewMode, DeviceItem, DeviceViewMode}; +use crate::ui::tree_list_model::{TreeListModel, ItemNodeRc}; /// Trait implemented by each of our ListModel implementations. pub trait GenericModel where Self: Sized { @@ -130,9 +125,9 @@ mod imp { use gtk::{gio, glib, prelude::*}; use std::cell::RefCell; - use crate::capture::{TrafficItem, TrafficViewMode, DeviceItem, DeviceViewMode}; - use crate::row_data::{TrafficRowData, DeviceRowData}; - use crate::tree_list_model::TreeListModel; + use crate::item::{TrafficItem, TrafficViewMode, DeviceItem, DeviceViewMode}; + use crate::ui::row_data::{TrafficRowData, DeviceRowData}; + use crate::ui::tree_list_model::TreeListModel; /// Define the inner type to be used in the GObject type system. macro_rules! model { diff --git a/src/record_ui.rs b/src/ui/record_ui.rs similarity index 97% rename from src/record_ui.rs rename to src/ui/record_ui.rs index 830d35b2..95861475 100644 --- a/src/record_ui.rs +++ b/src/ui/record_ui.rs @@ -1,3 +1,5 @@ +//! Code for recording UI interactions for testing purposes. + use std::collections::HashMap; use std::fs::File; use std::io::Write; @@ -8,9 +10,10 @@ use gtk::gio::prelude::ListModelExt; use itertools::Itertools; use serde::{Serialize, Deserialize}; -use crate::capture::{CaptureReader, ItemSource}; -use crate::model::GenericModel; -use crate::row_data::ToGenericRowData; +use crate::capture::CaptureReader; +use crate::item::ItemSource; +use super::model::GenericModel; +use super::row_data::ToGenericRowData; #[derive(Serialize, Deserialize)] pub enum UiAction { diff --git a/src/row_data.rs b/src/ui/row_data.rs similarity index 93% rename from src/row_data.rs rename to src/ui/row_data.rs index 87ee2170..1c0b2d30 100644 --- a/src/row_data.rs +++ b/src/ui/row_data.rs @@ -6,8 +6,8 @@ use gtk::subclass::prelude::*; #[cfg(any(test, feature="record-ui-test"))] use gtk::prelude::Cast; -use crate::capture::{TrafficItem, DeviceItem}; -use crate::tree_list_model::ItemNodeRc; +use crate::item::{TrafficItem, DeviceItem}; +use crate::ui::tree_list_model::ItemNodeRc; /// Trait implemented by each of our row data types. pub trait GenericRowData where Item: Clone { @@ -61,8 +61,8 @@ mod imp { use gtk::glib::{self, subclass::prelude::*}; use std::cell::RefCell; - use crate::capture::{TrafficItem, DeviceItem}; - use crate::tree_list_model::ItemNodeRc; + use crate::item::{TrafficItem, DeviceItem}; + use crate::ui::tree_list_model::ItemNodeRc; /// Define the inner type to be used in the GObject type system. macro_rules! row_data { diff --git a/src/test_replay.rs b/src/ui/test_replay.rs similarity index 97% rename from src/test_replay.rs rename to src/ui/test_replay.rs index 23a62a03..da80d3a5 100644 --- a/src/test_replay.rs +++ b/src/ui/test_replay.rs @@ -1,3 +1,5 @@ +//! Test the UI by replaying previously recorded interactions. + use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; @@ -7,13 +9,13 @@ use gtk::prelude::*; use itertools::assert_equal; use serde_json::Deserializer; -use crate::capture::TrafficViewMode; use crate::decoder::Decoder; +use crate::item::TrafficViewMode; use crate::pcap::Loader; -use crate::model::GenericModel; -use crate::row_data::{GenericRowData, TrafficRowData, DeviceRowData}; -use crate::record_ui::UiAction; use crate::ui::{ + model::GenericModel, + row_data::{GenericRowData, TrafficRowData, DeviceRowData}, + record_ui::UiAction, UserInterface, activate, reset_capture, diff --git a/src/tree_list_model.rs b/src/ui/tree_list_model.rs similarity index 98% rename from src/tree_list_model.rs rename to src/ui/tree_list_model.rs index 63b8d79f..92c4b8b5 100644 --- a/src/tree_list_model.rs +++ b/src/ui/tree_list_model.rs @@ -1,3 +1,8 @@ +//! Our custom implementation of TreeListModel. +//! +//! The concept is similar to the GTK TreeListModel type, but the +//! implementation is customised for Packetry's usage. + use std::cell::RefCell; use std::cmp::min; use std::collections::{BTreeMap, HashSet}; @@ -15,10 +20,11 @@ use gtk::gio::prelude::ListModelExt; use derive_more::AddAssign; use itertools::Itertools; -use crate::capture::{CaptureReader, ItemSource}; -use crate::model::GenericModel; -use crate::row_data::GenericRowData; -use crate::item_widget::ItemWidget; +use crate::capture::CaptureReader; +use crate::item::ItemSource; +use super::model::GenericModel; +use super::row_data::GenericRowData; +use super::item_widget::ItemWidget; type RootNodeRc = Rc>>; pub type ItemNodeRc = Rc>>; diff --git a/src/usb.rs b/src/usb.rs index 8615d9f2..e7d973e8 100644 --- a/src/usb.rs +++ b/src/usb.rs @@ -1,3 +1,5 @@ +//! Code describing the USB standard and its data types. + use std::collections::BTreeMap; use std::fmt::Formatter; use std::mem::size_of; @@ -20,8 +22,10 @@ use num_enum::{IntoPrimitive, FromPrimitive}; use derive_more::{From, Into, Display}; use usb_ids::FromId; -use crate::util::titlecase; -use crate::vec_map::VecMap; +use crate::util::{ + vec_map::VecMap, + titlecase +}; fn crc16(bytes: &[u8]) -> u16 { const CRC16: Crc = Crc::::new(&CRC_16_USB); @@ -152,6 +156,7 @@ byte_type!(ConfigField); byte_type!(InterfaceNum); byte_type!(InterfaceAlt); byte_type!(InterfaceField); +byte_type!(InterfaceEpNum); byte_type!(EndpointNum); byte_type!(EndpointField); byte_type!(EndpointAddr); @@ -1577,6 +1582,7 @@ pub mod prelude { InterfaceAlt, InterfaceKey, InterfaceField, + InterfaceEpNum, EndpointNum, EndpointField, HidField, diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index ae7f79c4..00000000 --- a/src/util.rs +++ /dev/null @@ -1,51 +0,0 @@ -use anyhow::{Error, bail}; -use num_format::{Locale, ToFormattedString}; -use humansize::{SizeFormatter, BINARY}; -use itertools::Itertools; - -pub fn fmt_count(count: u64) -> String { - count.to_formatted_string(&Locale::en) -} - -pub fn fmt_size(size: u64) -> String { - if size == 1 { - "1 byte".to_string() - } else if size < 1024 { - format!("{size} bytes") - } else { - format!("{}", SizeFormatter::new(size, BINARY)) - } -} - -pub fn handle_thread_panic(result: std::thread::Result) - -> Result -{ - match result { - Ok(x) => Ok(x), - Err(panic) => { - let msg = match ( - panic.downcast_ref::<&str>(), - panic.downcast_ref::()) - { - (Some(&s), _) => s, - (_, Some(s)) => s, - (None, None) => "" - }; - bail!("Worker thread panic: {msg}"); - } - } -} - -pub fn titlecase(text: &str) -> String { - format!("{}{}", - text - .chars() - .take(1) - .map(|c| c.to_uppercase().to_string()) - .join(""), - text - .chars() - .skip(1) - .collect::() - ) -} diff --git a/src/id.rs b/src/util/id.rs similarity index 90% rename from src/id.rs rename to src/util/id.rs index f9a1ffc5..6a37c11a 100644 --- a/src/id.rs +++ b/src/util/id.rs @@ -1,3 +1,7 @@ +//! The Id type and its traits. +//! +//! Used to enforce type safety of indices used in the capture database. + use std::fmt::{Debug, Display}; use std::marker::PhantomData; use std::mem::size_of; @@ -40,22 +44,6 @@ impl Debug for Id { } } -pub trait HasLength { - fn len(&self) -> u64; -} - -impl HasLength for Range> { - fn len(&self) -> u64 { - self.end.value - self.start.value - } -} - -impl HasLength for Range { - fn len(&self) -> u64 { - self.end - self.start - } -} - impl PartialEq> for Id { fn eq(&self, other: &Id) -> bool { self.value.eq(&other.value) diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 00000000..08d2b8ab --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,139 @@ +//! Utility code that doesn't belong anywhere specific. + +use std::ops::Range; + +use anyhow::{Error, bail}; +use num_format::{Locale, ToFormattedString}; +use humansize::{SizeFormatter, BINARY}; +use itertools::Itertools; + +pub mod id; +pub mod vec_map; +pub mod rcu; + +use id::Id; + +pub fn fmt_count(count: u64) -> String { + count.to_formatted_string(&Locale::en) +} + +pub fn fmt_size(size: u64) -> String { + if size == 1 { + "1 byte".to_string() + } else if size < 1024 { + format!("{size} bytes") + } else { + format!("{}", SizeFormatter::new(size, BINARY)) + } +} + +pub fn handle_thread_panic(result: std::thread::Result) + -> Result +{ + match result { + Ok(x) => Ok(x), + Err(panic) => { + let msg = match ( + panic.downcast_ref::<&str>(), + panic.downcast_ref::()) + { + (Some(&s), _) => s, + (_, Some(s)) => s, + (None, None) => "" + }; + bail!("Worker thread panic: {msg}"); + } + } +} + +pub fn titlecase(text: &str) -> String { + format!("{}{}", + text + .chars() + .take(1) + .map(|c| c.to_uppercase().to_string()) + .join(""), + text + .chars() + .skip(1) + .collect::() + ) +} + +pub struct Bytes<'src> { + pub partial: bool, + pub bytes: &'src [u8], +} + +impl<'src> Bytes<'src> { + pub fn first(max: usize, bytes: &'src [u8]) -> Self { + if bytes.len() > max { + Bytes { + partial: true, + bytes: &bytes[0..max], + } + } else { + Bytes { + partial: false, + bytes, + } + } + } + + fn looks_like_ascii(&self) -> bool { + let mut num_printable = 0; + for &byte in self.bytes { + if byte == 0 || byte >= 0x80 { + // Outside ASCII range. + return false; + } + // Count printable and pseudo-printable characters. + let printable = match byte { + c if (0x20..0x7E).contains(&c) => true, // printable range + 0x09 => true, // tab + 0x0A => true, // new line + 0x0D => true, // carriage return + _ => false + }; + if printable { + num_printable += 1; + } + } + // If the string is at least half printable, treat as ASCII. + num_printable > 0 && num_printable >= self.bytes.len() / 2 + } +} + +impl std::fmt::Display for Bytes<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.looks_like_ascii() { + write!(f, "'{}'", String::from_utf8( + self.bytes.iter() + .flat_map(|c| {std::ascii::escape_default(*c)}) + .collect::>()).unwrap())? + } else { + write!(f, "{:02X?}", self.bytes)? + }; + if self.partial { + write!(f, "...") + } else { + Ok(()) + } + } +} + +pub trait RangeLength { + fn len(&self) -> u64; +} + +impl RangeLength for Range> { + fn len(&self) -> u64 { + self.end.value - self.start.value + } +} + +impl RangeLength for Range { + fn len(&self) -> u64 { + self.end - self.start + } +} diff --git a/src/rcu.rs b/src/util/rcu.rs similarity index 93% rename from src/rcu.rs rename to src/util/rcu.rs index 7e4eb258..00fa17b9 100644 --- a/src/rcu.rs +++ b/src/util/rcu.rs @@ -1,3 +1,5 @@ +//! Implementation of the read-copy-update (RCU) pattern. + use std::sync::Arc; use arc_swap::ArcSwapAny; diff --git a/src/vec_map.rs b/src/util/vec_map.rs similarity index 96% rename from src/vec_map.rs rename to src/util/vec_map.rs index 5a16de32..e414c261 100644 --- a/src/vec_map.rs +++ b/src/util/vec_map.rs @@ -1,9 +1,11 @@ +//! A fast Vec-based map type, for use where keys are small bounded integers. + use std::iter::FilterMap; use std::ops::{Index, IndexMut}; use std::marker::PhantomData; use std::slice::Iter; -use crate::id::Id; +use crate::util::id::Id; pub trait Key { fn id(self) -> usize; diff --git a/src/version.rs b/src/version.rs index 397d72f9..649dccb3 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,3 +1,5 @@ +//! Version information. + use std::fmt::Write; use crate::built::*;