Skip to main content

pamoja_ffi/
lorawan.rs

1//! The C ABI for LoRaWAN 1.0.x MAC framing.
2//!
3//! These functions wrap [`pamoja_lorawan`] for callers that reach the SDK through
4//! the flat C boundary: the secured frame a long-range node puts on the air, and
5//! the over-the-air activation that hands it its session keys.
6//!
7//! A session and a device hold key material, so they cross as opaque handles and
8//! the keys never leave the library once set. An encoded frame comes back as a
9//! [`PamojaBuffer`], and a decoded one as a handle carrying its recovered payload.
10//! The header flags a sender chooses are only scalars, so they cross by value as
11//! [`PamojaLorawanFlags`].
12
13use std::ptr;
14
15use pamoja_lorawan::{
16    Device, Direction, Downlink, FrameHeader, JoinAccept, JoinGrant, JoinRequest, LorawanError,
17    MessageType, PhyPayload, RxData, Session, Uplink,
18};
19
20use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
21
22/// The largest LoRaWAN frame, in bytes, this build accepts.
23pub const PAMOJA_LORAWAN_FRAME_MAX: usize = 256;
24
25/// The largest application payload, in bytes, a single frame can carry.
26pub const PAMOJA_LORAWAN_PAYLOAD_MAX: usize = 243;
27
28/// The length of a LoRaWAN key, in bytes.
29pub const PAMOJA_LORAWAN_KEY_LEN: usize = 16;
30
31/// The length of a LoRaWAN EUI, in bytes.
32pub const PAMOJA_LORAWAN_EUI_LEN: usize = 8;
33
34/// The direction a frame traveled, which its MIC and encryption both fold in.
35#[repr(C)]
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum PamojaLorawanDirection {
38    /// From an end device up to the network.
39    Uplink = 0,
40    /// From the network down to an end device.
41    Downlink = 1,
42}
43
44/// The header flags a sender sets on a data frame.
45///
46/// Each is `1` for on and `0` for off. `fpending` applies to a downlink only and
47/// is ignored when encoding an uplink, and `adr_ack_req` applies to an uplink only
48/// and is ignored when encoding a downlink.
49#[repr(C)]
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct PamojaLorawanFlags {
52    /// Ask the far end to acknowledge this frame.
53    pub confirmed: u8,
54    /// Mark the frame as taking part in adaptive data rate.
55    pub adr: u8,
56    /// Acknowledge the last confirmed frame from the far end.
57    pub ack: u8,
58    /// Tell the device more downlink data is waiting.
59    pub fpending: u8,
60    /// Ask the network to answer, because a device running adaptive data rate has gone
61    /// too long without hearing it.
62    pub adr_ack_req: u8,
63}
64
65/// An opaque handle to an activated LoRaWAN session.
66///
67/// Holds a device address and the two session keys, and never hands them back.
68/// Release it with [`pamoja_lorawan_session_free`].
69pub struct PamojaLorawanSession {
70    pub(crate) session: Session,
71}
72
73/// An opaque handle to the root credentials of a device.
74///
75/// Holds the EUIs and the application key that over-the-air activation is built
76/// on. Release it with [`pamoja_lorawan_device_free`].
77pub struct PamojaLorawanDevice {
78    pub(crate) device: Device,
79}
80
81/// An opaque handle to an accepted join.
82///
83/// Read the network settings off it, then take the session it grants with
84/// [`pamoja_lorawan_join_accept_session`]. Release it with
85/// [`pamoja_lorawan_join_accept_free`].
86pub struct PamojaLorawanJoinAccept {
87    accept: JoinAccept,
88}
89
90/// An opaque handle to a decoded data frame.
91///
92/// What a successful [`pamoja_lorawan_session_decode`] produces: the header fields
93/// and the decrypted payload. Release it with [`pamoja_lorawan_rx_free`].
94pub struct PamojaLorawanRx {
95    rx: RxData,
96}
97
98/// Creates a session from a device address and its two session keys.
99///
100/// # Arguments
101///
102/// * `dev_addr` - the device address the network assigned.
103/// * `nwk_skey` - the 16-byte network session key, which authenticates frames.
104/// * `nwk_skey_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
105/// * `app_skey` - the 16-byte application session key, which encrypts payloads.
106/// * `app_skey_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
107/// * `out_session` - receives the new session.
108///
109/// # Returns
110///
111/// [`PamojaStatus::Ok`] on success, with `*out_session` set to a handle the caller
112/// must release with [`pamoja_lorawan_session_free`], or
113/// [`PamojaStatus::InvalidArgument`] if either key is the wrong length.
114///
115/// # Safety
116///
117/// Each key pointer must point to at least its stated length in readable bytes,
118/// and `out_session` must point to a writable `*mut PamojaLorawanSession`.
119#[no_mangle]
120pub unsafe extern "C" fn pamoja_lorawan_session_new(
121    dev_addr: u32,
122    nwk_skey: *const u8,
123    nwk_skey_len: usize,
124    app_skey: *const u8,
125    app_skey_len: usize,
126    out_session: *mut *mut PamojaLorawanSession,
127) -> PamojaStatus {
128    if out_session.is_null() {
129        set_last_error("out_session must not be null".to_owned());
130        return PamojaStatus::InvalidArgument;
131    }
132    let slot = &mut *out_session;
133    *slot = ptr::null_mut();
134
135    let nwk_skey = match key(nwk_skey, nwk_skey_len, "the network session key") {
136        Ok(key) => key,
137        Err(status) => return status,
138    };
139    let app_skey = match key(app_skey, app_skey_len, "the application session key") {
140        Ok(key) => key,
141        Err(status) => return status,
142    };
143
144    *slot = Box::into_raw(Box::new(PamojaLorawanSession {
145        session: Session::new(dev_addr, nwk_skey, app_skey),
146    }));
147    PamojaStatus::Ok
148}
149
150/// Returns the device address a session is bound to.
151///
152/// # Returns
153///
154/// The device address, or 0 if `session` is null.
155///
156/// # Safety
157///
158/// `session` must be a live handle from a call that produced one, or null.
159#[no_mangle]
160pub unsafe extern "C" fn pamoja_lorawan_session_dev_addr(
161    session: *const PamojaLorawanSession,
162) -> u32 {
163    if session.is_null() {
164        return 0;
165    }
166    (*session).session.dev_addr()
167}
168
169/// Encodes an uplink data frame, encrypting the payload and appending the MIC.
170///
171/// # Arguments
172///
173/// * `session` - the activated session to send from.
174/// * `fcnt` - the frame counter for this uplink.
175/// * `fport` - the port; `0` for MAC commands, otherwise an application port.
176/// * `payload` - the application payload to carry.
177/// * `payload_len` - its length.
178/// * `fopts` - the frame options to carry in the header, at most 15 bytes.
179/// * `fopts_len` - their length.
180/// * `flags` - the header flags to set; `fpending` is ignored on an uplink.
181/// * `out_frame` - receives the encoded frame.
182///
183/// # Returns
184///
185/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller
186/// must release with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or
187/// [`PamojaStatus::InvalidArgument`] if the payload and options do not fit one
188/// frame.
189///
190/// # Safety
191///
192/// `payload` and `fopts` must each point to at least their stated lengths in
193/// readable bytes when those lengths are non-zero, and `out_frame` must point to a
194/// writable `*mut PamojaBuffer`.
195#[no_mangle]
196#[allow(clippy::too_many_arguments)]
197pub unsafe extern "C" fn pamoja_lorawan_session_encode_uplink(
198    session: *const PamojaLorawanSession,
199    fcnt: u32,
200    fport: u8,
201    payload: *const u8,
202    payload_len: usize,
203    fopts: *const u8,
204    fopts_len: usize,
205    flags: PamojaLorawanFlags,
206    out_frame: *mut *mut PamojaBuffer,
207) -> PamojaStatus {
208    encode(
209        session,
210        payload,
211        payload_len,
212        fopts,
213        fopts_len,
214        out_frame,
215        |session, payload, fopts| {
216            let mut uplink = Uplink::new(fcnt, fport, payload).with_fopts(fopts);
217            if flags.confirmed != 0 {
218                uplink = uplink.confirmed();
219            }
220            if flags.adr != 0 {
221                uplink = uplink.with_adr();
222            }
223            if flags.ack != 0 {
224                uplink = uplink.with_ack();
225            }
226            if flags.adr_ack_req != 0 {
227                uplink = uplink.with_adr_ack_req();
228            }
229            session.encode_uplink(&uplink)
230        },
231    )
232}
233
234/// Encodes a downlink data frame, encrypting the payload and appending the MIC.
235///
236/// # Arguments
237///
238/// * `session` - the session the frame is addressed to.
239/// * `fcnt` - the frame counter for this downlink.
240/// * `fport` - the port; `0` for MAC commands, otherwise an application port.
241/// * `payload` - the application payload to carry.
242/// * `payload_len` - its length.
243/// * `fopts` - the frame options to carry in the header, at most 15 bytes.
244/// * `fopts_len` - their length.
245/// * `flags` - the header flags to set.
246/// * `out_frame` - receives the encoded frame.
247///
248/// # Returns
249///
250/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller
251/// must release with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or
252/// [`PamojaStatus::InvalidArgument`] if the payload and options do not fit one
253/// frame.
254///
255/// # Safety
256///
257/// `payload` and `fopts` must each point to at least their stated lengths in
258/// readable bytes when those lengths are non-zero, and `out_frame` must point to a
259/// writable `*mut PamojaBuffer`.
260#[no_mangle]
261#[allow(clippy::too_many_arguments)]
262pub unsafe extern "C" fn pamoja_lorawan_session_encode_downlink(
263    session: *const PamojaLorawanSession,
264    fcnt: u32,
265    fport: u8,
266    payload: *const u8,
267    payload_len: usize,
268    fopts: *const u8,
269    fopts_len: usize,
270    flags: PamojaLorawanFlags,
271    out_frame: *mut *mut PamojaBuffer,
272) -> PamojaStatus {
273    encode(
274        session,
275        payload,
276        payload_len,
277        fopts,
278        fopts_len,
279        out_frame,
280        |session, payload, fopts| {
281            let mut downlink = Downlink::new(fcnt, fport, payload).with_fopts(fopts);
282            if flags.confirmed != 0 {
283                downlink = downlink.confirmed();
284            }
285            if flags.adr != 0 {
286                downlink = downlink.with_adr();
287            }
288            if flags.ack != 0 {
289                downlink = downlink.with_ack();
290            }
291            if flags.fpending != 0 {
292                downlink = downlink.with_fpending();
293            }
294            session.encode_downlink(&downlink)
295        },
296    )
297}
298
299/// Decodes a received data frame: verifies the MIC, then decrypts the payload.
300///
301/// # Arguments
302///
303/// * `session` - the session the frame belongs to.
304/// * `bytes` - the frame exactly as it came off the radio.
305/// * `bytes_len` - its length.
306/// * `fcnt` - the full 32-bit frame counter expected for this frame; its low 16
307///   bits must match the counter the frame carries.
308/// * `out_rx` - receives the decoded frame.
309///
310/// # Returns
311///
312/// [`PamojaStatus::Ok`] on success, with `*out_rx` set to a handle the caller must
313/// release with [`pamoja_lorawan_rx_free`], [`PamojaStatus::Auth`] if the MIC does
314/// not verify or the counter does not match, or [`PamojaStatus::Codec`] if the
315/// frame is truncated or is not a data frame.
316///
317/// # Safety
318///
319/// `bytes` must point to at least `bytes_len` readable bytes when that length is
320/// non-zero, and `out_rx` must point to a writable `*mut PamojaLorawanRx`.
321#[no_mangle]
322pub unsafe extern "C" fn pamoja_lorawan_session_decode(
323    session: *const PamojaLorawanSession,
324    bytes: *const u8,
325    bytes_len: usize,
326    fcnt: u32,
327    out_rx: *mut *mut PamojaLorawanRx,
328) -> PamojaStatus {
329    if out_rx.is_null() {
330        set_last_error("out_rx must not be null".to_owned());
331        return PamojaStatus::InvalidArgument;
332    }
333    let slot = &mut *out_rx;
334    *slot = ptr::null_mut();
335
336    if session.is_null() {
337        set_last_error("session must not be null".to_owned());
338        return PamojaStatus::InvalidArgument;
339    }
340    let bytes = match read_bytes(bytes, bytes_len) {
341        Ok(bytes) => bytes,
342        Err(status) => return status,
343    };
344    match (*session).session.decode(&bytes, fcnt) {
345        Ok(rx) => {
346            *slot = Box::into_raw(Box::new(PamojaLorawanRx { rx }));
347            PamojaStatus::Ok
348        }
349        Err(error) => failed(error),
350    }
351}
352
353/// Releases a session handle.
354///
355/// Passing null is a no-op.
356///
357/// # Safety
358///
359/// `session` must be a handle from a call that produced one and that has not
360/// already been freed, or null. After this call it must not be used again.
361#[no_mangle]
362pub unsafe extern "C" fn pamoja_lorawan_session_free(session: *mut PamojaLorawanSession) {
363    if !session.is_null() {
364        drop(Box::from_raw(session));
365    }
366}
367
368/// Returns the direction a decoded frame traveled.
369///
370/// # Returns
371///
372/// The direction, or [`PamojaLorawanDirection::Uplink`] if `rx` is null.
373///
374/// # Safety
375///
376/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
377#[no_mangle]
378pub unsafe extern "C" fn pamoja_lorawan_rx_direction(
379    rx: *const PamojaLorawanRx,
380) -> PamojaLorawanDirection {
381    if rx.is_null() {
382        return PamojaLorawanDirection::Uplink;
383    }
384    match (*rx).rx.direction() {
385        Direction::Uplink => PamojaLorawanDirection::Uplink,
386        Direction::Downlink => PamojaLorawanDirection::Downlink,
387    }
388}
389
390/// Returns the device address a decoded frame carries.
391///
392/// # Returns
393///
394/// The device address, or 0 if `rx` is null.
395///
396/// # Safety
397///
398/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
399#[no_mangle]
400pub unsafe extern "C" fn pamoja_lorawan_rx_dev_addr(rx: *const PamojaLorawanRx) -> u32 {
401    if rx.is_null() {
402        return 0;
403    }
404    (*rx).rx.dev_addr()
405}
406
407/// Returns the low 16 bits of the frame counter a decoded frame carries.
408///
409/// # Returns
410///
411/// The counter, or 0 if `rx` is null.
412///
413/// # Safety
414///
415/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
416#[no_mangle]
417pub unsafe extern "C" fn pamoja_lorawan_rx_fcnt(rx: *const PamojaLorawanRx) -> u16 {
418    if rx.is_null() {
419        return 0;
420    }
421    (*rx).rx.fcnt()
422}
423
424/// Reports whether a decoded frame asks to be acknowledged.
425///
426/// # Returns
427///
428/// `true` when the frame is confirmed, or `false` if `rx` is null.
429///
430/// # Safety
431///
432/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
433#[no_mangle]
434pub unsafe extern "C" fn pamoja_lorawan_rx_confirmed(rx: *const PamojaLorawanRx) -> bool {
435    !rx.is_null() && (*rx).rx.confirmed()
436}
437
438/// Reports whether a decoded frame takes part in adaptive data rate.
439///
440/// # Returns
441///
442/// `true` when the ADR bit is set, or `false` if `rx` is null.
443///
444/// # Safety
445///
446/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
447#[no_mangle]
448pub unsafe extern "C" fn pamoja_lorawan_rx_adr(rx: *const PamojaLorawanRx) -> bool {
449    !rx.is_null() && (*rx).rx.adr()
450}
451
452/// Reports whether a decoded uplink asks the network to answer.
453///
454/// A device running adaptive data rate sets this once it has gone too long without a
455/// downlink. The bit is reserved on a downlink, which reads `false`.
456///
457/// # Returns
458///
459/// `true` when the ADRACKReq bit of an uplink is set, or `false` if `rx` is null.
460///
461/// # Safety
462///
463/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
464#[no_mangle]
465pub unsafe extern "C" fn pamoja_lorawan_rx_adr_ack_req(rx: *const PamojaLorawanRx) -> bool {
466    !rx.is_null() && (*rx).rx.adr_ack_req()
467}
468
469/// Reports whether a decoded uplink came from a device running Class B.
470///
471/// # Returns
472///
473/// `true` when the ClassB bit of an uplink is set, or `false` for a downlink or if `rx`
474/// is null.
475///
476/// # Safety
477///
478/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
479#[no_mangle]
480pub unsafe extern "C" fn pamoja_lorawan_rx_class_b(rx: *const PamojaLorawanRx) -> bool {
481    !rx.is_null() && (*rx).rx.class_b()
482}
483
484/// Reports whether a decoded frame acknowledges the last confirmed one.
485///
486/// # Returns
487///
488/// `true` when the ACK bit is set, or `false` if `rx` is null.
489///
490/// # Safety
491///
492/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
493#[no_mangle]
494pub unsafe extern "C" fn pamoja_lorawan_rx_ack(rx: *const PamojaLorawanRx) -> bool {
495    !rx.is_null() && (*rx).rx.ack()
496}
497
498/// Reports whether the network has more downlink data waiting.
499///
500/// # Returns
501///
502/// `true` when the frame-pending bit is set, or `false` if `rx` is null.
503///
504/// # Safety
505///
506/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
507#[no_mangle]
508pub unsafe extern "C" fn pamoja_lorawan_rx_fpending(rx: *const PamojaLorawanRx) -> bool {
509    !rx.is_null() && (*rx).rx.fpending()
510}
511
512/// Returns the port a decoded frame was sent on.
513///
514/// # Arguments
515///
516/// * `rx` - the decoded frame.
517/// * `out_fport` - receives the port.
518///
519/// # Returns
520///
521/// `true` when the frame carries a port, with `*out_fport` written, or `false`
522/// for a frame that carries only frame options and so has none.
523///
524/// # Safety
525///
526/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null,
527/// and `out_fport` must point to a writable `uint8_t`.
528#[no_mangle]
529pub unsafe extern "C" fn pamoja_lorawan_rx_fport(
530    rx: *const PamojaLorawanRx,
531    out_fport: *mut u8,
532) -> bool {
533    if rx.is_null() || out_fport.is_null() {
534        return false;
535    }
536    match (*rx).rx.fport() {
537        Some(fport) => {
538            *out_fport = fport;
539            true
540        }
541        None => false,
542    }
543}
544
545/// Returns a pointer to the frame options a decoded frame carries.
546///
547/// Use [`pamoja_lorawan_rx_fopts_len`] for the length. The pointer is valid until
548/// the frame is freed.
549///
550/// # Returns
551///
552/// A pointer to the options, or null if `rx` is null or there are none.
553///
554/// # Safety
555///
556/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
557#[no_mangle]
558pub unsafe extern "C" fn pamoja_lorawan_rx_fopts(rx: *const PamojaLorawanRx) -> *const u8 {
559    if rx.is_null() {
560        return ptr::null();
561    }
562    let fopts = (*rx).rx.fopts();
563    if fopts.is_empty() {
564        ptr::null()
565    } else {
566        fopts.as_ptr()
567    }
568}
569
570/// Returns the length in bytes of the frame options a decoded frame carries.
571///
572/// # Returns
573///
574/// The length, or 0 if `rx` is null.
575///
576/// # Safety
577///
578/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
579#[no_mangle]
580pub unsafe extern "C" fn pamoja_lorawan_rx_fopts_len(rx: *const PamojaLorawanRx) -> usize {
581    if rx.is_null() {
582        return 0;
583    }
584    (*rx).rx.fopts().len()
585}
586
587/// Returns a pointer to the decrypted payload of a decoded frame.
588///
589/// Use [`pamoja_lorawan_rx_payload_len`] for the length. The pointer is valid
590/// until the frame is freed.
591///
592/// # Returns
593///
594/// A pointer to the payload, or null if `rx` is null or the payload is empty.
595///
596/// # Safety
597///
598/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
599#[no_mangle]
600pub unsafe extern "C" fn pamoja_lorawan_rx_payload(rx: *const PamojaLorawanRx) -> *const u8 {
601    if rx.is_null() {
602        return ptr::null();
603    }
604    let payload = (*rx).rx.payload();
605    if payload.is_empty() {
606        ptr::null()
607    } else {
608        payload.as_ptr()
609    }
610}
611
612/// Returns the length in bytes of the decrypted payload of a decoded frame.
613///
614/// # Returns
615///
616/// The payload length, or 0 if `rx` is null.
617///
618/// # Safety
619///
620/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
621#[no_mangle]
622pub unsafe extern "C" fn pamoja_lorawan_rx_payload_len(rx: *const PamojaLorawanRx) -> usize {
623    if rx.is_null() {
624        return 0;
625    }
626    (*rx).rx.payload().len()
627}
628
629/// Releases a decoded frame handle.
630///
631/// Passing null is a no-op.
632///
633/// # Safety
634///
635/// `rx` must be a handle from [`pamoja_lorawan_session_decode`] that has not
636/// already been freed, or null. After this call it must not be used again.
637#[no_mangle]
638pub unsafe extern "C" fn pamoja_lorawan_rx_free(rx: *mut PamojaLorawanRx) {
639    if !rx.is_null() {
640        drop(Box::from_raw(rx));
641    }
642}
643
644/// Creates a device from the root credentials over-the-air activation uses.
645///
646/// # Arguments
647///
648/// * `dev_eui` - the 8-byte device EUI.
649/// * `dev_eui_len` - its length, which must be [`PAMOJA_LORAWAN_EUI_LEN`].
650/// * `app_eui` - the 8-byte application (join) EUI.
651/// * `app_eui_len` - its length, which must be [`PAMOJA_LORAWAN_EUI_LEN`].
652/// * `app_key` - the 16-byte application key the join exchange is secured with.
653/// * `app_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
654/// * `out_device` - receives the new device.
655///
656/// # Returns
657///
658/// [`PamojaStatus::Ok`] on success, with `*out_device` set to a handle the caller
659/// must release with [`pamoja_lorawan_device_free`], or
660/// [`PamojaStatus::InvalidArgument`] if any credential is the wrong length.
661///
662/// # Safety
663///
664/// Each pointer must point to at least its stated length in readable bytes, and
665/// `out_device` must point to a writable `*mut PamojaLorawanDevice`.
666#[no_mangle]
667pub unsafe extern "C" fn pamoja_lorawan_device_new(
668    dev_eui: *const u8,
669    dev_eui_len: usize,
670    app_eui: *const u8,
671    app_eui_len: usize,
672    app_key: *const u8,
673    app_key_len: usize,
674    out_device: *mut *mut PamojaLorawanDevice,
675) -> PamojaStatus {
676    if out_device.is_null() {
677        set_last_error("out_device must not be null".to_owned());
678        return PamojaStatus::InvalidArgument;
679    }
680    let slot = &mut *out_device;
681    *slot = ptr::null_mut();
682
683    let dev_eui = match eui(dev_eui, dev_eui_len, "the device EUI") {
684        Ok(eui) => eui,
685        Err(status) => return status,
686    };
687    let app_eui = match eui(app_eui, app_eui_len, "the application EUI") {
688        Ok(eui) => eui,
689        Err(status) => return status,
690    };
691    let app_key = match key(app_key, app_key_len, "the application key") {
692        Ok(key) => key,
693        Err(status) => return status,
694    };
695
696    *slot = Box::into_raw(Box::new(PamojaLorawanDevice {
697        device: Device::new(dev_eui, app_eui, app_key),
698    }));
699    PamojaStatus::Ok
700}
701
702/// Builds the join request a device broadcasts to activate.
703///
704/// # Arguments
705///
706/// * `device` - the device to activate.
707/// * `dev_nonce` - a nonce that must never repeat for this device, since the
708///   network rejects a replayed one.
709/// * `out_frame` - receives the encoded join request.
710///
711/// # Returns
712///
713/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller
714/// must release with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or
715/// [`PamojaStatus::InvalidArgument`] if `device` is null.
716///
717/// # Safety
718///
719/// `device` must be a live handle from [`pamoja_lorawan_device_new`], or null, and
720/// `out_frame` must point to a writable `*mut PamojaBuffer`.
721#[no_mangle]
722pub unsafe extern "C" fn pamoja_lorawan_device_join_request(
723    device: *const PamojaLorawanDevice,
724    dev_nonce: u16,
725    out_frame: *mut *mut PamojaBuffer,
726) -> PamojaStatus {
727    if out_frame.is_null() {
728        set_last_error("out_frame must not be null".to_owned());
729        return PamojaStatus::InvalidArgument;
730    }
731    let slot = &mut *out_frame;
732    *slot = ptr::null_mut();
733
734    if device.is_null() {
735        set_last_error("device must not be null".to_owned());
736        return PamojaStatus::InvalidArgument;
737    }
738    let request = (*device).device.join_request(dev_nonce);
739    *slot = PamojaBuffer::into_raw(request.as_bytes().to_vec());
740    PamojaStatus::Ok
741}
742
743/// Turns the join accept a network sent into the settings it grants.
744///
745/// # Arguments
746///
747/// * `device` - the device that sent the join request.
748/// * `bytes` - the join accept exactly as it arrived.
749/// * `bytes_len` - its length.
750/// * `dev_nonce` - the nonce the matching join request carried.
751/// * `out_accept` - receives the accepted join.
752///
753/// # Returns
754///
755/// [`PamojaStatus::Ok`] on success, with `*out_accept` set to a handle the caller
756/// must release with [`pamoja_lorawan_join_accept_free`],
757/// [`PamojaStatus::Auth`] if the MIC does not verify, or
758/// [`PamojaStatus::Codec`] if the frame is truncated or is not a join accept.
759///
760/// # Safety
761///
762/// `device` must be a live handle from [`pamoja_lorawan_device_new`], or null,
763/// `bytes` must point to at least `bytes_len` readable bytes when that length is
764/// non-zero, and `out_accept` must point to a writable
765/// `*mut PamojaLorawanJoinAccept`.
766#[no_mangle]
767pub unsafe extern "C" fn pamoja_lorawan_device_accept_join(
768    device: *const PamojaLorawanDevice,
769    bytes: *const u8,
770    bytes_len: usize,
771    dev_nonce: u16,
772    out_accept: *mut *mut PamojaLorawanJoinAccept,
773) -> PamojaStatus {
774    if out_accept.is_null() {
775        set_last_error("out_accept must not be null".to_owned());
776        return PamojaStatus::InvalidArgument;
777    }
778    let slot = &mut *out_accept;
779    *slot = ptr::null_mut();
780
781    if device.is_null() {
782        set_last_error("device must not be null".to_owned());
783        return PamojaStatus::InvalidArgument;
784    }
785    let bytes = match read_bytes(bytes, bytes_len) {
786        Ok(bytes) => bytes,
787        Err(status) => return status,
788    };
789    match (*device).device.accept_join(&bytes, dev_nonce) {
790        Ok(accept) => {
791            *slot = Box::into_raw(Box::new(PamojaLorawanJoinAccept { accept }));
792            PamojaStatus::Ok
793        }
794        Err(error) => failed(error),
795    }
796}
797
798/// Copies the device EUI a device was created with.
799///
800/// # Arguments
801///
802/// * `device` - the device.
803/// * `out_dev_eui` - receives [`PAMOJA_LORAWAN_EUI_LEN`] bytes, most-significant byte
804///   first.
805///
806/// # Returns
807///
808/// `true` when the identifier was written, or `false` if either pointer is null.
809///
810/// # Safety
811///
812/// `device` must be a live handle from [`pamoja_lorawan_device_new`], or null, and
813/// `out_dev_eui` must point to at least [`PAMOJA_LORAWAN_EUI_LEN`] writable bytes.
814#[no_mangle]
815pub unsafe extern "C" fn pamoja_lorawan_device_dev_eui(
816    device: *const PamojaLorawanDevice,
817    out_dev_eui: *mut u8,
818) -> bool {
819    if device.is_null() || out_dev_eui.is_null() {
820        return false;
821    }
822    let eui = (*device).device.dev_eui();
823    ptr::copy_nonoverlapping(eui.as_ptr(), out_dev_eui, PAMOJA_LORAWAN_EUI_LEN);
824    true
825}
826
827/// Releases a device handle.
828///
829/// Passing null is a no-op.
830///
831/// # Safety
832///
833/// `device` must be a handle from [`pamoja_lorawan_device_new`] that has not
834/// already been freed, or null. After this call it must not be used again.
835#[no_mangle]
836pub unsafe extern "C" fn pamoja_lorawan_device_free(device: *mut PamojaLorawanDevice) {
837    if !device.is_null() {
838        drop(Box::from_raw(device));
839    }
840}
841
842/// Returns the device address a join grants.
843///
844/// # Returns
845///
846/// The device address, or 0 if `accept` is null.
847///
848/// # Safety
849///
850/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
851/// null.
852#[no_mangle]
853pub unsafe extern "C" fn pamoja_lorawan_join_accept_dev_addr(
854    accept: *const PamojaLorawanJoinAccept,
855) -> u32 {
856    if accept.is_null() {
857        return 0;
858    }
859    (*accept).accept.dev_addr()
860}
861
862/// Returns the identifier of the network that accepted a join.
863///
864/// # Returns
865///
866/// The network identifier, or 0 if `accept` is null.
867///
868/// # Safety
869///
870/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
871/// null.
872#[no_mangle]
873pub unsafe extern "C" fn pamoja_lorawan_join_accept_net_id(
874    accept: *const PamojaLorawanJoinAccept,
875) -> u32 {
876    if accept.is_null() {
877        return 0;
878    }
879    (*accept).accept.net_id()
880}
881
882/// Returns the downlink settings byte a join grants.
883///
884/// # Returns
885///
886/// The settings byte, which carries the second receive window data rate and the
887/// first window offset, or 0 if `accept` is null.
888///
889/// # Safety
890///
891/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
892/// null.
893#[no_mangle]
894pub unsafe extern "C" fn pamoja_lorawan_join_accept_dl_settings(
895    accept: *const PamojaLorawanJoinAccept,
896) -> u8 {
897    if accept.is_null() {
898        return 0;
899    }
900    (*accept).accept.dl_settings()
901}
902
903/// Returns the delay before the first receive window, in seconds.
904///
905/// # Returns
906///
907/// The delay, or 0 if `accept` is null.
908///
909/// # Safety
910///
911/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
912/// null.
913#[no_mangle]
914pub unsafe extern "C" fn pamoja_lorawan_join_accept_rx_delay(
915    accept: *const PamojaLorawanJoinAccept,
916) -> u8 {
917    if accept.is_null() {
918        return 0;
919    }
920    (*accept).accept.rx_delay()
921}
922
923/// Returns how far below the uplink's data rate the first receive window listens.
924///
925/// # Returns
926///
927/// The RX1DROffset field of the downlink settings byte, or 0 if `accept` is null.
928///
929/// # Safety
930///
931/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
932/// null.
933#[no_mangle]
934pub unsafe extern "C" fn pamoja_lorawan_join_accept_rx1_dr_offset(
935    accept: *const PamojaLorawanJoinAccept,
936) -> u8 {
937    if accept.is_null() {
938        return 0;
939    }
940    (*accept).accept.rx1_dr_offset()
941}
942
943/// Returns the data rate the second receive window listens at.
944///
945/// # Returns
946///
947/// The RX2DataRate field of the downlink settings byte, or 0 if `accept` is null.
948///
949/// # Safety
950///
951/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
952/// null.
953#[no_mangle]
954pub unsafe extern "C" fn pamoja_lorawan_join_accept_rx2_data_rate(
955    accept: *const PamojaLorawanJoinAccept,
956) -> u8 {
957    if accept.is_null() {
958        return 0;
959    }
960    (*accept).accept.rx2_data_rate()
961}
962
963/// Returns the delay from the end of an uplink to the first receive window.
964///
965/// # Returns
966///
967/// The delay in microseconds, where an RxDelay of 0 means one second as the
968/// specification says, or 0 if `accept` is null.
969///
970/// # Safety
971///
972/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
973/// null.
974#[no_mangle]
975pub unsafe extern "C" fn pamoja_lorawan_join_accept_receive_delay_us(
976    accept: *const PamojaLorawanJoinAccept,
977) -> u32 {
978    if accept.is_null() {
979        return 0;
980    }
981    (*accept).accept.receive_delay_us()
982}
983
984/// Copies the channel list a join accept carried.
985///
986/// # Arguments
987///
988/// * `accept` - the accepted join.
989/// * `out_cflist` - receives the 16 CFList bytes exactly as they arrived.
990///
991/// # Returns
992///
993/// `true` when the accept carried a channel list and it was written, or `false` when it
994/// carried none or either pointer is null.
995///
996/// # Safety
997///
998/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or null,
999/// and `out_cflist` must point to at least 16 writable bytes.
1000#[no_mangle]
1001pub unsafe extern "C" fn pamoja_lorawan_join_accept_cflist(
1002    accept: *const PamojaLorawanJoinAccept,
1003    out_cflist: *mut u8,
1004) -> bool {
1005    if accept.is_null() || out_cflist.is_null() {
1006        return false;
1007    }
1008    match (*accept).accept.cflist() {
1009        Some(list) => {
1010            let bytes = list.to_bytes();
1011            ptr::copy_nonoverlapping(bytes.as_ptr(), out_cflist, bytes.len());
1012            true
1013        }
1014        None => false,
1015    }
1016}
1017
1018/// Takes the activated session a join grants.
1019///
1020/// # Arguments
1021///
1022/// * `accept` - the accepted join.
1023/// * `out_session` - receives the session.
1024///
1025/// # Returns
1026///
1027/// [`PamojaStatus::Ok`] on success, with `*out_session` set to a handle the caller
1028/// must release with [`pamoja_lorawan_session_free`], or
1029/// [`PamojaStatus::InvalidArgument`] if `accept` is null.
1030///
1031/// # Safety
1032///
1033/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
1034/// null, and `out_session` must point to a writable `*mut PamojaLorawanSession`.
1035#[no_mangle]
1036pub unsafe extern "C" fn pamoja_lorawan_join_accept_session(
1037    accept: *const PamojaLorawanJoinAccept,
1038    out_session: *mut *mut PamojaLorawanSession,
1039) -> PamojaStatus {
1040    if out_session.is_null() {
1041        set_last_error("out_session must not be null".to_owned());
1042        return PamojaStatus::InvalidArgument;
1043    }
1044    let slot = &mut *out_session;
1045    *slot = ptr::null_mut();
1046
1047    if accept.is_null() {
1048        set_last_error("accept must not be null".to_owned());
1049        return PamojaStatus::InvalidArgument;
1050    }
1051    *slot = Box::into_raw(Box::new(PamojaLorawanSession {
1052        session: (*accept).accept.session(),
1053    }));
1054    PamojaStatus::Ok
1055}
1056
1057/// Releases an accepted join handle.
1058///
1059/// Passing null is a no-op.
1060///
1061/// # Safety
1062///
1063/// `accept` must be a handle from [`pamoja_lorawan_device_accept_join`] that has
1064/// not already been freed, or null. After this call it must not be used again.
1065#[no_mangle]
1066pub unsafe extern "C" fn pamoja_lorawan_join_accept_free(accept: *mut PamojaLorawanJoinAccept) {
1067    if !accept.is_null() {
1068        drop(Box::from_raw(accept));
1069    }
1070}
1071
1072/// Encodes a data frame with whichever builder the caller asked for.
1073///
1074/// # Safety
1075///
1076/// `payload` and `fopts` must each point to at least their stated lengths in
1077/// readable bytes when those lengths are non-zero, and `out_frame` must point to a
1078/// writable `*mut PamojaBuffer`.
1079unsafe fn encode(
1080    session: *const PamojaLorawanSession,
1081    payload: *const u8,
1082    payload_len: usize,
1083    fopts: *const u8,
1084    fopts_len: usize,
1085    out_frame: *mut *mut PamojaBuffer,
1086    build: impl FnOnce(&Session, &[u8], &[u8]) -> Result<PhyPayload, LorawanError>,
1087) -> PamojaStatus {
1088    if out_frame.is_null() {
1089        set_last_error("out_frame must not be null".to_owned());
1090        return PamojaStatus::InvalidArgument;
1091    }
1092    let slot = &mut *out_frame;
1093    *slot = ptr::null_mut();
1094
1095    if session.is_null() {
1096        set_last_error("session must not be null".to_owned());
1097        return PamojaStatus::InvalidArgument;
1098    }
1099    let payload = match read_bytes(payload, payload_len) {
1100        Ok(bytes) => bytes,
1101        Err(status) => return status,
1102    };
1103    let fopts = match read_bytes(fopts, fopts_len) {
1104        Ok(bytes) => bytes,
1105        Err(status) => return status,
1106    };
1107    match build(&(*session).session, &payload, &fopts) {
1108        Ok(frame) => {
1109            *slot = PamojaBuffer::into_raw(frame.as_bytes().to_vec());
1110            PamojaStatus::Ok
1111        }
1112        Err(error) => failed(error),
1113    }
1114}
1115
1116/// Copies a borrowed 16-byte key.
1117///
1118/// # Safety
1119///
1120/// `bytes` must point to at least `len` readable bytes when that length is
1121/// non-zero.
1122unsafe fn key(bytes: *const u8, len: usize, what: &str) -> Result<[u8; 16], PamojaStatus> {
1123    let bytes = read_bytes(bytes, len)?;
1124    <[u8; PAMOJA_LORAWAN_KEY_LEN]>::try_from(&bytes[..]).map_err(|_| {
1125        set_last_error(format!(
1126            "{what} must be exactly {PAMOJA_LORAWAN_KEY_LEN} bytes"
1127        ));
1128        PamojaStatus::InvalidArgument
1129    })
1130}
1131
1132/// Copies a borrowed 8-byte EUI.
1133///
1134/// # Safety
1135///
1136/// `bytes` must point to at least `len` readable bytes when that length is
1137/// non-zero.
1138unsafe fn eui(bytes: *const u8, len: usize, what: &str) -> Result<[u8; 8], PamojaStatus> {
1139    let bytes = read_bytes(bytes, len)?;
1140    <[u8; PAMOJA_LORAWAN_EUI_LEN]>::try_from(&bytes[..]).map_err(|_| {
1141        set_last_error(format!(
1142            "{what} must be exactly {PAMOJA_LORAWAN_EUI_LEN} bytes"
1143        ));
1144        PamojaStatus::InvalidArgument
1145    })
1146}
1147
1148/// Records a LoRaWAN error and classifies it.
1149///
1150/// # Arguments
1151///
1152/// * `error` - the failure the LoRaWAN crate reported.
1153///
1154/// # Returns
1155///
1156/// [`PamojaStatus::Auth`] when a frame failed its integrity or counter check,
1157/// [`PamojaStatus::InvalidArgument`] when the caller asked for a frame that cannot
1158/// be built, and [`PamojaStatus::Codec`] when a received frame could not be read.
1159pub(crate) fn failed(error: LorawanError) -> PamojaStatus {
1160    set_last_error(error.to_string());
1161    match error {
1162        LorawanError::MicMismatch | LorawanError::FcntMismatch => PamojaStatus::Auth,
1163        LorawanError::PayloadTooLong => PamojaStatus::InvalidArgument,
1164        _ => PamojaStatus::Codec,
1165    }
1166}
1167
1168/// What kind of message a frame is, read from its header.
1169#[repr(C)]
1170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1171pub enum PamojaLorawanMessageType {
1172    /// A device asking to join a network.
1173    JoinRequest = 0,
1174    /// A network admitting a device.
1175    JoinAccept = 1,
1176    /// Data from a device that does not need acknowledging.
1177    UnconfirmedUp = 2,
1178    /// Data from a device that asks to be acknowledged.
1179    ConfirmedUp = 3,
1180    /// Data to a device that does not need acknowledging.
1181    UnconfirmedDown = 4,
1182    /// Data to a device that asks to be acknowledged.
1183    ConfirmedDown = 5,
1184}
1185
1186/// What a frame says about itself before any key is involved.
1187///
1188/// Every field is a scalar, so this crosses the boundary by value. `is_data` is
1189/// `1` when `dev_addr` and `fcnt` are meaningful, which is every message type
1190/// except the two join frames, and `has_fport` is `1` when `fport` is.
1191///
1192/// Nothing here is authenticated, since checking the MIC needs the session key.
1193/// Treat it as a routing hint until [`pamoja_lorawan_session_decode`] has verified
1194/// the frame.
1195#[repr(C)]
1196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1197pub struct PamojaLorawanHeader {
1198    /// The length of the still-encrypted payload, in bytes.
1199    pub payload_len: usize,
1200    /// The device address, meaningful only when `is_data` is `1`.
1201    pub dev_addr: u32,
1202    /// The low 16 bits of the frame counter, meaningful only when `is_data` is `1`.
1203    pub fcnt: u16,
1204    /// What kind of message the frame is.
1205    pub message_type: PamojaLorawanMessageType,
1206    /// The port the frame was sent on, meaningful only when `has_fport` is `1`.
1207    pub fport: u8,
1208    /// `1` for a data frame, `0` for one of the two join frames.
1209    pub is_data: u8,
1210    /// `1` when the frame carries a port rather than only frame options.
1211    pub has_fport: u8,
1212    /// `1` when the frame asks to be acknowledged.
1213    pub confirmed: u8,
1214    /// `1` when the frame takes part in adaptive data rate.
1215    pub adr: u8,
1216    /// `1` when the frame acknowledges the last confirmed one.
1217    pub ack: u8,
1218    /// `1` when the network has more downlink data waiting.
1219    pub fpending: u8,
1220    /// How many bytes of frame options the header carries, from 0 to 15.
1221    pub fopts_len: u8,
1222    /// `1` when an uplink asks the network to answer. Reserved on a downlink, which reads
1223    /// `0`.
1224    pub adr_ack_req: u8,
1225    /// `1` when an uplink comes from a device running Class B.
1226    pub class_b: u8,
1227}
1228
1229/// What a network grants a device that joined.
1230///
1231/// Every field is a scalar, so this crosses the boundary by value. The optional
1232/// channel list is passed alongside it, since it is bytes rather than a scalar.
1233#[repr(C)]
1234#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1235pub struct PamojaLorawanGrant {
1236    /// A nonce this network must not reuse for the device; low 24 bits only.
1237    pub app_nonce: u32,
1238    /// The network identifier; low 24 bits only.
1239    pub net_id: u32,
1240    /// The address to assign the device.
1241    pub dev_addr: u32,
1242    /// The downlink settings byte.
1243    pub dl_settings: u8,
1244    /// The delay before the first receive window, in seconds.
1245    pub rx_delay: u8,
1246}
1247
1248/// An opaque handle to a verified join-request.
1249///
1250/// Read it with the `pamoja_lorawan_join_request_*` calls, then release it with
1251/// [`pamoja_lorawan_join_request_free`].
1252pub struct PamojaLorawanJoinRequest {
1253    request: JoinRequest,
1254}
1255
1256/// Reads a frame far enough to route it, without any key.
1257///
1258/// A receiver holding many sessions uses this to find which one a frame belongs
1259/// to: the device address travels in the clear, so it can be read before the
1260/// session that would verify the frame is even known.
1261///
1262/// # Arguments
1263///
1264/// * `bytes` - the raw frame as it came off the radio.
1265/// * `bytes_len` - its length.
1266/// * `out_header` - receives the header fields.
1267///
1268/// # Returns
1269///
1270/// [`PamojaStatus::Ok`] on success, with `*out_header` filled in, or
1271/// [`PamojaStatus::Codec`] if the frame is truncated, carries a message type this
1272/// build does not read, or declares more frame options than it holds.
1273///
1274/// # Safety
1275///
1276/// `bytes` must point to at least `bytes_len` readable bytes when that length is
1277/// non-zero, and `out_header` must point to a writable [`PamojaLorawanHeader`].
1278#[no_mangle]
1279pub unsafe extern "C" fn pamoja_lorawan_header_parse(
1280    bytes: *const u8,
1281    bytes_len: usize,
1282    out_header: *mut PamojaLorawanHeader,
1283) -> PamojaStatus {
1284    if out_header.is_null() {
1285        set_last_error("out_header must not be null".to_owned());
1286        return PamojaStatus::InvalidArgument;
1287    }
1288    let bytes = match read_bytes(bytes, bytes_len) {
1289        Ok(bytes) => bytes,
1290        Err(status) => return status,
1291    };
1292    let header = match FrameHeader::parse(&bytes) {
1293        Ok(header) => header,
1294        Err(error) => return failed(error),
1295    };
1296
1297    *out_header = PamojaLorawanHeader {
1298        payload_len: header.payload_len(),
1299        dev_addr: header.dev_addr().unwrap_or(0),
1300        fcnt: header.fcnt().unwrap_or(0),
1301        message_type: match header.message_type() {
1302            MessageType::JoinRequest => PamojaLorawanMessageType::JoinRequest,
1303            MessageType::JoinAccept => PamojaLorawanMessageType::JoinAccept,
1304            MessageType::UnconfirmedUp => PamojaLorawanMessageType::UnconfirmedUp,
1305            MessageType::ConfirmedUp => PamojaLorawanMessageType::ConfirmedUp,
1306            MessageType::UnconfirmedDown => PamojaLorawanMessageType::UnconfirmedDown,
1307            MessageType::ConfirmedDown => PamojaLorawanMessageType::ConfirmedDown,
1308        },
1309        fport: header.fport().unwrap_or(0),
1310        is_data: u8::from(header.message_type().is_data()),
1311        has_fport: u8::from(header.fport().is_some()),
1312        confirmed: u8::from(header.confirmed()),
1313        adr: u8::from(header.adr()),
1314        ack: u8::from(header.ack()),
1315        fpending: u8::from(header.fpending()),
1316        fopts_len: header.fopts_len() as u8,
1317        adr_ack_req: u8::from(header.adr_ack_req()),
1318        class_b: u8::from(header.class_b()),
1319    };
1320    PamojaStatus::Ok
1321}
1322
1323/// Verifies a join-request and reads the identifiers out of it.
1324///
1325/// # Arguments
1326///
1327/// * `bytes` - the raw join-request as it came off the radio.
1328/// * `bytes_len` - its length.
1329/// * `app_key` - the 16-byte application root key the device shares.
1330/// * `app_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
1331/// * `out_request` - receives the verified request.
1332///
1333/// # Returns
1334///
1335/// [`PamojaStatus::Ok`] on success, with `*out_request` set to a handle the caller
1336/// must release with [`pamoja_lorawan_join_request_free`], [`PamojaStatus::Auth`]
1337/// if the MIC does not verify, or [`PamojaStatus::Codec`] if the frame is not a
1338/// well-formed join-request.
1339///
1340/// # Safety
1341///
1342/// `bytes` and `app_key` must each point to at least their stated lengths in
1343/// readable bytes, and `out_request` must point to a writable
1344/// `*mut PamojaLorawanJoinRequest`.
1345#[no_mangle]
1346pub unsafe extern "C" fn pamoja_lorawan_join_request_parse(
1347    bytes: *const u8,
1348    bytes_len: usize,
1349    app_key: *const u8,
1350    app_key_len: usize,
1351    out_request: *mut *mut PamojaLorawanJoinRequest,
1352) -> PamojaStatus {
1353    if out_request.is_null() {
1354        set_last_error("out_request must not be null".to_owned());
1355        return PamojaStatus::InvalidArgument;
1356    }
1357    let slot = &mut *out_request;
1358    *slot = ptr::null_mut();
1359
1360    let bytes = match read_bytes(bytes, bytes_len) {
1361        Ok(bytes) => bytes,
1362        Err(status) => return status,
1363    };
1364    let app_key = match key(app_key, app_key_len, "the application key") {
1365        Ok(key) => key,
1366        Err(status) => return status,
1367    };
1368    match JoinRequest::parse(&bytes, &app_key) {
1369        Ok(request) => {
1370            *slot = Box::into_raw(Box::new(PamojaLorawanJoinRequest { request }));
1371            PamojaStatus::Ok
1372        }
1373        Err(error) => failed(error),
1374    }
1375}
1376
1377/// Copies the device identifier out of a verified join-request.
1378///
1379/// # Arguments
1380///
1381/// * `request` - the verified request.
1382/// * `out_dev_eui` - receives [`PAMOJA_LORAWAN_EUI_LEN`] bytes, most-significant
1383///   byte first.
1384///
1385/// # Returns
1386///
1387/// `true` when the identifier was written, or `false` if either pointer is null.
1388///
1389/// # Safety
1390///
1391/// `request` must be a live handle from [`pamoja_lorawan_join_request_parse`], or
1392/// null, and `out_dev_eui` must point to at least
1393/// [`PAMOJA_LORAWAN_EUI_LEN`] writable bytes.
1394#[no_mangle]
1395pub unsafe extern "C" fn pamoja_lorawan_join_request_dev_eui(
1396    request: *const PamojaLorawanJoinRequest,
1397    out_dev_eui: *mut u8,
1398) -> bool {
1399    if request.is_null() || out_dev_eui.is_null() {
1400        return false;
1401    }
1402    let eui = (*request).request.dev_eui();
1403    ptr::copy_nonoverlapping(eui.as_ptr(), out_dev_eui, PAMOJA_LORAWAN_EUI_LEN);
1404    true
1405}
1406
1407/// Copies the application identifier out of a verified join-request.
1408///
1409/// # Arguments
1410///
1411/// * `request` - the verified request.
1412/// * `out_app_eui` - receives [`PAMOJA_LORAWAN_EUI_LEN`] bytes, most-significant
1413///   byte first.
1414///
1415/// # Returns
1416///
1417/// `true` when the identifier was written, or `false` if either pointer is null.
1418///
1419/// # Safety
1420///
1421/// `request` must be a live handle from [`pamoja_lorawan_join_request_parse`], or
1422/// null, and `out_app_eui` must point to at least
1423/// [`PAMOJA_LORAWAN_EUI_LEN`] writable bytes.
1424#[no_mangle]
1425pub unsafe extern "C" fn pamoja_lorawan_join_request_app_eui(
1426    request: *const PamojaLorawanJoinRequest,
1427    out_app_eui: *mut u8,
1428) -> bool {
1429    if request.is_null() || out_app_eui.is_null() {
1430        return false;
1431    }
1432    let eui = (*request).request.app_eui();
1433    ptr::copy_nonoverlapping(eui.as_ptr(), out_app_eui, PAMOJA_LORAWAN_EUI_LEN);
1434    true
1435}
1436
1437/// Returns the nonce a verified join-request carried.
1438///
1439/// A network must remember the nonces a device has used and refuse a repeat, since
1440/// replaying one would re-derive the same session keys.
1441///
1442/// # Returns
1443///
1444/// The DevNonce, or 0 if `request` is null.
1445///
1446/// # Safety
1447///
1448/// `request` must be a live handle from [`pamoja_lorawan_join_request_parse`], or
1449/// null.
1450#[no_mangle]
1451pub unsafe extern "C" fn pamoja_lorawan_join_request_dev_nonce(
1452    request: *const PamojaLorawanJoinRequest,
1453) -> u16 {
1454    if request.is_null() {
1455        return 0;
1456    }
1457    (*request).request.dev_nonce()
1458}
1459
1460/// Releases a verified join-request handle.
1461///
1462/// Passing null is a no-op.
1463///
1464/// # Safety
1465///
1466/// `request` must be a handle from [`pamoja_lorawan_join_request_parse`] that has
1467/// not already been freed, or null. After this call it must not be used again.
1468#[no_mangle]
1469pub unsafe extern "C" fn pamoja_lorawan_join_request_free(request: *mut PamojaLorawanJoinRequest) {
1470    if !request.is_null() {
1471        drop(Box::from_raw(request));
1472    }
1473}
1474
1475/// Builds the signed join-accept a network sends to admit a device.
1476///
1477/// # Arguments
1478///
1479/// * `grant` - the address and settings to grant.
1480/// * `cflist` - the optional 16-byte channel list, or null for none.
1481/// * `cflist_len` - its length, either 0 or 16.
1482/// * `app_key` - the 16-byte application root key the device shares.
1483/// * `app_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
1484/// * `dev_nonce` - the nonce the matching join-request carried.
1485/// * `out_frame` - receives the encoded join-accept.
1486///
1487/// # Returns
1488///
1489/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller
1490/// must release with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or
1491/// [`PamojaStatus::InvalidArgument`] if the key or the channel list is the wrong
1492/// length.
1493///
1494/// # Safety
1495///
1496/// `cflist` and `app_key` must each point to at least their stated lengths in
1497/// readable bytes, and `out_frame` must point to a writable `*mut PamojaBuffer`.
1498#[no_mangle]
1499pub unsafe extern "C" fn pamoja_lorawan_grant_accept(
1500    grant: PamojaLorawanGrant,
1501    cflist: *const u8,
1502    cflist_len: usize,
1503    app_key: *const u8,
1504    app_key_len: usize,
1505    dev_nonce: u16,
1506    out_frame: *mut *mut PamojaBuffer,
1507) -> PamojaStatus {
1508    if out_frame.is_null() {
1509        set_last_error("out_frame must not be null".to_owned());
1510        return PamojaStatus::InvalidArgument;
1511    }
1512    let slot = &mut *out_frame;
1513    *slot = ptr::null_mut();
1514
1515    let (grant, app_key) = match granted(grant, cflist, cflist_len, app_key, app_key_len) {
1516        Ok(pair) => pair,
1517        Err(status) => return status,
1518    };
1519    *slot = PamojaBuffer::into_raw(grant.accept(&app_key, dev_nonce).as_bytes().to_vec());
1520    PamojaStatus::Ok
1521}
1522
1523/// Derives the session a grant activates, the same one the device computes.
1524///
1525/// # Arguments
1526///
1527/// * `grant` - the address and settings granted.
1528/// * `cflist` - the optional 16-byte channel list, or null for none.
1529/// * `cflist_len` - its length, either 0 or 16.
1530/// * `app_key` - the 16-byte application root key the device shares.
1531/// * `app_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
1532/// * `dev_nonce` - the nonce the matching join-request carried.
1533/// * `out_session` - receives the session.
1534///
1535/// # Returns
1536///
1537/// [`PamojaStatus::Ok`] on success, with `*out_session` set to a handle the caller
1538/// must release with [`pamoja_lorawan_session_free`], or
1539/// [`PamojaStatus::InvalidArgument`] if the key or the channel list is the wrong
1540/// length.
1541///
1542/// # Safety
1543///
1544/// `cflist` and `app_key` must each point to at least their stated lengths in
1545/// readable bytes, and `out_session` must point to a writable
1546/// `*mut PamojaLorawanSession`.
1547#[no_mangle]
1548pub unsafe extern "C" fn pamoja_lorawan_grant_session(
1549    grant: PamojaLorawanGrant,
1550    cflist: *const u8,
1551    cflist_len: usize,
1552    app_key: *const u8,
1553    app_key_len: usize,
1554    dev_nonce: u16,
1555    out_session: *mut *mut PamojaLorawanSession,
1556) -> PamojaStatus {
1557    if out_session.is_null() {
1558        set_last_error("out_session must not be null".to_owned());
1559        return PamojaStatus::InvalidArgument;
1560    }
1561    let slot = &mut *out_session;
1562    *slot = ptr::null_mut();
1563
1564    let (grant, app_key) = match granted(grant, cflist, cflist_len, app_key, app_key_len) {
1565        Ok(pair) => pair,
1566        Err(status) => return status,
1567    };
1568    *slot = Box::into_raw(Box::new(PamojaLorawanSession {
1569        session: grant.session(&app_key, dev_nonce),
1570    }));
1571    PamojaStatus::Ok
1572}
1573
1574/// Rebuilds a Rust grant and the key it is used with from what crossed the ABI.
1575///
1576/// # Safety
1577///
1578/// `cflist` and `app_key` must each point to at least their stated lengths in
1579/// readable bytes when those lengths are non-zero.
1580unsafe fn granted(
1581    grant: PamojaLorawanGrant,
1582    cflist: *const u8,
1583    cflist_len: usize,
1584    app_key: *const u8,
1585    app_key_len: usize,
1586) -> Result<(JoinGrant, [u8; 16]), PamojaStatus> {
1587    let app_key = key(app_key, app_key_len, "the application key")?;
1588    let mut built = JoinGrant::new(grant.app_nonce, grant.net_id, grant.dev_addr)
1589        .with_dl_settings(grant.dl_settings)
1590        .with_rx_delay(grant.rx_delay);
1591
1592    let cflist = read_bytes(cflist, cflist_len)?;
1593    if !cflist.is_empty() {
1594        let Ok(cflist) = <[u8; 16]>::try_from(&cflist[..]) else {
1595            set_last_error("the channel list must be exactly 16 bytes".to_owned());
1596            return Err(PamojaStatus::InvalidArgument);
1597        };
1598        built = built.with_cflist(cflist);
1599    }
1600    Ok((built, app_key))
1601}
1602
1603#[cfg(test)]
1604mod tests {
1605    use super::*;
1606
1607    use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1608
1609    const NWK_SKEY: [u8; 16] = [0x2B; 16];
1610    const APP_SKEY: [u8; 16] = [0x99; 16];
1611
1612    /// Every flag off, which is a plain unconfirmed frame.
1613    fn quiet() -> PamojaLorawanFlags {
1614        PamojaLorawanFlags {
1615            confirmed: 0,
1616            adr: 0,
1617            ack: 0,
1618            fpending: 0,
1619            adr_ack_req: 0,
1620        }
1621    }
1622
1623    /// Creates a session over the test keys.
1624    ///
1625    /// # Safety
1626    ///
1627    /// The returned handle must be released with [`pamoja_lorawan_session_free`].
1628    unsafe fn session() -> *mut PamojaLorawanSession {
1629        let mut session = ptr::null_mut();
1630        assert_eq!(
1631            pamoja_lorawan_session_new(
1632                0x2601_1BDA,
1633                NWK_SKEY.as_ptr(),
1634                NWK_SKEY.len(),
1635                APP_SKEY.as_ptr(),
1636                APP_SKEY.len(),
1637                &mut session
1638            ),
1639            PamojaStatus::Ok
1640        );
1641        session
1642    }
1643
1644    #[test]
1645    fn the_constants_match_the_lorawan_crate() {
1646        assert_eq!(PAMOJA_LORAWAN_FRAME_MAX, pamoja_lorawan::MAX_FRAME);
1647        assert_eq!(PAMOJA_LORAWAN_PAYLOAD_MAX, pamoja_lorawan::MAX_PAYLOAD);
1648    }
1649
1650    #[test]
1651    fn a_confirmed_uplink_round_trips_through_the_boundary() {
1652        // Safety: every pointer below is valid and every handle is released.
1653        unsafe {
1654            let session = session();
1655            assert_eq!(pamoja_lorawan_session_dev_addr(session), 0x2601_1BDA);
1656
1657            let payload = b"temp=4.8";
1658            let mut frame = ptr::null_mut();
1659            let flags = PamojaLorawanFlags {
1660                confirmed: 1,
1661                adr: 1,
1662                ..quiet()
1663            };
1664            assert_eq!(
1665                pamoja_lorawan_session_encode_uplink(
1666                    session,
1667                    42,
1668                    1,
1669                    payload.as_ptr(),
1670                    payload.len(),
1671                    ptr::null(),
1672                    0,
1673                    flags,
1674                    &mut frame
1675                ),
1676                PamojaStatus::Ok
1677            );
1678            let on_air =
1679                std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1680                    .to_vec();
1681            pamoja_buffer_free(frame);
1682
1683            let mut rx = ptr::null_mut();
1684            assert_eq!(
1685                pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 42, &mut rx),
1686                PamojaStatus::Ok
1687            );
1688            assert_eq!(
1689                pamoja_lorawan_rx_direction(rx),
1690                PamojaLorawanDirection::Uplink
1691            );
1692            assert!(pamoja_lorawan_rx_confirmed(rx));
1693            assert!(pamoja_lorawan_rx_adr(rx));
1694            assert_eq!(pamoja_lorawan_rx_fcnt(rx), 42);
1695            assert_eq!(pamoja_lorawan_rx_dev_addr(rx), 0x2601_1BDA);
1696
1697            let mut fport = 0u8;
1698            assert!(pamoja_lorawan_rx_fport(rx, &mut fport));
1699            assert_eq!(fport, 1);
1700
1701            let recovered = std::slice::from_raw_parts(
1702                pamoja_lorawan_rx_payload(rx),
1703                pamoja_lorawan_rx_payload_len(rx),
1704            );
1705            assert_eq!(recovered, payload);
1706
1707            pamoja_lorawan_rx_free(rx);
1708            pamoja_lorawan_session_free(session);
1709        }
1710    }
1711
1712    #[test]
1713    fn a_downlink_carries_its_frame_options() {
1714        // Safety: every pointer below is valid and every handle is released.
1715        unsafe {
1716            let session = session();
1717            let fopts = [0x03u8, 0x50, 0x00];
1718            let mut frame = ptr::null_mut();
1719            let flags = PamojaLorawanFlags {
1720                fpending: 1,
1721                ..quiet()
1722            };
1723            assert_eq!(
1724                pamoja_lorawan_session_encode_downlink(
1725                    session,
1726                    7,
1727                    2,
1728                    ptr::null(),
1729                    0,
1730                    fopts.as_ptr(),
1731                    fopts.len(),
1732                    flags,
1733                    &mut frame
1734                ),
1735                PamojaStatus::Ok
1736            );
1737            let on_air =
1738                std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1739                    .to_vec();
1740            pamoja_buffer_free(frame);
1741
1742            let mut rx = ptr::null_mut();
1743            assert_eq!(
1744                pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 7, &mut rx),
1745                PamojaStatus::Ok
1746            );
1747            assert_eq!(
1748                pamoja_lorawan_rx_direction(rx),
1749                PamojaLorawanDirection::Downlink
1750            );
1751            assert!(pamoja_lorawan_rx_fpending(rx));
1752            let recovered = std::slice::from_raw_parts(
1753                pamoja_lorawan_rx_fopts(rx),
1754                pamoja_lorawan_rx_fopts_len(rx),
1755            );
1756            assert_eq!(recovered, fopts);
1757            assert_eq!(pamoja_lorawan_rx_payload_len(rx), 0);
1758            assert!(pamoja_lorawan_rx_payload(rx).is_null());
1759
1760            pamoja_lorawan_rx_free(rx);
1761            pamoja_lorawan_session_free(session);
1762        }
1763    }
1764
1765    #[test]
1766    fn a_forged_frame_fails_its_integrity_check() {
1767        // Safety: every pointer below is valid and every handle is released.
1768        unsafe {
1769            let session = session();
1770            let payload = b"reading";
1771            let mut frame = ptr::null_mut();
1772            assert_eq!(
1773                pamoja_lorawan_session_encode_uplink(
1774                    session,
1775                    1,
1776                    1,
1777                    payload.as_ptr(),
1778                    payload.len(),
1779                    ptr::null(),
1780                    0,
1781                    quiet(),
1782                    &mut frame
1783                ),
1784                PamojaStatus::Ok
1785            );
1786            let mut on_air =
1787                std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1788                    .to_vec();
1789            pamoja_buffer_free(frame);
1790            let last = on_air.len() - 1;
1791            on_air[last] ^= 0xFF;
1792
1793            let mut rx = ptr::null_mut();
1794            assert_eq!(
1795                pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 1, &mut rx),
1796                PamojaStatus::Auth
1797            );
1798            assert!(rx.is_null());
1799            pamoja_lorawan_session_free(session);
1800        }
1801    }
1802
1803    #[test]
1804    fn a_counter_that_does_not_match_is_refused() {
1805        // Safety: every pointer below is valid and every handle is released.
1806        unsafe {
1807            let session = session();
1808            let payload = b"reading";
1809            let mut frame = ptr::null_mut();
1810            assert_eq!(
1811                pamoja_lorawan_session_encode_uplink(
1812                    session,
1813                    1,
1814                    1,
1815                    payload.as_ptr(),
1816                    payload.len(),
1817                    ptr::null(),
1818                    0,
1819                    quiet(),
1820                    &mut frame
1821                ),
1822                PamojaStatus::Ok
1823            );
1824            let on_air =
1825                std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1826                    .to_vec();
1827            pamoja_buffer_free(frame);
1828
1829            let mut rx = ptr::null_mut();
1830            assert_eq!(
1831                pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 2, &mut rx),
1832                PamojaStatus::Auth
1833            );
1834            assert!(rx.is_null());
1835            pamoja_lorawan_session_free(session);
1836        }
1837    }
1838
1839    #[test]
1840    fn a_key_of_the_wrong_length_is_refused() {
1841        let short = [0u8; 8];
1842        let mut session = ptr::null_mut();
1843        // Safety: the buffers and out-pointer are valid.
1844        unsafe {
1845            assert_eq!(
1846                pamoja_lorawan_session_new(
1847                    1,
1848                    short.as_ptr(),
1849                    short.len(),
1850                    APP_SKEY.as_ptr(),
1851                    APP_SKEY.len(),
1852                    &mut session
1853                ),
1854                PamojaStatus::InvalidArgument
1855            );
1856            assert!(session.is_null());
1857        }
1858    }
1859
1860    #[test]
1861    fn a_join_request_crosses_the_boundary_byte_for_byte() {
1862        let dev_eui = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
1863        let app_eui = [0x11u8, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18];
1864        let app_key = [0x2Bu8; 16];
1865        // Safety: every pointer below is valid and every handle is released.
1866        unsafe {
1867            let mut device = ptr::null_mut();
1868            assert_eq!(
1869                pamoja_lorawan_device_new(
1870                    dev_eui.as_ptr(),
1871                    dev_eui.len(),
1872                    app_eui.as_ptr(),
1873                    app_eui.len(),
1874                    app_key.as_ptr(),
1875                    app_key.len(),
1876                    &mut device
1877                ),
1878                PamojaStatus::Ok
1879            );
1880
1881            let mut request = ptr::null_mut();
1882            assert_eq!(
1883                pamoja_lorawan_device_join_request(device, 0x0102, &mut request),
1884                PamojaStatus::Ok
1885            );
1886            let bytes =
1887                std::slice::from_raw_parts(pamoja_buffer_data(request), pamoja_buffer_len(request));
1888            let reference = Device::new(dev_eui, app_eui, app_key).join_request(0x0102);
1889            assert_eq!(bytes, reference.as_bytes());
1890            pamoja_buffer_free(request);
1891
1892            // A join accept the network never signed must not activate a session.
1893            let forged = [0x20u8; 17];
1894            let mut accept = ptr::null_mut();
1895            assert_eq!(
1896                pamoja_lorawan_device_accept_join(
1897                    device,
1898                    forged.as_ptr(),
1899                    forged.len(),
1900                    0x0102,
1901                    &mut accept
1902                ),
1903                PamojaStatus::Auth
1904            );
1905            assert!(accept.is_null());
1906
1907            pamoja_lorawan_device_free(device);
1908        }
1909    }
1910
1911    #[test]
1912    fn null_handles_are_tolerated() {
1913        // Safety: every call below is documented to accept null.
1914        unsafe {
1915            assert_eq!(pamoja_lorawan_session_dev_addr(ptr::null()), 0);
1916            assert_eq!(pamoja_lorawan_rx_fcnt(ptr::null()), 0);
1917            assert!(!pamoja_lorawan_rx_confirmed(ptr::null()));
1918            assert!(pamoja_lorawan_rx_payload(ptr::null()).is_null());
1919            assert_eq!(pamoja_lorawan_join_accept_dev_addr(ptr::null()), 0);
1920            pamoja_lorawan_session_free(ptr::null_mut());
1921            pamoja_lorawan_rx_free(ptr::null_mut());
1922            pamoja_lorawan_device_free(ptr::null_mut());
1923            pamoja_lorawan_join_accept_free(ptr::null_mut());
1924        }
1925    }
1926
1927    #[test]
1928    fn a_header_routes_a_frame_before_any_key_is_known() {
1929        // Safety: every pointer below is valid and every handle is released.
1930        unsafe {
1931            let session = session();
1932            let payload = b"temp=4.8";
1933            let mut frame = ptr::null_mut();
1934            let flags = PamojaLorawanFlags {
1935                confirmed: 1,
1936                adr: 1,
1937                ..quiet()
1938            };
1939            assert_eq!(
1940                pamoja_lorawan_session_encode_uplink(
1941                    session,
1942                    42,
1943                    1,
1944                    payload.as_ptr(),
1945                    payload.len(),
1946                    ptr::null(),
1947                    0,
1948                    flags,
1949                    &mut frame
1950                ),
1951                PamojaStatus::Ok
1952            );
1953            let on_air =
1954                std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1955                    .to_vec();
1956            pamoja_buffer_free(frame);
1957
1958            let mut header = std::mem::zeroed::<PamojaLorawanHeader>();
1959            assert_eq!(
1960                pamoja_lorawan_header_parse(on_air.as_ptr(), on_air.len(), &mut header),
1961                PamojaStatus::Ok
1962            );
1963            assert_eq!(header.message_type, PamojaLorawanMessageType::ConfirmedUp);
1964            assert_eq!(header.is_data, 1);
1965            assert_eq!(header.dev_addr, 0x2601_1BDA);
1966            assert_eq!(header.fcnt, 42);
1967            assert_eq!(header.has_fport, 1);
1968            assert_eq!(header.fport, 1);
1969            assert_eq!(header.confirmed, 1);
1970            assert_eq!(header.adr, 1);
1971            assert_eq!(header.payload_len, payload.len());
1972            pamoja_lorawan_session_free(session);
1973        }
1974    }
1975
1976    #[test]
1977    fn a_network_completes_an_activation_across_the_boundary() {
1978        let dev_eui = [0x00u8, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77];
1979        let app_eui = [0x88u8, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
1980        let app_key = [0xABu8; 16];
1981        let grant = PamojaLorawanGrant {
1982            app_nonce: 0x0003_0201,
1983            net_id: 0x0006_0504,
1984            dev_addr: 0x2601_1BDA,
1985            dl_settings: 0x00,
1986            rx_delay: 0x01,
1987        };
1988        // Safety: every pointer below is valid and every handle is released.
1989        unsafe {
1990            let mut device = ptr::null_mut();
1991            assert_eq!(
1992                pamoja_lorawan_device_new(
1993                    dev_eui.as_ptr(),
1994                    dev_eui.len(),
1995                    app_eui.as_ptr(),
1996                    app_eui.len(),
1997                    app_key.as_ptr(),
1998                    app_key.len(),
1999                    &mut device
2000                ),
2001                PamojaStatus::Ok
2002            );
2003
2004            // The device asks, and this network reads the request back.
2005            let mut request_frame = ptr::null_mut();
2006            assert_eq!(
2007                pamoja_lorawan_device_join_request(device, 0x1234, &mut request_frame),
2008                PamojaStatus::Ok
2009            );
2010            let on_air = std::slice::from_raw_parts(
2011                pamoja_buffer_data(request_frame),
2012                pamoja_buffer_len(request_frame),
2013            )
2014            .to_vec();
2015            pamoja_buffer_free(request_frame);
2016
2017            let mut request = ptr::null_mut();
2018            assert_eq!(
2019                pamoja_lorawan_join_request_parse(
2020                    on_air.as_ptr(),
2021                    on_air.len(),
2022                    app_key.as_ptr(),
2023                    app_key.len(),
2024                    &mut request
2025                ),
2026                PamojaStatus::Ok
2027            );
2028            let mut read_eui = [0u8; 8];
2029            assert!(pamoja_lorawan_join_request_dev_eui(
2030                request,
2031                read_eui.as_mut_ptr()
2032            ));
2033            assert_eq!(read_eui, dev_eui);
2034            assert!(pamoja_lorawan_join_request_app_eui(
2035                request,
2036                read_eui.as_mut_ptr()
2037            ));
2038            assert_eq!(read_eui, app_eui);
2039            let dev_nonce = pamoja_lorawan_join_request_dev_nonce(request);
2040            assert_eq!(dev_nonce, 0x1234);
2041            pamoja_lorawan_join_request_free(request);
2042
2043            // This network answers, and the device activates on the reply.
2044            let mut reply = ptr::null_mut();
2045            assert_eq!(
2046                pamoja_lorawan_grant_accept(
2047                    grant,
2048                    ptr::null(),
2049                    0,
2050                    app_key.as_ptr(),
2051                    app_key.len(),
2052                    dev_nonce,
2053                    &mut reply
2054                ),
2055                PamojaStatus::Ok
2056            );
2057            let accept_bytes =
2058                std::slice::from_raw_parts(pamoja_buffer_data(reply), pamoja_buffer_len(reply))
2059                    .to_vec();
2060            pamoja_buffer_free(reply);
2061
2062            let mut accept = ptr::null_mut();
2063            assert_eq!(
2064                pamoja_lorawan_device_accept_join(
2065                    device,
2066                    accept_bytes.as_ptr(),
2067                    accept_bytes.len(),
2068                    dev_nonce,
2069                    &mut accept
2070                ),
2071                PamojaStatus::Ok
2072            );
2073            assert_eq!(pamoja_lorawan_join_accept_dev_addr(accept), grant.dev_addr);
2074            assert_eq!(pamoja_lorawan_join_accept_net_id(accept), grant.net_id);
2075            assert_eq!(pamoja_lorawan_join_accept_rx_delay(accept), grant.rx_delay);
2076
2077            // Both sides now hold a session, and each can read what the other secures.
2078            let mut device_session = ptr::null_mut();
2079            assert_eq!(
2080                pamoja_lorawan_join_accept_session(accept, &mut device_session),
2081                PamojaStatus::Ok
2082            );
2083            let mut network_session = ptr::null_mut();
2084            assert_eq!(
2085                pamoja_lorawan_grant_session(
2086                    grant,
2087                    ptr::null(),
2088                    0,
2089                    app_key.as_ptr(),
2090                    app_key.len(),
2091                    dev_nonce,
2092                    &mut network_session
2093                ),
2094                PamojaStatus::Ok
2095            );
2096
2097            let payload = b"joined";
2098            let mut uplink = ptr::null_mut();
2099            assert_eq!(
2100                pamoja_lorawan_session_encode_uplink(
2101                    device_session,
2102                    1,
2103                    1,
2104                    payload.as_ptr(),
2105                    payload.len(),
2106                    ptr::null(),
2107                    0,
2108                    quiet(),
2109                    &mut uplink
2110                ),
2111                PamojaStatus::Ok
2112            );
2113            let uplink_bytes =
2114                std::slice::from_raw_parts(pamoja_buffer_data(uplink), pamoja_buffer_len(uplink))
2115                    .to_vec();
2116            pamoja_buffer_free(uplink);
2117
2118            let mut rx = ptr::null_mut();
2119            assert_eq!(
2120                pamoja_lorawan_session_decode(
2121                    network_session,
2122                    uplink_bytes.as_ptr(),
2123                    uplink_bytes.len(),
2124                    1,
2125                    &mut rx
2126                ),
2127                PamojaStatus::Ok,
2128                "the network reads what the device it just admitted sent"
2129            );
2130            let recovered = std::slice::from_raw_parts(
2131                pamoja_lorawan_rx_payload(rx),
2132                pamoja_lorawan_rx_payload_len(rx),
2133            );
2134            assert_eq!(recovered, payload);
2135
2136            pamoja_lorawan_rx_free(rx);
2137            pamoja_lorawan_session_free(network_session);
2138            pamoja_lorawan_session_free(device_session);
2139            pamoja_lorawan_join_accept_free(accept);
2140            pamoja_lorawan_device_free(device);
2141        }
2142    }
2143
2144    #[test]
2145    fn a_request_signed_with_another_key_is_refused_at_the_boundary() {
2146        let app_key = [0xABu8; 16];
2147        let other = [0x00u8; 16];
2148        // Safety: every pointer below is valid and every handle is released.
2149        unsafe {
2150            let mut device = ptr::null_mut();
2151            assert_eq!(
2152                pamoja_lorawan_device_new(
2153                    [0x11u8; 8].as_ptr(),
2154                    8,
2155                    [0x22u8; 8].as_ptr(),
2156                    8,
2157                    other.as_ptr(),
2158                    other.len(),
2159                    &mut device
2160                ),
2161                PamojaStatus::Ok
2162            );
2163            let mut frame = ptr::null_mut();
2164            pamoja_lorawan_device_join_request(device, 1, &mut frame);
2165            let on_air =
2166                std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
2167                    .to_vec();
2168            pamoja_buffer_free(frame);
2169            pamoja_lorawan_device_free(device);
2170
2171            let mut request = ptr::null_mut();
2172            assert_eq!(
2173                pamoja_lorawan_join_request_parse(
2174                    on_air.as_ptr(),
2175                    on_air.len(),
2176                    app_key.as_ptr(),
2177                    app_key.len(),
2178                    &mut request
2179                ),
2180                PamojaStatus::Auth
2181            );
2182            assert!(request.is_null());
2183        }
2184    }
2185
2186    #[test]
2187    fn the_adaptive_data_rate_bits_and_join_settings_cross_the_boundary() {
2188        // Safety: every pointer below is valid and every handle is released.
2189        unsafe {
2190            let session = session();
2191            let mut frame = ptr::null_mut();
2192            let flags = PamojaLorawanFlags {
2193                adr: 1,
2194                adr_ack_req: 1,
2195                ..quiet()
2196            };
2197            assert_eq!(
2198                pamoja_lorawan_session_encode_uplink(
2199                    session,
2200                    65,
2201                    1,
2202                    b"x".as_ptr(),
2203                    1,
2204                    ptr::null(),
2205                    0,
2206                    flags,
2207                    &mut frame
2208                ),
2209                PamojaStatus::Ok
2210            );
2211            let on_air =
2212                std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
2213                    .to_vec();
2214            pamoja_buffer_free(frame);
2215            assert_eq!(on_air[5], 0xC0, "FCtrl carries ADR and ADRACKReq");
2216
2217            let mut header = std::mem::zeroed::<PamojaLorawanHeader>();
2218            assert_eq!(
2219                pamoja_lorawan_header_parse(on_air.as_ptr(), on_air.len(), &mut header),
2220                PamojaStatus::Ok
2221            );
2222            assert_eq!((header.adr, header.adr_ack_req, header.class_b), (1, 1, 0));
2223
2224            let mut rx = ptr::null_mut();
2225            assert_eq!(
2226                pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 65, &mut rx),
2227                PamojaStatus::Ok
2228            );
2229            assert!(pamoja_lorawan_rx_adr_ack_req(rx));
2230            assert!(!pamoja_lorawan_rx_class_b(rx));
2231            pamoja_lorawan_rx_free(rx);
2232            pamoja_lorawan_session_free(session);
2233
2234            // An uplink header with the ClassB bit set, which FrameHeader reads without a
2235            // key: MHDR, DevAddr, FCtrl 0x10, FCnt, FPort, one payload byte and a MIC.
2236            let class_b = [
2237                0x40, 0xDA, 0x1B, 0x01, 0x26, 0x10, 0x01, 0x00, 0x01, 0x00, 1, 2, 3, 4,
2238            ];
2239            assert_eq!(
2240                pamoja_lorawan_header_parse(class_b.as_ptr(), class_b.len(), &mut header),
2241                PamojaStatus::Ok
2242            );
2243            assert_eq!((header.class_b, header.adr_ack_req), (1, 0));
2244
2245            // The EU868 join accept published in lora-packet issue 10.
2246            let app_key = [
2247                0xB6, 0xB5, 0x3F, 0x4A, 0x16, 0x8A, 0x7A, 0x88, 0xBD, 0xF7, 0xEA, 0x13, 0x5C, 0xE9,
2248                0xCF, 0xCA,
2249            ];
2250            let accept = [
2251                0x20, 0x4D, 0xD8, 0x5A, 0xE6, 0x08, 0xB8, 0x7F, 0xC4, 0x88, 0x99, 0x70, 0xB7, 0xD2,
2252                0x04, 0x2C, 0x9E, 0x72, 0x95, 0x9B, 0x00, 0x57, 0xAE, 0xD6, 0x09, 0x4B, 0x16, 0x00,
2253                0x3D, 0xF1, 0x2D, 0xE1, 0x45,
2254            ];
2255            let dev_eui = [0x70, 0xB3, 0xD5, 0x7E, 0xD0, 0x05, 0x12, 0x34];
2256            let mut device = ptr::null_mut();
2257            assert_eq!(
2258                pamoja_lorawan_device_new(
2259                    dev_eui.as_ptr(),
2260                    8,
2261                    [0u8; 8].as_ptr(),
2262                    8,
2263                    app_key.as_ptr(),
2264                    16,
2265                    &mut device
2266                ),
2267                PamojaStatus::Ok
2268            );
2269            let mut eui = [0u8; 8];
2270            assert!(pamoja_lorawan_device_dev_eui(device, eui.as_mut_ptr()));
2271            assert_eq!(eui, dev_eui);
2272
2273            let mut joined = ptr::null_mut();
2274            assert_eq!(
2275                pamoja_lorawan_device_accept_join(
2276                    device,
2277                    accept.as_ptr(),
2278                    accept.len(),
2279                    0xCC85,
2280                    &mut joined
2281                ),
2282                PamojaStatus::Ok
2283            );
2284            assert_eq!(pamoja_lorawan_join_accept_dl_settings(joined), 0x03);
2285            assert_eq!(pamoja_lorawan_join_accept_rx1_dr_offset(joined), 0);
2286            assert_eq!(pamoja_lorawan_join_accept_rx2_data_rate(joined), 3);
2287            assert_eq!(
2288                pamoja_lorawan_join_accept_receive_delay_us(joined),
2289                1_000_000
2290            );
2291            let mut cflist = [0u8; 16];
2292            assert!(pamoja_lorawan_join_accept_cflist(
2293                joined,
2294                cflist.as_mut_ptr()
2295            ));
2296            assert_eq!(
2297                cflist,
2298                [
2299                    0x18, 0x4F, 0x84, 0xE8, 0x56, 0x84, 0xB8, 0x5E, 0x84, 0x88, 0x66, 0x84, 0x58,
2300                    0x6E, 0x84, 0x00
2301                ]
2302            );
2303            pamoja_lorawan_join_accept_free(joined);
2304            pamoja_lorawan_device_free(device);
2305        }
2306    }
2307}