Skip to main content

packet_dissector_ngap/
lib.rs

1//! NGAP (NG Application Protocol) dissector.
2//!
3//! NGAP is the control-plane protocol between the gNB and the AMF in
4//! 5G networks. It runs over SCTP port 38412 and uses ASN.1 Aligned PER
5//! (APER) encoding.
6//!
7//! ## References
8//! - 3GPP TS 38.413: <https://www.3gpp.org/ftp/Specs/archive/38_series/38.413/>
9//! - ITU-T Rec. X.691 (APER): <https://www.itu.int/rec/T-REC-X.691>
10
11#![deny(missing_docs)]
12
13pub mod ie_id;
14pub mod ie_parsers;
15pub mod procedure_code;
16
17use packet_dissector_core::dissector::{DispatchHint, DissectResult, Dissector};
18use packet_dissector_core::error::PacketError;
19use packet_dissector_core::field::{FieldDescriptor, FieldType, FieldValue};
20use packet_dissector_core::packet::DissectBuffer;
21use packet_dissector_core::util::read_be_u16;
22
23static FD_INLINE_CRITICALITY: FieldDescriptor = FieldDescriptor {
24    name: "criticality",
25    display_name: "Criticality",
26    field_type: FieldType::U8,
27    optional: false,
28    children: None,
29    display_fn: Some(|v, _siblings| match v {
30        FieldValue::U8(c) => Some(criticality_name(*c)),
31        _ => None,
32    }),
33    format_fn: None,
34};
35
36static FD_INLINE_ID: FieldDescriptor = FieldDescriptor {
37    name: "id",
38    display_name: "ID",
39    field_type: FieldType::U16,
40    optional: false,
41    children: None,
42    display_fn: Some(|v, _siblings| match v {
43        FieldValue::U16(id) => Some(ie_id::ie_id_name(*id)),
44        _ => None,
45    }),
46    format_fn: None,
47};
48
49/// Descriptor for the ProtocolIE-Field Object container itself.
50///
51/// `display_fn` is invoked by
52/// [`DissectBuffer::resolve_container_display_name`] with the container's
53/// children, so the outer label resolves to the IE name instead of
54/// colliding with the inner `ID` field.
55static FD_IE: FieldDescriptor = FieldDescriptor {
56    name: "ie",
57    display_name: "IE",
58    field_type: FieldType::Object,
59    optional: false,
60    children: None,
61    display_fn: Some(|v, children| match v {
62        FieldValue::Object(_) => children.iter().find_map(|f| match (f.name(), &f.value) {
63            ("id", FieldValue::U16(id)) => Some(ie_id::ie_id_name(*id)),
64            _ => None,
65        }),
66        _ => None,
67    }),
68    format_fn: None,
69};
70
71static FD_INLINE_LENGTH: FieldDescriptor = FieldDescriptor::new("length", "Length", FieldType::U32);
72
73// Note: FD_INLINE_VALUE was removed — IE values are now pushed by
74// ie_parsers::push_ie_value using their own descriptors or a fallback.
75
76/// Minimum NGAP-PDU header size: PDU type (1) + procedure code (1) +
77/// criticality (1) = 3 bytes, before the value length determinant.
78///
79/// 3GPP TS 38.413, Section 9.4.2.
80const MIN_HEADER_SIZE: usize = 3;
81
82// Field descriptor indices.
83const FD_PDU_TYPE: usize = 0;
84const FD_PROCEDURE_CODE: usize = 1;
85const FD_CRITICALITY: usize = 2;
86const FD_VALUE_LENGTH: usize = 3;
87const FD_IES: usize = 4;
88
89// IE child field descriptor indices (used in tests to verify schema).
90#[cfg(test)]
91const CFD_ID: usize = 0;
92#[cfg(test)]
93const CFD_CRITICALITY: usize = 1;
94#[cfg(test)]
95const CFD_LENGTH: usize = 2;
96#[cfg(test)]
97const CFD_VALUE: usize = 3;
98
99/// Child field descriptors for each IE element in the `ies` array.
100///
101/// 3GPP TS 38.413, Section 9.4 — ProtocolIE-Field structure.
102static IE_CHILD_FIELDS: &[FieldDescriptor] = &[
103    FieldDescriptor {
104        name: "id",
105        display_name: "ID",
106        field_type: FieldType::U16,
107        optional: false,
108        children: None,
109        display_fn: Some(|v, _siblings| match v {
110            FieldValue::U16(id) => Some(ie_id::ie_id_name(*id)),
111            _ => None,
112        }),
113        format_fn: None,
114    },
115    FieldDescriptor {
116        name: "criticality",
117        display_name: "Criticality",
118        field_type: FieldType::U8,
119        optional: false,
120        children: None,
121        display_fn: Some(|v, _siblings| match v {
122            FieldValue::U8(c) => Some(criticality_name(*c)),
123            _ => None,
124        }),
125        format_fn: None,
126    },
127    FieldDescriptor::new("length", "Length", FieldType::U32),
128    FieldDescriptor::new("value", "Value", FieldType::Bytes),
129];
130
131static FIELD_DESCRIPTORS: &[FieldDescriptor] = &[
132    FieldDescriptor {
133        name: "pdu_type",
134        display_name: "PDU Type",
135        field_type: FieldType::U8,
136        optional: false,
137        children: None,
138        display_fn: Some(|v, _siblings| match v {
139            FieldValue::U8(t) => Some(pdu_type_name(*t)),
140            _ => None,
141        }),
142        format_fn: None,
143    },
144    FieldDescriptor {
145        name: "procedure_code",
146        display_name: "Procedure Code",
147        field_type: FieldType::U8,
148        optional: false,
149        children: None,
150        display_fn: Some(|v, _siblings| match v {
151            FieldValue::U8(c) => Some(procedure_code::procedure_code_name(*c)),
152            _ => None,
153        }),
154        format_fn: None,
155    },
156    FieldDescriptor {
157        name: "criticality",
158        display_name: "Criticality",
159        field_type: FieldType::U8,
160        optional: false,
161        children: None,
162        display_fn: Some(|v, _siblings| match v {
163            FieldValue::U8(c) => Some(criticality_name(*c)),
164            _ => None,
165        }),
166        format_fn: None,
167    },
168    FieldDescriptor::new("value_length", "Value Length", FieldType::U32),
169    FieldDescriptor::new("ies", "Information Elements", FieldType::Array)
170        .optional()
171        .with_children(IE_CHILD_FIELDS),
172];
173
174/// Returns a human-readable name for the NGAP-PDU CHOICE index.
175///
176/// 3GPP TS 38.413, Section 9.4.2.
177fn pdu_type_name(pdu_type: u8) -> &'static str {
178    match pdu_type {
179        0 => "initiatingMessage",
180        1 => "successfulOutcome",
181        2 => "unsuccessfulOutcome",
182        _ => "Unknown",
183    }
184}
185
186/// Returns a human-readable name for the NGAP criticality value.
187///
188/// 3GPP TS 38.413, Section 9.4 — Criticality ENUMERATED.
189fn criticality_name(criticality: u8) -> &'static str {
190    match criticality {
191        0 => "reject",
192        1 => "ignore",
193        2 => "notify",
194        _ => "Unknown",
195    }
196}
197
198/// Reads an APER length determinant from `data` starting at `pos`.
199///
200/// Returns `(length, bytes_consumed)`.
201///
202/// ITU-T Rec. X.691, Section 11.9.
203pub fn read_aper_length(data: &[u8], pos: usize) -> Result<(u32, usize), PacketError> {
204    if pos >= data.len() {
205        return Err(PacketError::Truncated {
206            expected: pos + 1,
207            actual: data.len(),
208        });
209    }
210    let first = data[pos];
211    if first & 0x80 == 0 {
212        // Short form: 0..127, encoded in 1 byte.
213        Ok((u32::from(first), 1))
214    } else if first & 0xC0 == 0x80 {
215        // Long form: 128..16383, encoded in 2 bytes.
216        if pos + 2 > data.len() {
217            return Err(PacketError::Truncated {
218                expected: pos + 2,
219                actual: data.len(),
220            });
221        }
222        let len = u32::from(first & 0x3F) << 8 | u32::from(data[pos + 1]);
223        Ok((len, 2))
224    } else {
225        // Fragmented form (>=16384) — not expected in typical NGAP messages.
226        Err(PacketError::InvalidHeader(
227            "APER fragmented length determinant not supported",
228        ))
229    }
230}
231
232/// Parses NGAP ProtocolIE-Container from `data` starting at `pos`,
233/// pushing fields directly into the [`DissectBuffer`].
234///
235/// Returns `bytes_consumed`.
236///
237/// 3GPP TS 38.413, Section 9.4 — ProtocolIE-Container.
238fn parse_ies<'pkt>(
239    buf: &mut DissectBuffer<'pkt>,
240    data: &'pkt [u8],
241    base_offset: usize,
242) -> Result<usize, PacketError> {
243    // IE count: constrained whole number 0..65535 → 2 bytes.
244    if data.len() < 2 {
245        return Err(PacketError::Truncated {
246            expected: 2,
247            actual: data.len(),
248        });
249    }
250    let ie_count = read_be_u16(data, 0)? as usize;
251    let mut pos: usize = 2;
252
253    for _ in 0..ie_count {
254        // Each IE: id (2 bytes) + criticality (1 byte) + value (length + data).
255        if pos + 3 > data.len() {
256            break;
257        }
258
259        let ie_id = read_be_u16(data, pos)?;
260        let ie_criticality = (data[pos + 2] >> 6) & 0x03;
261        let ie_start = base_offset + pos;
262        pos += 3;
263
264        // IE value: APER length determinant + raw bytes.
265        let (ie_value_len, len_bytes) = read_aper_length(data, pos)?;
266        pos += len_bytes;
267
268        let ie_value_len_usize = ie_value_len as usize;
269        if pos + ie_value_len_usize > data.len() {
270            break;
271        }
272
273        let ie_value_data = &data[pos..pos + ie_value_len_usize];
274        let ie_end = base_offset + pos + ie_value_len_usize;
275        let ie_value_offset = base_offset + pos;
276
277        // Begin Object container for this IE element.
278        let obj_idx = buf.begin_container(&FD_IE, FieldValue::Object(0..0), ie_start..ie_end);
279
280        buf.push_field(
281            &FD_INLINE_ID,
282            FieldValue::U16(ie_id),
283            ie_start..ie_start + 2,
284        );
285        buf.push_field(
286            &FD_INLINE_CRITICALITY,
287            FieldValue::U8(ie_criticality),
288            ie_start + 2..ie_start + 3,
289        );
290        buf.push_field(
291            &FD_INLINE_LENGTH,
292            FieldValue::U32(ie_value_len),
293            base_offset + pos - len_bytes..base_offset + pos,
294        );
295
296        // Parse the IE value into structured fields if possible.
297        ie_parsers::push_ie_value(buf, ie_id, ie_value_data, ie_value_offset);
298
299        buf.end_container(obj_idx);
300
301        pos += ie_value_len_usize;
302    }
303
304    Ok(pos)
305}
306
307/// NGAP (NG Application Protocol) dissector.
308///
309/// Parses NGAP-PDUs encoded with ASN.1 Aligned PER (APER) as specified
310/// in 3GPP TS 38.413. Extracts the PDU type, procedure code, criticality,
311/// and all top-level Information Elements.
312///
313/// 3GPP TS 38.413: <https://www.3gpp.org/ftp/Specs/archive/38_series/38.413/>
314pub struct NgapDissector;
315
316impl Dissector for NgapDissector {
317    fn name(&self) -> &'static str {
318        "NG Application Protocol"
319    }
320
321    fn short_name(&self) -> &'static str {
322        "NGAP"
323    }
324
325    fn field_descriptors(&self) -> &'static [FieldDescriptor] {
326        FIELD_DESCRIPTORS
327    }
328
329    fn dissect<'pkt>(
330        &self,
331        data: &'pkt [u8],
332        buf: &mut DissectBuffer<'pkt>,
333        offset: usize,
334    ) -> Result<DissectResult, PacketError> {
335        if data.len() < MIN_HEADER_SIZE {
336            return Err(PacketError::Truncated {
337                expected: MIN_HEADER_SIZE,
338                actual: data.len(),
339            });
340        }
341
342        // Byte 0: NGAP-PDU CHOICE (APER)
343        // 3GPP TS 38.413, Section 9.4.2 — NGAP-PDU ::= CHOICE
344        //   Bit 7: extension marker (0 = root)
345        //   Bits 6-5: choice index (0-2)
346        //   Bits 4-0: padding
347        let pdu_byte = data[0];
348        let extension = (pdu_byte >> 7) & 0x01;
349        let pdu_type = (pdu_byte >> 5) & 0x03;
350
351        if extension != 0 {
352            return Err(PacketError::InvalidHeader(
353                "NGAP-PDU extension not supported",
354            ));
355        }
356        if pdu_type > 2 {
357            return Err(PacketError::InvalidFieldValue {
358                field: "pdu_type",
359                value: u32::from(pdu_type),
360            });
361        }
362
363        // Byte 1: procedureCode (INTEGER 0..255)
364        // 3GPP TS 38.413, Section 9.4 — InitiatingMessage / SuccessfulOutcome /
365        // UnsuccessfulOutcome common fields.
366        let proc_code = data[1];
367
368        // Byte 2: criticality (ENUMERATED {reject, ignore, notify})
369        // 2 bits + 6 bits padding (APER octet-aligned)
370        let crit = (data[2] >> 6) & 0x03;
371
372        // Value field: APER OPEN TYPE with length determinant.
373        let mut pos: usize = 3;
374        let (value_length, len_bytes) = read_aper_length(data, pos)?;
375        pos += len_bytes;
376
377        let value_length_usize = value_length as usize;
378        if pos + value_length_usize > data.len() {
379            return Err(PacketError::Truncated {
380                expected: pos + value_length_usize,
381                actual: data.len(),
382            });
383        }
384
385        let total_consumed = pos + value_length_usize;
386
387        // Parse ProtocolIE-Container from the value field.
388        let value_data = &data[pos..pos + value_length_usize];
389        let ie_base_offset = offset + pos;
390
391        buf.begin_layer(
392            "NGAP",
393            None,
394            FIELD_DESCRIPTORS,
395            offset..offset + total_consumed,
396        );
397
398        buf.push_field(
399            &FIELD_DESCRIPTORS[FD_PDU_TYPE],
400            FieldValue::U8(pdu_type),
401            offset..offset + 1,
402        );
403        buf.push_field(
404            &FIELD_DESCRIPTORS[FD_PROCEDURE_CODE],
405            FieldValue::U8(proc_code),
406            offset + 1..offset + 2,
407        );
408        buf.push_field(
409            &FIELD_DESCRIPTORS[FD_CRITICALITY],
410            FieldValue::U8(crit),
411            offset + 2..offset + 3,
412        );
413        buf.push_field(
414            &FIELD_DESCRIPTORS[FD_VALUE_LENGTH],
415            FieldValue::U32(value_length),
416            offset + 3..offset + 3 + len_bytes,
417        );
418
419        // The value OPEN TYPE contains an APER-encoded SEQUENCE
420        // (e.g. NGSetupRequest) with a 1-byte preamble: extension bit (1)
421        // + padding (7). All NGAP message types follow the pattern
422        // `SEQUENCE { protocolIEs ProtocolIE-Container, ... }` with zero
423        // optional fields, so the preamble is always exactly 1 byte.
424        //
425        // 3GPP TS 38.413, Section 9.4 — message SEQUENCE definitions.
426        // ITU-T Rec. X.691, Section 18.1 — SEQUENCE preamble encoding.
427        const SEQUENCE_PREAMBLE_SIZE: usize = 1;
428
429        // Attempt to parse IEs; if the container is present.
430        if value_data.len() > SEQUENCE_PREAMBLE_SIZE {
431            let ie_data = &value_data[SEQUENCE_PREAMBLE_SIZE..];
432            let ie_offset = ie_base_offset + SEQUENCE_PREAMBLE_SIZE;
433
434            let arr_idx = buf.begin_container(
435                &FIELD_DESCRIPTORS[FD_IES],
436                FieldValue::Array(0..0),
437                ie_offset..ie_offset + ie_data.len(),
438            );
439            match parse_ies(buf, ie_data, ie_offset) {
440                Ok(_) => {}
441                Err(_) => {
442                    // Gracefully handle IE parse failures by exposing the
443                    // header fields without IEs, rather than failing the
444                    // entire dissection.
445                }
446            }
447            buf.end_container(arr_idx);
448        }
449
450        buf.end_layer();
451
452        Ok(DissectResult::new(total_consumed, DispatchHint::End))
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    //! # 3GPP TS 38.413 Coverage
459    //!
460    //! | Spec Section | Description                  | Test                              |
461    //! |--------------|------------------------------|-----------------------------------|
462    //! | 9.4.2        | NGAP-PDU CHOICE              | parse_ngap_initiating_message     |
463    //! | 9.4.2        | successfulOutcome             | parse_ngap_successful_outcome     |
464    //! | 9.4.2        | unsuccessfulOutcome           | parse_ngap_unsuccessful_outcome   |
465    //! | 9.4.2        | Invalid PDU type              | parse_ngap_invalid_pdu_type       |
466    //! | 9.4.2        | Truncated header              | parse_ngap_truncated              |
467    //! | 9.4          | Empty IE container            | parse_ngap_empty_ie_container     |
468    //! | 9.4          | ProtocolIE-Container          | parse_ngap_with_ies               |
469
470    use super::*;
471
472    /// Build a minimal NGAP-PDU (initiatingMessage, NGSetup, reject)
473    /// with the given value payload.
474    fn build_ngap_pdu(pdu_type: u8, proc_code: u8, crit: u8, value: &[u8]) -> Vec<u8> {
475        let mut pdu = Vec::new();
476        // Byte 0: extension(0) | pdu_type(2 bits) | padding(5 bits)
477        pdu.push(pdu_type << 5);
478        // Byte 1: procedure code
479        pdu.push(proc_code);
480        // Byte 2: criticality(2 bits) | padding(6 bits)
481        pdu.push(crit << 6);
482        // Value length determinant
483        if value.len() < 128 {
484            pdu.push(value.len() as u8);
485        } else {
486            let len = value.len() as u16;
487            pdu.push(0x80 | ((len >> 8) as u8 & 0x3F));
488            pdu.push((len & 0xFF) as u8);
489        }
490        pdu.extend_from_slice(value);
491        pdu
492    }
493
494    /// Build an APER-encoded message value containing a ProtocolIE-Container.
495    /// Includes the 1-byte SEQUENCE preamble (extension bit + padding).
496    /// Each IE is (id, criticality, value_bytes).
497    fn build_ie_container(ies: &[(u16, u8, &[u8])]) -> Vec<u8> {
498        let mut container = Vec::new();
499        // SEQUENCE preamble: extension bit (0) + 7 bits padding
500        container.push(0x00);
501        // IE count: 2 bytes
502        container.push((ies.len() >> 8) as u8);
503        container.push((ies.len() & 0xFF) as u8);
504        for (id, crit, value) in ies {
505            // IE id: 2 bytes
506            container.push((*id >> 8) as u8);
507            container.push((*id & 0xFF) as u8);
508            // IE criticality: 1 byte (2 bits + 6 padding)
509            container.push(*crit << 6);
510            // IE value length determinant
511            if value.len() < 128 {
512                container.push(value.len() as u8);
513            } else {
514                let len = value.len() as u16;
515                container.push(0x80 | ((len >> 8) as u8 & 0x3F));
516                container.push((len & 0xFF) as u8);
517            }
518            container.extend_from_slice(value);
519        }
520        container
521    }
522
523    #[test]
524    fn parse_ngap_initiating_message() {
525        // initiatingMessage, NGSetup (21), reject (0), empty container
526        let container = build_ie_container(&[]);
527        let data = build_ngap_pdu(0, 21, 0, &container);
528
529        let mut buf = DissectBuffer::new();
530        let result = NgapDissector.dissect(&data, &mut buf, 0).unwrap();
531
532        assert_eq!(result.bytes_consumed, data.len());
533
534        let layer = buf.layer_by_name("NGAP").unwrap();
535        assert_eq!(
536            buf.field_by_name(layer, "pdu_type").unwrap().value,
537            FieldValue::U8(0)
538        );
539        assert_eq!(
540            buf.resolve_display_name(layer, "pdu_type_name"),
541            Some("initiatingMessage")
542        );
543        assert_eq!(
544            buf.field_by_name(layer, "procedure_code").unwrap().value,
545            FieldValue::U8(21)
546        );
547        assert_eq!(
548            buf.resolve_display_name(layer, "procedure_code_name"),
549            Some("NGSetup")
550        );
551        assert_eq!(
552            buf.field_by_name(layer, "criticality").unwrap().value,
553            FieldValue::U8(0)
554        );
555        assert_eq!(
556            buf.resolve_display_name(layer, "criticality_name"),
557            Some("reject")
558        );
559    }
560
561    #[test]
562    fn parse_ngap_successful_outcome() {
563        let container = build_ie_container(&[]);
564        let data = build_ngap_pdu(1, 21, 0, &container);
565
566        let mut buf = DissectBuffer::new();
567        NgapDissector.dissect(&data, &mut buf, 0).unwrap();
568
569        let layer = buf.layer_by_name("NGAP").unwrap();
570        assert_eq!(
571            buf.resolve_display_name(layer, "pdu_type_name"),
572            Some("successfulOutcome")
573        );
574    }
575
576    #[test]
577    fn parse_ngap_unsuccessful_outcome() {
578        let container = build_ie_container(&[]);
579        let data = build_ngap_pdu(2, 14, 0, &container);
580
581        let mut buf = DissectBuffer::new();
582        NgapDissector.dissect(&data, &mut buf, 0).unwrap();
583
584        let layer = buf.layer_by_name("NGAP").unwrap();
585        assert_eq!(
586            buf.resolve_display_name(layer, "pdu_type_name"),
587            Some("unsuccessfulOutcome")
588        );
589        assert_eq!(
590            buf.resolve_display_name(layer, "procedure_code_name"),
591            Some("InitialContextSetup")
592        );
593    }
594
595    #[test]
596    fn parse_ngap_invalid_pdu_type() {
597        // pdu_type = 3 is invalid
598        let data = [0x60, 0x15, 0x00, 0x02, 0x00, 0x00];
599        let mut buf = DissectBuffer::new();
600        let result = NgapDissector.dissect(&data, &mut buf, 0);
601        assert!(result.is_err());
602    }
603
604    #[test]
605    fn parse_ngap_truncated() {
606        let data = [0x00, 0x15];
607        let mut buf = DissectBuffer::new();
608        let result = NgapDissector.dissect(&data, &mut buf, 0);
609        assert!(matches!(result, Err(PacketError::Truncated { .. })));
610    }
611
612    #[test]
613    fn parse_ngap_empty_ie_container() {
614        let container = build_ie_container(&[]);
615        let data = build_ngap_pdu(0, 21, 0, &container);
616
617        let mut buf = DissectBuffer::new();
618        NgapDissector.dissect(&data, &mut buf, 0).unwrap();
619
620        let layer = buf.layer_by_name("NGAP").unwrap();
621        let fields = buf.layer_fields(layer);
622        // 4 header fields + 1 empty Array container
623        assert_eq!(fields.len(), 5);
624        // The Array container should have no children.
625        if let FieldValue::Array(ref range) = fields[4].value {
626            assert!(range.is_empty());
627        } else {
628            panic!("expected Array");
629        }
630    }
631
632    #[test]
633    fn parse_ngap_with_ies() {
634        let ie_value_1 = [0x01, 0x02, 0x03]; // dummy AMF-UE-NGAP-ID value
635        let ie_value_2 = [0x04, 0x05]; // dummy RAN-UE-NGAP-ID value
636        let container = build_ie_container(&[
637            (10, 0, &ie_value_1), // AMF-UE-NGAP-ID, reject
638            (85, 0, &ie_value_2), // RAN-UE-NGAP-ID, reject
639        ]);
640        let data = build_ngap_pdu(0, 15, 0, &container); // InitialUEMessage
641
642        let mut buf = DissectBuffer::new();
643        NgapDissector.dissect(&data, &mut buf, 0).unwrap();
644
645        let layer = buf.layer_by_name("NGAP").unwrap();
646        assert_eq!(
647            buf.resolve_display_name(layer, "procedure_code_name"),
648            Some("InitialUEMessage")
649        );
650
651        // Find Object containers (IEs) in the layer fields.
652        let fields = buf.layer_fields(layer);
653        let ie_objects: Vec<_> = fields
654            .iter()
655            .filter(|f| matches!(f.value, FieldValue::Object(_)))
656            .collect();
657        assert_eq!(ie_objects.len(), 2);
658
659        // First IE: AMF-UE-NGAP-ID
660        if let FieldValue::Object(ref range) = ie_objects[0].value {
661            let ie_fields = buf.nested_fields(range);
662            let id_field = ie_fields.iter().find(|f| f.name() == "id").unwrap();
663            assert_eq!(id_field.value, FieldValue::U16(10));
664            let display_fn = id_field.descriptor.display_fn.unwrap();
665            assert_eq!(
666                display_fn(&id_field.value, ie_fields),
667                Some("AMF-UE-NGAP-ID")
668            );
669            let val_field = ie_fields.iter().find(|f| f.name() == "value").unwrap();
670            assert_eq!(val_field.value, FieldValue::Bytes(&[0x01, 0x02, 0x03]));
671        } else {
672            panic!("expected Object");
673        }
674
675        // Second IE: RAN-UE-NGAP-ID
676        if let FieldValue::Object(ref range) = ie_objects[1].value {
677            let ie_fields = buf.nested_fields(range);
678            let id_field = ie_fields.iter().find(|f| f.name() == "id").unwrap();
679            assert_eq!(id_field.value, FieldValue::U16(85));
680            let display_fn = id_field.descriptor.display_fn.unwrap();
681            assert_eq!(
682                display_fn(&id_field.value, ie_fields),
683                Some("RAN-UE-NGAP-ID")
684            );
685        } else {
686            panic!("expected Object");
687        }
688    }
689
690    #[test]
691    fn ie_container_resolves_to_ie_name() {
692        let ie_value = [0x01, 0x02, 0x03];
693        let container = build_ie_container(&[(10, 0, &ie_value)]); // AMF-UE-NGAP-ID
694        let data = build_ngap_pdu(0, 15, 0, &container);
695
696        let mut buf = DissectBuffer::new();
697        NgapDissector.dissect(&data, &mut buf, 0).unwrap();
698
699        // Find the IE Object container and verify its outer label resolves
700        // to the IE name rather than duplicating "ID".
701        let (ie_idx, ie_field) = buf
702            .fields()
703            .iter()
704            .enumerate()
705            .find(|(_, f)| matches!(f.value, FieldValue::Object(_)))
706            .expect("IE container not found");
707        assert_eq!(ie_field.name(), "ie");
708        assert_eq!(ie_field.display_name(), "IE");
709        assert_eq!(
710            buf.resolve_container_display_name(ie_idx as u32),
711            Some("AMF-UE-NGAP-ID")
712        );
713    }
714
715    #[test]
716    fn parse_ngap_with_offset() {
717        let container = build_ie_container(&[(10, 0, &[0x01])]);
718        let data = build_ngap_pdu(0, 21, 0, &container);
719
720        let mut buf = DissectBuffer::new();
721        let base_offset = 100;
722        NgapDissector.dissect(&data, &mut buf, base_offset).unwrap();
723
724        let layer = buf.layer_by_name("NGAP").unwrap();
725        assert_eq!(layer.range.start, base_offset);
726        assert_eq!(layer.range.end, base_offset + data.len());
727    }
728
729    #[test]
730    fn parse_ngap_long_length_determinant() {
731        // Build a value payload > 127 bytes to trigger the 2-byte length form.
732        let ie_value = vec![0xAA; 200];
733        let container = build_ie_container(&[(38, 0, &ie_value)]); // NAS-PDU
734        let data = build_ngap_pdu(0, 4, 1, &container); // DownlinkNASTransport, ignore
735
736        let mut buf = DissectBuffer::new();
737        NgapDissector.dissect(&data, &mut buf, 0).unwrap();
738
739        let layer = buf.layer_by_name("NGAP").unwrap();
740        assert_eq!(
741            buf.resolve_display_name(layer, "procedure_code_name"),
742            Some("DownlinkNASTransport")
743        );
744        assert_eq!(
745            buf.resolve_display_name(layer, "criticality_name"),
746            Some("ignore")
747        );
748
749        // Find IE Object containers.
750        let fields = buf.layer_fields(layer);
751        let ie_objects: Vec<_> = fields
752            .iter()
753            .filter(|f| matches!(f.value, FieldValue::Object(_)))
754            .collect();
755        assert_eq!(ie_objects.len(), 1);
756        if let FieldValue::Object(ref range) = ie_objects[0].value {
757            let ie_fields = buf.nested_fields(range);
758            let val_field = ie_fields.iter().find(|f| f.name() == "value").unwrap();
759            if let FieldValue::Bytes(bytes) = &val_field.value {
760                assert_eq!(bytes.len(), 200);
761            } else {
762                panic!("expected Bytes");
763            }
764        }
765    }
766
767    #[test]
768    fn field_descriptors_accessible() {
769        let d = NgapDissector;
770        assert_eq!(d.field_descriptors().len(), 5);
771        assert_eq!(
772            d.field_descriptors()[FD_IES].children,
773            Some(IE_CHILD_FIELDS)
774        );
775    }
776
777    #[test]
778    #[allow(unused_variables)]
779    fn unused_child_field_indices_compile() {
780        // Ensure all CFD_* constants are used and valid.
781        let _ = IE_CHILD_FIELDS[CFD_ID];
782        let _ = IE_CHILD_FIELDS[CFD_CRITICALITY];
783        let _ = IE_CHILD_FIELDS[CFD_LENGTH];
784        let _ = IE_CHILD_FIELDS[CFD_VALUE];
785    }
786}