oxirush-aper
Aligned PER (ITU-T X.691) in Rust, for bindings generated from 3GPP ASN.1:
a reader and a writer of the fields, and the types of the values, in
no_std and with no dependency unless a feature asks for one.
use ;
[a0, c8, 30, 01, 80]
5
200
3
2 items: true false
the value does not encode: 8 is outside its constraint (0..7)
Each example is followed by its output.
A Reader and a Writer have a method for each kind of field of X.691 that
NGAP and S1AP use, and take the bounds of its constraint as arguments.
Installation
[]
= "0.1"
# With what generated bindings write and read their JSON trees with:
# oxirush-aper = { version = "0.1", features = ["inspect"] }
| Feature | Adds |
|---|---|
inspect |
json: values as trees of oxirush_utils::tree, in the form of the JSON Encoding Rules (X.697), with a readable form for a few 3GPP types, such as "208-93" for a PLMN identity |
check, which says how a PDU deviates from the object set of its message,
needs no feature.
The minimum supported Rust version is 1.88. The crate needs alloc, and has
no unsafe code. oxirush-ngap and oxirush-s1ap depend on one exact version
of it and are released with it: a program that uses them takes it through
them, as oxirush_ngap::aper.
A protocol of its own
A codec writes and reads the fields of a type in order, each with the bounds of its constraint, as the generated bindings of oxirush-ngap and oxirush-s1ap do. Here, a SEQUENCE with an extension marker, an OPTIONAL member, an extensible ENUMERATED, a character string and a SEQUENCE OF:
use ;
use Cow;
// Cell ::= SEQUENCE {
// id INTEGER (0..4095),
// kind ENUMERATED { macro, small, ... },
// name PrintableString (SIZE (1..150)) OPTIONAL,
// neighbours SEQUENCE (SIZE (0..8)) OF INTEGER (0..4095),
// ...
// }
[40, 01, 23, 41, 40, 63, 65, 6c, 6c, 2d, 31, 20, 01, 24, 01, 25]
Cell { id: 291, kind: Small, name: Some("cell-1"), neighbours: [292, 293] }
borrowed: true
a value of Cell does not encode: a size of 9 is outside its constraint (0..8)
these octets do not decode as Cell: the octets end before the value does
The types of the values
| ASN.1 | Rust |
|---|---|
BOOLEAN, NULL |
bool, () |
INTEGER with a constraint |
The native number that holds its range, as u16 for (0..65535) |
INTEGER with an extension marker, or without a constraint |
u64, or i64 when its root goes below zero |
OCTET STRING (SIZE (n)) |
[u8; n] |
Another OCTET STRING, an open type |
Octets<'a> |
BIT STRING of 64 bits at most, without an extension marker |
Bits |
Another BIT STRING |
BitString<'a> |
PrintableString, VisibleString, IA5String, UTF8String |
Cow<'a, str> |
OBJECT IDENTIFIER |
ObjectIdentifier |
Octets borrows the octets that were decoded, or owns its own.
into_owned() makes a value that outlives its input.
use Octets;
7e0041 borrowed: true
7e0041 borrowed: false
Bit strings
Bits is a number with a length, for the identifiers of 3GPP: it is Copy,
and compares as a value. BitString holds a string of any length.
use ;
0x155 of 10 bits: [55, 40]
true
1024 does not fit in 10 bits
[10, 0, 0, 1] of 32 bits
An open type
An open type is read as its octets, which borrow from the input, and is decoded once its type is known. A writer writes it in place, without a buffer of its own.
use ;
[02, 12, 34]
1234: 0x1234
Octets::as_borrowed gives the octets of an open type for as long as the
input lives, to decode a value that borrows from them.
Extension additions
use ;
true true Some(7)
What the reader accepts
A writer gives a value its one encoding. A reader also reads a complete
value in another form, as a peer's encoder may write it. A reader made with
Reader::noting records each such form as a Fault, with its clause and
the bit where it is.
Fault |
What is read | X.691 |
|---|---|---|
LengthInTwoOctets |
A length of 127 or less in two octets | 11.9.3.6 |
SmallFragment |
A fragment before another that is not the largest | 11.9.3.8.1 |
SmallNumberAsLarge |
A normally small number of 63 or less in the form of 64 and more | 11.6.1 |
LongNumber |
A whole number with octets that only extend it, up to the octets that a number of 64 bits may have | 11.3.6, 11.4.6 |
Padding |
Padding bits that are not zero | 11.1.4 |
LongArc |
An arc of an object identifier that starts with an octet that holds nothing | X.690 8.19.2 |
use ;
aa
at bit 0, a length of 127 or less is in two octets (X.691 §11.9.3.6)
these octets do not decode as OCTET STRING: at bit 0, a length of 127 or less is in two octets (X.691 §11.9.3.6): the value is read, and a sender by the clause writes it otherwise
decode and a reader of Reader::new note nothing, and Reader::faults
has what a noting reader noted. Every reader refuses an encoding that
announces something that is not there, or holds no value: an extension bit
with no addition present, a number of no octet, a number, size or index
outside its constraint, octets after a complete encoding.
The macros of a protocol crate
oxirush-ngap and oxirush-s1ap have macros that build a message and read or
change its IEs by name (build_ngap!, extract_ngap_ies!,
with_ngap_ie_mut!, and the same with s1ap), and readers of one IE by its
marker type. This crate has their code. Here they are on the toy protocol of
tests/toy/mod.rs:
// The toy protocol: a SETUP REQUEST of an Identity, a Name and a list of Items
include!;
use *;
000100130000020001000200070002400605674e422d31
7 Some("gNB-1")
Some(Ok(Identity(7))), IE 1
include!;
use *;
item 1
item 2
Some(Ok(Identity(9)))
[Identity(7), Identity(8)]
Identity (IE 1), at position 1, is in the message already, at position 0
With oxirush-ngap
The bindings of oxirush-ngap are
written on this crate, which it re-exports as oxirush_ngap::aper. A
program can read the value of an IE with a Reader, by the bounds of its
type in TS 38.413, as the generated codec of the IE does.
use ;
use NGAP_PDU;
AMF-UE-NGAP-ID 42
RAN-UE-NGAP-ID 7
IE 15: [05, 00]
true
Limits
- It has no unaligned variant.
- It has no
REAL, time types,SETorSET OF,BMPString,UniversalStringorNumericString, and no permitted alphabet constraint. DEFAULTmembers and extension addition groups have no method of their own: the caller writes the bits of the preamble.- An
INTEGERbeyond 64 bits is a decoding error. - It does not parse ASN.1: the generator in
generator/, which is in this repository but not published, writes the bindings of oxirush-ngap and oxirush-s1ap ahead of time.
Performance
The crate against the derive of rasn 0.29, on five values of NGAP with the same type in both: written by hand as the generated bindings of oxirush-ngap write it, and derived for rasn. Both read and write the same octets. A ratio is the time of rasn over this crate's:
| Value (octets) | Decoding | rasn / this crate | Encoding | rasn / this crate |
|---|---|---|---|---|
| AMF-UE-NGAP-ID (2) | 11 ns | 2.9x | 21 ns | 2.7x |
| NAS-PDU (61) | 11 ns | 4.6x | 60 ns | 1.3x |
| AMFName (18) | 34 ns | 4.3x | 50 ns | 3.5x |
| GUAMI (7) | 30 ns | 8.8x | 30 ns | 4.7x |
| UserLocationInformationNR (19) | 35 ns | 8.1x | 71 ns | 3.6x |
Decoding these values allocates nothing. Measured at 65d0e09 on one core of
an AMD Ryzen 7 5700G (Linux 7.0, Rust 1.98.1) on 2026-10-18.
benches/ has the cycles, the instructions and the
allocations of each, and the method. From the root of the repository:
Tests and fuzzing
The worked examples of X.691 Annex A are reproduced octet for octet. Each
field is tested against its clause and compared with
rasn on generated values: tests/rasn.rs
names the clauses where rasn 0.29 reads or writes otherwise. fuzz/ has two
cargo-fuzz targets: fields reads
fields and constraints that the octets choose and writes them back, and
value reads a value of one type with this crate and with rasn, which must
read the same value and write the same octets, or both refuse it.
Each Rust block of this page is checked with what it prints
(tests/readme.rs says how). The block that uses
oxirush-ngap runs in the package of family/, which takes it at
the revision that its manifest names. CONTRIBUTING.md
says what else the repository holds.
A pull request comes with a test for what it changes. Sign off your commits
(git commit -s): by contributing you agree to the Developer Certificate
of Origin. SECURITY.md
says how to report a vulnerability.
The OxiRush family
- oxirush-utils: the 3GPP identities, and the JSON tree with its paths that the others share.
- oxirush-aper: aligned PER, the runtime of the NGAP and S1AP codecs.
- oxirush-nas: the NAS messages of 5GS and EPS.
- oxirush-security: NAS security, AKA and the derivation of keys.
- oxirush-ngap: NGAP, between a gNB and an AMF.
- oxirush-s1ap: S1AP, between an eNB and an MME.
- oxirush-gtp-u: GTP-U tunnels, with a test UPF.
- oxirush-sctp: SCTP sockets, which carry NGAP and S1AP.
License
Copyright 2025-2026 Valentin D'Emmanuele. Licensed under the Apache License, Version 2.0: see LICENSE.