Skip to main content

netget/server/ngap/
codec.rs

1//! NGAP (3GPP TS 38.413) PDU structure, the IEs of the procedures NetGet speaks, and message
2//! builders — shared by the AMF (server) and the gNB (client).
3//!
4//! # What is decoded and what is skipped
5//!
6//! An NGAP PDU is a CHOICE of initiating / successful / unsuccessful message, each carrying a
7//! procedure code, a criticality and an open type holding a `ProtocolIE-Container`: a list of
8//! `(id, criticality, open type)` fields. [`decode_pdu`] walks that outer structure for every
9//! procedure, so any NGAP message can be framed, named and have its IE ids reported. The IE
10//! *values* are decoded only for the procedures listed in [`procedure_name`]'s implemented set:
11//! NG Setup, Initial UE Message, Downlink/Uplink NAS Transport, Error Indication, NG Reset and UE
12//! Context Release. An IE NetGet does not decode stays as its raw octets and is surfaced by id —
13//! the open-type framing is exactly what lets a receiver do that honestly.
14//!
15//! Every value is checked against its ASN.1 constraint on the way in *and* on the way out, so an
16//! action cannot put a 33-bit gNB ID or an eleven-digit MCC on the wire.
17//!
18//! # Bounds
19//!
20//! [`MAX_PDU_LEN`] bounds the PDU, [`MAX_IES`] the IE count per container, [`MAX_NAS_PDU_LEN`] the
21//! NAS-PDU, and every `SEQUENCE OF` is bounded by its own 38.413 constraint *and* by
22//! [`MAX_LIST_ITEMS`] in total per decode, so the structure handed to the model is bounded however
23//! the peer nests its lists. No decoder here recurses.
24
25use super::aper::{self, AperError, Reader, Writer};
26use serde_json::{json, Map, Value};
27
28/// SCTP Payload Protocol Identifier for NGAP (TS 38.412 section 7).
29pub const PPID: u32 = 60;
30/// SCTP port the AMF listens on (TS 38.412 section 7, IANA).
31pub const SCTP_PORT: u16 = 38412;
32/// Largest NGAP PDU accepted or written. NGAP itself has no length field; this is the bound on
33/// one SCTP message (or one lab-transport frame).
34pub const MAX_PDU_LEN: usize = 65_535;
35/// IEs accepted in one `ProtocolIE-Container`. The constraint allows 65535; the largest message in
36/// NetGet's subset carries about ten.
37pub const MAX_IES: usize = 64;
38/// Largest NAS-PDU accepted or written. A 5GMM message NetGet handles is under a hundred octets;
39/// 8 KiB leaves room for any capability container a UE might add without approaching the APER
40/// fragmentation threshold.
41pub const MAX_NAS_PDU_LEN: usize = 8_192;
42/// `SEQUENCE OF` elements decoded per PDU, summed across every list in it.
43pub const MAX_LIST_ITEMS: usize = 1_024;
44
45// Procedure codes (TS 38.413 section 9.4.7).
46pub const PROC_AMF_CONFIGURATION_UPDATE: u8 = 0;
47pub const PROC_DOWNLINK_NAS_TRANSPORT: u8 = 4;
48pub const PROC_ERROR_INDICATION: u8 = 9;
49pub const PROC_INITIAL_UE_MESSAGE: u8 = 15;
50pub const PROC_NG_RESET: u8 = 20;
51pub const PROC_NG_SETUP: u8 = 21;
52pub const PROC_RAN_CONFIGURATION_UPDATE: u8 = 35;
53pub const PROC_UE_CONTEXT_RELEASE: u8 = 41;
54pub const PROC_UPLINK_NAS_TRANSPORT: u8 = 46;
55
56// IE ids (TS 38.413 section 9.4.7).
57pub const IE_AMF_NAME: u16 = 1;
58pub const IE_AMF_SET_ID: u16 = 3;
59pub const IE_AMF_UE_NGAP_ID: u16 = 10;
60pub const IE_CAUSE: u16 = 15;
61pub const IE_CRITICALITY_DIAGNOSTICS: u16 = 19;
62pub const IE_DEFAULT_PAGING_DRX: u16 = 21;
63pub const IE_FIVE_G_S_TMSI: u16 = 26;
64pub const IE_GLOBAL_RAN_NODE_ID: u16 = 27;
65pub const IE_NAS_PDU: u16 = 38;
66pub const IE_PLMN_SUPPORT_LIST: u16 = 80;
67pub const IE_RAN_NODE_NAME: u16 = 82;
68pub const IE_RAN_UE_NGAP_ID: u16 = 85;
69pub const IE_RELATIVE_AMF_CAPACITY: u16 = 86;
70pub const IE_RESET_TYPE: u16 = 88;
71pub const IE_RRC_ESTABLISHMENT_CAUSE: u16 = 90;
72pub const IE_SERVED_GUAMI_LIST: u16 = 96;
73pub const IE_SUPPORTED_TA_LIST: u16 = 102;
74pub const IE_TIME_TO_WAIT: u16 = 107;
75pub const IE_UE_ASSOCIATED_LOGICAL_NG_CONNECTION_LIST: u16 = 111;
76pub const IE_UE_CONTEXT_REQUEST: u16 = 112;
77pub const IE_UE_NGAP_IDS: u16 = 114;
78pub const IE_USER_LOCATION_INFORMATION: u16 = 121;
79
80pub const MAX_AMF_UE_NGAP_ID: u64 = (1 << 40) - 1;
81pub const MAX_RAN_UE_NGAP_ID: u64 = u32::MAX as u64;
82
83/// Name of a procedure code, for logs and events. `None` for a code NetGet has no name for.
84pub fn procedure_name(code: u8) -> Option<&'static str> {
85    Some(match code {
86        0 => "AMFConfigurationUpdate",
87        1 => "AMFStatusIndication",
88        4 => "DownlinkNASTransport",
89        9 => "ErrorIndication",
90        12 => "HandoverPreparation",
91        14 => "InitialContextSetup",
92        15 => "InitialUEMessage",
93        20 => "NGReset",
94        21 => "NGSetup",
95        24 => "Paging",
96        25 => "PathSwitchRequest",
97        26 => "PDUSessionResourceModify",
98        29 => "PDUSessionResourceRelease",
99        30 => "PDUSessionResourceSetup",
100        35 => "RANConfigurationUpdate",
101        40 => "UEContextModification",
102        41 => "UEContextRelease",
103        42 => "UEContextReleaseRequest",
104        44 => "UERadioCapabilityInfoIndication",
105        46 => "UplinkNASTransport",
106        _ => return None,
107    })
108}
109
110/// Name of an IE id NetGet decodes, for events. Others are reported by number.
111pub fn ie_name(id: u16) -> Option<&'static str> {
112    Some(match id {
113        IE_AMF_NAME => "AMFName",
114        IE_AMF_SET_ID => "AMFSetID",
115        IE_AMF_UE_NGAP_ID => "AMF-UE-NGAP-ID",
116        IE_CAUSE => "Cause",
117        IE_CRITICALITY_DIAGNOSTICS => "CriticalityDiagnostics",
118        IE_DEFAULT_PAGING_DRX => "DefaultPagingDRX",
119        IE_FIVE_G_S_TMSI => "FiveG-S-TMSI",
120        IE_GLOBAL_RAN_NODE_ID => "GlobalRANNodeID",
121        IE_NAS_PDU => "NAS-PDU",
122        IE_PLMN_SUPPORT_LIST => "PLMNSupportList",
123        IE_RAN_NODE_NAME => "RANNodeName",
124        IE_RAN_UE_NGAP_ID => "RAN-UE-NGAP-ID",
125        IE_RELATIVE_AMF_CAPACITY => "RelativeAMFCapacity",
126        IE_RESET_TYPE => "ResetType",
127        IE_RRC_ESTABLISHMENT_CAUSE => "RRCEstablishmentCause",
128        IE_SERVED_GUAMI_LIST => "ServedGUAMIList",
129        IE_SUPPORTED_TA_LIST => "SupportedTAList",
130        IE_TIME_TO_WAIT => "TimeToWait",
131        IE_UE_ASSOCIATED_LOGICAL_NG_CONNECTION_LIST => "UE-associatedLogicalNG-connectionList",
132        IE_UE_CONTEXT_REQUEST => "UEContextRequest",
133        IE_UE_NGAP_IDS => "UE-NGAP-IDs",
134        IE_USER_LOCATION_INFORMATION => "UserLocationInformation",
135        _ => return None,
136    })
137}
138
139// ============================================================================
140// Errors carry the Cause the peer is answered with
141// ============================================================================
142
143/// A PDU NetGet refuses, with the NGAP Cause that tells the peer why. The detail is for the log
144/// only; nothing in it is put on the wire.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct ProtocolError {
147    pub cause: Cause,
148    pub detail: String,
149}
150
151impl std::fmt::Display for ProtocolError {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        write!(f, "{} ({})", self.detail, self.cause.label())
154    }
155}
156
157impl std::error::Error for ProtocolError {}
158
159impl From<AperError> for ProtocolError {
160    fn from(e: AperError) -> Self {
161        ProtocolError {
162            cause: Cause::protocol(PROTOCOL_TRANSFER_SYNTAX_ERROR),
163            detail: e.0,
164        }
165    }
166}
167
168fn abstract_reject(detail: impl Into<String>) -> ProtocolError {
169    ProtocolError {
170        cause: Cause::protocol(PROTOCOL_ABSTRACT_SYNTAX_ERROR_REJECT),
171        detail: detail.into(),
172    }
173}
174
175fn semantic(detail: impl Into<String>) -> ProtocolError {
176    ProtocolError {
177        cause: Cause::protocol(PROTOCOL_SEMANTIC_ERROR),
178        detail: detail.into(),
179    }
180}
181
182pub type CodecResult<T> = Result<T, ProtocolError>;
183
184/// An encode refusal: what the action asked for cannot be put on the wire.
185#[derive(Debug, Clone)]
186pub struct EncodeError(pub String);
187
188impl std::fmt::Display for EncodeError {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        f.write_str(&self.0)
191    }
192}
193
194impl std::error::Error for EncodeError {}
195
196impl From<AperError> for EncodeError {
197    fn from(e: AperError) -> Self {
198        EncodeError(e.0)
199    }
200}
201
202pub type EncodeResult<T> = Result<T, EncodeError>;
203
204fn enc_err<T>(msg: impl Into<String>) -> EncodeResult<T> {
205    Err(EncodeError(msg.into()))
206}
207
208// ============================================================================
209// Cause (TS 38.413 section 9.3.1.2)
210// ============================================================================
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum CauseGroup {
214    RadioNetwork,
215    Transport,
216    Nas,
217    Protocol,
218    Misc,
219}
220
221const RADIO_NETWORK_ROOT: &[&str] = &[
222    "unspecified",
223    "txnrelocoverall-expiry",
224    "successful-handover",
225    "release-due-to-ngran-generated-reason",
226    "release-due-to-5gc-generated-reason",
227    "handover-cancelled",
228    "partial-handover",
229    "ho-failure-in-target-5GC-ngran-node-or-target-system",
230    "ho-target-not-allowed",
231    "tngrelocoverall-expiry",
232    "tngrelocprep-expiry",
233    "cell-not-available",
234    "unknown-targetID",
235    "no-radio-resources-available-in-target-cell",
236    "unknown-local-UE-NGAP-ID",
237    "inconsistent-remote-UE-NGAP-ID",
238    "handover-desirable-for-radio-reason",
239    "time-critical-handover",
240    "resource-optimisation-handover",
241    "reduce-load-in-serving-cell",
242    "user-inactivity",
243    "radio-connection-with-ue-lost",
244    "radio-resources-not-available",
245    "invalid-qos-combination",
246    "failure-in-radio-interface-procedure",
247    "interaction-with-other-procedure",
248    "unknown-PDU-session-ID",
249    "unkown-qos-flow-ID",
250    "multiple-PDU-session-ID-instances",
251    "multiple-qos-flow-ID-instances",
252    "encryption-and-or-integrity-protection-algorithms-not-supported",
253    "ng-intra-system-handover-triggered",
254    "ng-inter-system-handover-triggered",
255    "xn-handover-triggered",
256    "not-supported-5QI-value",
257    "ue-context-transfer",
258    "ims-voice-eps-fallback-or-rat-fallback-triggered",
259    "up-integrity-protection-not-possible",
260    "up-confidentiality-protection-not-possible",
261    "slice-not-supported",
262    "ue-in-rrc-inactive-state-not-reachable",
263    "redirection",
264    "resources-not-available-for-the-slice",
265    "ue-max-integrity-protected-data-rate-reason",
266    "release-due-to-cn-detected-mobility",
267];
268const RADIO_NETWORK_EXT: &[&str] = &[
269    "n26-interface-not-available",
270    "release-due-to-pre-emption",
271    "multiple-location-reporting-reference-ID-instances",
272    "rsn-not-available-for-the-up",
273    "npn-access-denied",
274    "cag-only-access-denied",
275    "insufficient-ue-capabilities",
276    "redcap-ue-not-supported",
277    "unknown-MBS-Session-ID",
278    "indicated-MBS-session-area-information-not-served-by-the-gNB",
279    "inconsistent-slice-info-for-the-session",
280    "misaligned-association-for-multicast-unicast",
281    "eredcap-ue-not-supported",
282    "two-rx-xr-ue-not-supported",
283    "aerial-ue-flight-information-reporting-initiation-failure",
284    "unknown-RAN-AIoT-Device-NGAP-ID",
285    "requested-AIoT-service-area-information-not-served-by-the-gNB",
286    "unknown-AIoT-session",
287    "aiot-device-not-reachable",
288    "multiple-AIoT-session-ID-instances",
289];
290const TRANSPORT_ROOT: &[&str] = &["transport-resource-unavailable", "unspecified"];
291const NAS_ROOT: &[&str] = &[
292    "normal-release",
293    "authentication-failure",
294    "deregister",
295    "unspecified",
296];
297const NAS_EXT: &[&str] = &[
298    "uE-not-in-PLMN-serving-area",
299    "mobile-IAB-not-authorized",
300    "iAB-not-authorized",
301    "no-aiot-NAS-response",
302];
303const PROTOCOL_ROOT: &[&str] = &[
304    "transfer-syntax-error",
305    "abstract-syntax-error-reject",
306    "abstract-syntax-error-ignore-and-notify",
307    "message-not-compatible-with-receiver-state",
308    "semantic-error",
309    "abstract-syntax-error-falsely-constructed-message",
310    "unspecified",
311];
312const MISC_ROOT: &[&str] = &[
313    "control-processing-overload",
314    "not-enough-user-plane-processing-resources",
315    "hardware-failure",
316    "om-intervention",
317    "unknown-PLMN-or-SNPN",
318    "unspecified",
319];
320
321pub const RADIO_UNKNOWN_LOCAL_UE_NGAP_ID: u8 = 14;
322pub const RADIO_INCONSISTENT_REMOTE_UE_NGAP_ID: u8 = 15;
323pub const NAS_NORMAL_RELEASE: u8 = 0;
324pub const NAS_UNSPECIFIED: u8 = 3;
325pub const PROTOCOL_TRANSFER_SYNTAX_ERROR: u8 = 0;
326pub const PROTOCOL_ABSTRACT_SYNTAX_ERROR_REJECT: u8 = 1;
327pub const PROTOCOL_MESSAGE_NOT_COMPATIBLE_WITH_RECEIVER_STATE: u8 = 3;
328pub const PROTOCOL_SEMANTIC_ERROR: u8 = 4;
329pub const PROTOCOL_UNSPECIFIED: u8 = 6;
330pub const MISC_CONTROL_PROCESSING_OVERLOAD: u8 = 0;
331pub const MISC_HARDWARE_FAILURE: u8 = 2;
332pub const MISC_OM_INTERVENTION: u8 = 3;
333pub const MISC_UNKNOWN_PLMN: u8 = 4;
334pub const MISC_UNSPECIFIED: u8 = 5;
335
336impl CauseGroup {
337    const ALL: [CauseGroup; 5] = [
338        CauseGroup::RadioNetwork,
339        CauseGroup::Transport,
340        CauseGroup::Nas,
341        CauseGroup::Protocol,
342        CauseGroup::Misc,
343    ];
344
345    pub fn name(self) -> &'static str {
346        match self {
347            CauseGroup::RadioNetwork => "radioNetwork",
348            CauseGroup::Transport => "transport",
349            CauseGroup::Nas => "nas",
350            CauseGroup::Protocol => "protocol",
351            CauseGroup::Misc => "misc",
352        }
353    }
354
355    fn tables(self) -> (&'static [&'static str], &'static [&'static str]) {
356        match self {
357            CauseGroup::RadioNetwork => (RADIO_NETWORK_ROOT, RADIO_NETWORK_EXT),
358            CauseGroup::Transport => (TRANSPORT_ROOT, &[]),
359            CauseGroup::Nas => (NAS_ROOT, NAS_EXT),
360            CauseGroup::Protocol => (PROTOCOL_ROOT, &[]),
361            CauseGroup::Misc => (MISC_ROOT, &[]),
362        }
363    }
364}
365
366/// An NGAP Cause: a group and an index in that group's root (or, when `extended`, its extension
367/// list).
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub struct Cause {
370    pub group: CauseGroup,
371    pub extended: bool,
372    pub index: u8,
373}
374
375fn normalize(s: &str) -> String {
376    s.trim().to_ascii_lowercase().replace('_', "-")
377}
378
379impl Cause {
380    pub fn protocol(index: u8) -> Self {
381        Cause {
382            group: CauseGroup::Protocol,
383            extended: false,
384            index,
385        }
386    }
387    pub fn misc(index: u8) -> Self {
388        Cause {
389            group: CauseGroup::Misc,
390            extended: false,
391            index,
392        }
393    }
394    pub fn radio(index: u8) -> Self {
395        Cause {
396            group: CauseGroup::RadioNetwork,
397            extended: false,
398            index,
399        }
400    }
401    pub fn nas(index: u8) -> Self {
402        Cause {
403            group: CauseGroup::Nas,
404            extended: false,
405            index,
406        }
407    }
408
409    pub fn value_name(&self) -> String {
410        let (root, ext) = self.group.tables();
411        let table = if self.extended { ext } else { root };
412        table
413            .get(self.index as usize)
414            .map(|s| s.to_string())
415            .unwrap_or_else(|| format!("{}{}", if self.extended { "ext-" } else { "" }, self.index))
416    }
417
418    /// `group/value`, the form logs and errors use.
419    pub fn label(&self) -> String {
420        format!("{}/{}", self.group.name(), self.value_name())
421    }
422
423    pub fn to_json(&self) -> Value {
424        json!({"group": self.group.name(), "value": self.value_name()})
425    }
426
427    /// Accepts `{"group": "misc", "value": "unknown-PLMN-or-SNPN"}` or `"misc/unknown-PLMN-or-SNPN"`.
428    /// Names match case-insensitively and with `_` for `-`. A bare value name is accepted when it
429    /// is unambiguous across the groups.
430    pub fn from_json(v: &Value) -> EncodeResult<Cause> {
431        let (group, value) = match v {
432            Value::String(s) => match s.split_once('/') {
433                Some((g, val)) => (Some(g.to_string()), val.to_string()),
434                None => (None, s.clone()),
435            },
436            Value::Object(m) => (
437                m.get("group").and_then(Value::as_str).map(str::to_string),
438                m.get("value")
439                    .and_then(Value::as_str)
440                    .map(str::to_string)
441                    .ok_or_else(|| EncodeError("cause.value must be a string".into()))?,
442            ),
443            _ => return enc_err("cause must be \"group/value\" or {\"group\", \"value\"}"),
444        };
445        let wanted = normalize(&value);
446        let groups: Vec<CauseGroup> = match &group {
447            Some(g) => {
448                let g = normalize(g).replace('-', "");
449                let found = CauseGroup::ALL
450                    .iter()
451                    .copied()
452                    .find(|c| c.name().to_ascii_lowercase() == g);
453                match found {
454                    Some(c) => vec![c],
455                    None => {
456                        return enc_err(format!(
457                        "cause group {g:?} is not one of radioNetwork, transport, nas, protocol, misc"
458                    ))
459                    }
460                }
461            }
462            None => CauseGroup::ALL.to_vec(),
463        };
464        let mut hits = Vec::new();
465        for g in groups {
466            let (root, ext) = g.tables();
467            for (i, name) in root.iter().enumerate() {
468                if normalize(name) == wanted {
469                    hits.push(Cause {
470                        group: g,
471                        extended: false,
472                        index: i as u8,
473                    });
474                }
475            }
476            for (i, name) in ext.iter().enumerate() {
477                if normalize(name) == wanted {
478                    hits.push(Cause {
479                        group: g,
480                        extended: true,
481                        index: i as u8,
482                    });
483                }
484            }
485        }
486        match hits.len() {
487            1 => Ok(hits[0]),
488            0 => enc_err(format!(
489                "cause value {value:?} is not defined{}; e.g. misc/unknown-PLMN-or-SNPN, \
490                 misc/control-processing-overload, nas/normal-release, \
491                 radioNetwork/unspecified, protocol/semantic-error",
492                group.map(|g| format!(" in group {g}")).unwrap_or_default()
493            )),
494            _ => enc_err(format!(
495                "cause value {value:?} exists in more than one group; write it as group/value"
496            )),
497        }
498    }
499
500    fn encode(&self, w: &mut Writer) -> EncodeResult<()> {
501        let gi = CauseGroup::ALL
502            .iter()
503            .position(|g| *g == self.group)
504            .unwrap_or(0) as u64;
505        // Cause ::= CHOICE { radioNetwork, transport, nas, protocol, misc, choice-Extensions }:
506        // six root alternatives and no extension marker.
507        put_choice(w, gi, 6, "Cause")?;
508        let (root, ext) = self.group.tables();
509        let limit = if self.extended { ext.len() } else { root.len() };
510        if self.index as usize >= limit {
511            return enc_err(format!("cause index {} out of range", self.index));
512        }
513        // Every group's ENUMERATED is extensible.
514        w.enumerated(
515            self.index as u64,
516            self.extended,
517            root.len() as u64,
518            true,
519            "Cause",
520        )?;
521        Ok(())
522    }
523
524    fn decode(r: &mut Reader) -> CodecResult<Cause> {
525        let gi = choice(r, 6, "Cause")? as usize;
526        let group = CauseGroup::ALL[gi];
527        let (root, _) = group.tables();
528        let (extended, index) = r.enumerated(root.len() as u64, true, "Cause value")?;
529        Ok(Cause {
530            group,
531            extended,
532            index: index as u8,
533        })
534    }
535}
536
537// ============================================================================
538// PDU framing
539// ============================================================================
540
541#[derive(Debug, Clone, Copy, PartialEq, Eq)]
542pub enum PduKind {
543    Initiating,
544    Successful,
545    Unsuccessful,
546}
547
548impl PduKind {
549    pub fn name(self) -> &'static str {
550        match self {
551            PduKind::Initiating => "initiatingMessage",
552            PduKind::Successful => "successfulOutcome",
553            PduKind::Unsuccessful => "unsuccessfulOutcome",
554        }
555    }
556    fn index(self) -> u64 {
557        match self {
558            PduKind::Initiating => 0,
559            PduKind::Successful => 1,
560            PduKind::Unsuccessful => 2,
561        }
562    }
563}
564
565#[derive(Debug, Clone, Copy, PartialEq, Eq)]
566pub enum Criticality {
567    Reject,
568    Ignore,
569    Notify,
570}
571
572impl Criticality {
573    fn from_index(i: u64) -> Criticality {
574        match i {
575            0 => Criticality::Reject,
576            1 => Criticality::Ignore,
577            _ => Criticality::Notify,
578        }
579    }
580    fn index(self) -> u64 {
581        match self {
582            Criticality::Reject => 0,
583            Criticality::Ignore => 1,
584            Criticality::Notify => 2,
585        }
586    }
587    pub fn name(self) -> &'static str {
588        match self {
589            Criticality::Reject => "reject",
590            Criticality::Ignore => "ignore",
591            Criticality::Notify => "notify",
592        }
593    }
594}
595
596/// One `ProtocolIE-Field`, its value still an encoded open type.
597#[derive(Debug, Clone, PartialEq, Eq)]
598pub struct Ie {
599    pub id: u16,
600    pub criticality: Criticality,
601    pub value: Vec<u8>,
602}
603
604impl Ie {
605    pub fn new(id: u16, criticality: Criticality, value: Vec<u8>) -> Self {
606        Ie {
607            id,
608            criticality,
609            value,
610        }
611    }
612}
613
614/// One NGAP PDU with its IEs framed but not interpreted.
615#[derive(Debug, Clone, PartialEq, Eq)]
616pub struct Pdu {
617    pub kind: PduKind,
618    pub procedure_code: u8,
619    pub criticality: Criticality,
620    pub ies: Vec<Ie>,
621}
622
623impl Pdu {
624    pub fn new(kind: PduKind, procedure_code: u8, criticality: Criticality, ies: Vec<Ie>) -> Self {
625        Pdu {
626            kind,
627            procedure_code,
628            criticality,
629            ies,
630        }
631    }
632
633    pub fn ie(&self, id: u16) -> Option<&Ie> {
634        self.ies.iter().find(|ie| ie.id == id)
635    }
636
637    /// `"NGSetup initiatingMessage"`, or the code when NetGet has no name for it.
638    pub fn label(&self) -> String {
639        match procedure_name(self.procedure_code) {
640            Some(n) => format!("{n} {}", self.kind.name()),
641            None => format!("procedure {} {}", self.procedure_code, self.kind.name()),
642        }
643    }
644
645    /// The ids of IEs NetGet did not interpret for this message, for events.
646    pub fn uninterpreted_ies(&self, interpreted: &[u16]) -> Vec<Value> {
647        self.ies
648            .iter()
649            .filter(|ie| !interpreted.contains(&ie.id))
650            .map(|ie| {
651                json!({
652                    "id": ie.id,
653                    "name": ie_name(ie.id),
654                    "criticality": ie.criticality.name(),
655                    "length": ie.value.len(),
656                })
657            })
658            .collect()
659    }
660
661    /// The mandatory IE `id`, or the abstract-syntax refusal 38.413 section 10.3.5 prescribes.
662    pub fn require(&self, id: u16) -> CodecResult<&Ie> {
663        self.ie(id).ok_or_else(|| {
664            abstract_reject(format!(
665                "{} is missing mandatory IE {} ({})",
666                self.label(),
667                id,
668                ie_name(id).unwrap_or("?")
669            ))
670        })
671    }
672}
673
674/// Decode the outer structure of one NGAP PDU.
675pub fn decode_pdu(bytes: &[u8]) -> CodecResult<Pdu> {
676    if bytes.is_empty() {
677        return Err(AperError("empty PDU".into()).into());
678    }
679    if bytes.len() > MAX_PDU_LEN {
680        return Err(AperError(format!(
681            "PDU of {} octets exceeds {MAX_PDU_LEN}",
682            bytes.len()
683        ))
684        .into());
685    }
686    let mut r = Reader::new(bytes);
687    if r.extension_bit("NGAP-PDU")? {
688        return Err(AperError("NGAP-PDU choice extension".into()).into());
689    }
690    let kind = match r.constrained(0, 2, "NGAP-PDU")? {
691        0 => PduKind::Initiating,
692        1 => PduKind::Successful,
693        _ => PduKind::Unsuccessful,
694    };
695    let procedure_code = r.constrained(0, 255, "procedureCode")? as u8;
696    let criticality = Criticality::from_index(r.constrained(0, 2, "criticality")?);
697    let content = r.open_type("message value")?;
698    if !r.at_end() {
699        return Err(AperError("octets after the PDU".into()).into());
700    }
701
702    let mut c = Reader::new(content);
703    let extended = c.extension_bit("message")?;
704    let count = c.size(0, 65_535, "ProtocolIE-Container")?;
705    if count > MAX_IES {
706        return Err(AperError(format!(
707            "{count} IEs in one message exceeds the bound of {MAX_IES}"
708        ))
709        .into());
710    }
711    let mut ies = Vec::with_capacity(count);
712    for _ in 0..count {
713        let id = c.constrained(0, 65_535, "ProtocolIE-ID")? as u16;
714        let crit = Criticality::from_index(c.constrained(0, 2, "IE criticality")?);
715        let value = c.open_type("IE value")?.to_vec();
716        if ies.iter().any(|ie: &Ie| ie.id == id) {
717            // 38.413 section 10.3.6: an IE appearing more than once is a falsely constructed
718            // message.
719            return Err(ProtocolError {
720                cause: Cause::protocol(5),
721                detail: format!("IE {id} appears more than once"),
722            });
723        }
724        ies.push(Ie::new(id, crit, value));
725    }
726    if extended {
727        c.skip_extension_additions("message extension additions")?;
728    }
729    Ok(Pdu {
730        kind,
731        procedure_code,
732        criticality,
733        ies,
734    })
735}
736
737/// Encode one NGAP PDU.
738pub fn encode_pdu(pdu: &Pdu) -> EncodeResult<Vec<u8>> {
739    if pdu.ies.len() > MAX_IES {
740        return enc_err(format!("{} IEs exceeds {MAX_IES}", pdu.ies.len()));
741    }
742    let mut c = Writer::new();
743    c.bit(false);
744    c.size(pdu.ies.len(), 0, 65_535, "ProtocolIE-Container")?;
745    for ie in &pdu.ies {
746        c.constrained(ie.id as u64, 0, 65_535, "ProtocolIE-ID")?;
747        c.constrained(ie.criticality.index(), 0, 2, "IE criticality")?;
748        c.open_type(&ie.value, "IE value")?;
749    }
750    let content = c.finish();
751
752    let mut w = Writer::new();
753    w.bit(false);
754    w.constrained(pdu.kind.index(), 0, 2, "NGAP-PDU")?;
755    w.constrained(pdu.procedure_code as u64, 0, 255, "procedureCode")?;
756    w.constrained(pdu.criticality.index(), 0, 2, "criticality")?;
757    w.open_type(&content, "message value")?;
758    let out = w.finish();
759    if out.len() > MAX_PDU_LEN {
760        return enc_err(format!("PDU of {} octets exceeds {MAX_PDU_LEN}", out.len()));
761    }
762    Ok(out)
763}
764
765/// The procedure code and kind of an encoded PDU, read without decoding the rest.
766pub fn peek(bytes: &[u8]) -> Option<(PduKind, u8)> {
767    let mut r = Reader::new(bytes);
768    if r.extension_bit("NGAP-PDU").ok()? {
769        return None;
770    }
771    let kind = match r.constrained(0, 2, "NGAP-PDU").ok()? {
772        0 => PduKind::Initiating,
773        1 => PduKind::Successful,
774        _ => PduKind::Unsuccessful,
775    };
776    let code = r.constrained(0, 255, "procedureCode").ok()? as u8;
777    Some((kind, code))
778}
779
780/// True when the PDU is UE-associated signalling — it carries an AMF or RAN UE NGAP ID — which
781/// TS 38.412 section 7 sends on a non-zero SCTP stream. Non-UE-associated signalling (NG Setup,
782/// NG Reset, a UE-less Error Indication) uses stream 0.
783pub fn is_ue_associated(bytes: &[u8]) -> bool {
784    match decode_pdu(bytes) {
785        Ok(p) => p.ies.iter().any(|ie| {
786            matches!(
787                ie.id,
788                IE_AMF_UE_NGAP_ID | IE_RAN_UE_NGAP_ID | IE_UE_NGAP_IDS
789            )
790        }),
791        Err(_) => false,
792    }
793}
794
795// ============================================================================
796// A per-decode budget on SEQUENCE OF elements
797// ============================================================================
798
799/// Counts list elements across one PDU so nested lists cannot multiply into an unbounded event.
800pub struct Budget(usize);
801
802impl Default for Budget {
803    fn default() -> Self {
804        Budget(MAX_LIST_ITEMS)
805    }
806}
807
808impl Budget {
809    fn take(&mut self, n: usize, what: &str) -> CodecResult<()> {
810        if n > self.0 {
811            return Err(AperError(format!(
812                "{what}: list elements in one PDU exceed the bound of {MAX_LIST_ITEMS}"
813            ))
814            .into());
815        }
816        self.0 -= n;
817        Ok(())
818    }
819}
820
821// ============================================================================
822// Primitive IEs
823// ============================================================================
824
825/// A PLMN identity, three TBCD octets (TS 38.413 section 9.3.3.5, TS 24.008 figure 10.5.13).
826#[derive(Debug, Clone, PartialEq, Eq)]
827pub struct Plmn {
828    pub mcc: String,
829    pub mnc: String,
830}
831
832impl Plmn {
833    pub fn decode_octets(b: &[u8]) -> CodecResult<Plmn> {
834        if b.len() != 3 {
835            return Err(semantic("PLMN identity is not three octets"));
836        }
837        let d = [
838            b[0] & 0x0F,
839            b[0] >> 4,
840            b[1] & 0x0F,
841            b[1] >> 4,
842            b[2] & 0x0F,
843            b[2] >> 4,
844        ];
845        let digit = |x: u8| -> CodecResult<char> {
846            if x > 9 {
847                Err(semantic("PLMN identity has a non-decimal digit"))
848            } else {
849                Ok((b'0' + x) as char)
850            }
851        };
852        let mcc: String = [digit(d[0])?, digit(d[1])?, digit(d[2])?].iter().collect();
853        let mnc: String = if d[3] == 0xF {
854            [digit(d[4])?, digit(d[5])?].iter().collect()
855        } else {
856            [digit(d[4])?, digit(d[5])?, digit(d[3])?].iter().collect()
857        };
858        Ok(Plmn { mcc, mnc })
859    }
860
861    pub fn octets(&self) -> [u8; 3] {
862        let m: Vec<u8> = self.mcc.bytes().map(|c| c - b'0').collect();
863        let n: Vec<u8> = self.mnc.bytes().map(|c| c - b'0').collect();
864        let mnc3 = if n.len() == 3 { n[2] } else { 0xF };
865        [m[1] << 4 | m[0], mnc3 << 4 | m[2], n[1] << 4 | n[0]]
866    }
867
868    pub fn to_json(&self) -> Value {
869        json!({"mcc": self.mcc, "mnc": self.mnc})
870    }
871
872    /// `{"mcc": "001", "mnc": "01"}`, or a string `"001-01"` / `"00101"` (five digits is a
873    /// two-digit MNC, six a three-digit one).
874    pub fn from_json(v: &Value) -> EncodeResult<Plmn> {
875        let (mcc, mnc) = match v {
876            Value::Object(m) => (
877                json_digits(m.get("mcc"), "plmn.mcc")?,
878                json_digits(m.get("mnc"), "plmn.mnc")?,
879            ),
880            Value::String(s) => {
881                let s = s.trim();
882                if let Some((a, b)) = s.split_once(['-', '/']) {
883                    (a.to_string(), b.to_string())
884                } else if s.len() == 5 || s.len() == 6 {
885                    (s[..3].to_string(), s[3..].to_string())
886                } else {
887                    return enc_err(format!(
888                        "plmn {s:?} must be \"MCC-MNC\" or {{\"mcc\", \"mnc\"}}"
889                    ));
890                }
891            }
892            _ => return enc_err("plmn must be {\"mcc\": \"001\", \"mnc\": \"01\"}"),
893        };
894        if mcc.len() != 3 || !mcc.bytes().all(|c| c.is_ascii_digit()) {
895            return enc_err(format!("mcc {mcc:?} must be three decimal digits"));
896        }
897        if !(2..=3).contains(&mnc.len()) || !mnc.bytes().all(|c| c.is_ascii_digit()) {
898            return enc_err(format!("mnc {mnc:?} must be two or three decimal digits"));
899        }
900        Ok(Plmn { mcc, mnc })
901    }
902
903    fn decode(r: &mut Reader) -> CodecResult<Plmn> {
904        let b = r.fixed_octets(3, "PLMNIdentity")?;
905        Plmn::decode_octets(&b)
906    }
907
908    fn encode(&self, w: &mut Writer) {
909        w.fixed_octets(&self.octets());
910    }
911}
912
913fn json_digits(v: Option<&Value>, what: &str) -> EncodeResult<String> {
914    match v {
915        Some(Value::String(s)) => Ok(s.trim().to_string()),
916        Some(Value::Number(n)) => enc_err(format!(
917            "{what} must be a string so leading zeros survive (got {n})"
918        )),
919        _ => enc_err(format!("{what} is required")),
920    }
921}
922
923pub fn json_u64(v: &Value, field: &str, max: u64) -> EncodeResult<u64> {
924    let n = match v.get(field) {
925        Some(Value::Number(n)) => n
926            .as_u64()
927            .ok_or_else(|| EncodeError(format!("{field} must be a non-negative integer")))?,
928        Some(Value::String(s)) => {
929            let s = s.trim();
930            if let Some(hex) = s.strip_prefix("0x") {
931                u64::from_str_radix(hex, 16)
932                    .map_err(|_| EncodeError(format!("{field} {s:?} is not a number")))?
933            } else {
934                s.parse()
935                    .map_err(|_| EncodeError(format!("{field} {s:?} is not a number")))?
936            }
937        }
938        None | Some(Value::Null) => return enc_err(format!("{field} is required")),
939        _ => return enc_err(format!("{field} must be a number")),
940    };
941    if n > max {
942        return enc_err(format!("{field} {n} is above its maximum {max}"));
943    }
944    Ok(n)
945}
946
947pub fn json_opt_u64(v: &Value, field: &str, max: u64) -> EncodeResult<Option<u64>> {
948    match v.get(field) {
949        None | Some(Value::Null) => Ok(None),
950        Some(_) => json_u64(v, field, max).map(Some),
951    }
952}
953
954fn opt_extension_container(r: &mut Reader, present: bool, budget: &mut Budget) -> CodecResult<()> {
955    if present {
956        let n = r.size(1, 65_535, "ProtocolExtensionContainer")?;
957        budget.take(n, "ProtocolExtensionContainer")?;
958        for _ in 0..n {
959            r.constrained(0, 65_535, "ProtocolExtensionID")?;
960            r.constrained(0, 2, "extension criticality")?;
961            r.open_type("extension value")?;
962        }
963    }
964    Ok(())
965}
966
967fn finish_sequence(r: &mut Reader, extended: bool) -> CodecResult<()> {
968    if extended {
969        r.skip_extension_additions("extension additions")?;
970    }
971    Ok(())
972}
973
974/// An NGAP CHOICE. 38.413 writes its CHOICEs with a final `choice-Extensions` *root*
975/// alternative (a ProtocolIE-SingleContainer) instead of an extension marker, so there is no
976/// extension bit: the index is a constrained whole number over every alternative including that
977/// one. `alternatives` counts `choice-Extensions`. Selecting it is refused here; callers that can
978/// carry on without it check for [`CHOICE_EXTENSIONS`] first.
979fn choice(r: &mut Reader, alternatives: u64, what: &str) -> CodecResult<u64> {
980    let i = r.constrained(0, alternatives - 1, what)?;
981    if i == alternatives - 1 {
982        return Err(semantic(format!(
983            "{what} selects its choice-Extensions alternative, which NetGet does not decode"
984        )));
985    }
986    Ok(i)
987}
988
989/// Marker for the `choice-Extensions` index of a CHOICE with `alternatives` alternatives.
990pub const CHOICE_EXTENSIONS: &str = "choice-Extensions";
991
992fn put_choice(w: &mut Writer, index: u64, alternatives: u64, what: &str) -> EncodeResult<()> {
993    Ok(w.constrained(index, 0, alternatives - 1, what)?)
994}
995
996/// Decode an IE value with `f`, requiring it to consume the whole open type.
997fn decode_ie<T>(ie: &Ie, f: impl FnOnce(&mut Reader) -> CodecResult<T>) -> CodecResult<T> {
998    let mut r = Reader::new(&ie.value);
999    let v = f(&mut r)?;
1000    if !r.at_end() {
1001        return Err(AperError(format!(
1002            "IE {} ({}) has octets after its value",
1003            ie.id,
1004            ie_name(ie.id).unwrap_or("?")
1005        ))
1006        .into());
1007    }
1008    Ok(v)
1009}
1010
1011fn encode_ie(
1012    id: u16,
1013    crit: Criticality,
1014    f: impl FnOnce(&mut Writer) -> EncodeResult<()>,
1015) -> EncodeResult<Ie> {
1016    let mut w = Writer::new();
1017    f(&mut w)?;
1018    Ok(Ie::new(id, crit, w.finish()))
1019}
1020
1021pub fn decode_amf_ue_ngap_id(ie: &Ie) -> CodecResult<u64> {
1022    decode_ie(ie, |r| {
1023        Ok(r.constrained(0, MAX_AMF_UE_NGAP_ID, "AMF-UE-NGAP-ID")?)
1024    })
1025}
1026
1027pub fn decode_ran_ue_ngap_id(ie: &Ie) -> CodecResult<u64> {
1028    decode_ie(ie, |r| {
1029        Ok(r.constrained(0, MAX_RAN_UE_NGAP_ID, "RAN-UE-NGAP-ID")?)
1030    })
1031}
1032
1033pub fn amf_ue_ngap_id_ie(id: u64, crit: Criticality) -> EncodeResult<Ie> {
1034    encode_ie(IE_AMF_UE_NGAP_ID, crit, |w| {
1035        Ok(w.constrained(id, 0, MAX_AMF_UE_NGAP_ID, "AMF-UE-NGAP-ID")?)
1036    })
1037}
1038
1039pub fn ran_ue_ngap_id_ie(id: u64, crit: Criticality) -> EncodeResult<Ie> {
1040    encode_ie(IE_RAN_UE_NGAP_ID, crit, |w| {
1041        Ok(w.constrained(id, 0, MAX_RAN_UE_NGAP_ID, "RAN-UE-NGAP-ID")?)
1042    })
1043}
1044
1045pub fn decode_nas_pdu(ie: &Ie) -> CodecResult<Vec<u8>> {
1046    decode_ie(ie, |r| {
1047        Ok(r.octet_string(MAX_NAS_PDU_LEN, "NAS-PDU")?.to_vec())
1048    })
1049}
1050
1051pub fn nas_pdu_ie(nas: &[u8]) -> EncodeResult<Ie> {
1052    if nas.is_empty() || nas.len() > MAX_NAS_PDU_LEN {
1053        return enc_err(format!(
1054            "NAS-PDU of {} octets is outside 1..{MAX_NAS_PDU_LEN}",
1055            nas.len()
1056        ));
1057    }
1058    encode_ie(IE_NAS_PDU, Criticality::Reject, |w| {
1059        Ok(w.octet_string(nas, "NAS-PDU")?)
1060    })
1061}
1062
1063pub fn decode_cause(ie: &Ie) -> CodecResult<Cause> {
1064    decode_ie(ie, Cause::decode)
1065}
1066
1067pub fn cause_ie(cause: &Cause, crit: Criticality) -> EncodeResult<Ie> {
1068    encode_ie(IE_CAUSE, crit, |w| cause.encode(w))
1069}
1070
1071const TIME_TO_WAIT: &[&str] = &["v1s", "v2s", "v5s", "v10s", "v20s", "v60s"];
1072const PAGING_DRX: &[&str] = &["v32", "v64", "v128", "v256"];
1073const RRC_CAUSE_ROOT: &[&str] = &[
1074    "emergency",
1075    "highPriorityAccess",
1076    "mt-Access",
1077    "mo-Signalling",
1078    "mo-Data",
1079    "mo-VoiceCall",
1080    "mo-VideoCall",
1081    "mo-SMS",
1082    "mps-PriorityAccess",
1083    "mcs-PriorityAccess",
1084];
1085const RRC_CAUSE_EXT: &[&str] = &["notAvailable", "mo-ExceptionData"];
1086
1087fn enum_index(table: &[&str], v: &str, what: &str) -> EncodeResult<u64> {
1088    let want = normalize(v);
1089    table
1090        .iter()
1091        .position(|n| normalize(n) == want)
1092        .map(|i| i as u64)
1093        .ok_or_else(|| EncodeError(format!("{what} {v:?} is not one of {}", table.join(", "))))
1094}
1095
1096pub fn time_to_wait_ie(v: &str) -> EncodeResult<Ie> {
1097    // Seconds are accepted as well as the ASN.1 names: "10" or "10s" mean v10s.
1098    let name = if v.starts_with('v') {
1099        v.to_string()
1100    } else {
1101        format!("v{}s", v.trim_end_matches('s'))
1102    };
1103    let i = enum_index(TIME_TO_WAIT, &name, "time_to_wait")?;
1104    encode_ie(IE_TIME_TO_WAIT, Criticality::Ignore, |w| {
1105        Ok(w.enumerated(i, false, 6, true, "TimeToWait")?)
1106    })
1107}
1108
1109pub fn decode_time_to_wait(ie: &Ie) -> CodecResult<String> {
1110    decode_ie(ie, |r| {
1111        let (ext, i) = r.enumerated(6, true, "TimeToWait")?;
1112        Ok(if ext {
1113            format!("ext-{i}")
1114        } else {
1115            TIME_TO_WAIT[i as usize].to_string()
1116        })
1117    })
1118}
1119
1120fn decode_paging_drx(ie: &Ie) -> CodecResult<String> {
1121    decode_ie(ie, |r| {
1122        let (ext, i) = r.enumerated(4, true, "PagingDRX")?;
1123        Ok(if ext {
1124            format!("ext-{i}")
1125        } else {
1126            PAGING_DRX[i as usize].to_string()
1127        })
1128    })
1129}
1130
1131fn decode_rrc_cause(ie: &Ie) -> CodecResult<String> {
1132    decode_ie(ie, |r| {
1133        let (ext, i) = r.enumerated(10, true, "RRCEstablishmentCause")?;
1134        let table = if ext { RRC_CAUSE_EXT } else { RRC_CAUSE_ROOT };
1135        Ok(table
1136            .get(i as usize)
1137            .map(|s| s.to_string())
1138            .unwrap_or_else(|| format!("ext-{i}")))
1139    })
1140}
1141
1142fn rrc_cause_ie(v: &str) -> EncodeResult<Ie> {
1143    let (ext, i) = match enum_index(RRC_CAUSE_ROOT, v, "rrc_establishment_cause") {
1144        Ok(i) => (false, i),
1145        Err(_) => (
1146            true,
1147            enum_index(RRC_CAUSE_EXT, v, "rrc_establishment_cause").map_err(|_| {
1148                EncodeError(format!(
1149                    "rrc_establishment_cause {v:?} is not one of {}, {}",
1150                    RRC_CAUSE_ROOT.join(", "),
1151                    RRC_CAUSE_EXT.join(", ")
1152                ))
1153            })?,
1154        ),
1155    };
1156    encode_ie(IE_RRC_ESTABLISHMENT_CAUSE, Criticality::Ignore, |w| {
1157        Ok(w.enumerated(i, ext, 10, true, "RRCEstablishmentCause")?)
1158    })
1159}
1160
1161fn tac_octets(tac: u64) -> [u8; 3] {
1162    [(tac >> 16) as u8, (tac >> 8) as u8, tac as u8]
1163}
1164
1165fn decode_tac(r: &mut Reader) -> CodecResult<u64> {
1166    let b = r.fixed_octets(3, "TAC")?;
1167    Ok(((b[0] as u64) << 16) | ((b[1] as u64) << 8) | b[2] as u64)
1168}
1169
1170// ============================================================================
1171// S-NSSAI and slice lists
1172// ============================================================================
1173
1174fn decode_snssai(r: &mut Reader, budget: &mut Budget) -> CodecResult<Value> {
1175    let ext = r.extension_bit("S-NSSAI")?;
1176    let has_sd = r.bit("S-NSSAI")?;
1177    let has_ext = r.bit("S-NSSAI")?;
1178    let sst = r.fixed_octets(1, "SST")?[0];
1179    let mut v = json!({"sst": sst});
1180    if has_sd {
1181        let sd = r.fixed_octets(3, "SD")?;
1182        v["sd"] = json!(hex::encode(sd));
1183    }
1184    opt_extension_container(r, has_ext, budget)?;
1185    finish_sequence(r, ext)?;
1186    Ok(v)
1187}
1188
1189/// `{"sst": 1, "sd": "010203"}`; `sd` may also be a number.
1190fn encode_snssai(w: &mut Writer, v: &Value) -> EncodeResult<()> {
1191    let sst = json_u64(v, "sst", 255)?;
1192    let sd = match v.get("sd") {
1193        None | Some(Value::Null) => None,
1194        Some(Value::String(s)) => {
1195            let s = s.trim().trim_start_matches("0x");
1196            if s.len() != 6 {
1197                return enc_err(format!("sd {s:?} must be six hex digits"));
1198            }
1199            Some(
1200                u32::from_str_radix(s, 16)
1201                    .map_err(|_| EncodeError(format!("sd {s:?} is not hex")))?,
1202            )
1203        }
1204        Some(Value::Number(_)) => Some(json_u64(v, "sd", 0xFF_FFFF)? as u32),
1205        Some(_) => return enc_err("sd must be six hex digits"),
1206    };
1207    w.bit(false);
1208    w.bit(sd.is_some());
1209    w.bit(false);
1210    w.fixed_octets(&[sst as u8]);
1211    if let Some(sd) = sd {
1212        w.fixed_octets(&[(sd >> 16) as u8, (sd >> 8) as u8, sd as u8]);
1213    }
1214    Ok(())
1215}
1216
1217fn decode_slice_list(r: &mut Reader, budget: &mut Budget) -> CodecResult<Vec<Value>> {
1218    let n = r.size(1, 1024, "SliceSupportList")?;
1219    budget.take(n, "SliceSupportList")?;
1220    let mut out = Vec::with_capacity(n);
1221    for _ in 0..n {
1222        let ext = r.extension_bit("SliceSupportItem")?;
1223        let has_ext = r.bit("SliceSupportItem")?;
1224        out.push(decode_snssai(r, budget)?);
1225        opt_extension_container(r, has_ext, budget)?;
1226        finish_sequence(r, ext)?;
1227    }
1228    Ok(out)
1229}
1230
1231fn json_list<'a>(v: &'a Value, field: &str) -> EncodeResult<&'a Vec<Value>> {
1232    v.get(field)
1233        .and_then(Value::as_array)
1234        .ok_or_else(|| EncodeError(format!("{field} must be a list")))
1235}
1236
1237fn encode_slice_list(w: &mut Writer, slices: &[Value]) -> EncodeResult<()> {
1238    w.size(slices.len(), 1, 1024, "slices")?;
1239    for s in slices {
1240        w.bit(false);
1241        w.bit(false);
1242        encode_snssai(w, s)?;
1243    }
1244    Ok(())
1245}
1246
1247// ============================================================================
1248// NG Setup IEs
1249// ============================================================================
1250
1251/// GlobalRANNodeID — gNB form decoded; ng-eNB and N3IWF forms named only.
1252pub fn decode_global_ran_node_id(ie: &Ie, budget: &mut Budget) -> CodecResult<Value> {
1253    decode_ie(ie, |r| match choice(r, 4, "GlobalRANNodeID")? {
1254        0 => {
1255            let ext = r.extension_bit("GlobalGNB-ID")?;
1256            let has_ext = r.bit("GlobalGNB-ID")?;
1257            let plmn = Plmn::decode(r)?;
1258            choice(r, 2, "GNB-ID")?;
1259            let (id, bits) = r.varying_bits(22, 32, "gNB-ID")?;
1260            opt_extension_container(r, has_ext, budget)?;
1261            finish_sequence(r, ext)?;
1262            Ok(json!({"type": "gnb", "plmn": plmn.to_json(), "gnb_id": id, "gnb_id_bits": bits}))
1263        }
1264        1 => Err(semantic(
1265            "an ng-eNB GlobalRANNodeID; NetGet's AMF serves gNBs",
1266        )),
1267        _ => Err(semantic(
1268            "an N3IWF GlobalRANNodeID; NetGet's AMF serves gNBs",
1269        )),
1270    })
1271}
1272
1273pub fn global_gnb_id_ie(plmn: &Plmn, gnb_id: u64, bits: u32) -> EncodeResult<Ie> {
1274    encode_ie(IE_GLOBAL_RAN_NODE_ID, Criticality::Reject, |w| {
1275        put_choice(w, 0, 4, "GlobalRANNodeID")?;
1276        w.bit(false);
1277        w.bit(false);
1278        plmn.encode(w);
1279        put_choice(w, 0, 2, "GNB-ID")?;
1280        w.varying_bits(gnb_id, bits, 22, 32, "gnb_id")?;
1281        Ok(())
1282    })
1283}
1284
1285pub fn decode_name(ie: &Ie) -> CodecResult<String> {
1286    decode_ie(ie, |r| Ok(r.printable_string(1, 150, "name")?))
1287}
1288
1289pub fn name_ie(id: u16, crit: Criticality, name: &str) -> EncodeResult<Ie> {
1290    encode_ie(id, crit, |w| Ok(w.printable_string(name, 1, 150, "name")?))
1291}
1292
1293pub fn decode_supported_ta_list(ie: &Ie, budget: &mut Budget) -> CodecResult<Vec<Value>> {
1294    decode_ie(ie, |r| {
1295        let n = r.size(1, 256, "SupportedTAList")?;
1296        budget.take(n, "SupportedTAList")?;
1297        let mut out = Vec::with_capacity(n);
1298        for _ in 0..n {
1299            let ext = r.extension_bit("SupportedTAItem")?;
1300            let has_ext = r.bit("SupportedTAItem")?;
1301            let tac = decode_tac(r)?;
1302            let m = r.size(1, 12, "BroadcastPLMNList")?;
1303            budget.take(m, "BroadcastPLMNList")?;
1304            let mut plmns = Vec::with_capacity(m);
1305            for _ in 0..m {
1306                let bext = r.extension_bit("BroadcastPLMNItem")?;
1307                let bhas = r.bit("BroadcastPLMNItem")?;
1308                let plmn = Plmn::decode(r)?;
1309                let slices = decode_slice_list(r, budget)?;
1310                opt_extension_container(r, bhas, budget)?;
1311                finish_sequence(r, bext)?;
1312                plmns.push(json!({"plmn": plmn.to_json(), "slices": slices}));
1313            }
1314            opt_extension_container(r, has_ext, budget)?;
1315            finish_sequence(r, ext)?;
1316            out.push(json!({"tac": tac, "broadcast_plmns": plmns}));
1317        }
1318        Ok(out)
1319    })
1320}
1321
1322/// `[{"tac": 1, "broadcast_plmns": [{"plmn": {...}, "slices": [{"sst": 1}]}]}]`
1323pub fn supported_ta_list_ie(list: &[Value]) -> EncodeResult<Ie> {
1324    encode_ie(IE_SUPPORTED_TA_LIST, Criticality::Reject, |w| {
1325        w.size(list.len(), 1, 256, "supported_ta_list")?;
1326        for ta in list {
1327            let tac = json_u64(ta, "tac", 0xFF_FFFF)?;
1328            let plmns = json_list(ta, "broadcast_plmns")?;
1329            w.bit(false);
1330            w.bit(false);
1331            w.fixed_octets(&tac_octets(tac));
1332            w.size(plmns.len(), 1, 12, "broadcast_plmns")?;
1333            for p in plmns {
1334                let plmn = Plmn::from_json(p.get("plmn").unwrap_or(&Value::Null))?;
1335                w.bit(false);
1336                w.bit(false);
1337                plmn.encode(w);
1338                encode_slice_list(w, json_list(p, "slices")?)?;
1339            }
1340        }
1341        Ok(())
1342    })
1343}
1344
1345pub fn paging_drx_ie(v: &str) -> EncodeResult<Ie> {
1346    let i = enum_index(PAGING_DRX, v, "default_paging_drx")?;
1347    encode_ie(IE_DEFAULT_PAGING_DRX, Criticality::Ignore, |w| {
1348        Ok(w.enumerated(i, false, 4, true, "PagingDRX")?)
1349    })
1350}
1351
1352pub fn decode_served_guami_list(ie: &Ie, budget: &mut Budget) -> CodecResult<Vec<Value>> {
1353    decode_ie(ie, |r| {
1354        let n = r.size(1, 256, "ServedGUAMIList")?;
1355        budget.take(n, "ServedGUAMIList")?;
1356        let mut out = Vec::with_capacity(n);
1357        for _ in 0..n {
1358            let ext = r.extension_bit("ServedGUAMIItem")?;
1359            let has_backup = r.bit("ServedGUAMIItem")?;
1360            let has_ext = r.bit("ServedGUAMIItem")?;
1361            let mut g = decode_guami(r, budget)?;
1362            if has_backup {
1363                g["backup_amf_name"] = json!(r.printable_string(1, 150, "backupAMFName")?);
1364            }
1365            opt_extension_container(r, has_ext, budget)?;
1366            finish_sequence(r, ext)?;
1367            out.push(g);
1368        }
1369        Ok(out)
1370    })
1371}
1372
1373fn decode_guami(r: &mut Reader, budget: &mut Budget) -> CodecResult<Value> {
1374    let ext = r.extension_bit("GUAMI")?;
1375    let has_ext = r.bit("GUAMI")?;
1376    let plmn = Plmn::decode(r)?;
1377    let region = r.fixed_bits(8, "AMFRegionID")?;
1378    let set = r.fixed_bits(10, "AMFSetID")?;
1379    let pointer = r.fixed_bits(6, "AMFPointer")?;
1380    opt_extension_container(r, has_ext, budget)?;
1381    finish_sequence(r, ext)?;
1382    Ok(
1383        json!({"plmn": plmn.to_json(), "amf_region_id": region, "amf_set_id": set, "amf_pointer": pointer}),
1384    )
1385}
1386
1387/// `[{"plmn": {...}, "amf_region_id": 2, "amf_set_id": 1, "amf_pointer": 0}]`
1388pub fn served_guami_list_ie(list: &[Value]) -> EncodeResult<Ie> {
1389    encode_ie(IE_SERVED_GUAMI_LIST, Criticality::Reject, |w| {
1390        w.size(list.len(), 1, 256, "served_guami")?;
1391        for g in list {
1392            let plmn = Plmn::from_json(g.get("plmn").unwrap_or(&Value::Null))?;
1393            let region = json_u64(g, "amf_region_id", 0xFF)?;
1394            let set = json_u64(g, "amf_set_id", 0x3FF)?;
1395            let pointer = json_u64(g, "amf_pointer", 0x3F)?;
1396            let backup = g.get("backup_amf_name").and_then(Value::as_str);
1397            w.bit(false);
1398            w.bit(backup.is_some());
1399            w.bit(false);
1400            w.bit(false);
1401            w.bit(false);
1402            plmn.encode(w);
1403            w.fixed_bits(region, 8);
1404            w.fixed_bits(set, 10);
1405            w.fixed_bits(pointer, 6);
1406            if let Some(b) = backup {
1407                w.printable_string(b, 1, 150, "backup_amf_name")?;
1408            }
1409        }
1410        Ok(())
1411    })
1412}
1413
1414pub fn relative_amf_capacity_ie(c: u64) -> EncodeResult<Ie> {
1415    encode_ie(IE_RELATIVE_AMF_CAPACITY, Criticality::Ignore, |w| {
1416        Ok(w.constrained(c, 0, 255, "relative_amf_capacity")?)
1417    })
1418}
1419
1420pub fn decode_relative_amf_capacity(ie: &Ie) -> CodecResult<u64> {
1421    decode_ie(ie, |r| Ok(r.constrained(0, 255, "RelativeAMFCapacity")?))
1422}
1423
1424pub fn decode_plmn_support_list(ie: &Ie, budget: &mut Budget) -> CodecResult<Vec<Value>> {
1425    decode_ie(ie, |r| {
1426        let n = r.size(1, 12, "PLMNSupportList")?;
1427        budget.take(n, "PLMNSupportList")?;
1428        let mut out = Vec::with_capacity(n);
1429        for _ in 0..n {
1430            let ext = r.extension_bit("PLMNSupportItem")?;
1431            let has_ext = r.bit("PLMNSupportItem")?;
1432            let plmn = Plmn::decode(r)?;
1433            let slices = decode_slice_list(r, budget)?;
1434            opt_extension_container(r, has_ext, budget)?;
1435            finish_sequence(r, ext)?;
1436            out.push(json!({"plmn": plmn.to_json(), "slices": slices}));
1437        }
1438        Ok(out)
1439    })
1440}
1441
1442/// `[{"plmn": {...}, "slices": [{"sst": 1, "sd": "010203"}]}]`
1443pub fn plmn_support_list_ie(list: &[Value]) -> EncodeResult<Ie> {
1444    encode_ie(IE_PLMN_SUPPORT_LIST, Criticality::Reject, |w| {
1445        w.size(list.len(), 1, 12, "plmn_support")?;
1446        for p in list {
1447            let plmn = Plmn::from_json(p.get("plmn").unwrap_or(&Value::Null))?;
1448            w.bit(false);
1449            w.bit(false);
1450            plmn.encode(w);
1451            encode_slice_list(w, json_list(p, "slices")?)?;
1452        }
1453        Ok(())
1454    })
1455}
1456
1457// ============================================================================
1458// UE-associated IEs
1459// ============================================================================
1460
1461/// UserLocationInformation: NR decoded fully, E-UTRA decoded, N3IWF named.
1462pub fn decode_user_location(ie: &Ie, budget: &mut Budget) -> CodecResult<Value> {
1463    decode_ie(ie, |r| {
1464        // EUTRA, NR, N3IWF, choice-Extensions (TNGF / TWIF / W-AGF).
1465        match r.constrained(0, 3, "UserLocationInformation")? {
1466            choice @ (0 | 1) => {
1467                let ext = r.extension_bit("UserLocationInformation")?;
1468                let has_ts = r.bit("UserLocationInformation")?;
1469                let has_ext = r.bit("UserLocationInformation")?;
1470                // E-UTRA-CGI / NR-CGI
1471                let cext = r.extension_bit("CGI")?;
1472                let chas = r.bit("CGI")?;
1473                let cplmn = Plmn::decode(r)?;
1474                let cell = if choice == 1 {
1475                    r.fixed_bits(36, "NRCellIdentity")?
1476                } else {
1477                    r.fixed_bits(28, "EUTRACellIdentity")?
1478                };
1479                opt_extension_container(r, chas, budget)?;
1480                finish_sequence(r, cext)?;
1481                // TAI
1482                let text = r.extension_bit("TAI")?;
1483                let thas = r.bit("TAI")?;
1484                let tplmn = Plmn::decode(r)?;
1485                let tac = decode_tac(r)?;
1486                opt_extension_container(r, thas, budget)?;
1487                finish_sequence(r, text)?;
1488                let mut v = json!({
1489                    "type": if choice == 1 { "nr" } else { "eutra" },
1490                    "cgi": {"plmn": cplmn.to_json(), "cell_id": cell},
1491                    "tai": {"plmn": tplmn.to_json(), "tac": tac},
1492                });
1493                if has_ts {
1494                    let ts = r.fixed_octets(4, "TimeStamp")?;
1495                    v["time_stamp"] = json!(u32::from_be_bytes([ts[0], ts[1], ts[2], ts[3]]));
1496                }
1497                opt_extension_container(r, has_ext, budget)?;
1498                finish_sequence(r, ext)?;
1499                Ok(v)
1500            }
1501            other => {
1502                // N3IWF or a choice extension: not interpreted. The open type already bounded it
1503                // and this value is the whole IE, so the rest is consumed unread.
1504                while !r.at_end() {
1505                    r.bits(8, "UserLocationInformation")?;
1506                }
1507                Ok(json!({"type": if other == 2 { "n3iwf" } else { CHOICE_EXTENSIONS }}))
1508            }
1509        }
1510    })
1511}
1512
1513/// NR user location: `{"cgi": {"plmn", "cell_id"}, "tai": {"plmn", "tac"}}`.
1514pub fn user_location_nr_ie(v: &Value, crit: Criticality) -> EncodeResult<Ie> {
1515    let cgi = v
1516        .get("cgi")
1517        .ok_or_else(|| EncodeError("user_location.cgi is required".into()))?;
1518    let tai = v
1519        .get("tai")
1520        .ok_or_else(|| EncodeError("user_location.tai is required".into()))?;
1521    let cplmn = Plmn::from_json(cgi.get("plmn").unwrap_or(&Value::Null))?;
1522    let cell = json_u64(cgi, "cell_id", (1 << 36) - 1)?;
1523    let tplmn = Plmn::from_json(tai.get("plmn").unwrap_or(&Value::Null))?;
1524    let tac = json_u64(tai, "tac", 0xFF_FFFF)?;
1525    encode_ie(IE_USER_LOCATION_INFORMATION, crit, |w| {
1526        put_choice(w, 1, 4, "UserLocationInformation")?;
1527        w.bit(false);
1528        w.bit(false);
1529        w.bit(false);
1530        w.bit(false);
1531        w.bit(false);
1532        cplmn.encode(w);
1533        w.fixed_bits(cell, 36);
1534        w.bit(false);
1535        w.bit(false);
1536        tplmn.encode(w);
1537        w.fixed_octets(&tac_octets(tac));
1538        Ok(())
1539    })
1540}
1541
1542fn decode_five_g_s_tmsi(ie: &Ie, budget: &mut Budget) -> CodecResult<Value> {
1543    decode_ie(ie, |r| {
1544        let ext = r.extension_bit("FiveG-S-TMSI")?;
1545        let has_ext = r.bit("FiveG-S-TMSI")?;
1546        let set = r.fixed_bits(10, "AMFSetID")?;
1547        let pointer = r.fixed_bits(6, "AMFPointer")?;
1548        let t = r.fixed_octets(4, "FiveG-TMSI")?;
1549        opt_extension_container(r, has_ext, budget)?;
1550        finish_sequence(r, ext)?;
1551        Ok(json!({"amf_set_id": set, "amf_pointer": pointer,
1552                  "tmsi": u32::from_be_bytes([t[0], t[1], t[2], t[3]])}))
1553    })
1554}
1555
1556/// UE-NGAP-IDs: `(amf_ue_ngap_id, Some(ran_ue_ngap_id))` for the pair form.
1557pub fn decode_ue_ngap_ids(ie: &Ie, budget: &mut Budget) -> CodecResult<(u64, Option<u64>)> {
1558    decode_ie(ie, |r| {
1559        if choice(r, 3, "UE-NGAP-IDs")? == 0 {
1560            let ext = r.extension_bit("UE-NGAP-ID-pair")?;
1561            let has_ext = r.bit("UE-NGAP-ID-pair")?;
1562            let amf = r.constrained(0, MAX_AMF_UE_NGAP_ID, "AMF-UE-NGAP-ID")?;
1563            let ran = r.constrained(0, MAX_RAN_UE_NGAP_ID, "RAN-UE-NGAP-ID")?;
1564            opt_extension_container(r, has_ext, budget)?;
1565            finish_sequence(r, ext)?;
1566            Ok((amf, Some(ran)))
1567        } else {
1568            Ok((
1569                r.constrained(0, MAX_AMF_UE_NGAP_ID, "AMF-UE-NGAP-ID")?,
1570                None,
1571            ))
1572        }
1573    })
1574}
1575
1576pub fn ue_ngap_ids_ie(amf: u64, ran: Option<u64>) -> EncodeResult<Ie> {
1577    encode_ie(IE_UE_NGAP_IDS, Criticality::Reject, |w| {
1578        match ran {
1579            Some(ran) => {
1580                put_choice(w, 0, 3, "UE-NGAP-IDs")?;
1581                w.bit(false);
1582                w.bit(false);
1583                w.constrained(amf, 0, MAX_AMF_UE_NGAP_ID, "AMF-UE-NGAP-ID")?;
1584                w.constrained(ran, 0, MAX_RAN_UE_NGAP_ID, "RAN-UE-NGAP-ID")?;
1585            }
1586            None => {
1587                put_choice(w, 1, 3, "UE-NGAP-IDs")?;
1588                w.constrained(amf, 0, MAX_AMF_UE_NGAP_ID, "AMF-UE-NGAP-ID")?;
1589            }
1590        }
1591        Ok(())
1592    })
1593}
1594
1595/// One `UE-associatedLogicalNG-connectionItem`: either id may be absent.
1596pub type ConnectionItem = (Option<u64>, Option<u64>);
1597
1598fn decode_connection_list(r: &mut Reader, budget: &mut Budget) -> CodecResult<Vec<ConnectionItem>> {
1599    let n = r.size(1, 65_536, "UE-associatedLogicalNG-connectionList")?;
1600    budget.take(n, "UE-associatedLogicalNG-connectionList")?;
1601    let mut out = Vec::with_capacity(n);
1602    for _ in 0..n {
1603        let ext = r.extension_bit("UE-associatedLogicalNG-connectionItem")?;
1604        let has_amf = r.bit("item")?;
1605        let has_ran = r.bit("item")?;
1606        let has_ext = r.bit("item")?;
1607        let amf = if has_amf {
1608            Some(r.constrained(0, MAX_AMF_UE_NGAP_ID, "AMF-UE-NGAP-ID")?)
1609        } else {
1610            None
1611        };
1612        let ran = if has_ran {
1613            Some(r.constrained(0, MAX_RAN_UE_NGAP_ID, "RAN-UE-NGAP-ID")?)
1614        } else {
1615            None
1616        };
1617        opt_extension_container(r, has_ext, budget)?;
1618        finish_sequence(r, ext)?;
1619        out.push((amf, ran));
1620    }
1621    Ok(out)
1622}
1623
1624fn encode_connection_list(w: &mut Writer, items: &[ConnectionItem]) -> EncodeResult<()> {
1625    w.size(items.len(), 1, 65_536, "connection list")?;
1626    for (amf, ran) in items {
1627        w.bit(false);
1628        w.bit(amf.is_some());
1629        w.bit(ran.is_some());
1630        w.bit(false);
1631        if let Some(a) = amf {
1632            w.constrained(*a, 0, MAX_AMF_UE_NGAP_ID, "AMF-UE-NGAP-ID")?;
1633        }
1634        if let Some(r) = ran {
1635            w.constrained(*r, 0, MAX_RAN_UE_NGAP_ID, "RAN-UE-NGAP-ID")?;
1636        }
1637    }
1638    Ok(())
1639}
1640
1641/// ResetType: `None` is reset-all, `Some(items)` the partial form.
1642pub fn decode_reset_type(ie: &Ie, budget: &mut Budget) -> CodecResult<Option<Vec<ConnectionItem>>> {
1643    decode_ie(ie, |r| {
1644        if choice(r, 3, "ResetType")? == 0 {
1645            r.enumerated(1, true, "ResetAll")?;
1646            Ok(None)
1647        } else {
1648            Ok(Some(decode_connection_list(r, budget)?))
1649        }
1650    })
1651}
1652
1653pub fn reset_type_ie(partial: Option<&[ConnectionItem]>) -> EncodeResult<Ie> {
1654    encode_ie(IE_RESET_TYPE, Criticality::Reject, |w| {
1655        match partial {
1656            None => {
1657                put_choice(w, 0, 3, "ResetType")?;
1658                w.enumerated(0, false, 1, true, "ResetAll")?;
1659            }
1660            Some(items) => {
1661                put_choice(w, 1, 3, "ResetType")?;
1662                encode_connection_list(w, items)?;
1663            }
1664        }
1665        Ok(())
1666    })
1667}
1668
1669pub fn decode_connection_list_ie(ie: &Ie, budget: &mut Budget) -> CodecResult<Vec<ConnectionItem>> {
1670    decode_ie(ie, |r| decode_connection_list(r, budget))
1671}
1672
1673pub fn connection_list_ie(items: &[ConnectionItem]) -> EncodeResult<Ie> {
1674    encode_ie(
1675        IE_UE_ASSOCIATED_LOGICAL_NG_CONNECTION_LIST,
1676        Criticality::Ignore,
1677        |w| encode_connection_list(w, items),
1678    )
1679}
1680
1681// ============================================================================
1682// Decoded messages
1683// ============================================================================
1684
1685/// NG Setup Request, decoded for the model (TS 38.413 section 9.2.6.1).
1686pub fn parse_ng_setup_request(pdu: &Pdu) -> CodecResult<Value> {
1687    let mut budget = Budget::default();
1688    let node = decode_global_ran_node_id(pdu.require(IE_GLOBAL_RAN_NODE_ID)?, &mut budget)?;
1689    let tas = decode_supported_ta_list(pdu.require(IE_SUPPORTED_TA_LIST)?, &mut budget)?;
1690    let drx = decode_paging_drx(pdu.require(IE_DEFAULT_PAGING_DRX)?)?;
1691    let name = match pdu.ie(IE_RAN_NODE_NAME) {
1692        Some(ie) => Some(decode_name(ie)?),
1693        None => None,
1694    };
1695    Ok(json!({
1696        "global_ran_node_id": node,
1697        "ran_node_name": name,
1698        "supported_ta_list": tas,
1699        "default_paging_drx": drx,
1700        "uninterpreted_ies": pdu.uninterpreted_ies(&[
1701            IE_GLOBAL_RAN_NODE_ID, IE_SUPPORTED_TA_LIST, IE_DEFAULT_PAGING_DRX, IE_RAN_NODE_NAME
1702        ]),
1703    }))
1704}
1705
1706/// NG Setup Response, decoded for the gNB's model (TS 38.413 section 9.2.6.2).
1707pub fn parse_ng_setup_response(pdu: &Pdu) -> CodecResult<Value> {
1708    let mut budget = Budget::default();
1709    let name = decode_name(pdu.require(IE_AMF_NAME)?)?;
1710    let guamis = decode_served_guami_list(pdu.require(IE_SERVED_GUAMI_LIST)?, &mut budget)?;
1711    let capacity = decode_relative_amf_capacity(pdu.require(IE_RELATIVE_AMF_CAPACITY)?)?;
1712    let plmns = decode_plmn_support_list(pdu.require(IE_PLMN_SUPPORT_LIST)?, &mut budget)?;
1713    Ok(json!({
1714        "amf_name": name,
1715        "served_guami": guamis,
1716        "relative_amf_capacity": capacity,
1717        "plmn_support": plmns,
1718        "uninterpreted_ies": pdu.uninterpreted_ies(&[
1719            IE_AMF_NAME, IE_SERVED_GUAMI_LIST, IE_RELATIVE_AMF_CAPACITY, IE_PLMN_SUPPORT_LIST
1720        ]),
1721    }))
1722}
1723
1724/// NG Setup Failure, or any message carrying an optional Cause and Time to Wait.
1725pub fn parse_cause_message(pdu: &Pdu) -> CodecResult<Value> {
1726    let mut v = Map::new();
1727    if let Some(ie) = pdu.ie(IE_CAUSE) {
1728        v.insert("cause".into(), decode_cause(ie)?.to_json());
1729    }
1730    if let Some(ie) = pdu.ie(IE_TIME_TO_WAIT) {
1731        v.insert("time_to_wait".into(), json!(decode_time_to_wait(ie)?));
1732    }
1733    if let Some(ie) = pdu.ie(IE_AMF_UE_NGAP_ID) {
1734        v.insert("amf_ue_ngap_id".into(), json!(decode_amf_ue_ngap_id(ie)?));
1735    }
1736    if let Some(ie) = pdu.ie(IE_RAN_UE_NGAP_ID) {
1737        v.insert("ran_ue_ngap_id".into(), json!(decode_ran_ue_ngap_id(ie)?));
1738    }
1739    v.insert(
1740        "uninterpreted_ies".into(),
1741        json!(pdu.uninterpreted_ies(&[
1742            IE_CAUSE,
1743            IE_TIME_TO_WAIT,
1744            IE_AMF_UE_NGAP_ID,
1745            IE_RAN_UE_NGAP_ID
1746        ])),
1747    );
1748    Ok(Value::Object(v))
1749}
1750
1751/// The UE-associated fields of an Initial UE Message.
1752pub struct InitialUe {
1753    pub ran_ue_ngap_id: u64,
1754    pub nas: Vec<u8>,
1755    pub user_location: Value,
1756    pub rrc_establishment_cause: String,
1757    pub five_g_s_tmsi: Option<Value>,
1758    pub uninterpreted: Vec<Value>,
1759}
1760
1761/// Initial UE Message (TS 38.413 section 9.2.5.1).
1762pub fn parse_initial_ue_message(pdu: &Pdu) -> CodecResult<InitialUe> {
1763    let mut budget = Budget::default();
1764    let ran = decode_ran_ue_ngap_id(pdu.require(IE_RAN_UE_NGAP_ID)?)?;
1765    let nas = decode_nas_pdu(pdu.require(IE_NAS_PDU)?)?;
1766    if nas.is_empty() {
1767        return Err(semantic("empty NAS-PDU"));
1768    }
1769    let uli = decode_user_location(pdu.require(IE_USER_LOCATION_INFORMATION)?, &mut budget)?;
1770    let cause = decode_rrc_cause(pdu.require(IE_RRC_ESTABLISHMENT_CAUSE)?)?;
1771    let tmsi = match pdu.ie(IE_FIVE_G_S_TMSI) {
1772        Some(ie) => Some(decode_five_g_s_tmsi(ie, &mut budget)?),
1773        None => None,
1774    };
1775    Ok(InitialUe {
1776        ran_ue_ngap_id: ran,
1777        nas,
1778        user_location: uli,
1779        rrc_establishment_cause: cause,
1780        five_g_s_tmsi: tmsi,
1781        uninterpreted: pdu.uninterpreted_ies(&[
1782            IE_RAN_UE_NGAP_ID,
1783            IE_NAS_PDU,
1784            IE_USER_LOCATION_INFORMATION,
1785            IE_RRC_ESTABLISHMENT_CAUSE,
1786            IE_FIVE_G_S_TMSI,
1787        ]),
1788    })
1789}
1790
1791/// The fields of a Downlink or Uplink NAS Transport.
1792pub struct NasTransport {
1793    pub amf_ue_ngap_id: u64,
1794    pub ran_ue_ngap_id: u64,
1795    pub nas: Vec<u8>,
1796    pub user_location: Option<Value>,
1797    pub uninterpreted: Vec<Value>,
1798}
1799
1800/// Downlink (9.2.5.2) or Uplink (9.2.5.3) NAS Transport.
1801pub fn parse_nas_transport(pdu: &Pdu) -> CodecResult<NasTransport> {
1802    let mut budget = Budget::default();
1803    let amf = decode_amf_ue_ngap_id(pdu.require(IE_AMF_UE_NGAP_ID)?)?;
1804    let ran = decode_ran_ue_ngap_id(pdu.require(IE_RAN_UE_NGAP_ID)?)?;
1805    let nas = decode_nas_pdu(pdu.require(IE_NAS_PDU)?)?;
1806    if nas.is_empty() {
1807        return Err(semantic("empty NAS-PDU"));
1808    }
1809    let uli = match pdu.ie(IE_USER_LOCATION_INFORMATION) {
1810        Some(ie) => Some(decode_user_location(ie, &mut budget)?),
1811        None if pdu.procedure_code == PROC_UPLINK_NAS_TRANSPORT => {
1812            return Err(abstract_reject(
1813                "UplinkNASTransport is missing mandatory IE 121 (UserLocationInformation)",
1814            ))
1815        }
1816        None => None,
1817    };
1818    Ok(NasTransport {
1819        amf_ue_ngap_id: amf,
1820        ran_ue_ngap_id: ran,
1821        nas,
1822        user_location: uli,
1823        uninterpreted: pdu.uninterpreted_ies(&[
1824            IE_AMF_UE_NGAP_ID,
1825            IE_RAN_UE_NGAP_ID,
1826            IE_NAS_PDU,
1827            IE_USER_LOCATION_INFORMATION,
1828        ]),
1829    })
1830}
1831
1832// ============================================================================
1833// Builders
1834// ============================================================================
1835
1836fn build(kind: PduKind, code: u8, crit: Criticality, ies: Vec<Ie>) -> EncodeResult<Vec<u8>> {
1837    encode_pdu(&Pdu::new(kind, code, crit, ies))
1838}
1839
1840/// NG Setup Response from the model's fields.
1841pub fn ng_setup_response(
1842    amf_name: &str,
1843    served_guami: &[Value],
1844    relative_amf_capacity: u64,
1845    plmn_support: &[Value],
1846) -> EncodeResult<Vec<u8>> {
1847    build(
1848        PduKind::Successful,
1849        PROC_NG_SETUP,
1850        Criticality::Reject,
1851        vec![
1852            name_ie(IE_AMF_NAME, Criticality::Reject, amf_name)?,
1853            served_guami_list_ie(served_guami)?,
1854            relative_amf_capacity_ie(relative_amf_capacity)?,
1855            plmn_support_list_ie(plmn_support)?,
1856        ],
1857    )
1858}
1859
1860pub fn ng_setup_failure(cause: &Cause, time_to_wait: Option<&str>) -> EncodeResult<Vec<u8>> {
1861    let mut ies = vec![cause_ie(cause, Criticality::Ignore)?];
1862    if let Some(t) = time_to_wait {
1863        ies.push(time_to_wait_ie(t)?);
1864    }
1865    build(
1866        PduKind::Unsuccessful,
1867        PROC_NG_SETUP,
1868        Criticality::Reject,
1869        ies,
1870    )
1871}
1872
1873pub fn ng_setup_request(
1874    plmn: &Plmn,
1875    gnb_id: u64,
1876    gnb_id_bits: u32,
1877    ran_node_name: Option<&str>,
1878    supported_ta_list: &[Value],
1879    paging_drx: &str,
1880) -> EncodeResult<Vec<u8>> {
1881    let mut ies = vec![global_gnb_id_ie(plmn, gnb_id, gnb_id_bits)?];
1882    if let Some(n) = ran_node_name {
1883        ies.push(name_ie(IE_RAN_NODE_NAME, Criticality::Ignore, n)?);
1884    }
1885    ies.push(supported_ta_list_ie(supported_ta_list)?);
1886    ies.push(paging_drx_ie(paging_drx)?);
1887    build(PduKind::Initiating, PROC_NG_SETUP, Criticality::Reject, ies)
1888}
1889
1890pub fn downlink_nas_transport(amf: u64, ran: u64, nas: &[u8]) -> EncodeResult<Vec<u8>> {
1891    build(
1892        PduKind::Initiating,
1893        PROC_DOWNLINK_NAS_TRANSPORT,
1894        Criticality::Ignore,
1895        vec![
1896            amf_ue_ngap_id_ie(amf, Criticality::Reject)?,
1897            ran_ue_ngap_id_ie(ran, Criticality::Reject)?,
1898            nas_pdu_ie(nas)?,
1899        ],
1900    )
1901}
1902
1903pub fn uplink_nas_transport(
1904    amf: u64,
1905    ran: u64,
1906    nas: &[u8],
1907    user_location: &Value,
1908) -> EncodeResult<Vec<u8>> {
1909    build(
1910        PduKind::Initiating,
1911        PROC_UPLINK_NAS_TRANSPORT,
1912        Criticality::Ignore,
1913        vec![
1914            amf_ue_ngap_id_ie(amf, Criticality::Reject)?,
1915            ran_ue_ngap_id_ie(ran, Criticality::Reject)?,
1916            nas_pdu_ie(nas)?,
1917            user_location_nr_ie(user_location, Criticality::Ignore)?,
1918        ],
1919    )
1920}
1921
1922pub fn initial_ue_message(
1923    ran: u64,
1924    nas: &[u8],
1925    user_location: &Value,
1926    rrc_establishment_cause: &str,
1927) -> EncodeResult<Vec<u8>> {
1928    build(
1929        PduKind::Initiating,
1930        PROC_INITIAL_UE_MESSAGE,
1931        Criticality::Ignore,
1932        vec![
1933            ran_ue_ngap_id_ie(ran, Criticality::Reject)?,
1934            nas_pdu_ie(nas)?,
1935            user_location_nr_ie(user_location, Criticality::Reject)?,
1936            rrc_cause_ie(rrc_establishment_cause)?,
1937        ],
1938    )
1939}
1940
1941pub fn error_indication(
1942    amf: Option<u64>,
1943    ran: Option<u64>,
1944    cause: Option<&Cause>,
1945) -> EncodeResult<Vec<u8>> {
1946    let mut ies = Vec::new();
1947    if let Some(a) = amf {
1948        ies.push(amf_ue_ngap_id_ie(a, Criticality::Ignore)?);
1949    }
1950    if let Some(r) = ran {
1951        ies.push(ran_ue_ngap_id_ie(r, Criticality::Ignore)?);
1952    }
1953    if let Some(c) = cause {
1954        ies.push(cause_ie(c, Criticality::Ignore)?);
1955    }
1956    build(
1957        PduKind::Initiating,
1958        PROC_ERROR_INDICATION,
1959        Criticality::Ignore,
1960        ies,
1961    )
1962}
1963
1964pub fn ng_reset(cause: &Cause, partial: Option<&[ConnectionItem]>) -> EncodeResult<Vec<u8>> {
1965    build(
1966        PduKind::Initiating,
1967        PROC_NG_RESET,
1968        Criticality::Reject,
1969        vec![
1970            cause_ie(cause, Criticality::Ignore)?,
1971            reset_type_ie(partial)?,
1972        ],
1973    )
1974}
1975
1976/// NG Reset Acknowledge; for a partial reset the list of connections reset is echoed (38.413
1977/// section 8.7.4.2.1).
1978pub fn ng_reset_acknowledge(partial: Option<&[ConnectionItem]>) -> EncodeResult<Vec<u8>> {
1979    let mut ies = Vec::new();
1980    if let Some(items) = partial {
1981        ies.push(connection_list_ie(items)?);
1982    }
1983    build(PduKind::Successful, PROC_NG_RESET, Criticality::Reject, ies)
1984}
1985
1986pub fn ue_context_release_command(
1987    amf: u64,
1988    ran: Option<u64>,
1989    cause: &Cause,
1990) -> EncodeResult<Vec<u8>> {
1991    build(
1992        PduKind::Initiating,
1993        PROC_UE_CONTEXT_RELEASE,
1994        Criticality::Reject,
1995        vec![
1996            ue_ngap_ids_ie(amf, ran)?,
1997            cause_ie(cause, Criticality::Ignore)?,
1998        ],
1999    )
2000}
2001
2002pub fn ue_context_release_complete(amf: u64, ran: u64) -> EncodeResult<Vec<u8>> {
2003    build(
2004        PduKind::Successful,
2005        PROC_UE_CONTEXT_RELEASE,
2006        Criticality::Reject,
2007        vec![
2008            amf_ue_ngap_id_ie(amf, Criticality::Ignore)?,
2009            ran_ue_ngap_id_ie(ran, Criticality::Ignore)?,
2010        ],
2011    )
2012}
2013
2014/// Every IE id of an encoded PDU, for logs; `None` when it does not decode.
2015pub fn summarize(bytes: &[u8]) -> Option<String> {
2016    let p = decode_pdu(bytes).ok()?;
2017    Some(p.label())
2018}
2019
2020/// Re-export so callers name one error type for "the peer sent garbage".
2021pub use aper::AperError as WireError;