oxirush_s1ap/lib.rs
1/*
2 OxiRush
3 Copyright 2025 Valentin D'Emmanuele
4
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16*/
17
18//! A codec for S1AP, the S1 Application Protocol of 3GPP TS 36.413, in aligned PER.
19//!
20//! ```rust
21//! use oxirush_s1ap::{build_s1ap, extract_s1ap_ies, s1ap::*};
22//!
23//! fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! // Decode a UE CONTEXT RELEASE REQUEST
25//! let bytes = hex::decode("0012401500000300000002002a000800020007000240020280")?;
26//! let pdu = S1AP_PDU::decode(&bytes)?;
27//! assert_eq!((pdu.procedure_code(), pdu.is_initiating()), (18, true));
28//! println!("{}", pdu.procedure_name());
29//! # assert_eq!(pdu.procedure_name(), "UEContextReleaseRequest");
30//!
31//! // Read its IEs by name. An `opt` field is `None` when its IE is not there,
32//! // and what follows `=>` is what the field keeps of the decoded value
33//! let request: UEContextReleaseRequest = pdu.decode_value()?;
34//! extract_s1ap_ies!(request, UEContextReleaseRequest,
35//! opt mme_id: u32 = MME_UE_S1AP_ID(id),
36//! opt enb_id: u32 = eNB_UE_S1AP_ID(id),
37//! opt cause: Cause = Cause(cause) => cause,
38//! );
39//! println!("MME={mme_id:?} eNB={enb_id:?} cause={cause:?}");
40//! # assert_eq!(format!("MME={mme_id:?} eNB={enb_id:?} cause={cause:?}"), "MME=Some(42) eNB=Some(7) cause=Some(radioNetwork(user_inactivity))");
41//!
42//! // Build it with another cause: the direction, the procedure, the criticality
43//! // of the PDU and the message, then each IE with its criticality
44//! let rebuilt = build_s1ap!(InitiatingMessage, UEContextReleaseRequest,
45//! IGNORE, UEContextReleaseRequest,
46//!
47//! REJECT MME_UE_S1AP_ID(42u32),
48//! REJECT eNB_UE_S1AP_ID(7u32),
49//! IGNORE Cause(Cause::radioNetwork(CauseRadioNetwork::radio_connection_with_ue_lost)),
50//! )?;
51//!
52//! // Encode it
53//! let encoded = hex::encode(rebuilt.encode()?);
54//! assert_eq!(encoded, "0012401500000300000002002a0008000200070002400202a0");
55//! Ok(())
56//! }
57//! ```
58//!
59//! ```text
60//! UEContextReleaseRequest
61//! MME=Some(42) eNB=Some(7) cause=Some(radioNetwork(user_inactivity))
62//! ```
63//!
64//! The types of [`s1ap`] are generated from the ASN.1 modules of the specification and
65//! checked in: `rasn-compiler` resolves the modules, and the generator of the crate
66//! writes each type with its codec on [`oxirush-aper`](aper).
67//!
68//! # Tasks
69//!
70//! The [README](https://github.com/linouxis9/oxirush-s1ap#readme) has an example of
71//! each, with what it prints.
72//!
73//! | Task | Items |
74//! |------|-------|
75//! | Answer an eNB, from S1 SETUP to the INITIAL CONTEXT SETUP | [`build_s1ap!`], [`build_s1ap_ie!`], [`extract_s1ap_ies!`], [`helpers`], [`S1AP_PDU::ie`](s1ap::S1APPDU::ie) |
76//! | Decode a PDU | [`S1AP_PDU::decode`](s1ap::S1APPDU::decode), [`S1AP_PDU::decode_borrowed`](s1ap::S1APPDU::decode_borrowed), [`S1AP_PDU::kind`](s1ap::S1APPDU::kind), [`S1AP_PDU::decode_value`](s1ap::S1APPDU::decode_value) |
77//! | Build a PDU | [`build_s1ap!`], [`S1AP_PDU::of_message`](s1ap::S1APPDU::of_message) |
78//! | Read the IEs of a message | [`extract_s1ap_ies!`], [`ie`], [`with_s1ap_ie_mut!`], [`build_s1ap_ie!`] |
79//! | A list of single containers, and a handover container, typed | [`S1AP_PDU::items`](s1ap::S1APPDU::items), [`s1ap::decode_open_type`], [`s1ap::encode_open_type`] |
80//! | Check a message, and answer one that is faulty | [`S1AP_PDU::check`](s1ap::S1APPDU::check), [`S1AP_PDU::check_structure`](s1ap::S1APPDU::check_structure), [`Deviation`], [`S1AP_PDU::criticality_diagnostics_for_response`](s1ap::S1APPDU::criticality_diagnostics_for_response) |
81//! | A PDU as JSON | [`S1AP_PDU::to_tree`](s1ap::S1APPDU::to_tree), [`S1AP_PDU::to_view`](s1ap::S1APPDU::to_view), [`S1AP_PDU::with_view`](s1ap::S1APPDU::with_view), [`inspect`] |
82//! | Write a message from its tree | [`S1AP_PDU::from_tree`](s1ap::S1APPDU::from_tree) |
83//!
84// Without `inspect` the items of the JSON forms in the tasks are no links: the links are
85// checked in the build of every feature, which docs.rs makes.
86#![cfg_attr(not(feature = "inspect"), allow(rustdoc::broken_intra_doc_links))]
87//! Every function and macro that can fail returns an [`Error`]: its [`ErrorKind`] is
88//! what a caller matches on, and its `Display` says what is wrong. `error.ie()` names
89//! the IE of an error of the macros.
90//!
91//! ```
92//! # use std::fmt::Write as _;
93//! # let mut printed = String::new();
94//! # macro_rules! println { ($($argument:tt)*) => { writeln!(printed, $($argument)*).unwrap() } }
95//! use oxirush_s1ap::{ErrorKind, s1ap::*};
96//!
97//! // A UE CONTEXT RELEASE REQUEST is not decoded as an S1 SETUP REQUEST
98//! let bytes = hex::decode("0012401500000300000002002a000800020007000240020280").unwrap();
99//! let pdu = S1AP_PDU::decode(&bytes).unwrap();
100//! let wrong = pdu.decode_value::<S1SetupRequest>().unwrap_err();
101//! assert_eq!(wrong.kind(), ErrorKind::WrongMessage);
102//! println!("{wrong}");
103//!
104//! // Octets that end early
105//! let short = S1AP_PDU::decode(&bytes[..10]).unwrap_err();
106//! assert_eq!(short.kind(), ErrorKind::Decode);
107//! println!("{short}");
108//! # assert_eq!(printed, r##"the message of this PDU is UEContextReleaseRequest, not S1SetupRequest: kind() says which message a PDU holds
109//! # these octets do not decode as S1AP-PDU: the octets end before the value does, in its field `value`
110//! # "##);
111//! ```
112//!
113//! ```text
114//! the message of this PDU is UEContextReleaseRequest, not S1SetupRequest: kind() says which message a PDU holds
115//! these octets do not decode as S1AP-PDU: the octets end before the value does, in its field `value`
116//! ```
117//!
118//! The codec builds, encodes and decodes a message whatever its IEs are. `check()` of
119//! a PDU reports how it deviates from what the ASN.1 assigns its message, each as a
120//! [`Deviation`]: a criticality of the PDU or of an IE that is not the assigned one, an
121//! IE that the message cannot have, has twice or has out of order, a mandatory IE that
122//! is missing, an IE whose value does not decode or is of a later version of its type,
123//! an encoding in another form than the canonical one, and a procedure that the
124//! specification does not have. `criticality_diagnostics()` of the PDU gathers them
125//! into the Criticality Diagnostics IE of the answer.
126//!
127//! # Values that borrow, and values that are kept
128//!
129//! A value that is decoded borrows the octets that it holds from what it was decoded
130//! from: the NAS-PDU of a message is a slice of the PDU, and nothing is copied. A type
131//! that holds octets has a lifetime for that, as `InitialUEMessage<'a>` and
132//! `NASPDU<'a>`. A type that holds none, as `Cause`, `TAI`, `EUTRANCGI` or `GUMMEI`,
133//! has no lifetime. The extension container of a value owns its octets, as a peer
134//! seldom sends one, so that it gives the value no lifetime.
135//!
136//! | To | Write |
137//! | --- | --- |
138//! | Read a message where it was received | `let pdu = S1AP_PDU::decode(&octets)?;` then `pdu.decode_value()?`: the message borrows from `pdu`, which is a variable of its own |
139//! | Keep a value after the PDU is gone | `value.into_owned()`, which gives the same type with the lifetime `'static`, as `NASPDU<'static>`: this is the type to write in a struct |
140//! | Build a value | From data that it owns: `NASPDU::from(vec)` is a `NASPDU<'static>` |
141//! | Read a PDU in the buffer that it was received in | [`S1AP_PDU::decode_borrowed`](s1ap::S1APPDU::decode_borrowed), which copies nothing: the PDU borrows from the buffer |
142//!
143//! [`S1AP_PDU`](s1ap::S1AP_PDU) is the PDU that owns its octets, `S1APPDU<'static>`,
144//! and has no lifetime to write.
145//!
146//! ```
147//! use oxirush_s1ap::{Error, build_s1ap, extract_s1ap_ies, s1ap::*};
148//!
149//! /// What a node keeps of a UE after its first message.
150//! struct Ue {
151//! enb_id: u32,
152//! // The context owns these octets: `'static`, and no lifetime on `Ue`
153//! nas: NASPDU<'static>,
154//! }
155//!
156//! fn admit(pdu: &S1AP_PDU) -> Result<Ue, Error> {
157//! // `message` borrows from `pdu`, and `nas` from `message`
158//! let message: InitialUEMessage = pdu.decode_value()?;
159//! extract_s1ap_ies!(message, InitialUEMessage,
160//! req enb_id: u32 = eNB_UE_S1AP_ID(id),
161//! req nas: NASPDU = NAS_PDU(nas) => nas,
162//! );
163//! // Kept beyond the PDU: the octets are copied here, once
164//! Ok(Ue { enb_id, nas: nas.into_owned() })
165//! }
166//!
167//! let pdu = build_s1ap!(InitiatingMessage, initialUEMessage, IGNORE, InitialUEMessage,
168//! REJECT eNB_UE_S1AP_ID(7u32),
169//! REJECT NAS_PDU(vec![0x07, 0x41, 0x71]),
170//! )?;
171//! let ue = admit(&pdu)?;
172//! drop(pdu);
173//! assert_eq!((ue.enb_id, &ue.nas.0[..]), (7, &[0x07, 0x41, 0x71][..]));
174//! # Ok::<(), Error>(())
175//! ```
176//!
177//! A message may be held across an `.await`. The future of this handler owns the
178//! buffer, the PDU borrows from the buffer, the message from the PDU and the NAS-PDU
179//! from the message. The future is `Send`, as every value of the bindings is.
180//!
181//! ```
182//! use oxirush_s1ap::{Error, build_s1ap, extract_s1ap_ies, s1ap::*};
183//!
184//! async fn deliver(nas: &[u8]) {
185//! assert_eq!(nas, [0x07, 0x5e, 0x03]);
186//! }
187//!
188//! async fn handle(received: Vec<u8>) -> Result<(), Error> {
189//! let pdu = S1AP_PDU::decode_borrowed(&received)?;
190//! let message: UplinkNASTransport = pdu.decode_value()?;
191//! extract_s1ap_ies!(message, UplinkNASTransport,
192//! req nas: NASPDU = NAS_PDU(nas) => nas,
193//! );
194//! // `nas` is a slice of `received`, here and after the await
195//! deliver(&nas.0).await;
196//! assert!(nas.0.is_borrowed());
197//! Ok(())
198//! }
199//!
200//! let transport = build_s1ap!(InitiatingMessage, uplinkNASTransport, IGNORE, UplinkNASTransport,
201//! REJECT MME_UE_S1AP_ID(1u32),
202//! REJECT eNB_UE_S1AP_ID(7u32),
203//! REJECT NAS_PDU(vec![0x07, 0x5e, 0x03]),
204//! )?;
205//! let handler = handle(transport.encode()?);
206//!
207//! // A runtime may move the handler to another thread
208//! fn is_send(_: &impl Send) {}
209//! is_send(&handler);
210//! # let mut handler = std::pin::pin!(handler);
211//! # let mut context = std::task::Context::from_waker(std::task::Waker::noop());
212//! # assert!(handler.as_mut().poll(&mut context).is_ready());
213//! # Ok::<(), Error>(())
214//! ```
215//!
216//! # The names of the fields
217//!
218//! A field has the words of the identifier that ASN.1 gives its member, in lower case
219//! with `_` between them, as a reader of the specification says them: `pLMNidentity` is
220//! `plmn_identity`, `tAC` is `tac`, `iE-Extensions` is `ie_extensions`, `eNB-ID` is
221//! `enb_id`, `mME-Code` is `mme_code`, `gTP-TEID` is `gtp_teid`.
222//!
223//! 1. A hyphen ends a word, and so does a lower-case letter or a digit before a
224//! capital: `procedureCode` is `procedure_code`.
225//! 2. The first letter of an identifier belongs to the capitals after it: the
226//! identifier starts in lower case whatever its first word is.
227//! 3. Capitals in a row are an abbreviation, and the last of them starts a word when a
228//! lower-case letter follows. When the capitals without it are no abbreviation of
229//! the list and the capitals with it are one, they end the abbreviation:
230//! `nRCellIdentity` is `nr_cell_identity`, and `pLMNIdentity` is `plmn_identity` as
231//! `pLMNidentity` is. An `s` after them is their plural: `protocolIEs` is
232//! `protocol_ies`.
233//! 4. Capitals that are several abbreviations of the list are these:
234//! `eNBX2TransportLayerAddresses` is `enb_x2_transport_layer_addresses`.
235//! 5. An abbreviation that the specifications write with a lower-case letter is one
236//! word wherever it is: `a2X`, `AIoT`, `IoT`, `QoS`, `QoE`, `ProSe`, `eNB`, `gNB`,
237//! `IPsec`, `IPv4`, `IPv6`, `NRPPa`, `LPPa`, `n3IWF`, `eDRX`, `eCGI`. `gNB-ID` is
238//! `gnb_id`.
239//! 6. `fiveG` and `fiveGS` are `five_g` and `five_gs`, and `5GS` is one word.
240//!
241//! The list of the abbreviations is `ABBREVIATIONS` of `generator/src/names.rs` in
242//! oxirush-aper, with the identifiers that the specifications write otherwise than
243//! their others, as `parentTImeSource`. `build/fields.tsv` of the repository has each
244//! field of each type with the name that rasn-compiler gives it, which the fields had
245//! before this rule and have in 0.2.0 for every field of a type that 0.2.0 has but one,
246//! the `excludedCellsToAddModList` of `InterSystemMeasurementItem`. The JSON forms keep
247//! the identifiers of ASN.1.
248//!
249//! A `SEQUENCE` is a struct that a literal builds, and its `new` takes the members in
250//! their order. An extensible `CHOICE` or `ENUMERATED` is `#[non_exhaustive]`: a later
251//! version of the specification adds alternatives and values to it, which a `match`
252//! leaves room for with a `_` arm. A member that a later version adds to a `SEQUENCE`
253//! changes its struct and its `new`, and comes with a breaking release of the crate.
254//!
255//! # The types of the values
256//!
257//! | ASN.1 | Rust |
258//! | --- | --- |
259//! | `INTEGER` with a constraint | The native number that holds its range: `MMEUES1APID(pub u32)` for `(0..4294967295)` |
260//! | `INTEGER` whose constraint has an extension marker | `u64`: `ERABID(pub u64)` for `(0..15, ...)` |
261//! | `OCTET STRING (SIZE (n))` | `[u8; n]`: `TAC(pub [u8; 2])` |
262//! | Another `OCTET STRING`, and an open type | [`Octets`]: `NASPDU<'a>(pub Octets<'a>)` |
263//! | `BIT STRING` of 64 bits at most, without an extension marker | [`Bits`], a number with a length, which is `Copy`. A type of one size is made of the number: `CellIdentity::new(0x1234507)?` |
264//! | Another `BIT STRING` | [`BitString`], which borrows its octets when it is longer than 64 bits |
265//! | `PrintableString`, `VisibleString`, `UTF8String` | `Cow<'a, str>`: `MMEname<'a>` |
266//! | `ENUMERATED`, `CHOICE`, `SEQUENCE`, `SEQUENCE OF` | An enum, an enum, a struct, and a `Vec` in a struct |
267//!
268//! A bit string is a [`Bits`] when its type says that 64 bits hold it. A string that
269//! may be longer, and one whose size has an extension marker, which a later release
270//! may send longer, is a [`BitString`]:
271//!
272//! | [`Bits`] | [`BitString`] |
273//! | --- | --- |
274//! | `ENBID::macroENB_ID` (20 bits), `ENBID::homeENB_ID` (28) | `TransportLayerAddress` (1 to 160, ...) |
275//! | `CellIdentity` (28), `NRCellIdentity` (36), `CSGId` (27) | `SecurityKey` (256) |
276//! | `MaskedIMEISV` (64), `GNBID` (22 to 32) | `EncryptionAlgorithms`, `IntegrityProtectionAlgorithms`, `NRencryptionAlgorithms` and `NRintegrityProtectionAlgorithms` (16, ...) |
277//! | `UEIdentityIndexValue` (10), `MessageIdentifier`, `SerialNumber` (16), `InterfacesToTrace` (8) | `EnGNBID` (22 to 32, ...), `ReceiveStatusofULPDCPSDUs` (4096) |
278//!
279//! The bindings hold an `INTEGER` in 64 bits. A number that an extension marker lets
280//! a later release send beyond that does not decode, with an error of the kind
281//! `Decode` that says `the number does not fit in 64 bits`. The PDU still decodes:
282//! its IEs are octets until one is read, and the JSON forms of the `inspect` feature
283//! show an IE that does not decode as the octets received, and write them back.
284//!
285//! # The OxiRush family
286//!
287//! - [oxirush-utils](https://docs.rs/oxirush-utils): the 3GPP identities, and the JSON tree with its paths that the others share.
288//! - [oxirush-aper](https://docs.rs/oxirush-aper): aligned PER, the runtime of the NGAP and S1AP codecs.
289//! - [oxirush-nas](https://docs.rs/oxirush-nas): the NAS messages of 5GS and EPS.
290//! - [oxirush-security](https://docs.rs/oxirush-security): NAS security, AKA and the derivation of keys.
291//! - [oxirush-ngap](https://docs.rs/oxirush-ngap): NGAP, between a gNB and an AMF.
292//! - [oxirush-s1ap](https://docs.rs/oxirush-s1ap): S1AP, between an eNB and an MME.
293//! - [oxirush-gtp-u](https://docs.rs/oxirush-gtp-u): GTP-U tunnels, with a test UPF.
294//! - [oxirush-sctp](https://docs.rs/oxirush-sctp): SCTP sockets, which carry NGAP and S1AP.
295
296#![forbid(unsafe_code)]
297#![cfg_attr(docsrs, feature(doc_cfg))]
298
299extern crate alloc;
300
301pub mod helpers;
302// What the fuzz targets and the oracle take from the inside of the crate, with
303// `--cfg oxirush_s1ap_hooks`: the module `hooks`.
304oxirush_aper::__hooks! { cfg oxirush_s1ap_hooks, bindings s1ap, pdu S1APPDU, owned S1AP_PDU }
305#[cfg(feature = "inspect")]
306pub mod inspect;
307#[cfg(feature = "inspect")]
308mod inspect_registry;
309pub mod macros;
310/// The types of the ASN.1 modules of TS 36.413, as the generator writes them:
311/// [`S1AP_PDU`](s1ap::S1AP_PDU) and its three directions, each message, each IE and
312/// each type of a value, which decode and encode themselves in aligned PER.
313///
314/// ```
315/// use oxirush_s1ap::s1ap::{MMEUES1APID, decode_open_type, encode_open_type};
316///
317/// // `MME-UE-S1AP-ID ::= INTEGER (0..4294967295)`, as the value of its IE holds it
318/// let octets = encode_open_type(&MMEUES1APID(42))?;
319/// assert_eq!(octets, [0x00, 0x2a]);
320/// assert_eq!(decode_open_type::<MMEUES1APID>(&octets)?, MMEUES1APID(42));
321/// # Ok::<(), oxirush_s1ap::Error>(())
322/// ```
323// The bindings are formatted where they are generated, by the rustfmt of the toolchain
324// that the generator runs with: a later one breaks some of their long lines otherwise.
325#[rustfmt::skip]
326pub mod s1ap;
327mod pdu;
328#[doc(hidden)]
329pub mod registry;
330
331/// The aligned PER runtime that the bindings are made of, at the version that this
332/// crate is built with. The values of the bindings are its types, such as
333/// [`Octets`], [`Bits`] and [`BitString`],
334/// and the error of the crate is its error: a crate that names them takes them from
335/// here, and needs no dependency on `oxirush-aper` of its own.
336pub use oxirush_aper as aper;
337/// The identities of 3GPP networks as values, of the `oxirush-utils` that this crate
338/// is built with: a `PLMNidentity` converts to and from its `Plmn`, which has the MCC and
339/// the MNC.
340pub use oxirush_utils::identity;
341/// The trees of `oxirush-utils`, of the version that this crate is built with: a PDU and
342/// each value of the bindings as JSON whose members keep the order of the specification,
343/// [`tree::Value`], and `tree!`, which writes one in the code as `serde_json::json!` does.
344/// A crate that reads or writes them takes them from here.
345#[cfg(feature = "inspect")]
346pub use oxirush_utils::tree;
347
348pub use oxirush_aper::protocol::Message;
349pub use oxirush_aper::{
350 AsnType, BitString, Bits, Decode, Encode, Error, ErrorKind, Fault, Faults, Octets,
351};
352/// ```
353/// use oxirush_s1ap::{Deviation, build_s1ap, s1ap::*};
354///
355/// // An INITIAL UE MESSAGE of its first IE alone
356/// let pdu = build_s1ap!(InitiatingMessage, initialUEMessage, InitialUEMessage, eNB_UE_S1AP_ID(7u32))?;
357/// let deviations: Vec<Deviation> = pdu.check()?;
358/// assert!(matches!(deviations[0], Deviation::MissingIe { id: 26, .. }));
359/// assert_eq!(deviations[0].to_string(), "NAS-PDU (IE 26) is mandatory, and the message does not have it");
360/// # Ok::<(), oxirush_s1ap::Error>(())
361/// ```
362pub use registry::Deviation;
363pub use registry::S1ap;
364/// The IEs of the protocol, one marker type each, by the names that the macros take:
365/// `ies::<T>()` and `ie::<T>()` of a PDU and of a message take them, as
366/// `pdu.ie::<ie::Cause>()`. Each implements [`aper::protocol::Ie`], which gives its
367/// identifier and the type of its value.
368pub use registry::ie;
369pub use s1ap::S1apPduKind;
370
371/// The examples of the README, which the documentation tests compile and run.
372#[cfg(all(doctest, feature = "serde"))]
373#[doc = include_str!("../README.md")]
374struct Readme;
375
376/// Version of oxirush-s1ap.
377pub const VERSION: &str = env!("CARGO_PKG_VERSION");