tramex_tools/interface/parser/
mod.rs1pub mod hex_extractor;
4pub mod parser_basic;
5pub mod parser_gtpu;
6pub mod parser_nas;
7pub mod parser_ngap;
8pub mod parser_phy;
9pub mod parser_rrc;
10
11use crate::data::AdditionalInfos;
12use crate::data::Trace;
13
14use crate::errors::ErrorCode;
15use crate::errors::TramexError;
16use crate::interface::layer::Layer;
17use crate::interface::types::Direction;
18use crate::tramex_error;
19use chrono::NaiveTime;
20use chrono::Timelike;
21use std::str::FromStr;
22
23#[derive(Debug, Clone, Default)]
26pub struct ParsedHeader {
27 pub timestamp: i64,
29 pub layer: Layer,
31 pub direction: Direction,
33 pub ue_id: Option<u64>,
35 pub cell: Option<u64>,
37 pub rnti: Option<u64>,
39 pub frame: Option<u16>,
41 pub slot: Option<u8>,
43 pub channel: Option<String>,
45 pub connection_info: Option<String>,
47}
48
49impl ParsedHeader {
50 pub fn from_file_line(first_line: &str) -> Result<Self, ParsingError> {
56 let parts: Vec<&str> = first_line.split_whitespace().collect();
57 if parts.len() < 3 {
58 return Err(ParsingError::new(format!("Not enough parts in line: {}", first_line), 0));
59 }
60
61 let timestamp = parse_timestamp(first_line)?;
62
63 let layer = Layer::from_str(parts[1].trim_start_matches('[').trim_end_matches(']'))
64 .map_err(|_| ParsingError::new(format!("Unknown layer: {}", parts[1]), 0))?;
65
66 let direction = Direction::from_str(parts[2]).unwrap_or(Direction::NA);
67
68 let connection_info = match layer {
70 Layer::NGAP => {
71 if parts.len() > 5 && parts[5].contains(':') {
72 Some(parts[5].to_string())
73 } else {
74 None
75 }
76 }
77 Layer::GTPU => {
78 if parts.len() > 3 && parts[3].contains(':') {
79 Some(parts[3].to_string())
80 } else {
81 None
82 }
83 }
84 _ => None,
85 };
86
87 Ok(Self {
88 timestamp,
89 layer,
90 direction,
91 ue_id: None,
92 cell: None,
93 rnti: None,
94 frame: None,
95 slot: None,
96 channel: None,
97 connection_info,
98 })
99 }
100}
101
102#[derive(Debug)]
104pub struct ParsingError {
105 pub message: String,
107
108 pub line_idx: u64,
110}
111
112impl ParsingError {
113 pub fn new(message: String, line_idx: u64) -> Self {
115 Self { message, line_idx }
116 }
117}
118
119#[inline]
121pub fn parsing_error_to_tramex_error(error: ParsingError, idx: u64) -> TramexError {
122 let index = idx + error.line_idx;
123 tramex_error!(format!("{} (line {})", error.message, index), ErrorCode::FileParsing)
124}
125
126pub trait FileParser {
128 fn parse_additional_infos(line: &[String]) -> Result<AdditionalInfos, ParsingError>;
132
133 fn parse(lines: &[String]) -> Result<Trace, ParsingError>;
137}
138
139pub trait LayerParser {
143 fn parse_layer(header: &ParsedHeader, data_lines: &[String]) -> Result<AdditionalInfos, ParsingError>;
147}
148
149pub fn extract_file_payload(first_line: &str, layer: &Layer) -> String {
153 let parts: Vec<&str> = first_line.split_whitespace().collect();
154 match layer {
161 Layer::RRC => {
162 if parts.len() > 5 {
163 parts[5..].join(" ")
164 } else {
165 first_line.to_string()
166 }
167 }
168 Layer::NAS => {
169 if parts.len() > 4 {
170 parts[4..].join(" ")
171 } else {
172 first_line.to_string()
173 }
174 }
175 Layer::NGAP => {
176 let start = if parts.len() > 5 && parts[5].contains(':') { 6 } else { 5 };
178 if parts.len() > start {
179 parts[start..].join(" ")
180 } else {
181 first_line.to_string()
182 }
183 }
184 Layer::GTPU => {
185 let start = if parts.len() > 3 && parts[3].contains(':') { 4 } else { 3 };
187 if parts.len() > start {
188 parts[start..].join(" ")
189 } else {
190 first_line.to_string()
191 }
192 }
193 _ => first_line.to_string(),
194 }
195}
196
197pub fn build_trace(header: &ParsedHeader, additional_infos: AdditionalInfos, data_lines: &[String]) -> Trace {
200 use crate::interface::association::TraceRelation;
201 let binary = hex_extractor::extract_binary_from_lines(data_lines);
202 Trace {
203 timestamp: header.timestamp,
204 layer: header.layer.clone(),
205 additional_infos,
206 text: Some(data_lines.to_vec()),
207 binary,
208 relation: TraceRelation::default(),
209 }
210}
211
212#[inline]
214pub fn time_to_milliseconds(time: &NaiveTime) -> i64 {
215 let hours_in_ms = time.hour() as i64 * 3_600_000;
216 let minutes_in_ms = time.minute() as i64 * 60_000;
217 let seconds_in_ms = time.second() as i64 * 1000;
218 let milliseconds = time.nanosecond() as i64 / 1_000_000; hours_in_ms + minutes_in_ms + seconds_in_ms + milliseconds
221}
222
223pub fn parse_timestamp(first_line: &str) -> Result<i64, ParsingError> {
228 let parts: Vec<&str> = first_line.split_whitespace().collect();
229 if parts.is_empty() {
230 return Err(ParsingError::new("Empty line, cannot parse timestamp".to_string(), 0));
231 }
232
233 let date = chrono::NaiveTime::parse_from_str(parts[0], "%H:%M:%S%.3f")
234 .map_err(|_| ParsingError::new(format!("Error parsing timestamp '{}' in line: {}", parts[0], first_line), 0))?;
235
236 Ok(time_to_milliseconds(&date))
237}
238
239#[inline]
241pub fn eof_error(line_idx: u64) -> TramexError {
242 tramex_error!(
243 format!("End of file (line {})", line_idx),
244 crate::errors::ErrorCode::EndOfFile
245 )
246}