From 53814a80764e4c495298e5a66e427a86efcea0d5 Mon Sep 17 00:00:00 2001 From: "kevin.russ" Date: Tue, 16 Jun 2026 10:01:53 +0200 Subject: [PATCH] Add someip-statistics view --- Cargo.lock | 5 +- Cargo.toml | 2 +- crates/app/Cargo.toml | 2 + crates/app/src/host/command.rs | 7 + crates/app/src/host/common/dlt_stats.rs | 10 +- crates/app/src/host/common/mod.rs | 1 + crates/app/src/host/common/someip_stats.rs | 787 ++++++++++++++++++ crates/app/src/host/message.rs | 7 +- crates/app/src/host/service/mod.rs | 85 +- .../service/storage/recent/legacy/actions.rs | 13 +- crates/app/src/host/ui/mod.rs | 19 + .../host/ui/session_setup/main_config/dlt.rs | 22 +- .../host/ui/session_setup/main_config/mod.rs | 9 +- .../ui/session_setup/main_config/someip.rs | 359 ++++++++ crates/app/src/host/ui/session_setup/mod.rs | 12 +- .../src/host/ui/session_setup/state/mod.rs | 45 +- .../ui/session_setup/state/parsers/dlt.rs | 7 + .../ui/session_setup/state/parsers/mod.rs | 10 +- .../ui/session_setup/state/parsers/someip.rs | 158 +++- .../app/src/host/ui/storage/recent/session.rs | 7 +- .../src/host/ui/storage/recent/source_key.rs | 1 + .../app/src/host/ui/storage/recent/storage.rs | 1 + crates/core/parsers/Cargo.toml | 1 + crates/core/parsers/src/dlt/fmt.rs | 17 +- crates/core/parsers/src/someip.rs | 114 ++- .../benches/someip_legacy_producer.rs | 4 +- .../core/processor/benches/someip_producer.rs | 4 +- .../core/session/src/handlers/export_raw.rs | 8 +- .../session/src/handlers/observing/mod.rs | 10 +- .../core/session/tests/snapshot_tests/mod.rs | 18 +- .../observe_dlt_with_someip_session.snap | 5 +- .../observe_someip_legacy_session.snap | 2 +- ...nap => observe_someip_pcapng_session.snap} | 2 +- crates/core/sources/src/socket/udp.rs | 5 +- crates/file_tools/src/lib.rs | 6 +- crates/stypes/src/observe/extending.rs | 14 + crates/stypes/src/observe/mod.rs | 8 + development/resources/{ => someip}/someip.dlt | Bin development/resources/{ => someip}/someip.xml | 0 development/resources/someip/tcp/someip.pcap | Bin 0 -> 6382 bytes .../resources/someip/tcp/someip.pcapng | Bin 0 -> 7396 bytes .../resources/{ => someip/udp}/someip.pcap | Bin .../resources/{ => someip/udp}/someip.pcapng | Bin 43 files changed, 1652 insertions(+), 135 deletions(-) create mode 100644 crates/app/src/host/common/someip_stats.rs create mode 100644 crates/app/src/host/ui/session_setup/main_config/someip.rs rename crates/core/session/tests/snapshot_tests/snapshots/{observe_someip_bcapng_session.snap => observe_someip_pcapng_session.snap} (99%) rename development/resources/{ => someip}/someip.dlt (100%) rename development/resources/{ => someip}/someip.xml (100%) create mode 100644 development/resources/someip/tcp/someip.pcap create mode 100644 development/resources/someip/tcp/someip.pcapng rename development/resources/{ => someip/udp}/someip.pcap (100%) rename development/resources/{ => someip/udp}/someip.pcapng (100%) diff --git a/Cargo.lock b/Cargo.lock index 5aa0423ce2..e717aaa056 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,6 +299,7 @@ dependencies = [ "merging", "nucleo-matcher", "parsers", + "pcap-parser", "plugins_host", "processor", "regex", @@ -312,6 +313,7 @@ dependencies = [ "serialport", "session", "shell_tools", + "someip-messages", "sources", "stypes", "tar", @@ -4194,6 +4196,7 @@ dependencies = [ "someip-payload", "someip_tools", "stringreader", + "stypes", "thiserror 2.0.18", ] @@ -5637,7 +5640,7 @@ dependencies = [ [[package]] name = "someip-messages" version = "0.3.1" -source = "git+https://github.com/esrlabs/someip#59b27a6689d72948c4569bc6037c2387c1a661ed" +source = "git+https://github.com/esrlabs/someip?rev=59b27a6689d72948c4569bc6037c2387c1a661ed#59b27a6689d72948c4569bc6037c2387c1a661ed" dependencies = [ "byteorder", "derive_builder", diff --git a/Cargo.toml b/Cargo.toml index 66b05e5e46..8f8b794500 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,7 +110,7 @@ anstyle-parse = "1.0" unicode-segmentation = "1.13" rayon = "1" socket2 = "0.5" -someip-messages = { git = "https://github.com/esrlabs/someip" } +someip-messages = { git = "https://github.com/esrlabs/someip", rev = "59b27a6689d72948c4569bc6037c2387c1a661ed" } tar = "0.4" flate2 = "1.1" diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 44f939c15d..d43989e439 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -52,6 +52,8 @@ blake3.workspace = true nucleo-matcher.workspace = true anstyle-parse.workspace = true unicode-segmentation.workspace = true +pcap-parser.workspace = true +someip-messages.workspace = true #TODO: Replace env logger with log4rs env_logger.workspace = true diff --git a/crates/app/src/host/command.rs b/crates/app/src/host/command.rs index 48c6bd3d09..4b6be73f19 100644 --- a/crates/app/src/host/command.rs +++ b/crates/app/src/host/command.rs @@ -46,6 +46,7 @@ pub enum HostCommand { /// Reopen a recent-session snapshot with the requested intent. OpenRecentSession(Box), DltStatistics(Box), + SomeipStatistics(Box), StartSession(Box), /// Imports named presets from the provided file. ImportPresets(PathBuf), @@ -112,6 +113,12 @@ pub struct DltStatisticsParam { pub source_paths: Vec, } +#[derive(Debug, Clone)] +pub struct SomeipStatisticsParam { + pub session_setup_id: Uuid, + pub source_paths: Vec, +} + #[derive(Debug, Clone)] pub struct ScanFavoriteFoldersParam { /// Local request identifier echoed back with the scan result. diff --git a/crates/app/src/host/common/dlt_stats.rs b/crates/app/src/host/common/dlt_stats.rs index 5f3ece0e1b..51ac4ec249 100644 --- a/crates/app/src/host/common/dlt_stats.rs +++ b/crates/app/src/host/common/dlt_stats.rs @@ -30,20 +30,26 @@ pub fn dlt_statistics(sources: Vec) -> Result { /// The statistics-info of DLT files. #[derive(Debug, Default, Clone)] pub struct DltStatistics { + /// The total number of DLT messages. counter: usize, + /// The overall distribution of log-levels. pub total: LevelDistribution, + /// The app-id specific distribution of log-levels. pub app_ids: FxHashMap, + // The context-id specific distribution of log-levels. pub ctx_ids: FxHashMap, + /// The ecu-id specific distribution of log-levels. pub ecu_ids: FxHashMap, } impl DltStatistics { + /// Returns the total number of message-ids. pub fn count(&self) -> usize { self.app_ids.len() + self.ctx_ids.len() + self.ecu_ids.len() } } -/// The Level distribution of DLT messages. +/// The level distribution of DLT messages. #[derive(Debug, Default, Clone)] pub struct LevelDistribution { pub fatal: FxHashSet, @@ -57,6 +63,7 @@ pub struct LevelDistribution { } impl LevelDistribution { + /// Returns the total number of messages. pub fn count(&self) -> usize { self.fatal.len() + self.error.len() @@ -68,6 +75,7 @@ impl LevelDistribution { + self.invalid.len() } + /// Returns the type specific numbers of messages. pub fn values(&self) -> [usize; 8] { [ self.fatal.len(), diff --git a/crates/app/src/host/common/mod.rs b/crates/app/src/host/common/mod.rs index a4c3326e5d..4d970756cd 100644 --- a/crates/app/src/host/common/mod.rs +++ b/crates/app/src/host/common/mod.rs @@ -3,5 +3,6 @@ pub mod colors; pub mod dlt_stats; pub mod file_utls; pub mod parsers; +pub mod someip_stats; pub mod sources; pub mod ui_utls; diff --git a/crates/app/src/host/common/someip_stats.rs b/crates/app/src/host/common/someip_stats.rs new file mode 100644 index 0000000000..cf4548c994 --- /dev/null +++ b/crates/app/src/host/common/someip_stats.rs @@ -0,0 +1,787 @@ +use anyhow::Result; +use pcap_parser::{ + Block, LegacyPcapReader, PcapBlockOwned, PcapError, PcapNGReader, traits::PcapReaderIterator, +}; +use rustc_hash::FxHashMap; +use someip_messages::*; +use std::io::Read; +use std::{fs::File, path::PathBuf}; + +/// Collects the SOME/IP statistics from the given source files. +pub fn someip_statistics(sources: Vec) -> Result { + let mut statistics = SomeipStatistics::default(); + + for source in sources { + let file = File::open(&source) + .map_err(|error| format!("{:?}: failed to open source: {}", source, error))?; + + let mut collector = SomeipStatisticsCollector::default(); + + match source.extension().and_then(|ext| ext.to_str()) { + Some(ext) if ext.eq_ignore_ascii_case("pcap") => { + let mut reader = LegacyPcapReader::new(65536, file).map_err(|error| { + format!("{:?}: failed to create pcap reader: {}", source, error) + })?; + + collect_statistics_from_pcap(&mut reader, &mut statistics, &mut collector) + .map_err(|error| format!("{:?}: {}", source, error))?; + } + + Some(ext) if ext.eq_ignore_ascii_case("pcapng") => { + let mut reader = PcapNGReader::new(65536, file).map_err(|error| { + format!("{:?}: failed to create pcapng reader: {}", source, error) + })?; + + collect_statistics_from_pcapng(&mut reader, &mut statistics, &mut collector) + .map_err(|error| format!("{:?}: {}", source, error))?; + } + + _ => { + return Err(format!("unsupported source: {:?}", source)); + } + } + } + + Ok(statistics) +} + +/// The statistics-info of SOME/IP files. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct SomeipStatistics { + /// The overall distribution of messages-types. + pub total: MessageDistribution, + /// The message-id specific distribution of messages-types. + pub messages: FxHashMap, +} + +impl SomeipStatistics { + /// Returns the total number of message-ids. + pub fn count(&self) -> usize { + self.messages.len() + } + + /// Returns the distribution of messages for the given id, + /// or if not found, adds and returns an empty one. + pub fn message(&mut self, id: MessageId) -> &mut MessageDistribution { + self.messages.entry(id).or_default() + } +} + +/// The type distribution of SOME/IP messages. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct MessageDistribution { + pub sd: usize, + pub event: usize, + pub request: usize, + pub response: usize, + pub fire_forget: usize, + pub error: usize, +} + +impl MessageDistribution { + /// Returns the total number of messages. + pub fn count(&self) -> usize { + self.sd + self.event + self.request + self.response + self.fire_forget + self.error + } + + /// Returns the type specific numbers of messages. + pub fn values(&self) -> [usize; 6] { + [ + self.sd, + self.event, + self.request, + self.response, + self.fire_forget, + self.error, + ] + } + + pub fn merge(&mut self, other: &MessageDistribution) -> &mut Self { + self.sd += other.sd; + self.event += other.event; + self.request += other.request; + self.response += other.response; + self.fire_forget += other.fire_forget; + self.error += other.error; + self + } +} + +fn collect_statistics( + statistics: &mut SomeipStatistics, + mut payload: &[u8], +) -> Result, someip_messages::Error> { + loop { + if payload.is_empty() { + return Ok(Vec::new()); + } + + if payload.len() < Header::LENGTH { + return Ok(payload.to_vec()); + } + + match Message::from_slice(payload) { + Ok(message) => { + let consumed = match &message { + Message::Sd(header, _) | Message::Rpc(header, _) => header.message_len(), + + Message::CookieClient | Message::CookieServer => Header::LENGTH, + }; + + count_message(statistics, message); + + payload = &payload[consumed..]; + } + + Err(someip_messages::Error::NotEnoughData { .. }) => { + return Ok(payload.to_vec()); + } + + Err(error) => { + return Err(error); + } + } + } +} + +fn count_message(statistics: &mut SomeipStatistics, message: Message) { + match message { + Message::Sd(header, _) => { + let message_id = header.message_id().clone(); + + match header.message_type() { + MessageType::Notification => { + statistics.total.sd += 1; + statistics.message(message_id).sd += 1; + } + MessageType::Error => { + statistics.total.error += 1; + statistics.message(message_id).error += 1; + } + _ => {} + } + } + + Message::Rpc(header, _) => { + let message_id = header.message_id().clone(); + + match header.message_type() { + MessageType::Notification | MessageType::TpNotification => { + statistics.total.event += 1; + statistics.message(message_id).event += 1; + } + MessageType::Request | MessageType::TpRequest => { + statistics.total.request += 1; + statistics.message(message_id).request += 1; + } + MessageType::Response | MessageType::TpResponse => { + statistics.total.response += 1; + statistics.message(message_id).response += 1; + } + MessageType::RequestNoReturn | MessageType::TpRequestNoReturn => { + statistics.total.fire_forget += 1; + statistics.message(message_id).fire_forget += 1; + } + MessageType::Error | MessageType::TpError => { + statistics.total.error += 1; + statistics.message(message_id).error += 1; + } + } + } + + Message::CookieClient | Message::CookieServer => {} + } +} + +fn collect_statistics_from_pcap( + reader: &mut LegacyPcapReader, + statistics: &mut SomeipStatistics, + collector: &mut SomeipStatisticsCollector, +) -> Result<(), String> { + loop { + match reader.next() { + Ok((offset, block)) => { + if let PcapBlockOwned::Legacy(b) = block + && let Some(transport) = extract_ethernet_payload(b.data) + { + collect_statistics_from_transport_payload(collector, statistics, transport)?; + } + + reader.consume(offset); + } + Err(pcap_parser::PcapError::Eof) => break, + Err(e) => { + eprintln!("PCAP error: {:?}", e); + break; + } + } + } + + Ok(()) +} + +fn collect_statistics_from_pcapng( + reader: &mut PcapNGReader, + statistics: &mut SomeipStatistics, + collector: &mut SomeipStatisticsCollector, +) -> Result<(), String> { + loop { + match reader.next() { + Ok((offset, block)) => { + if let PcapBlockOwned::NG(block) = block { + match block { + Block::EnhancedPacket(epb) => { + if let Some(transport) = extract_ethernet_payload(epb.data) { + collect_statistics_from_transport_payload( + collector, statistics, transport, + )?; + } + } + + Block::SimplePacket(spb) => { + if let Some(transport) = extract_ethernet_payload(spb.data) { + collect_statistics_from_transport_payload( + collector, statistics, transport, + )?; + } + } + + _ => {} + } + } + + reader.consume(offset); + } + + Err(PcapError::Eof) => break, + + Err(PcapError::Incomplete(_)) => { + reader + .refill() + .map_err(|e| format!("PCAPNG refill error: {:?}", e))?; + } + + Err(e) => { + return Err(format!("PCAPNG error: {:?}", e)); + } + } + } + + Ok(()) +} + +fn collect_statistics_from_transport_payload( + collector: &mut SomeipStatisticsCollector, + statistics: &mut SomeipStatistics, + transport: TransportPayload<'_>, +) -> Result<(), String> { + match transport { + TransportPayload::Udp(payload) => { + let remaining = collect_statistics(statistics, payload) + .map_err(|error| format!("failed to parse SOME/IP UDP payload: {:?}", error))?; + + if !remaining.is_empty() { + eprintln!( + "incomplete SOME/IP UDP payload: {} remaining bytes", + remaining.len() + ); + } + } + + TransportPayload::Tcp { flow, payload } => { + let buffer = collector.tcp_buffers.entry(flow).or_default(); + buffer.extend_from_slice(payload); + + let remaining = collect_statistics(statistics, buffer).map_err(|error| { + format!("failed to parse SOME/IP TCP stream {:?}: {:?}", flow, error) + })?; + + *buffer = remaining; + } + } + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum IpAddrKey { + V4([u8; 4]), + V6([u8; 16]), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct TcpFlowKey { + src_ip: IpAddrKey, + dst_ip: IpAddrKey, + src_port: u16, + dst_port: u16, +} + +enum TransportPayload<'a> { + Udp(&'a [u8]), + Tcp { flow: TcpFlowKey, payload: &'a [u8] }, +} + +#[derive(Debug, Default)] +struct SomeipStatisticsCollector { + tcp_buffers: FxHashMap>, +} + +fn extract_ethernet_payload(frame: &[u8]) -> Option> { + if frame.len() < 14 { + return None; + } + + let mut offset = 12usize; + let mut ethertype = u16::from_be_bytes([frame[offset], frame[offset + 1]]); + offset += 2; + + while ethertype == 0x8100 || ethertype == 0x88A8 { + if frame.len() < offset + 4 { + return None; + } + + offset += 2; + ethertype = u16::from_be_bytes([frame[offset], frame[offset + 1]]); + offset += 2; + } + + match ethertype { + 0x0800 => extract_ipv4_payload(frame, offset), + 0x86DD => extract_ipv6_payload(frame, offset), + _ => None, + } +} + +fn extract_ipv4_payload(frame: &[u8], ip_start: usize) -> Option> { + if frame.len() < ip_start + 20 { + return None; + } + + let ver_ihl = frame[ip_start]; + let version = ver_ihl >> 4; + let ihl = (ver_ihl & 0x0f) as usize * 4; + + if version != 4 || ihl < 20 || frame.len() < ip_start + ihl { + return None; + } + + let total_len = u16::from_be_bytes([frame[ip_start + 2], frame[ip_start + 3]]) as usize; + if total_len < ihl || frame.len() < ip_start + total_len { + return None; + } + + let src_ip = IpAddrKey::V4(frame[ip_start + 12..ip_start + 16].try_into().ok()?); + let dst_ip = IpAddrKey::V4(frame[ip_start + 16..ip_start + 20].try_into().ok()?); + + let protocol = frame[ip_start + 9]; + let transport_start = ip_start + ihl; + let ip_payload_end = ip_start + total_len; + + extract_transport_payload( + frame, + transport_start, + ip_payload_end, + protocol, + src_ip, + dst_ip, + ) +} + +fn extract_ipv6_payload(frame: &[u8], ip_start: usize) -> Option> { + const IPV6_HEADER_LEN: usize = 40; + + if frame.len() < ip_start + IPV6_HEADER_LEN { + return None; + } + + let version = frame[ip_start] >> 4; + if version != 6 { + return None; + } + + let payload_len = u16::from_be_bytes([frame[ip_start + 4], frame[ip_start + 5]]) as usize; + let next_header = frame[ip_start + 6]; + + let src_ip = IpAddrKey::V6(frame[ip_start + 8..ip_start + 24].try_into().ok()?); + let dst_ip = IpAddrKey::V6(frame[ip_start + 24..ip_start + 40].try_into().ok()?); + + let transport_start = ip_start + IPV6_HEADER_LEN; + let ip_payload_end = transport_start.checked_add(payload_len)?; + + if ip_payload_end > frame.len() { + return None; + } + + extract_transport_payload( + frame, + transport_start, + ip_payload_end, + next_header, + src_ip, + dst_ip, + ) +} + +fn extract_transport_payload( + frame: &[u8], + transport_start: usize, + ip_payload_end: usize, + protocol: u8, + src_ip: IpAddrKey, + dst_ip: IpAddrKey, +) -> Option> { + match protocol { + 17 => { + extract_udp_payload(frame, transport_start, ip_payload_end).map(TransportPayload::Udp) + } + + 6 => extract_tcp_payload(frame, transport_start, ip_payload_end, src_ip, dst_ip), + + _ => None, + } +} + +fn extract_udp_payload(frame: &[u8], udp_start: usize, ip_payload_end: usize) -> Option<&[u8]> { + if udp_start + 8 > ip_payload_end { + return None; + } + + let udp_len = u16::from_be_bytes([frame[udp_start + 4], frame[udp_start + 5]]) as usize; + if udp_len < 8 { + return None; + } + + let payload_start = udp_start + 8; + let payload_end = udp_start.checked_add(udp_len)?; + + if payload_end > ip_payload_end { + return None; + } + + Some(&frame[payload_start..payload_end]) +} + +fn extract_tcp_payload( + frame: &[u8], + tcp_start: usize, + ip_payload_end: usize, + src_ip: IpAddrKey, + dst_ip: IpAddrKey, +) -> Option> { + if tcp_start + 20 > ip_payload_end { + return None; + } + + let src_port = u16::from_be_bytes([frame[tcp_start], frame[tcp_start + 1]]); + let dst_port = u16::from_be_bytes([frame[tcp_start + 2], frame[tcp_start + 3]]); + + let data_offset = (frame[tcp_start + 12] >> 4) as usize * 4; + if data_offset < 20 { + return None; + } + + let payload_start = tcp_start.checked_add(data_offset)?; + + if payload_start > ip_payload_end { + return None; + } + + Some(TransportPayload::Tcp { + flow: TcpFlowKey { + src_ip, + dst_ip, + src_port, + dst_port, + }, + payload: &frame[payload_start..ip_payload_end], + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn messages() -> FxHashMap { + let mut messages = FxHashMap::default(); + + messages.insert( + MessageId { + service_id: 123, + method_id: 32773, + }, + MessageDistribution { + sd: 0, + event: 22, + request: 0, + response: 0, + fire_forget: 0, + error: 0, + }, + ); + + messages.insert( + MessageId { + service_id: 65535, + method_id: 33024, + }, + MessageDistribution { + sd: 33, + event: 0, + request: 0, + response: 0, + fire_forget: 0, + error: 0, + }, + ); + + messages + } + + #[test] + fn test_statistics_from_udp_pcap() { + let sources: Vec = + ["../../development/resources/someip/udp/someip.pcap".into()].to_vec(); + + let statistics = someip_statistics(sources).expect("stats"); + + assert_eq!( + statistics, + SomeipStatistics { + total: MessageDistribution { + sd: 33, + event: 22, + request: 0, + response: 0, + fire_forget: 0, + error: 0, + }, + messages: messages(), + } + ); + } + + #[test] + fn test_statistics_from_tcp_pcap() { + let sources: Vec = + ["../../development/resources/someip/tcp/someip.pcap".into()].to_vec(); + + let statistics = someip_statistics(sources).expect("stats"); + + assert_eq!( + statistics, + SomeipStatistics { + total: MessageDistribution { + sd: 33, + event: 22, + request: 0, + response: 0, + fire_forget: 0, + error: 0, + }, + messages: messages(), + } + ); + } + + #[test] + fn test_statistics_from_udp_pcapng() { + let sources: Vec = + ["../../development/resources/someip/udp/someip.pcapng".into()].to_vec(); + + let statistics = someip_statistics(sources).expect("stats"); + + assert_eq!( + statistics, + SomeipStatistics { + total: MessageDistribution { + sd: 33, + event: 22, + request: 0, + response: 0, + fire_forget: 0, + error: 0, + }, + messages: messages(), + } + ); + } + + #[test] + fn test_statistics_from_tcp_pcapng() { + let sources: Vec = + ["../../development/resources/someip/tcp/someip.pcapng".into()].to_vec(); + + let statistics = someip_statistics(sources).expect("stats"); + + assert_eq!( + statistics, + SomeipStatistics { + total: MessageDistribution { + sd: 33, + event: 22, + request: 0, + response: 0, + fire_forget: 0, + error: 0, + }, + messages: messages(), + } + ); + } + + #[test] + fn test_statistics_with_incomplete_message() { + let partial = [ + 0x00, 0x7b, 0x80, 0x05, // message id + 0x00, 0x00, 0x00, // incomplete + ]; + + let mut statistics = SomeipStatistics::default(); + + let remaining = collect_statistics(&mut statistics, &partial).expect("parse"); + + assert_eq!(remaining, partial); + assert_eq!(statistics.total.count(), 0); + } + + #[test] + fn test_statistics_with_multiple_messages_in_one_payload() { + let msg = [ + 0x00, 0x7b, 0x80, 0x05, // message id + 0x00, 0x00, 0x00, 0x08, // length + 0x00, 0x00, 0x00, 0x01, // request id + 0x01, // protocol version + 0x01, // interface version + 0x02, // notification + 0x00, // return code + ]; + + let mut payload = Vec::new(); + payload.extend_from_slice(&msg); + payload.extend_from_slice(&msg); + + let mut statistics = SomeipStatistics::default(); + + let remaining = collect_statistics(&mut statistics, &payload).expect("parse"); + + assert!(remaining.is_empty()); + assert_eq!(statistics.total.event, 2); + } + + #[test] + fn test_statistics_with_complete_message_and_partial() { + let complete = [ + 0x00, 0x7b, 0x80, 0x05, // message id + 0x00, 0x00, 0x00, 0x08, // length + 0x00, 0x00, 0x00, 0x01, // request id + 0x01, // protocol version + 0x01, // interface version + 0x02, // notification + 0x00, // return code + ]; + + let partial = [ + 0x00, 0x7b, 0x80, // incomplete + ]; + + let mut payload = Vec::new(); + payload.extend_from_slice(&complete); + payload.extend_from_slice(&partial); + + let mut statistics = SomeipStatistics::default(); + + let remaining = collect_statistics(&mut statistics, &payload).expect("parse"); + + assert_eq!(remaining, partial); + assert_eq!(statistics.total.event, 1); + } + + #[test] + fn test_statistics_with_invalid_message() { + let invalid = [ + 0x00, 0x7b, 0x80, 0x05, // message id + 0x00, 0x00, 0x00, 0x08, // length + 0x00, 0x00, 0x00, 0x01, // request id + 0x01, // protocol version + 0x01, // interface version + 0xc8, // invalid type + 0x00, // return code + ]; + + let mut statistics = SomeipStatistics::default(); + + let result = collect_statistics(&mut statistics, &invalid); + + assert!(result.is_err()); + assert_eq!(statistics.total.count(), 0); + } + + #[test] + fn test_statistics_with_message_assembled_from_two_payloads() { + let message = [ + 0x00, 0x7b, 0x80, 0x05, // message id + 0x00, 0x00, 0x00, 0x08, // length + 0x00, 0x00, 0x00, 0x01, // request id + 0x01, // protocol version + 0x01, // interface version + 0x02, // notification + 0x00, // return code + ]; + + let first_payload = &message[..7]; + let second_payload = &message[7..]; + + let mut statistics = SomeipStatistics::default(); + + let mut buffer = Vec::new(); + + buffer.extend_from_slice(first_payload); + buffer = collect_statistics(&mut statistics, &buffer).expect("first parse"); + + assert_eq!(buffer, first_payload); + assert_eq!(statistics.total.count(), 0); + + buffer.extend_from_slice(second_payload); + buffer = collect_statistics(&mut statistics, &buffer).expect("second parse"); + + assert!(buffer.is_empty()); + assert_eq!(statistics.total.event, 1); + + let message_id = MessageId { + service_id: 123, + method_id: 32773, + }; + + assert_eq!(statistics.message(message_id).event, 1); + } + + #[test] + fn message_distribution() { + let mut a = MessageDistribution { + sd: 1, + event: 2, + request: 3, + response: 4, + fire_forget: 5, + error: 6, + }; + + let b = MessageDistribution { + sd: 10, + event: 20, + request: 30, + response: 40, + fire_forget: 50, + error: 60, + }; + + assert_eq!(a.count(), 21); + assert_eq!(a.values(), [1, 2, 3, 4, 5, 6]); + + a.merge(&b); + + assert_eq!(a.count(), 231); + assert_eq!(a.values(), [11, 22, 33, 44, 55, 66]); + } +} diff --git a/crates/app/src/host/message.rs b/crates/app/src/host/message.rs index 83eec67e94..dfba92818f 100644 --- a/crates/app/src/host/message.rs +++ b/crates/app/src/host/message.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use crate::{ host::{ - common::dlt_stats::DltStatistics, + common::{dlt_stats::DltStatistics, someip_stats::SomeipStatistics}, ui::{ multi_setup::state::MultiFileState, registry::presets::Preset, @@ -35,6 +35,11 @@ pub enum HostMessage { /// Collected statistics, or `None` when collection failed. statistics: Option>, }, + /// The collected Someip statistics on a file for a SessionSetup + SomeipStatistics { + setup_session_id: Uuid, + statistics: Option>, + }, /// A new session has been successfully created. SessionCreated { session: Box, diff --git a/crates/app/src/host/service/mod.rs b/crates/app/src/host/service/mod.rs index 0a8768bf4e..4c59564ebf 100644 --- a/crates/app/src/host/service/mod.rs +++ b/crates/app/src/host/service/mod.rs @@ -14,7 +14,7 @@ use uuid::Uuid; use parsers::dlt::DltFilterConfig; use stypes::{ DltParserSettings, FileFormat, NativeError, NativeErrorKind, ObserveOptions, ObserveOrigin, - ParserType, Severity, SomeIpParserSettings, Transport, + ParserType, Severity, SomeIpParserSettings, SomeipFilterConfig, Transport, }; use crate::{ @@ -22,9 +22,12 @@ use crate::{ host::{ command::{ DltStatisticsParam, ExportPresetsParam, HostCommand, ScanFavoriteFoldersParam, - StartSessionParam, + SomeipStatisticsParam, StartSessionParam, + }, + common::{ + dlt_stats::dlt_statistics, parsers::ParserNames, someip_stats::someip_statistics, + sources::StreamNames, }, - common::{dlt_stats::dlt_statistics, parsers::ParserNames, sources::StreamNames}, communication::ServiceHandle, error::HostError, message::{HostMessage, ImportFormat, PluginReadmeLoaded, PresetsImported}, @@ -223,17 +226,24 @@ impl HostService { HostCommand::ConnectionSessionSetup { stream, parser } => { self.connection_session_setup(stream, parser).await } - HostCommand::DltStatistics(statistics_param) => { let DltStatisticsParam { session_setup_id, source_paths, } = *statistics_param; - self.collect_statistics(session_setup_id, source_paths) + self.collect_dlt_statistics(session_setup_id, source_paths) .await?; } + HostCommand::SomeipStatistics(statistics_param) => { + let SomeipStatisticsParam { + session_setup_id, + source_paths, + } = *statistics_param; + self.collect_someip_statistics(session_setup_id, source_paths) + .await?; + } HostCommand::StartSession(start_params) => { let StartSessionParam { parser, @@ -432,7 +442,9 @@ impl HostService { let format = file::get_file_format(&file_path).map_err(InitSessionError::IO)?; let parser = match format { FileFormat::PcapNG | FileFormat::PcapLegacy => { - ParserConfig::SomeIP(SomeIpParserConfig::new()) + ParserConfig::SomeIP(Box::new(SomeIpParserConfig::new(Some(vec![ + file_path.clone(), + ])))) } FileFormat::Text => ParserConfig::Text, FileFormat::Binary => { @@ -619,7 +631,7 @@ impl HostService { let parser = match format { FileFormat::PcapNG | FileFormat::PcapLegacy => { - ParserConfig::SomeIP(SomeIpParserConfig::new()) + ParserConfig::SomeIP(Box::new(SomeIpParserConfig::new(Some(files.clone())))) } FileFormat::Text => ParserConfig::Text, FileFormat::Binary => { @@ -736,7 +748,7 @@ impl HostService { let parser = match parser { ParserNames::Dlt => ParserConfig::Dlt(Box::new(DltParserConfig::new(false, None))), - ParserNames::SomeIP => ParserConfig::SomeIP(SomeIpParserConfig::default()), + ParserNames::SomeIP => ParserConfig::SomeIP(Box::new(SomeIpParserConfig::new(None))), ParserNames::Text => ParserConfig::Text, ParserNames::Plugins => ParserConfig::Plugins(Box::new(PluginParserConfig::new())), }; @@ -749,7 +761,7 @@ impl HostService { .await; } - async fn collect_statistics( + async fn collect_dlt_statistics( &self, setup_session_id: Uuid, source_paths: Vec, @@ -786,6 +798,43 @@ impl HostService { Ok(()) } + async fn collect_someip_statistics( + &self, + setup_session_id: Uuid, + source_paths: Vec, + ) -> Result<(), HostError> { + let senders = self.communication.senders.clone(); + tokio::task::spawn_blocking(move || { + match someip_statistics(source_paths) { + Ok(statistics) => { + Handle::current().block_on(async move { + senders + .send_message(HostMessage::SomeipStatistics { + setup_session_id, + statistics: Some(Box::new(statistics)), + }) + .await; + }); + } + Err(error) => { + Handle::current().block_on(async move { + senders + .send_notification(AppNotification::Error(error)) + .await; + senders + .send_message(HostMessage::SomeipStatistics { + setup_session_id, + statistics: None, + }) + .await; + }); + } + }; + }); + + Ok(()) + } + async fn import_presets(&self, path: PathBuf) -> Result<(), HostError> { let task_path = path.clone(); let report = tokio::task::spawn_blocking(move || { @@ -983,6 +1032,19 @@ impl HostService { ParserType::Dlt(dlt_config) } ParserConfig::SomeIP(config) => { + let selected_ids = &config.someip_tables.message_table.selected_ids; + let filter_config = if selected_ids.is_empty() { + None + } else { + let mut message_ids = Vec::new(); + for id in selected_ids { + message_ids.push((id.service_id, id.method_id)); + } + Some(SomeipFilterConfig { + messages: message_ids, + }) + }; + let fibex_file_paths = config.fibex_files.is_empty().not().then(|| { config .fibex_files @@ -991,7 +1053,10 @@ impl HostService { .collect() }); - let someip_settings = SomeIpParserSettings { fibex_file_paths }; + let someip_settings = SomeIpParserSettings { + filter_config, + fibex_file_paths, + }; ParserType::SomeIp(someip_settings) } diff --git a/crates/app/src/host/service/storage/recent/legacy/actions.rs b/crates/app/src/host/service/storage/recent/legacy/actions.rs index bbbaff9b5b..72cde8b894 100644 --- a/crates/app/src/host/service/storage/recent/legacy/actions.rs +++ b/crates/app/src/host/service/storage/recent/legacy/actions.rs @@ -345,11 +345,16 @@ fn parse_dlt_settings(payload: &Value) -> DltParserSettings { } fn parse_someip_settings(payload: &Value) -> SomeIpParserSettings { - let fibex_file_paths = payload - .as_object() - .and_then(|object| string_vec_field(object, &["fibex_file_paths", "fibexFilePaths"])); + let Some(object) = payload.as_object() else { + return SomeIpParserSettings::default(); + }; - SomeIpParserSettings { fibex_file_paths } + SomeIpParserSettings { + filter_config: object + .get("filter_config") + .and_then(|value| from_value(value.clone()).ok()), + fibex_file_paths: string_vec_field(object, &["fibex_file_paths", "fibexFilePaths"]), + } } fn parse_plugin_settings(payload: &Value) -> Result { diff --git a/crates/app/src/host/ui/mod.rs b/crates/app/src/host/ui/mod.rs index 5dba6809cb..c09bd26669 100644 --- a/crates/app/src/host/ui/mod.rs +++ b/crates/app/src/host/ui/mod.rs @@ -191,6 +191,25 @@ impl Host { config.update_summary(); } } + HostMessage::SomeipStatistics { + setup_session_id, + statistics, + } => { + if let Some(setup) = self + .tabs + .tabs_mut() + .iter_mut() + .filter_map(|tab| match tab { + HostTab::SessionSetup(setup) => Some(setup), + _ => None, + }) + .find(|setup| setup.id() == setup_session_id) + && let ParserConfig::SomeIP(config) = &mut setup.state.parser + { + config.someip_statistics = Some(Box::new(*statistics.unwrap_or_default())); + config.update_summary(); + } + } HostMessage::SessionCreated { session, session_setup_id, diff --git a/crates/app/src/host/ui/session_setup/main_config/dlt.rs b/crates/app/src/host/ui/session_setup/main_config/dlt.rs index dabab632b9..446583d27f 100644 --- a/crates/app/src/host/ui/session_setup/main_config/dlt.rs +++ b/crates/app/src/host/ui/session_setup/main_config/dlt.rs @@ -15,7 +15,7 @@ pub fn render_statistics(parser: &mut DltParserConfig, ui: &mut Ui) -> RenderOut }); }); - return RenderOutcome::CollectStatistics; + return RenderOutcome::CollectDltStatistics; } ui.vertical(|ui| { @@ -92,15 +92,7 @@ pub mod summary { .vscroll(false) .striped(true) .resizable(false) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) + .columns(Column::remainder(), COLUMN_NAMES.len()) .header(24.0, |mut header| { for name in COLUMN_NAMES.iter() { header.col(|ui| { @@ -263,15 +255,7 @@ pub mod statistics { .striped(true) .resizable(false) .sense(Sense::click()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) - .column(Column::remainder()) + .columns(Column::remainder(), COLUMN_NAMES.len()) .header(24.0, |mut header| { for (idx, name) in COLUMN_NAMES.iter().enumerate() { header.col(|ui| { diff --git a/crates/app/src/host/ui/session_setup/main_config/mod.rs b/crates/app/src/host/ui/session_setup/main_config/mod.rs index 2305dc359b..a4f85f53bc 100644 --- a/crates/app/src/host/ui/session_setup/main_config/mod.rs +++ b/crates/app/src/host/ui/session_setup/main_config/mod.rs @@ -30,6 +30,7 @@ mod dlt; pub mod process; mod recent; mod serial; +mod someip; mod tcp; pub mod udp; @@ -102,11 +103,11 @@ fn render_files( ui: &mut Ui, ) -> RenderOutcome { match parser { + ParserConfig::SomeIP(someip) => someip::render_statistics(someip, ui), ParserConfig::Dlt(dlt) if dlt.with_storage_header => dlt::render_statistics(dlt, ui), - ParserConfig::Dlt(..) - | ParserConfig::SomeIP(..) - | ParserConfig::Text - | ParserConfig::Plugins(..) => RenderOutcome::None, + ParserConfig::Dlt(..) | ParserConfig::Text | ParserConfig::Plugins(..) => { + RenderOutcome::None + } } } diff --git a/crates/app/src/host/ui/session_setup/main_config/someip.rs b/crates/app/src/host/ui/session_setup/main_config/someip.rs new file mode 100644 index 0000000000..8ff6cea23b --- /dev/null +++ b/crates/app/src/host/ui/session_setup/main_config/someip.rs @@ -0,0 +1,359 @@ +use super::RenderOutcome; +use crate::host::ui::session_setup::state::parsers::SomeIpParserConfig; +use egui::{ScrollArea, Ui}; + +pub fn render_statistics(parser: &mut SomeIpParserConfig, ui: &mut Ui) -> RenderOutcome { + if parser.someip_statistics.is_none() { + ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| { + let available = ui.available_height(); + ui.add_space(available * 0.45); + + ui.vertical_centered(|ui| { + ui.spinner(); + ui.add_space(8.0); + ui.label("Analyzing SOME/IP structure..."); + }); + }); + + return RenderOutcome::CollectSomeipStatistics; + } + + ui.vertical(|ui| { + if parser.someip_tables.take_changed() { + parser.update_summary(); + } + + if let Some(someip_statistics) = &parser.someip_statistics { + summary::table(ui, &parser.someip_summary); + summary::chart(ui, &parser.someip_summary); + ui.separator(); + + ScrollArea::vertical() + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.vertical(|ui| { + statistics::table( + ui, + "Messages", + &someip_statistics.messages, + &mut parser.someip_tables.message_table, + ); + }); + }); + } + }); + + RenderOutcome::None +} + +pub mod summary { + use egui::{Color32, RichText, Sense, TextStyle, Ui, vec2}; + use egui_extras::{Column, TableBuilder}; + use egui_plot::{Bar, BarChart, Plot}; + + use crate::host::ui::session_setup::state::parsers::someip::SomeipSummary; + + const COLUMN_NAMES: [&str; 7] = [ + "TOTAL", + "SD", + "EVENT", + "REQUEST", + "RESPONSE", + "FIREFORGET", + "ERROR", + ]; + + const MESSAGE_WITH_COLORS: [(&str, Color32); 6] = [ + ("SD", Color32::from_rgb(255, 69, 0)), + ("EVENT", Color32::from_rgb(255, 140, 0)), + ("REQUEST", Color32::from_rgb(60, 179, 113)), + ("RESPONSE", Color32::from_rgb(30, 144, 255)), + ("FIREFORGET", Color32::from_rgb(138, 43, 226)), + ("ERROR", Color32::from_rgb(192, 192, 192)), + ]; + + pub fn table(ui: &mut Ui, summary: &SomeipSummary) { + let title = format!("Summary ({} / {})", summary.total.ids, summary.selected.ids); + ui.label(RichText::new(title).strong()); + ui.add_space(5.0); + + TableBuilder::new(ui) + .id_salt("summary_table") + .vscroll(false) + .striped(true) + .resizable(false) + .columns(Column::remainder(), COLUMN_NAMES.len()) + .header(24.0, |mut header| { + for name in COLUMN_NAMES.iter() { + header.col(|ui| { + ui.label(*name); + }); + } + }) + .body(|mut body| { + for (count, levels) in [ + (summary.total.count, summary.total.messages), + (summary.selected.count, summary.selected.messages), + ] { + body.row(20.0, |mut row_ui| { + row_ui.col(|ui| { + ui.label(count.to_string()); + }); + for level in levels { + row_ui.col(|ui| { + ui.label(level.to_string()); + }); + } + }); + } + }); + } + + pub fn chart(ui: &mut Ui, summary: &SomeipSummary) { + let mut charts: Vec = Vec::new(); + + for (i, (_, color)) in MESSAGE_WITH_COLORS.iter().enumerate() { + let total = percent(summary.total.messages[i] as f64, summary.total.count as f64); + let selected = percent( + summary.selected.messages[i] as f64, + summary.total.count as f64, + ); + let mut chart = BarChart::new( + format!("summary_chart_bar_{}", i), + vec![ + Bar::new(0.5, total).fill(*color), + Bar::new(0.0, selected).fill(*color), + ], + ) + .width(0.4) + .color(*color) + .horizontal(); + + if let Some(prev) = charts.last() { + chart = chart.stack_on(&[prev]); + } + + charts.push(chart); + } + + let back = BarChart::new( + "summary_chart_background", + vec![Bar::new(0.5, 100.0), Bar::new(0.0, 100.0)], + ) + .width(0.4) + .color(Color32::from_rgb(192, 192, 192)) + .horizontal(); + + charts.insert(0, back); + + Plot::new("summary_chart") + .height(70.0) + .show_background(false) + .allow_drag(false) + .allow_zoom(false) + .allow_scroll(false) + .allow_boxed_zoom(false) + .show_grid(false) + .show_axes(false) + .show_x(false) + .show_y(false) + .x_axis_formatter(|_, _| String::new()) + .y_axis_formatter(|_, _| String::new()) + .show(ui, |plot_ui| { + for chart in charts { + plot_ui.bar_chart(chart); + } + }); + + // chart legend + ui.horizontal(|ui| { + for (level, color) in MESSAGE_WITH_COLORS.iter() { + let (rect, _) = ui.allocate_exact_size(vec2(10.0, 10.0), Sense::empty()); + ui.painter().rect_filled(rect, 2.0, *color); + ui.label(RichText::new(*level).text_style(TextStyle::Small)); + } + }); + } + + fn percent(value: f64, max: f64) -> f64 { + if max > 0.0 { value / max * 100.0 } else { 0.0 } + } +} + +pub mod statistics { + use egui::{Button, RichText, Sense, TextWrapMode, Ui}; + use egui_extras::{Column, TableBuilder}; + use rustc_hash::FxHashMap; + use someip_messages::MessageId; + use std::cmp::Ordering; + + use crate::{ + common::phosphor::icons, + host::{ + common::someip_stats::MessageDistribution, + ui::session_setup::state::parsers::someip::TableConfig, + }, + }; + + const COLUMN_NAMES: [&str; 7] = [ + "ID", + "SD", + "EVENT", + "REQUEST", + "RESPONSE", + "FIREFORGET", + "ERROR", + ]; + + pub fn table( + ui: &mut Ui, + name: &str, + stats: &FxHashMap, + state: &mut TableConfig, + ) { + ui.horizontal(|ui| { + let title = format!("{} ({} / {})", name, stats.len(), state.selected_ids.len()); + ui.label(RichText::new(title).strong()); + + let icon = if state.is_collapsed { + icons::regular::CARET_RIGHT + } else { + icons::regular::CARET_DOWN + }; + + if ui + .add( + Button::new(RichText::new(icon).size(12.0)) + .frame(false) + .small(), + ) + .clicked() + { + state.is_collapsed = !state.is_collapsed; + } + }); + + ui.add_space(5.0); + if state.is_collapsed { + return; + } + + let mut rows = Vec::new(); + for (id, messages) in stats { + rows.push((id, messages.values())); + } + if let Some(sort) = state.column_sort { + sort_rows(&mut rows, sort); + } + + TableBuilder::new(ui) + .id_salt(format!("statistics_table_{}", name)) + .vscroll(false) + .striped(true) + .resizable(false) + .sense(Sense::click()) + .columns(Column::remainder(), COLUMN_NAMES.len()) + .header(24.0, |mut header| { + for (idx, name) in COLUMN_NAMES.iter().enumerate() { + header.col(|ui| { + if is_sortable_column(&rows, idx) { + let is_sorted = state.column_sort.is_some_and(|(col, _)| col == idx); + + let icon = if is_sorted && let Some((_, asc)) = state.column_sort { + if asc { + format!(" {}", icons::regular::SORT_ASCENDING) + } else { + format!(" {}", icons::regular::SORT_DESCENDING) + } + } else { + "".to_string() + }; + + let response = ui.add( + Button::new(format!("{name}{icon}")) + .wrap_mode(TextWrapMode::Extend), + ); + + if response.clicked() { + if is_sorted && let Some((_, asc)) = &mut state.column_sort { + *asc = !*asc; + } else if idx == 0 { + state.column_sort = Some((idx, true)); + } else { + state.column_sort = Some((idx, false)); + } + } + } else { + ui.label(name.to_string()); + } + }); + } + }) + .body(|mut body| { + for (id, levels) in rows { + body.row(20.0, |mut row_ui| { + let is_selected = state.selected_ids.contains(id); + row_ui.set_selected(is_selected); + + row_ui.col(|ui| { + ui.label(format!("{}:{}", id.service_id, id.method_id)); + }); + + for level in levels { + row_ui.col(|ui| { + ui.label(level.to_string()); + }); + } + + let response = row_ui.response(); + + if response.clicked() { + if is_selected { + state.selected_ids.remove(id); + } else { + state.selected_ids.insert(id.clone()); + } + state.is_changed = true; + } + }); + } + }); + } + + fn is_sortable_column(rows: &[(&MessageId, [usize; 6])], col: usize) -> bool { + let Some(first) = rows.first() else { + return false; + }; + + rows.iter().skip(1).any(|row| match col { + 0 => row.0 != first.0, + 1 => row.1[0] != first.1[0], + 2 => row.1[1] != first.1[1], + 3 => row.1[2] != first.1[2], + 4 => row.1[3] != first.1[3], + 5 => row.1[4] != first.1[4], + 6 => row.1[5] != first.1[5], + _ => false, + }) + } + + fn sort_rows(rows: &mut [(&MessageId, [usize; 6])], sort: (usize, bool)) { + rows.sort_by(|a, b| { + let a_0 = (a.0.service_id, a.0.method_id); + let b_0 = (b.0.service_id, b.0.method_id); + let ord = match sort.0 { + 0 => a_0.cmp(&b_0), + 1 => a.1[0].cmp(&b.1[0]), + 2 => a.1[1].cmp(&b.1[1]), + 3 => a.1[2].cmp(&b.1[2]), + 4 => a.1[3].cmp(&b.1[3]), + 5 => a.1[4].cmp(&b.1[4]), + 6 => a.1[5].cmp(&b.1[5]), + _ => Ordering::Equal, + } + .then_with(|| a_0.cmp(&b_0)); + + if sort.1 { ord } else { ord.reverse() } + }); + } +} diff --git a/crates/app/src/host/ui/session_setup/mod.rs b/crates/app/src/host/ui/session_setup/mod.rs index a3375c15c5..2bbc61f8eb 100644 --- a/crates/app/src/host/ui/session_setup/mod.rs +++ b/crates/app/src/host/ui/session_setup/mod.rs @@ -37,7 +37,8 @@ pub struct SessionSetup { /// The outcome of render routines in children components of this view. #[derive(Debug, Clone, Copy, PartialEq)] pub enum RenderOutcome { - CollectStatistics, + CollectDltStatistics, + CollectSomeipStatistics, StartSession, None, } @@ -96,9 +97,14 @@ impl SessionSetup { ui.centered_and_justified(|ui| { let outcome = self.render_main_config(recent_sessions, plugins, actions, ui); match outcome { - RenderOutcome::CollectStatistics => { + RenderOutcome::CollectDltStatistics => { if self.state.is_valid() { - self.state.collect_statistics(&self.cmd_tx, actions); + self.state.collect_dlt_statistics(&self.cmd_tx, actions); + } + } + RenderOutcome::CollectSomeipStatistics => { + if self.state.is_valid() { + self.state.collect_someip_statistics(&self.cmd_tx, actions); } } RenderOutcome::StartSession => { diff --git a/crates/app/src/host/ui/session_setup/state/mod.rs b/crates/app/src/host/ui/session_setup/state/mod.rs index 868498d5be..fda55208ea 100644 --- a/crates/app/src/host/ui/session_setup/state/mod.rs +++ b/crates/app/src/host/ui/session_setup/state/mod.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use stypes::FileFormat; use crate::host::{ - command::{DltStatisticsParam, HostCommand, StartSessionParam}, + command::{DltStatisticsParam, HostCommand, SomeipStatisticsParam, StartSessionParam}, common::{parsers::ParserNames, sources::StreamNames}, ui::{ UiActions, @@ -54,7 +54,19 @@ impl SessionSetupState { ParserConfig::Dlt(Box::new(DltParserConfig::new(false, None))) } }, - ParserNames::SomeIP => ParserConfig::SomeIP(SomeIpParserConfig::new()), + ParserNames::SomeIP => match &self.source { + ByteSourceConfig::File(file) => { + ParserConfig::SomeIP(Box::new(SomeIpParserConfig::new(Some(vec![ + file.path.clone(), + ])))) + } + ByteSourceConfig::Concat(files) => ParserConfig::SomeIP(Box::new( + SomeIpParserConfig::new(Some(files.iter().map(|f| f.path.clone()).collect())), + )), + ByteSourceConfig::Stream(..) => { + ParserConfig::SomeIP(Box::new(SomeIpParserConfig::new(None))) + } + }, ParserNames::Text => ParserConfig::Text, ParserNames::Plugins => ParserConfig::Plugins(Box::new(PluginParserConfig::new())), }; @@ -108,7 +120,7 @@ impl SessionSetupState { self.source.is_valid() && self.parser.is_valid() } - pub fn collect_statistics( + pub fn collect_dlt_statistics( &mut self, cmd_tx: &Sender, actions: &mut UiActions, @@ -129,6 +141,27 @@ impl SessionSetupState { } } + pub fn collect_someip_statistics( + &mut self, + cmd_tx: &Sender, + actions: &mut UiActions, + ) { + debug_assert!(self.is_valid()); + + if let ParserConfig::SomeIP(config) = &mut self.parser + && let Some(source_paths) = config.source_paths.take() + { + let param = SomeipStatisticsParam { + session_setup_id: self.id, + source_paths, + }; + + let cmd = HostCommand::SomeipStatistics(Box::new(param)); + + actions.try_send_command(cmd_tx, cmd); + } + } + pub fn start_session( &self, cmd_tx: &Sender, @@ -169,7 +202,7 @@ mod tests { let mut state = SessionSetupState::new( Uuid::new_v4(), ByteSourceConfig::File(file("trace.pcapng", FileFormat::PcapNG)), - ParserConfig::SomeIP(SomeIpParserConfig::new()), + ParserConfig::SomeIP(Box::new(SomeIpParserConfig::new(None))), ); state.update_parser(ParserNames::Dlt); @@ -185,7 +218,7 @@ mod tests { let mut state = SessionSetupState::new( Uuid::new_v4(), ByteSourceConfig::File(file("trace.dlt", FileFormat::Binary)), - ParserConfig::SomeIP(SomeIpParserConfig::new()), + ParserConfig::SomeIP(Box::new(SomeIpParserConfig::new(None))), ); state.update_parser(ParserNames::Dlt); @@ -204,7 +237,7 @@ mod tests { file("first.pcap", FileFormat::PcapLegacy), file("second.pcap", FileFormat::PcapLegacy), ]), - ParserConfig::SomeIP(SomeIpParserConfig::new()), + ParserConfig::SomeIP(Box::new(SomeIpParserConfig::new(None))), ); state.update_parser(ParserNames::Dlt); diff --git a/crates/app/src/host/ui/session_setup/state/parsers/dlt.rs b/crates/app/src/host/ui/session_setup/state/parsers/dlt.rs index 56cb4561e9..bc08359cea 100644 --- a/crates/app/src/host/ui/session_setup/state/parsers/dlt.rs +++ b/crates/app/src/host/ui/session_setup/state/parsers/dlt.rs @@ -206,16 +206,23 @@ impl Display for DltLogLevel { } } +/// The summary on DLT messages. #[derive(Debug, Default, Clone)] pub struct DltSummary { + /// The summary on all messages. pub total: LevelSummary, + /// The summary on selected messages. pub selected: LevelSummary, } +/// A specific summary on DLT messages. #[derive(Debug, Default, Clone)] pub struct LevelSummary { + /// The number of message ids. pub ids: usize, + /// The total number of messages. pub count: usize, + /// The specific number of log levels. pub levels: [usize; 8], } diff --git a/crates/app/src/host/ui/session_setup/state/parsers/mod.rs b/crates/app/src/host/ui/session_setup/state/parsers/mod.rs index 600e513637..65bd2a7685 100644 --- a/crates/app/src/host/ui/session_setup/state/parsers/mod.rs +++ b/crates/app/src/host/ui/session_setup/state/parsers/mod.rs @@ -9,13 +9,13 @@ pub use dlt::DltParserConfig; pub use plugins::PluginParserConfig; use stypes::ObserveOptions; -use crate::host::ui::session_setup::state::parsers::someip::SomeIpParserConfig; +pub use crate::host::ui::session_setup::state::parsers::someip::SomeIpParserConfig; /// Parser Configurations to be used in the front-end. #[derive(Debug, Clone)] pub enum ParserConfig { Dlt(Box), - SomeIP(SomeIpParserConfig), + SomeIP(Box), Text, /// Parser plugin setup state. Plugins(Box), @@ -27,9 +27,9 @@ impl ParserConfig { stypes::ParserType::Dlt(settings) => Self::Dlt(Box::new( DltParserConfig::from_observe_options(settings, &options.origin), )), - stypes::ParserType::SomeIp(settings) => { - Self::SomeIP(SomeIpParserConfig::from_parser_settings(settings)) - } + stypes::ParserType::SomeIp(settings) => Self::SomeIP(Box::new( + SomeIpParserConfig::from_observe_options(settings, &options.origin), + )), stypes::ParserType::Text(()) => Self::Text, stypes::ParserType::Plugin(settings) => { let config = PluginParserConfig::from_settings(settings.clone()); diff --git a/crates/app/src/host/ui/session_setup/state/parsers/someip.rs b/crates/app/src/host/ui/session_setup/state/parsers/someip.rs index eeb865a0b0..4a4fb3fd27 100644 --- a/crates/app/src/host/ui/session_setup/state/parsers/someip.rs +++ b/crates/app/src/host/ui/session_setup/state/parsers/someip.rs @@ -1,20 +1,40 @@ +use super::FibexFileInfo; +use crate::host::common::someip_stats::{MessageDistribution, SomeipStatistics}; use itertools::Itertools; +use rustc_hash::{FxHashMap, FxHashSet}; +use someip_messages::MessageId; use std::path::PathBuf; -use stypes::SomeIpParserSettings; - -use super::FibexFileInfo; +use stypes::{ObserveOrigin, SomeIpParserSettings}; #[derive(Debug, Clone, Default)] pub struct SomeIpParserConfig { + pub source_paths: Option>, pub fibex_files: Vec, + pub someip_statistics: Option>, + pub someip_summary: Box, + pub someip_tables: Box, } impl SomeIpParserConfig { - pub fn new() -> Self { - Self::default() + pub fn new(source_paths: Option>) -> Self { + SomeIpParserConfig { + source_paths, + ..Self::default() + } } - pub fn from_parser_settings(settings: &SomeIpParserSettings) -> Self { + pub fn from_observe_options(settings: &SomeIpParserSettings, origin: &ObserveOrigin) -> Self { + let source_paths = match origin { + ObserveOrigin::File(_, _, path_buf) => Some(vec![path_buf.to_owned()]), + ObserveOrigin::Concat(items) => Some( + items + .iter() + .map(|(_, _, path)| path.to_owned()) + .collect_vec(), + ), + ObserveOrigin::Stream(..) => None, + }; + let fibex_files = settings .fibex_file_paths .as_ref() @@ -27,6 +47,130 @@ impl SomeIpParserConfig { }) .unwrap_or_default(); - Self { fibex_files } + Self { + source_paths, + fibex_files, + someip_statistics: None, + someip_summary: Box::new(SomeipSummary::default()), + someip_tables: Box::new(SomeipTables::default()), + } + } + + pub fn update_summary(&mut self) { + if let Some(someip_statistics) = &self.someip_statistics { + *self.someip_summary = SomeipSummary::new(someip_statistics, &self.someip_tables); + } + } +} + +/// The summary on SOME/IP messages. +#[derive(Debug, Default, Clone)] +pub struct SomeipSummary { + /// The summary on all messages. + pub total: ServiceSummary, + /// The summary on selected messages. + pub selected: ServiceSummary, +} + +impl SomeipSummary { + pub fn new(stats: &SomeipStatistics, tables: &SomeipTables) -> Self { + let messages = + collect(&tables.message_table.selected_ids, &stats.messages).unwrap_or_default(); + + SomeipSummary { + total: ServiceSummary { + ids: stats.count(), + count: stats.total.count(), + messages: stats.total.values(), + }, + selected: ServiceSummary { + ids: tables.count(), + count: messages.count(), + messages: messages.values(), + }, + } + } +} + +/// A specific summary on SOME/IP messages. +#[derive(Debug, Default, Clone)] +pub struct ServiceSummary { + /// The number of message ids. + pub ids: usize, + /// The total number of messages. + pub count: usize, + /// The specific number of message types. + pub messages: [usize; 6], +} + +#[derive(Debug, Clone, Default)] +pub struct SomeipTables { + pub message_table: TableConfig, +} + +impl SomeipTables { + pub fn count(&self) -> usize { + self.message_table.selected_ids.len() + } + + pub fn take_changed(&mut self) -> bool { + self.message_table.take_changed() + } +} + +#[derive(Debug, Clone)] +pub struct TableConfig { + pub selected_ids: FxHashSet, + pub column_sort: Option<(usize, bool)>, + pub is_changed: bool, + pub is_collapsed: bool, +} + +impl Default for TableConfig { + fn default() -> Self { + TableConfig { + selected_ids: FxHashSet::default(), + column_sort: None, + is_changed: false, + is_collapsed: true, + } + } +} + +impl TableConfig { + pub fn take_changed(&mut self) -> bool { + if self.is_changed { + self.is_changed = false; + return true; + } + + false + } +} + +fn collect( + selected_ids: &FxHashSet, + ids_with_messages: &FxHashMap, +) -> Option { + if selected_ids.is_empty() { + None + } else { + Some(merge(selected_ids, ids_with_messages)) } } + +fn merge( + selected_ids: &FxHashSet, + ids_with_messages: &FxHashMap, +) -> MessageDistribution { + let mut messages = MessageDistribution::default(); + + for message in selected_ids + .iter() + .filter_map(|id| ids_with_messages.get(id)) + { + messages.merge(message); + } + + messages +} diff --git a/crates/app/src/host/ui/storage/recent/session.rs b/crates/app/src/host/ui/storage/recent/session.rs index 24ae125b22..65f82f5065 100644 --- a/crates/app/src/host/ui/storage/recent/session.rs +++ b/crates/app/src/host/ui/storage/recent/session.rs @@ -479,8 +479,8 @@ mod tests { use processor::search::filter::SearchFilter; use stypes::{ - DltParserSettings, ObserveOptions, ObserveOrigin, ParserType, TCPTransportConfig, - Transport, UDPTransportConfig, + DltParserSettings, ObserveOptions, ObserveOrigin, ParserType, SomeIpParserSettings, + TCPTransportConfig, Transport, UDPTransportConfig, }; use crate::{common::time::unix_timestamp_now, host::common::colors::StoredColorPair}; @@ -573,7 +573,8 @@ mod tests { bind_addr: String::from("127.0.0.1:5556"), }), ), - parser: ParserType::SomeIp(stypes::SomeIpParserSettings { + parser: ParserType::SomeIp(SomeIpParserSettings { + filter_config: None, fibex_file_paths: Some(vec![String::from("/tmp/one.xml")]), }), }); diff --git a/crates/app/src/host/ui/storage/recent/source_key.rs b/crates/app/src/host/ui/storage/recent/source_key.rs index 4e18aaa799..c9c957b2c5 100644 --- a/crates/app/src/host/ui/storage/recent/source_key.rs +++ b/crates/app/src/host/ui/storage/recent/source_key.rs @@ -221,6 +221,7 @@ mod tests { path, FileFormat::Text, stypes::ParserType::SomeIp(stypes::SomeIpParserSettings { + filter_config: None, fibex_file_paths: None, }), )); diff --git a/crates/app/src/host/ui/storage/recent/storage.rs b/crates/app/src/host/ui/storage/recent/storage.rs index 7a532ec741..7f70a995f7 100644 --- a/crates/app/src/host/ui/storage/recent/storage.rs +++ b/crates/app/src/host/ui/storage/recent/storage.rs @@ -293,6 +293,7 @@ mod tests { path, stypes::FileFormat::Text, ParserType::SomeIp(stypes::SomeIpParserSettings { + filter_config: None, fibex_file_paths: None, }), )); diff --git a/crates/core/parsers/Cargo.toml b/crates/core/parsers/Cargo.toml index e5fa01e82a..464b5f584b 100644 --- a/crates/core/parsers/Cargo.toml +++ b/crates/core/parsers/Cargo.toml @@ -24,6 +24,7 @@ thiserror.workspace = true rand.workspace = true someip-messages.workspace = true someip-payload.workspace = true +stypes.workspace = true [dev-dependencies] stringreader.workspace = true diff --git a/crates/core/parsers/src/dlt/fmt.rs b/crates/core/parsers/src/dlt/fmt.rs index 8c9604d786..3c04d8ceb8 100644 --- a/crates/core/parsers/src/dlt/fmt.rs +++ b/crates/core/parsers/src/dlt/fmt.rs @@ -607,14 +607,17 @@ impl fmt::Display for FormattableMessage<'_> { }) && let Some(slice) = slices.get(1) { - match SomeipParser::parse_message(self.fibex_someip_metadata, slice, None) { + match SomeipParser::parse_message(None, self.fibex_someip_metadata, slice, None) + { Ok((_, message)) => { - let prefix = slices.first().map_or_else(String::default, |s| { - parse_prefix(s) - .ok() - .map_or_else(String::default, |p| format!("{} ", p.1)) - }); - return write!(f, "SOME/IP {prefix}{message:?}"); + if let Some(message) = message { + let prefix = slices.first().map_or_else(String::default, |s| { + parse_prefix(s) + .ok() + .map_or_else(String::default, |p| format!("{} ", p.1)) + }); + return write!(f, "SOME/IP {prefix}{message:?}"); + } } Err(error) => { return write!(f, "SOME/IP '{error}' {slice:02X?}"); diff --git a/crates/core/parsers/src/someip.rs b/crates/core/parsers/src/someip.rs index 6507a9d5c2..15fbb65613 100644 --- a/crates/core/parsers/src/someip.rs +++ b/crates/core/parsers/src/someip.rs @@ -15,6 +15,7 @@ use someip_payload::{ fibex2som::FibexTypes, som::{SOMParser, SOMType}, }; +use stypes::SomeipFilterConfig; use lazy_static::lazy_static; use log::{debug, error}; @@ -187,39 +188,47 @@ impl FibexTypeCache { /// A parser for SOME/IP log messages. pub struct SomeipParser { + filter_config: Option, fibex_metadata: Option, } -impl Default for SomeipParser { - fn default() -> Self { - Self::new() - } -} - impl SomeipParser { /// Creates a new parser. - pub fn new() -> Self { + pub fn new(filter_config: Option) -> Self { SomeipParser { + filter_config, fibex_metadata: None, } } /// Creates a new parser with the given files. - pub fn from_fibex_files(paths: Vec) -> Self { + pub fn from_fibex_files( + filter_config: Option, + paths: Vec, + ) -> Self { SomeipParser { + filter_config, fibex_metadata: FibexMetadata::from_fibex_files(paths), } } /// Parses a SOME/IP message (header and payload) from the given input. + /// Returns None if message was filtered out. pub(crate) fn parse_message( + filter_config: Option<&SomeipFilterConfig>, fibex_metadata: Option<&FibexMetadata>, input: &[u8], timestamp: Option, - ) -> Result<(usize, SomeipLogMessage), Error> { + ) -> Result<(usize, Option), Error> { let time = timestamp.unwrap_or(0); match Message::from_slice(input) { Ok(Message::Sd(header, payload)) => { + let filtered = match filter_config { + Some(config) => !config + .messages + .contains(&(header.message_id.service_id, header.message_id.method_id)), + None => false, + }; let len = header.message_len(); debug!("at {time} : SD Message ({len} bytes)"); Ok(( @@ -228,14 +237,24 @@ impl SomeipParser { } else { len }, - SomeipLogMessage::from( - sd_message_string(&header, &payload), - input[..len].to_vec(), - ), + if filtered { + None + } else { + Some(SomeipLogMessage::from( + sd_message_string(&header, &payload), + input[..len].to_vec(), + )) + }, )) } Ok(Message::Rpc(header, payload)) => { + let filtered = match filter_config { + Some(config) => !config + .messages + .contains(&(header.message_id.service_id, header.message_id.method_id)), + None => false, + }; let len = header.message_len(); debug!("at {time} : RPC Message ({len:?} bytes)"); Ok(( @@ -244,14 +263,19 @@ impl SomeipParser { } else { len }, - SomeipLogMessage::from( - rpc_message_string(fibex_metadata, &header, &payload), - input[..len].to_vec(), - ), + if filtered { + None + } else { + Some(SomeipLogMessage::from( + rpc_message_string(fibex_metadata, &header, &payload), + input[..len].to_vec(), + )) + }, )) } Ok(Message::CookieClient) => { + let filtered = filter_config.is_some(); let len = Header::LENGTH; debug!("at {time} : MCC Message"); Ok(( @@ -260,14 +284,19 @@ impl SomeipParser { } else { len }, - SomeipLogMessage::from( - String::from("MCC"), // Magic-Cookie-Client - input[..len].to_vec(), - ), + if filtered { + None + } else { + Some(SomeipLogMessage::from( + String::from("MCC"), // Magic-Cookie-Client + input[..len].to_vec(), + )) + }, )) } Ok(Message::CookieServer) => { + let filtered = filter_config.is_some(); let len = Header::LENGTH; debug!("at {time} : MCS Message"); Ok(( @@ -276,10 +305,14 @@ impl SomeipParser { } else { len }, - SomeipLogMessage::from( - String::from("MCS"), // Magic-Cookie-Server - input[..len].to_vec(), - ), + if filtered { + None + } else { + Some(SomeipLogMessage::from( + String::from("MCS"), // Magic-Cookie-Server + input[..len].to_vec(), + )) + }, )) } @@ -304,8 +337,13 @@ impl SingleParser for SomeipParser { input: &[u8], timestamp: Option, ) -> Result, Error> { - SomeipParser::parse_message(self.fibex_metadata.as_ref(), input, timestamp) - .map(|(rest, message)| ParseOutput::new(rest, Some(ParseYield::from(message)))) + SomeipParser::parse_message( + self.filter_config.as_ref(), + self.fibex_metadata.as_ref(), + input, + timestamp, + ) + .map(|(rest, message)| ParseOutput::new(rest, message.map(ParseYield::from))) } } @@ -598,7 +636,7 @@ mod test { fn parse_error_no_data() { let input: &[u8] = &[]; - let mut parser = SomeipParser::new(); + let mut parser = SomeipParser::new(None); let result = parser.parse_item(input, None); match result { @@ -617,7 +655,7 @@ mod test { 0x01, 0x01, 0x01, 0x00, // proto(u8), version(u8), messageType,(u8) returnCode(u8) ]; - let mut parser = SomeipParser::new(); + let mut parser = SomeipParser::new(None); let result = parser.parse_item(input, None); match result { @@ -636,7 +674,7 @@ mod test { 0x01, 0x01, 0x01, 0x00, // proto(u8), version(u8), messageType,(u8) returnCode(u8) ]; - let mut parser = SomeipParser::new(); + let mut parser = SomeipParser::new(None); let ParseOutput { consumed, message } = parser.parse_item(input, None).unwrap(); assert_eq!(consumed, input.len()); @@ -658,7 +696,7 @@ mod test { 0x01, 0x01, 0x02, 0x00, // proto(u8), version(u8), messageType,(u8) returnCode(u8) ]; - let mut parser = SomeipParser::new(); + let mut parser = SomeipParser::new(None); let ParseOutput { consumed, message } = parser.parse_item(input, None).unwrap(); assert_eq!(consumed, input.len()); @@ -680,7 +718,7 @@ mod test { 0x01, 0x01, 0x02, 0x00, // proto(u8), version(u8), messageType,(u8) returnCode(u8) ]; - let mut parser = SomeipParser::new(); + let mut parser = SomeipParser::new(None); let ParseOutput { consumed, message } = parser.parse_item(input, None).unwrap(); assert_eq!(consumed, input.len()); @@ -710,6 +748,7 @@ mod test { let fibex_metadata = test_metadata(); let mut parser = SomeipParser { + filter_config: None, fibex_metadata: Some(fibex_metadata), }; @@ -740,7 +779,7 @@ mod test { 0x01, 0x02, // payload([u8;2]) ]; - let mut parser = SomeipParser::new(); + let mut parser = SomeipParser::new(None); let ParseOutput { consumed, message } = parser.parse_item(input, None).unwrap(); assert_eq!(consumed, input.len()); @@ -771,6 +810,7 @@ mod test { let fibex_metadata = test_metadata(); let mut parser = SomeipParser { + filter_config: None, fibex_metadata: Some(fibex_metadata), }; @@ -803,6 +843,7 @@ mod test { let fibex_metadata = test_metadata(); let mut parser = SomeipParser { + filter_config: None, fibex_metadata: Some(fibex_metadata), }; @@ -835,6 +876,7 @@ mod test { let fibex_metadata = test_metadata(); let mut parser = SomeipParser { + filter_config: None, fibex_metadata: Some(fibex_metadata), }; @@ -867,6 +909,7 @@ mod test { let fibex_metadata = test_metadata(); let mut parser = SomeipParser { + filter_config: None, fibex_metadata: Some(fibex_metadata), }; @@ -899,6 +942,7 @@ mod test { let fibex_metadata = test_metadata(); let mut parser = SomeipParser { + filter_config: None, fibex_metadata: Some(fibex_metadata), }; @@ -931,7 +975,7 @@ mod test { 0x00, 0x00, 0x00, 0x00, // options-length(u32) ]; - let mut parser = SomeipParser::new(); + let mut parser = SomeipParser::new(None); let ParseOutput { consumed, message } = parser.parse_item(input, None).unwrap(); assert_eq!(consumed, input.len()); @@ -977,7 +1021,7 @@ mod test { 0x00, 0x11, 0x75, 0x30, // reserved(u8), proto(u8), port(u16) ]; - let mut parser = SomeipParser::new(); + let mut parser = SomeipParser::new(None); let ParseOutput { consumed, message } = parser.parse_item(input, None).unwrap(); assert_eq!(consumed, input.len()); diff --git a/crates/core/processor/benches/someip_legacy_producer.rs b/crates/core/processor/benches/someip_legacy_producer.rs index 32fc449536..6d78bcebeb 100644 --- a/crates/core/processor/benches/someip_legacy_producer.rs +++ b/crates/core/processor/benches/someip_legacy_producer.rs @@ -43,8 +43,8 @@ fn someip_legacy_producer(c: &mut Criterion) { fn create_someip_parser(config_path: Option<&PathBuf>) -> SomeipParser { match config_path { - Some(p) => SomeipParser::from_fibex_files(vec![p.to_owned()]), - None => SomeipParser::new(), + Some(p) => SomeipParser::from_fibex_files(None, vec![p.to_owned()]), + None => SomeipParser::new(None), } } diff --git a/crates/core/processor/benches/someip_producer.rs b/crates/core/processor/benches/someip_producer.rs index c0126dfd91..946c81eee4 100644 --- a/crates/core/processor/benches/someip_producer.rs +++ b/crates/core/processor/benches/someip_producer.rs @@ -43,8 +43,8 @@ fn someip_producer(c: &mut Criterion) { fn create_someip_parser(config_path: Option<&PathBuf>) -> SomeipParser { match config_path { - Some(p) => SomeipParser::from_fibex_files(vec![p.to_owned()]), - None => SomeipParser::new(), + Some(p) => SomeipParser::from_fibex_files(None, vec![p.to_owned()]), + None => SomeipParser::new(None), } } diff --git a/crates/core/session/src/handlers/export_raw.rs b/crates/core/session/src/handlers/export_raw.rs index 27dba1ede8..bc2710c39c 100644 --- a/crates/core/session/src/handlers/export_raw.rs +++ b/crates/core/session/src/handlers/export_raw.rs @@ -146,10 +146,14 @@ async fn export( export_runner(producer, dest, sections, read_to_end, false, cancel).await } stypes::ParserType::SomeIp(settings) => { + let filter_config = settings.filter_config.clone(); let parser = if let Some(files) = settings.fibex_file_paths.as_ref() { - SomeipParser::from_fibex_files(files.iter().map(PathBuf::from).collect()) + SomeipParser::from_fibex_files( + filter_config, + files.iter().map(PathBuf::from).collect(), + ) } else { - SomeipParser::new() + SomeipParser::new(filter_config) }; let producer = MessageProducer::new(parser, source); export_runner(producer, dest, sections, read_to_end, false, cancel).await diff --git a/crates/core/session/src/handlers/observing/mod.rs b/crates/core/session/src/handlers/observing/mod.rs index 78e8917b96..732c161da6 100644 --- a/crates/core/session/src/handlers/observing/mod.rs +++ b/crates/core/session/src/handlers/observing/mod.rs @@ -100,11 +100,13 @@ async fn run_source_intern( run_producer(operation_api, state, source_id, producer, rx_tail, rx_sde).await } stypes::ParserType::SomeIp(settings) => { + let filter_config = settings.filter_config.clone(); let someip_parser = match &settings.fibex_file_paths { - Some(paths) => { - SomeipParser::from_fibex_files(paths.iter().map(PathBuf::from).collect()) - } - None => SomeipParser::new(), + Some(paths) => SomeipParser::from_fibex_files( + filter_config, + paths.iter().map(PathBuf::from).collect(), + ), + None => SomeipParser::new(filter_config), }; let producer = MessageProducer::new(someip_parser, source); run_producer(operation_api, state, source_id, producer, rx_tail, rx_sde).await diff --git a/crates/core/session/tests/snapshot_tests/mod.rs b/crates/core/session/tests/snapshot_tests/mod.rs index f60f53aac6..6a236d2415 100644 --- a/crates/core/session/tests/snapshot_tests/mod.rs +++ b/crates/core/session/tests/snapshot_tests/mod.rs @@ -34,10 +34,10 @@ async fn observe_dlt_session() { }); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn observe_dlt_with_someip_session() { - let input = "../../../development/resources/someip.dlt"; - let fibex_file = "../../../development/resources/someip.xml"; + let input = "../../../development/resources/someip/someip.dlt"; + let fibex_file = "../../../development/resources/someip/someip.xml"; assert!( PathBuf::from(fibex_file).exists(), @@ -69,9 +69,9 @@ async fn observe_dlt_with_someip_session() { // SomeIP request search in parsing session and searcher use block_in_place // on CPU heavy blocks. #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn observe_someip_bcapng_session() { - let input = "../../../development/resources/someip.pcapng"; - let fibex_file = "../../../development/resources/someip.xml"; +async fn observe_someip_pcapng_session() { + let input = "../../../development/resources/someip/udp/someip.pcapng"; + let fibex_file = "../../../development/resources/someip/someip.xml"; assert!( PathBuf::from(fibex_file).exists(), @@ -79,6 +79,7 @@ async fn observe_someip_bcapng_session() { ); let parser_settings = stypes::SomeIpParserSettings { + filter_config: None, fibex_file_paths: Some(vec![String::from(fibex_file)]), }; @@ -103,8 +104,8 @@ async fn observe_someip_bcapng_session() { // on CPU heavy blocks. #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn observe_someip_legacy_session() { - let input = "../../../development/resources/someip.pcap"; - let fibex_file = "../../../development/resources/someip.xml"; + let input = "../../../development/resources/someip/udp/someip.pcap"; + let fibex_file = "../../../development/resources/someip/someip.xml"; assert!( PathBuf::from(fibex_file).exists(), @@ -112,6 +113,7 @@ async fn observe_someip_legacy_session() { ); let parser_settings = stypes::SomeIpParserSettings { + filter_config: None, fibex_file_paths: Some(vec![String::from(fibex_file)]), }; diff --git a/crates/core/session/tests/snapshot_tests/snapshots/observe_dlt_with_someip_session.snap b/crates/core/session/tests/snapshot_tests/snapshots/observe_dlt_with_someip_session.snap index 9e9b1eb5eb..91e2379c5f 100644 --- a/crates/core/session/tests/snapshot_tests/snapshots/observe_dlt_with_someip_session.snap +++ b/crates/core/session/tests/snapshot_tests/snapshots/observe_dlt_with_someip_session.snap @@ -1,13 +1,12 @@ --- -source: session/tests/snapshot_tests/mod.rs +source: crates/core/session/tests/snapshot_tests/mod.rs description: Snapshot for DLT file with SomeIP network trace. info: filter_config: ~ fibex_file_paths: - - "../../../development/resources/someip.xml" + - "../../../development/resources/someip/someip.xml" with_storage_header: true tz: ~ -snapshot_kind: text --- session_file: - "2024-02-20T13:17:26.713537000Z\u0004ECU1\u00041\u0004571\u0004204\u000428138506\u0004ECU1\u0004APP1\u0004C1\u0004IPC\u0004SOME/IP 0.0.0.0:0 >> INST:1 RPC SERV:123 METH:32773 LENG:16 CLID:0 SEID:58252 IVER:1 MSTP:2 RETC:0 TestService::timeEvent {\u0006\ttimestamp (INT64) : 1683656786973,\u0006}" diff --git a/crates/core/session/tests/snapshot_tests/snapshots/observe_someip_legacy_session.snap b/crates/core/session/tests/snapshot_tests/snapshots/observe_someip_legacy_session.snap index 32d85080ee..4d5bf4ad27 100644 --- a/crates/core/session/tests/snapshot_tests/snapshots/observe_someip_legacy_session.snap +++ b/crates/core/session/tests/snapshot_tests/snapshots/observe_someip_legacy_session.snap @@ -3,7 +3,7 @@ source: session/tests/snapshot_tests/mod.rs description: Snapshot for SomeIP file with Pcap Legacy byte source. info: fibex_file_paths: - - "../../../development/resources/someip.xml" + - "../../../development/resources/someip/someip.xml" snapshot_kind: text --- session_file: diff --git a/crates/core/session/tests/snapshot_tests/snapshots/observe_someip_bcapng_session.snap b/crates/core/session/tests/snapshot_tests/snapshots/observe_someip_pcapng_session.snap similarity index 99% rename from crates/core/session/tests/snapshot_tests/snapshots/observe_someip_bcapng_session.snap rename to crates/core/session/tests/snapshot_tests/snapshots/observe_someip_pcapng_session.snap index 4327c667bb..f60c77b0a3 100644 --- a/crates/core/session/tests/snapshot_tests/snapshots/observe_someip_bcapng_session.snap +++ b/crates/core/session/tests/snapshot_tests/snapshots/observe_someip_pcapng_session.snap @@ -3,7 +3,7 @@ source: session/tests/snapshot_tests/mod.rs description: Snapshot for SomeIP file with Pcapng byte source. info: fibex_file_paths: - - "../../../development/resources/someip.xml" + - "../../../development/resources/someip/someip.xml" snapshot_kind: text --- session_file: diff --git a/crates/core/sources/src/socket/udp.rs b/crates/core/sources/src/socket/udp.rs index 36394949b2..e9c4e2e493 100644 --- a/crates/core/sources/src/socket/udp.rs +++ b/crates/core/sources/src/socket/udp.rs @@ -202,8 +202,9 @@ mod tests { const SENDER: &str = "127.0.0.1:4002"; const RECEIVER: &str = "127.0.0.1:5002"; - const SENT_LEN: usize = MAX_DATAGRAM_SIZE; - const CONSUME_LEN: usize = MAX_DATAGRAM_SIZE / 2; + // MAX_DATAGRAM_SIZE not supported on some OS (eg MacOS) for UDP via local-host. + const SENT_LEN: usize = 8 * 1024; + const CONSUME_LEN: usize = SENT_LEN / 2; let send_socket = UdpSocket::bind(SENDER) .await diff --git a/crates/file_tools/src/lib.rs b/crates/file_tools/src/lib.rs index 19b3f96d03..57d8ae7ea6 100644 --- a/crates/file_tools/src/lib.rs +++ b/crates/file_tools/src/lib.rs @@ -59,10 +59,10 @@ mod test { "../../development/resources/attachments.dlt" ))?); assert!(is_binary(String::from( - "../../development/resources/someip.pcap" + "../../development/resources/someip/udp/someip.pcap" ))?); assert!(is_binary(String::from( - "../../development/resources/someip.pcapng" + "../../development/resources/someip/udp/someip.pcapng" ))?); Ok(()) } @@ -76,7 +76,7 @@ mod test { "../../development/resources/sample_utf_8.txt" ))?); assert!(!is_binary(String::from( - "../../development/resources/someip.xml" + "../../development/resources/someip/someip.xml" ))?); Ok(()) } diff --git a/crates/stypes/src/observe/extending.rs b/crates/stypes/src/observe/extending.rs index e50877ff31..43828ef6c1 100644 --- a/crates/stypes/src/observe/extending.rs +++ b/crates/stypes/src/observe/extending.rs @@ -82,6 +82,20 @@ impl DltParserSettings { } } +impl Default for SomeIpParserSettings { + /// Provides a default implementation for `SomeIpParserSettings`. + /// + /// # Defaults + /// - `filter_config`: `None` + /// - `fibex_file_paths`: `None` + fn default() -> Self { + Self { + filter_config: None, + fibex_file_paths: None, + } + } +} + #[derive(Error, Debug)] /// Represents errors related to networking operations. pub enum NetError { diff --git a/crates/stypes/src/observe/mod.rs b/crates/stypes/src/observe/mod.rs index 8c820ea8be..ecbf2a91d3 100644 --- a/crates/stypes/src/observe/mod.rs +++ b/crates/stypes/src/observe/mod.rs @@ -55,10 +55,18 @@ pub struct DltParserSettings { /// Settings for the SomeIp parser. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SomeIpParserSettings { + /// Configuration for filtering SOME/IP messages. + pub filter_config: Option, /// Paths to FIBEX files for additional interpretation of `payload` content. pub fibex_file_paths: Option>, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SomeipFilterConfig { + /// List of messages to filter for (service_id, method_id) + pub messages: Vec<(u16, u16)>, +} + /// Describes the transport source for a session. #[derive(Debug, Serialize, Deserialize, Clone)] pub enum Transport { diff --git a/development/resources/someip.dlt b/development/resources/someip/someip.dlt similarity index 100% rename from development/resources/someip.dlt rename to development/resources/someip/someip.dlt diff --git a/development/resources/someip.xml b/development/resources/someip/someip.xml similarity index 100% rename from development/resources/someip.xml rename to development/resources/someip/someip.xml diff --git a/development/resources/someip/tcp/someip.pcap b/development/resources/someip/tcp/someip.pcap new file mode 100644 index 0000000000000000000000000000000000000000..e93103d4e00822ac74e2d94b0799602a5c7df0f3 GIT binary patch literal 6382 zcmb`LeN2^A9LLYSyog!6R4TFIaH8n^=VGQtxm^~h%t?foH^I6TnWPvx7@1a=pp83M z(Q09nO_=6v*_-0RwKfb24Hq)lG{s?~#KI*4E>ha<_dW2ObMA$E>e=mX&)vCi-_P%N z&gVJL13j(B8?7vu$wx4=(2LC4O+|++Fqe`G{^Jfq!=c|4VP{WzX17AtGPfU!s-#IZf}7GpsZZ?R}L zGngTZMU~scIJDe`O2RIMvmNwB98)TBCNevQMol3c<}R$>E9tt6DHk(F4I z>En9^ohku+mX`iaI_Py(KJ--}VIFeY6*^U7f}RNauFK&pow9ID>D}I81KoXE(#sP; z#sl<{N*e5Oh0t>({hZV3xyI7k7K`+2+VthANRRteN#`>t(yNqo8lpENeIuoR-LEs~ z&F}1FQ0;F>R}4xq(nsGBGZ&CSsATLHok43>pwIAn*(Z0o)Tks!R$?<|u>~K}tp1BZ ziJ-Gzb@Zx5pm%-ZLqCdmZcv_>2h;w7euU`$4Z6D-^t|^aof{Nw(2HNA(X?VKgi2;y z*R5fU8+|6F$v(M3SB*+?=8H;9uOa9UY3cE8pxeT|=@v1|*#ipQDb|9`m0L%WN*Zv9j@b67NxHk z)){n`YPW=L*oSn*pmUTtVbBaQa{(EIN^Xqk47wPBKKFhr`-DN345O0l&qXDs_aM;6 zwDg&=pszpaLlx|wDaSLJ?D#pFbJrk!^)V$Kdytb3KC_!E zXfUmx-R(J)9`k_Cp!HT5)WOpggB(Ws$m?R=0x}4d{P3X8pxVFDXKRn_6KiPYwqPgE z>JpWhHVE_|we-P*pcgOmp>H+jA*)WIPwoOO`0zj6g@GP9M`zGyhd@7)F6rE$ml2QN z^U7#otT7FxK^K~kUZ+ie@-)&z5|wlqB-3{(>0UeBg{E9eFOAn3H2+-~bUPO5ib4A+ zb7BvAOw3$B2BDJO3v~unY(bw_w#h!ZK@CPFS%sn!(*}Y5k(NHb4)k2N5B;K`Cn$82 z27#WoNN3Q}9MG2>kaTWPt)Q3EXj+3VHz0j>f-ZgQEu`Pze;ze6+U|F8Kgzr&zaZAexO>NRQ~{!`3cKn9_b2AW0xSFy4C&}RX!WTH4(mN{$+>{y;;!LDs+7pK{EYCCEd%Q z{<3^Z@1xrN??GFtV9*y!k**k2M41x?k^TS2e-u$kFZH>Da&b(VMQ6z=^f?eM`{V`< z8I?GqL?xyT0)6gs9sS_Ppr2gqO%Gmd&^KOC=q3#Uy_)F$4VqO4dRo4ubA!Y+G|jI; z0|iKDkLc2iN{~KOt)#;sneNx1fr0``@1%9}Z_w-$Fo=yKT`@@PaKlT*%muUup^}^0 zH5}?kpU?dv`-DNkPO*lKq7u^vfqqv@XK#S+$nc@>7xWhtx=DjTKdfEj%6!nfKag~8 ikT`o%{Teiwg7noZbOxoqhxCYNlyn#*)BPGWnDP(1A2u!k literal 0 HcmV?d00001 diff --git a/development/resources/someip/tcp/someip.pcapng b/development/resources/someip/tcp/someip.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..5b45bc68ea9c82d34d278a32b1826bc9f20f59c4 GIT binary patch literal 7396 zcmcJTe@xV69LJwK0e_UrnHsrpFfDDgI$J9uo#zCT7-EW``3uWTQxscdh8l!z5X-LB z{E=+h)TYaR`Xl_Kb*5wvH#OXJrBjE_5=-QCTG;6GevapPK6l(brQUtFFFg0-`+dK@ z?|VHDU#>^bo;~gtLgWrl8{nYd?HwDQ!{fdkzlDgC;{V`BY4?fsUWC^jL|2mIwB#8dLt>0u^Zg2m))>G@DH_B~%QhWQ=MkH3?@o@Zb zIGn1abgt@rFcH;%^GJN4sfaD%6Mui+`B^8_~V6j#u=tUR86Ceq=O zhkjN$_U%s=0y(&To;RnjE${Ix+hV$-7K3?Z*lXTdJnnj z{T+GC%}gXpH{aKN$H2|Hne`So$2@^9dexdP(k$%@-yhkGWYzrw+K8qx+75 zn_agYv$#2+8eL>$n=YiAXB1mus%E za@`!O`k95>IOS%LD~C@SS4!}9+T`54X3P7r4tcAdVtMe= zHhebETX`x*fv1tVAbD=!XslBOW**aXlT~4<2%E< zS@^LnFK;jM;)k(3xM}iUWqCnv@;vTl$wimN&D0Oz=Cx$xF*nyzio8Dm)4J~%xVg01 zWpQ)K9CUGNzUe}`xlyrAnx)yI&KDy7}wI@NV||!yh^mCJ#%D$_sLn=W#bP9!a&hdFmqE zOy7?z=H^LteXZAZ-!X9W46#LCH*fSr7tc$nsxJZb$B;7Tk}d!Bkyz{ zmWOrI$a zp;~fqj7(C*2=T#5O}_h58#@0vd&Nvx)xNm(Yu$-&v)vu;n$CBX99ymIpU|v`bfVU!j4@3v!d^aW^-7Khomn zoJzR)RT}b`o3mA3E6u&wefM7L8)9gj|E=FFbaX0(aq+}@NSNXx8-Ghh`io2 OSsvUpc@f=IdH(`vRIbth literal 0 HcmV?d00001 diff --git a/development/resources/someip.pcap b/development/resources/someip/udp/someip.pcap similarity index 100% rename from development/resources/someip.pcap rename to development/resources/someip/udp/someip.pcap diff --git a/development/resources/someip.pcapng b/development/resources/someip/udp/someip.pcapng similarity index 100% rename from development/resources/someip.pcapng rename to development/resources/someip/udp/someip.pcapng