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