Skip to main content

c_its_parser/
lib.rs

1//! Parser and encoder for ETSI C-ITS (V2X) messages including GeoNetworking headers and optionally Radiotap and IEEE 802.11 headers.
2//! It supports UPER, XER and JER ASN.1 encodings for parsing and encoding.
3//!
4//! Supported messages/ standards: See documentation of the [`ItsMessage`] variants.
5//!
6//! ## Decoding
7//!
8//! A packet can be decoded using the [`de::decode`] function:
9//!
10//! ```
11//! let data = &[0x02, 0x02, 0xde, 0x14, 0x0c, 0xe5]; // provide actual message buffer here
12//! #[cfg(feature = "_etsi")]
13//! match c_its_parser::de::decode(data, c_its_parser::Headers::RadioTap802LlcGnBtp) {
14//!     #[cfg(feature = "cam_1_4_1")]
15//!     Ok(c_its_parser::ItsMessage::Cam {
16//!         geonetworking: _,
17//!         transport: _,
18//!         etsi: cam,
19//!     }) => {
20//!         println!("Got a CAM: {cam:?}")
21//!     }
22//!     Ok(msg) => println!("Got: {msg:?}"),
23//!     Err(err) => println!("Failed to parse message: {err}"),
24//! }
25//! ```
26//!
27//! The `headers` argument needs to specify which headers are present: None, GeoNetworking + BTP or Radiotap + 802.11p + LLC + GeoNetworking + BTP.
28//! Headers are expected to be present in binary form.
29//! When no headers are present, it can auto-detect the ASN.1 encoding (UPER/ XER/ JER) and decodes the message.
30//! This means, that XER and JER message buffers can only be decoded without headers.
31//!
32//! ## Encoding
33//!
34//! To encode an [`ItsMessage`] struct, call the [`encode()`](`ItsMessage::encode`) method supplying the intended encoding rules.
35//! Again, XER and JER messages can only be encoded without headers.
36//! GeoNetworking and transport (BTP) headers will be added when present if UPER encoding is used.
37//!
38//! ## Feature Flags
39//!
40//! This library has several feature flags to allow fine-grained control over the feature set and additional dependencies.
41//!
42//! By default, all V2X messages and conversion to and from JSON are enabled.
43//! If only some messages, or even just specific versions of messages are needed, they can be enabled one-by-one, e.g. using `denm` to enable both `denm_1_3_1` and `denm_2_2_1` support.
44//!
45//! When no parsing of the geonetworking and pcap headers is needed, the `transport` feature can be disabled.
46//!
47//! Besides parsing, the Rust API also provides helper functions to convert between ETSI data types and "normal"/ SI units.
48//! Additional conversions are only available by adding some feature flags:
49//!
50//! - `time`: Enable conversions to [chrono](https://crates.io/crates/chrono) timestamps
51//! - `geo`: Enable conversions to [geo-types](https://crates.io/crates/geo-types) (as lon/lat coordinates in degrees)
52
53#![cfg_attr(not(target_arch = "wasm32"), no_std)]
54#![cfg_attr(docsrs, feature(doc_cfg))]
55
56extern crate alloc;
57
58#[cfg(any(feature = "std", target_arch = "wasm32", test))]
59extern crate std;
60
61pub mod de;
62#[cfg(feature = "_etsi")]
63pub mod en;
64#[allow(clippy::all, clippy::pedantic, clippy::nursery, dead_code)]
65pub mod standards;
66
67#[cfg(feature = "geo")]
68pub mod geo_utils;
69#[cfg(feature = "time")]
70pub mod time_utils;
71
72#[cfg(feature = "transport")]
73pub mod pcap;
74#[cfg(feature = "transport")]
75pub mod transport;
76
77#[cfg(feature = "transport")]
78pub use geonetworking as gn;
79#[cfg(feature = "transport")]
80#[deprecated = "use `pcap::remove_pcap_headers` instead"]
81pub use pcap::remove_pcap_headers;
82#[cfg(all(target_arch = "wasm32", any(feature = "json", feature = "_etsi")))]
83use wasm_bindgen::prelude::*;
84
85#[cfg(all(target_arch = "wasm32", feature = "json"))]
86#[wasm_bindgen(getter_with_clone)]
87#[derive(Debug, Clone, PartialEq, Default)]
88/// Wrapper for the stringified JSON of headers and ITS ETSI message
89pub struct JsonItsMessage {
90    /// Optional GeoNetworking header, encoded as stringified JSON
91    pub geonetworking: Option<String>,
92    /// Optional transport header, encoded as stringified JSON
93    pub transport: Option<String>,
94    /// Optional ITS ETSI message, encoded as a UTF-8 String for JER and XER, as a hex string for UPER
95    pub its: Option<String>,
96    /// Optional ITS ETSI message type, as specified in ETSI TS 102 894-2
97    /// - 1  - `denm`              - for Decentralized Environmental Notification Message (DENM) as specified in ETSI EN 302 637-3 [2],
98    /// - 2  - `cam`               - for Cooperative Awareness Message (CAM) as specified in ETSI EN 302 637-2 [1],
99    /// - 3  - `poi`               - for Point of Interest message as specified in ETSI TS 101 556-1 [9],
100    /// - 4  - `spatem`            - for Signal Phase And Timing Extended Message (SPATEM) as specified in ETSI TS 103 301 [15],
101    /// - 5  - `mapem`             - for MAP Extended Message (MAPEM) as specified in ETSI TS 103 301 [15],
102    /// - 6  - `ivim`              - for in Vehicle Information Message (IVIM) as specified in ETSI TS 103 301 [15],
103    /// - 7  - `ev-rsr`            - for Electric vehicle recharging spot reservation message, as defined in ETSI TS 101 556-3 [11],
104    /// - 8  - `tistpgtransaction` - for messages for Tyre Information System (TIS) and Tyre Pressure Gauge (TPG) interoperability, as specified in ETSI TS 101 556-2 [10],
105    /// - 9  - `srem`              - for Signal Request Extended Message as specified in ETSI TS 103 301 [15],
106    /// - 10 - `ssem`              - for Signal request Status Extended Message as specified in ETSI TS 103 301 [15],
107    /// - 11 - `evcsn`             - for Electrical Vehicle Charging Spot Notification message as specified in ETSI TS 101 556-1 [9],
108    /// - 12 - `saem`              - for Services Announcement Extended Message as specified in ETSI EN 302 890-1 [17],
109    /// - 13 - `rtcmem`            - for Radio Technical Commission for Maritime Services Extended Message (RTCMEM) as specified in ETSI TS 103 301 [15],
110    /// - 14 - `cpm`               - reserved for Collective Perception Message (CPM),
111    /// - 15 - `imzm`              - for Interference Management Zone Message (IMZM) as specified in ETSI TS 103 724 [13],
112    /// - 16 - `vam`               - for Vulnerable Road User Awareness Message as specified in ETSI TS 130 300-3 [12],
113    /// - 17 - `dsm`               - reserved for Diagnosis, logging and Status Message,
114    /// - 18 - `pcim`              - reserved for Parking Control Infrastructure Message,
115    /// - 19 - `pcvm`              - reserved for Parking Control Vehicle Message,
116    /// - 20 - `mcm`               - reserved for Manoeuver Coordination Message,
117    /// - 21 - `pam`               - reserved for Parking Availability Message,
118    /// - 22-255                   - reserved for future usage.
119    pub message_type: u8,
120}
121
122#[cfg(all(target_arch = "wasm32", feature = "json"))]
123#[wasm_bindgen]
124impl JsonItsMessage {
125    #[wasm_bindgen(constructor)]
126    pub fn from(
127        its: Option<String>,
128        geonetworking: Option<String>,
129        transport: Option<String>,
130        message_type: u8,
131    ) -> Self {
132        Self {
133            its,
134            geonetworking,
135            transport,
136            message_type,
137        }
138    }
139}
140
141#[cfg(feature = "_etsi")]
142#[derive(Debug, Clone, PartialEq)]
143/// Wrapper for C-ITS messages
144///
145/// Each message consists of the `etsi` data and can optionally contain a `transport` (BTP) and a `geonetworking` header.
146pub enum ItsMessage<'a> {
147    #[cfg(feature = "denm_1_3_1")]
148    /// ETSI EN 302 637-3 v1.3.1 DENM
149    DenmV1 {
150        geonetworking: Option<geonetworking::Packet<'a>>,
151        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
152        etsi: alloc::boxed::Box<standards::denm_1_3_1::denm_pdu_descriptions::DENM>,
153    },
154    #[cfg(feature = "denm_2_2_1")]
155    /// ETSI TS 103 831 v2.2.1 (or v2.1.1) DENM
156    DenmV2 {
157        geonetworking: Option<geonetworking::Packet<'a>>,
158        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
159        etsi: alloc::boxed::Box<standards::denm_2_2_1::denm_pdu_description::DENM>,
160    },
161    #[cfg(feature = "cam_1_4_1")]
162    /// ETSI TS 103 301 v2.2.1 (or v2.1.1 or v1.3.1) CAM
163    Cam {
164        geonetworking: Option<geonetworking::Packet<'a>>,
165        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
166        etsi: alloc::boxed::Box<standards::cam_1_4_1::cam_pdu_descriptions::CAM>,
167    },
168    #[cfg(feature = "spatem_2_2_1")]
169    /// ETSI TS 103 301 v2.2.1 (or v2.1.1 or v1.3.1) SPATEM
170    Spatem {
171        geonetworking: Option<geonetworking::Packet<'a>>,
172        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
173        etsi: alloc::boxed::Box<standards::spatem_2_2_1::spatem_pdu_descriptions::SPATEM>,
174    },
175    #[cfg(feature = "mapem_2_2_1")]
176    /// ETSI TS 103 301 v2.2.1 (or v2.1.1 or v1.3.1) MAPEM
177    Mapem {
178        geonetworking: Option<geonetworking::Packet<'a>>,
179        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
180        etsi: alloc::boxed::Box<standards::mapem_2_2_1::mapem_pdu_descriptions::MAPEM>,
181    },
182    #[cfg(feature = "ivim_2_1_1")]
183    /// ETSI TS 103 301 v2.1.1 (or v1.3.1) IVIM
184    IvimV1 {
185        geonetworking: Option<geonetworking::Packet<'a>>,
186        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
187        etsi: alloc::boxed::Box<standards::ivim_2_1_1::ivim_pdu_descriptions::IVIM>,
188    },
189    #[cfg(feature = "ivim_2_2_1")]
190    /// ETSI TS 103 301 v2.2.1 IVIM
191    IvimV2 {
192        geonetworking: Option<geonetworking::Packet<'a>>,
193        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
194        etsi: alloc::boxed::Box<standards::ivim_2_2_1::ivim_pdu_descriptions::IVIM>,
195    },
196    #[cfg(feature = "srem_2_2_1")]
197    /// ETSI TS 103 301 v2.2.1 (or v2.1.1 or v1.3.1) SREM
198    Srem {
199        geonetworking: Option<geonetworking::Packet<'a>>,
200        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
201        etsi: alloc::boxed::Box<standards::srem_2_2_1::srem_pdu_descriptions::SREM>,
202    },
203    #[cfg(feature = "ssem_2_2_1")]
204    /// ETSI TS 103 301 v2.2.1 (or v2.1.1 or v1.3.1) SSEM
205    Ssem {
206        geonetworking: Option<geonetworking::Packet<'a>>,
207        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
208        etsi: alloc::boxed::Box<standards::ssem_2_2_1::ssem_pdu_descriptions::SSEM>,
209    },
210    #[cfg(feature = "cpm_1")]
211    /// ETSI TR 103 562 v2.1.1 CPM
212    CpmV1 {
213        geonetworking: Option<geonetworking::Packet<'a>>,
214        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
215        etsi: alloc::boxed::Box<standards::cpm_1::cpm_pdu_descriptions::CPM>,
216    },
217    #[cfg(feature = "cpm_2_1_1")]
218    /// ETSI TS 103 324 v2.1.1 CPM
219    CpmV2 {
220        geonetworking: Option<geonetworking::Packet<'a>>,
221        transport: Option<alloc::boxed::Box<transport::TransportHeader>>,
222        etsi: alloc::boxed::Box<
223            standards::cpm_2_1_1::cpm_pdu_descriptions::CollectivePerceptionMessage,
224        >,
225    },
226}
227
228#[cfg(feature = "_etsi")]
229impl<'a> ItsMessage<'a> {
230    /// Returns the `geonetworking` field of the `ItsMessage` variant
231    #[must_use]
232    pub fn get_geonetworking(&self) -> Option<geonetworking::Packet<'a>> {
233        let (gn, _) = self.get_headers();
234        gn.clone()
235    }
236
237    /// Returns the `transport` field of the `ItsMessage` variant
238    #[must_use]
239    pub fn get_transport(&self) -> Option<alloc::boxed::Box<transport::TransportHeader>> {
240        let (_, tp) = self.get_headers();
241        tp.clone()
242    }
243
244    /// Returns the `geonetworking` and `transport` fields of the `ItsMessage` variant
245    #[must_use]
246    fn get_headers(
247        &self,
248    ) -> (
249        &Option<geonetworking::Packet<'a>>,
250        &Option<alloc::boxed::Box<transport::TransportHeader>>,
251    ) {
252        match self {
253            #[cfg(feature = "denm_1_3_1")]
254            ItsMessage::DenmV1 {
255                geonetworking,
256                transport,
257                etsi: _,
258            } => (geonetworking, transport),
259            #[cfg(feature = "denm_2_2_1")]
260            ItsMessage::DenmV2 {
261                geonetworking,
262                transport,
263                etsi: _,
264            } => (geonetworking, transport),
265            #[cfg(feature = "cam_1_4_1")]
266            ItsMessage::Cam {
267                geonetworking,
268                transport,
269                etsi: _,
270            } => (geonetworking, transport),
271            #[cfg(feature = "spatem_2_2_1")]
272            ItsMessage::Spatem {
273                geonetworking,
274                transport,
275                etsi: _,
276            } => (geonetworking, transport),
277            #[cfg(feature = "mapem_2_2_1")]
278            ItsMessage::Mapem {
279                geonetworking,
280                transport,
281                etsi: _,
282            } => (geonetworking, transport),
283            #[cfg(feature = "ivim_2_1_1")]
284            ItsMessage::IvimV1 {
285                geonetworking,
286                transport,
287                etsi: _,
288            } => (geonetworking, transport),
289            #[cfg(feature = "ivim_2_2_1")]
290            ItsMessage::IvimV2 {
291                geonetworking,
292                transport,
293                etsi: _,
294            } => (geonetworking, transport),
295            #[cfg(feature = "srem_2_2_1")]
296            ItsMessage::Srem {
297                geonetworking,
298                transport,
299                etsi: _,
300            } => (geonetworking, transport),
301            #[cfg(feature = "ssem_2_2_1")]
302            ItsMessage::Ssem {
303                geonetworking,
304                transport,
305                etsi: _,
306            } => (geonetworking, transport),
307            #[cfg(feature = "cpm_1")]
308            ItsMessage::CpmV1 {
309                geonetworking,
310                transport,
311                etsi: _,
312            } => (geonetworking, transport),
313            #[cfg(feature = "cpm_2_1_1")]
314            ItsMessage::CpmV2 {
315                geonetworking,
316                transport,
317                etsi: _,
318            } => (geonetworking, transport),
319        }
320    }
321}
322
323#[cfg(feature = "_etsi")]
324impl From<&ItsMessage<'_>> for standards::extensions::ItsMessageId {
325    fn from(val: &ItsMessage<'_>) -> Self {
326        match val {
327            #[cfg(feature = "denm_1_3_1")]
328            ItsMessage::DenmV1 {
329                geonetworking: _,
330                transport: _,
331                etsi: _,
332            } => Self::Denm,
333            #[cfg(feature = "denm_2_2_1")]
334            ItsMessage::DenmV2 {
335                geonetworking: _,
336                transport: _,
337                etsi: _,
338            } => Self::Denm,
339            #[cfg(feature = "cam_1_4_1")]
340            ItsMessage::Cam {
341                geonetworking: _,
342                transport: _,
343                etsi: _,
344            } => Self::Cam,
345            #[cfg(feature = "spatem_2_2_1")]
346            ItsMessage::Spatem {
347                geonetworking: _,
348                transport: _,
349                etsi: _,
350            } => Self::Spatem,
351            #[cfg(feature = "mapem_2_2_1")]
352            ItsMessage::Mapem {
353                geonetworking: _,
354                transport: _,
355                etsi: _,
356            } => Self::Mapem,
357            #[cfg(feature = "ivim_2_1_1")]
358            ItsMessage::IvimV1 {
359                geonetworking: _,
360                transport: _,
361                etsi: _,
362            } => Self::Ivim,
363            #[cfg(feature = "ivim_2_2_1")]
364            ItsMessage::IvimV2 {
365                geonetworking: _,
366                transport: _,
367                etsi: _,
368            } => Self::Ivim,
369            #[cfg(feature = "srem_2_2_1")]
370            ItsMessage::Srem {
371                geonetworking: _,
372                transport: _,
373                etsi: _,
374            } => Self::Srem,
375            #[cfg(feature = "ssem_2_2_1")]
376            ItsMessage::Ssem {
377                geonetworking: _,
378                transport: _,
379                etsi: _,
380            } => Self::Ssem,
381            #[cfg(feature = "cpm_1")]
382            ItsMessage::CpmV1 {
383                geonetworking: _,
384                transport: _,
385                etsi: _,
386            } => Self::Cpm,
387            #[cfg(feature = "cpm_2_1_1")]
388            ItsMessage::CpmV2 {
389                geonetworking: _,
390                transport: _,
391                etsi: _,
392            } => Self::Cpm,
393        }
394    }
395}
396
397#[cfg(feature = "_etsi")]
398#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
399#[derive(Copy, Clone, Debug, PartialEq)]
400/// Choice which message headers are present in the binary message buffer
401pub enum Headers {
402    /// No headers before V2X message
403    None,
404    /// Binary message with GeoNetworking and BTP headers
405    GnBtp,
406    /// Binary message with Radiotap, IEEE 802.11p, LLC, GeoNetworking and BTP headers
407    RadioTap802LlcGnBtp,
408    /// Binary message with IEEE 802.11p, LLC, GeoNetworking and BTP headers
409    IEEE802LlcGnBtp,
410}
411
412#[cfg(feature = "_etsi")]
413#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
414#[derive(Copy, Clone, Debug, PartialEq)]
415/// Choice of ASN.1 encoding rule
416pub enum EncodingRules {
417    UPER,
418    XER,
419    JER,
420}
421
422#[cfg(any(
423    feature = "_etsi",
424    all(target_arch = "wasm32", feature = "_etsi", feature = "json"),
425    all(test, feature = "_etsi")
426))]
427impl EncodingRules {
428    pub(crate) fn codec(self) -> rasn::Codec {
429        match self {
430            EncodingRules::UPER => rasn::Codec::Uper,
431            EncodingRules::XER => rasn::Codec::Xer,
432            EncodingRules::JER => rasn::Codec::Jer,
433        }
434    }
435}
436
437#[cfg(any(feature = "transport", feature = "_etsi"))]
438pub(crate) fn map_err_to_string<E: core::fmt::Debug>(error: E) -> alloc::string::String {
439    alloc::format!("{error:?}")
440}