tramex_tools/interface/parser/
parser_ngap.rs

1//! Parser for NGAP traces
2use super::ParsingError;
3use super::hex_extractor::extract_binary_from_lines;
4use crate::data::{AdditionalInfos, Trace};
5use crate::interface::association::TraceRelation;
6use std::str::FromStr;
7
8use crate::interface::{layer::Layer, types::Direction};
9
10use super::{FileParser, LayerParser, ParsedHeader};
11
12#[derive(Debug, Clone)]
13/// Data structure to store the NGAP message type
14pub struct NGAPInfos {
15    /// Direction of the message (TO/FROM)
16    /// Check direction NGAP: Currently TO = BST -> CN, FROM = CN -> BST
17    pub direction: Direction,
18
19    /// Message type (e.g., "UE context release complete", "Initial context setup request")
20    pub message_type: String,
21
22    /// Connection info (e.g., "127.0.1.100:38412")
23    pub connection_info: Option<String>,
24}
25
26/// NGAP Parser
27pub struct NGAPParser;
28
29impl FileParser for NGAPParser {
30    fn parse_additional_infos(lines: &[String]) -> Result<AdditionalInfos, ParsingError> {
31        let line = &lines[0];
32        let parts: Vec<&str> = line.split_whitespace().collect();
33
34        // Example: "13:20:58.310 [NGAP] TO 0080 003d 127.0.1.100:38412 UE context release complete"
35        // Parts: ["13:20:58.310", "[NGAP]", "TO", "0080", "003d", "127.0.1.100:38412", "UE", "context", "release", "complete"]
36        if parts.len() < 6 {
37            return Err(ParsingError::new(
38                "Could not find enough (6) parameters for NGAP".to_string(),
39                1,
40            ));
41        }
42
43        let direction_result = Direction::from_str(parts[2]);
44        let direction = match direction_result {
45            Ok(d) => d,
46            Err(_) => {
47                return Err(ParsingError::new(
48                    format!("The direction could not be parsed in the part {:?} of {}", parts[2], line),
49                    1,
50                ));
51            }
52        };
53
54        // Connection info is typically at parts[5] (IP:port)
55        let connection_info = if parts.len() > 5 && parts[5].contains(':') {
56            Some(parts[5].to_string())
57        } else {
58            None
59        };
60
61        // Message type is everything after connection info
62        let message_start_idx = if connection_info.is_some() { 6 } else { 5 };
63        let message_type = if parts.len() > message_start_idx {
64            parts[message_start_idx..].join(" ")
65        } else {
66            "Unknown".to_string()
67        };
68
69        Ok(AdditionalInfos::NGAPInfos(NGAPInfos {
70            direction,
71            message_type,
72            connection_info,
73        }))
74    }
75
76    fn parse(lines: &[String]) -> Result<Trace, ParsingError> {
77        let additional_infos = match Self::parse_additional_infos(lines) {
78            Ok(m) => m,
79            Err(e) => {
80                return Err(e);
81            }
82        };
83
84        let text = lines.to_vec();
85        let binary = extract_binary_from_lines(lines);
86
87        // Parse timestamp from first line
88        let timestamp = if let Some(first_line) = lines.first() {
89            super::parse_timestamp(first_line)?
90        } else {
91            0
92        };
93
94        let trace = Trace {
95            timestamp,
96            layer: Layer::NGAP,
97            additional_infos,
98            text: Some(text),
99            binary,
100            relation: TraceRelation::default(),
101        };
102        Ok(trace)
103    }
104}
105
106impl LayerParser for NGAPParser {
107    /// Parse NGAP payload lines.
108    /// data_lines[0] = "ID1 ID2 IP:PORT message type" (e.g. "00fc 009a 127.0.1.100:38412 UE context release request")
109    fn parse_layer(header: &ParsedHeader, data_lines: &[String]) -> Result<AdditionalInfos, ParsingError> {
110        if data_lines.is_empty() {
111            return Err(ParsingError::new("NGAP: empty data lines".to_string(), 0));
112        }
113        let first_line = &data_lines[0];
114        let parts: Vec<&str> = first_line.split_whitespace().collect();
115
116        // Try to find connection info (IP:port) and message type from the data line
117        let mut connection_info = header.connection_info.clone();
118        let mut message_start_idx = 0;
119
120        // Look for IP:port pattern in the parts
121        for (i, part) in parts.iter().enumerate() {
122            if part.contains(':') && part.contains('.') {
123                connection_info = Some(part.to_string());
124                message_start_idx = i + 1;
125                break;
126            }
127        }
128
129        let message_type = if parts.len() > message_start_idx && message_start_idx > 0 {
130            parts[message_start_idx..].join(" ")
131        } else if !parts.is_empty() {
132            parts.join(" ")
133        } else {
134            "Unknown".to_string()
135        };
136
137        Ok(AdditionalInfos::NGAPInfos(NGAPInfos {
138            direction: header.direction.clone(),
139            message_type,
140            connection_info,
141        }))
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_parse_ngap_line() {
151        let line = "13:20:58.310 [NGAP] TO 0080 003d 127.0.1.100:38412 UE context release complete";
152        let lines = vec![line.to_string()];
153
154        let result = NGAPParser::parse_additional_infos(&lines);
155        assert!(result.is_ok());
156
157        match result.unwrap() {
158            AdditionalInfos::NGAPInfos(infos) => {
159                assert_eq!(infos.direction, Direction::TO);
160                assert_eq!(infos.message_type, "UE context release complete");
161                assert_eq!(infos.connection_info, Some("127.0.1.100:38412".to_string()));
162            }
163            _ => panic!("Expected NGAPInfos"),
164        }
165    }
166
167    #[test]
168    fn test_parse_ngap_from() {
169        let line = "13:20:58.310 [NGAP] FROM 0080 003d 127.0.1.100:38412 Downlink NAS transport";
170        let lines = vec![line.to_string()];
171
172        let result = NGAPParser::parse_additional_infos(&lines);
173        assert!(result.is_ok());
174
175        match result.unwrap() {
176            AdditionalInfos::NGAPInfos(infos) => {
177                assert_eq!(infos.direction, Direction::FROM);
178                assert_eq!(infos.message_type, "Downlink NAS transport");
179            }
180            _ => panic!("Expected NGAPInfos"),
181        }
182    }
183
184    #[test]
185    fn test_parse_ngap_no_connection_info() {
186        let line = "13:20:58.310 [NGAP] TO 0080 003d UE context release complete";
187        let lines = vec![line.to_string()];
188
189        let result = NGAPParser::parse_additional_infos(&lines);
190        assert!(result.is_ok());
191
192        match result.unwrap() {
193            AdditionalInfos::NGAPInfos(infos) => {
194                assert_eq!(infos.connection_info, None);
195                assert_eq!(infos.message_type, "UE context release complete");
196            }
197            _ => panic!("Expected NGAPInfos"),
198        }
199    }
200
201    #[test]
202    fn test_parse_ngap_invalid_direction() {
203        let line = "13:20:58.310 [NGAP] INVALID 0080 003d 127.0.1.100:38412 UE context release complete";
204        let lines = vec![line.to_string()];
205
206        let result = NGAPParser::parse_additional_infos(&lines);
207        assert!(result.is_err());
208    }
209}