Skip to main content

pamoja_ffi/
mesh.rs

1//! The C ABI for mesh packet framing.
2//!
3//! These functions wrap [`pamoja_mesh`] for callers that reach the SDK through the
4//! flat C boundary: an addressed packet that hops node to node across a radio mesh,
5//! and the duplicate suppressor that stops a flood from circulating forever.
6//!
7//! A frame carries a payload, so it crosses as an opaque handle like every other
8//! payload-bearing type here. The duplicate cache is sized when it is built rather
9//! than by the const generic the Rust crate uses, since a const generic cannot
10//! cross a C ABI at all; [`PAMOJA_MESH_SEEN_DEFAULT_CAPACITY`] is what a caller
11//! with no reason to choose should pass.
12
13use std::ptr;
14
15use pamoja_mesh::{crc16, DynamicSeenCache, Frame, MeshError};
16
17use crate::{read_bytes, set_last_error, PamojaStatus};
18
19/// The largest mesh frame, in bytes, including its header and checksum.
20pub const PAMOJA_MESH_FRAME_MAX: usize = 250;
21
22/// The length of a mesh frame header, in bytes.
23pub const PAMOJA_MESH_HEADER_LEN: usize = 12;
24
25/// The bytes a frame spends on its header and checksum together.
26pub const PAMOJA_MESH_OVERHEAD: usize = 14;
27
28/// The largest payload a single mesh frame can carry, in bytes.
29pub const PAMOJA_MESH_PAYLOAD_MAX: usize = 236;
30
31/// The mesh protocol version this build speaks.
32pub const PAMOJA_MESH_VERSION: u8 = 1;
33
34/// The hop limit a frame starts with unless one is set.
35pub const PAMOJA_MESH_DEFAULT_HOP_LIMIT: u8 = 3;
36
37/// The destination address that means every node.
38pub const PAMOJA_MESH_BROADCAST: u32 = 0xFFFF_FFFF;
39
40/// A reasonable duplicate-cache size for a caller with no reason to choose one.
41pub const PAMOJA_MESH_SEEN_DEFAULT_CAPACITY: usize = 64;
42
43/// An opaque handle to a mesh frame.
44///
45/// Read it with the `pamoja_mesh_frame_*` calls, then release it with
46/// [`pamoja_mesh_frame_free`].
47pub struct PamojaMeshFrame {
48    frame: Frame,
49}
50
51/// An opaque handle to a cache of recently seen packets.
52///
53/// Feed it every frame a node receives; it answers whether that packet is new, so
54/// a node relays each packet once however many copies reach it. Release it with
55/// [`pamoja_mesh_seen_free`].
56pub struct PamojaSeenCache {
57    seen: DynamicSeenCache,
58}
59
60/// Builds a mesh frame addressed to one node.
61///
62/// # Arguments
63///
64/// * `src` - the address of this node.
65/// * `dst` - the address the frame is for, or [`PAMOJA_MESH_BROADCAST`].
66/// * `id` - the sequence number identifying this packet from this source.
67/// * `payload` - the bytes to carry.
68/// * `payload_len` - the payload length in bytes.
69/// * `out_frame` - receives the new frame.
70///
71/// # Returns
72///
73/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a handle the caller
74/// must release with [`pamoja_mesh_frame_free`], or
75/// [`PamojaStatus::InvalidArgument`] if the payload is larger than
76/// [`PAMOJA_MESH_PAYLOAD_MAX`].
77///
78/// # Safety
79///
80/// `payload` must point to at least `payload_len` readable bytes when that length
81/// is non-zero, and `out_frame` must point to a writable `*mut PamojaMeshFrame`.
82#[no_mangle]
83pub unsafe extern "C" fn pamoja_mesh_frame_new(
84    src: u32,
85    dst: u32,
86    id: u16,
87    payload: *const u8,
88    payload_len: usize,
89    out_frame: *mut *mut PamojaMeshFrame,
90) -> PamojaStatus {
91    build(payload, payload_len, out_frame, |bytes| {
92        Frame::new(src, dst, id, bytes)
93    })
94}
95
96/// Builds a mesh frame addressed to every node.
97///
98/// # Arguments
99///
100/// * `src` - the address of this node.
101/// * `id` - the sequence number identifying this packet from this source.
102/// * `payload` - the bytes to carry.
103/// * `payload_len` - the payload length in bytes.
104/// * `out_frame` - receives the new frame.
105///
106/// # Returns
107///
108/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a handle the caller
109/// must release with [`pamoja_mesh_frame_free`], or
110/// [`PamojaStatus::InvalidArgument`] if the payload is too long.
111///
112/// # Safety
113///
114/// `payload` must point to at least `payload_len` readable bytes when that length
115/// is non-zero, and `out_frame` must point to a writable `*mut PamojaMeshFrame`.
116#[no_mangle]
117pub unsafe extern "C" fn pamoja_mesh_frame_broadcast(
118    src: u32,
119    id: u16,
120    payload: *const u8,
121    payload_len: usize,
122    out_frame: *mut *mut PamojaMeshFrame,
123) -> PamojaStatus {
124    build(payload, payload_len, out_frame, |bytes| {
125        Frame::broadcast(src, id, bytes)
126    })
127}
128
129/// Parses a frame received off a radio.
130///
131/// # Arguments
132///
133/// * `bytes` - the frame exactly as it arrived.
134/// * `bytes_len` - its length.
135/// * `out_frame` - receives the parsed frame.
136///
137/// # Returns
138///
139/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a handle the caller
140/// must release with [`pamoja_mesh_frame_free`], or [`PamojaStatus::Codec`] if the
141/// frame is truncated, of an unknown version, or fails its checksum.
142///
143/// # Safety
144///
145/// `bytes` must point to at least `bytes_len` readable bytes when that length is
146/// non-zero, and `out_frame` must point to a writable `*mut PamojaMeshFrame`.
147#[no_mangle]
148pub unsafe extern "C" fn pamoja_mesh_frame_parse(
149    bytes: *const u8,
150    bytes_len: usize,
151    out_frame: *mut *mut PamojaMeshFrame,
152) -> PamojaStatus {
153    build(bytes, bytes_len, out_frame, Frame::parse)
154}
155
156/// Sets the number of relays a frame may still take.
157///
158/// # Arguments
159///
160/// * `frame` - the frame to adjust.
161/// * `hop_limit` - the new hop limit.
162///
163/// # Safety
164///
165/// `frame` must be a live handle from a call that produced one, or null.
166#[no_mangle]
167pub unsafe extern "C" fn pamoja_mesh_frame_set_hop_limit(
168    frame: *mut PamojaMeshFrame,
169    hop_limit: u8,
170) {
171    if frame.is_null() {
172        return;
173    }
174    let frame = &mut *frame;
175    frame.frame = frame.frame.with_hop_limit(hop_limit);
176}
177
178/// Returns the protocol version a frame declares.
179///
180/// # Returns
181///
182/// The version, or 0 if `frame` is null.
183///
184/// # Safety
185///
186/// `frame` must be a live handle from a call that produced one, or null.
187#[no_mangle]
188pub unsafe extern "C" fn pamoja_mesh_frame_version(frame: *const PamojaMeshFrame) -> u8 {
189    if frame.is_null() {
190        return 0;
191    }
192    (*frame).frame.version()
193}
194
195/// Returns the address of the node a frame came from.
196///
197/// # Returns
198///
199/// The source address, or 0 if `frame` is null.
200///
201/// # Safety
202///
203/// `frame` must be a live handle from a call that produced one, or null.
204#[no_mangle]
205pub unsafe extern "C" fn pamoja_mesh_frame_src(frame: *const PamojaMeshFrame) -> u32 {
206    if frame.is_null() {
207        return 0;
208    }
209    (*frame).frame.src()
210}
211
212/// Returns the address a frame is addressed to.
213///
214/// # Returns
215///
216/// The destination address, or 0 if `frame` is null.
217///
218/// # Safety
219///
220/// `frame` must be a live handle from a call that produced one, or null.
221#[no_mangle]
222pub unsafe extern "C" fn pamoja_mesh_frame_dst(frame: *const PamojaMeshFrame) -> u32 {
223    if frame.is_null() {
224        return 0;
225    }
226    (*frame).frame.dst()
227}
228
229/// Returns the sequence number that identifies a packet from its source.
230///
231/// # Returns
232///
233/// The sequence number, or 0 if `frame` is null.
234///
235/// # Safety
236///
237/// `frame` must be a live handle from a call that produced one, or null.
238#[no_mangle]
239pub unsafe extern "C" fn pamoja_mesh_frame_id(frame: *const PamojaMeshFrame) -> u16 {
240    if frame.is_null() {
241        return 0;
242    }
243    (*frame).frame.id()
244}
245
246/// Returns how many further relays a frame may take.
247///
248/// # Returns
249///
250/// The hop limit, or 0 if `frame` is null.
251///
252/// # Safety
253///
254/// `frame` must be a live handle from a call that produced one, or null.
255#[no_mangle]
256pub unsafe extern "C" fn pamoja_mesh_frame_hop_limit(frame: *const PamojaMeshFrame) -> u8 {
257    if frame.is_null() {
258        return 0;
259    }
260    (*frame).frame.hop_limit()
261}
262
263/// Reports whether a frame is addressed to every node.
264///
265/// # Returns
266///
267/// `true` for a broadcast, or `false` if `frame` is null.
268///
269/// # Safety
270///
271/// `frame` must be a live handle from a call that produced one, or null.
272#[no_mangle]
273pub unsafe extern "C" fn pamoja_mesh_frame_is_broadcast(frame: *const PamojaMeshFrame) -> bool {
274    !frame.is_null() && (*frame).frame.is_broadcast()
275}
276
277/// Returns a pointer to the payload a frame carries.
278///
279/// Use [`pamoja_mesh_frame_payload_len`] for the length. The pointer is valid
280/// until the frame is freed.
281///
282/// # Returns
283///
284/// A pointer to the payload, or null if `frame` is null or the payload is empty.
285///
286/// # Safety
287///
288/// `frame` must be a live handle from a call that produced one, or null.
289#[no_mangle]
290pub unsafe extern "C" fn pamoja_mesh_frame_payload(frame: *const PamojaMeshFrame) -> *const u8 {
291    if frame.is_null() {
292        return ptr::null();
293    }
294    let payload = (*frame).frame.payload();
295    if payload.is_empty() {
296        ptr::null()
297    } else {
298        payload.as_ptr()
299    }
300}
301
302/// Returns the length in bytes of the payload a frame carries.
303///
304/// # Returns
305///
306/// The payload length, or 0 if `frame` is null.
307///
308/// # Safety
309///
310/// `frame` must be a live handle from a call that produced one, or null.
311#[no_mangle]
312pub unsafe extern "C" fn pamoja_mesh_frame_payload_len(frame: *const PamojaMeshFrame) -> usize {
313    if frame.is_null() {
314        return 0;
315    }
316    (*frame).frame.payload().len()
317}
318
319/// Returns a pointer to the whole frame as it goes on the air.
320///
321/// Use [`pamoja_mesh_frame_bytes_len`] for the length. The pointer is valid until
322/// the frame is freed.
323///
324/// # Returns
325///
326/// A pointer to the encoded frame, or null if `frame` is null.
327///
328/// # Safety
329///
330/// `frame` must be a live handle from a call that produced one, or null.
331#[no_mangle]
332pub unsafe extern "C" fn pamoja_mesh_frame_bytes(frame: *const PamojaMeshFrame) -> *const u8 {
333    if frame.is_null() {
334        return ptr::null();
335    }
336    (*frame).frame.as_bytes().as_ptr()
337}
338
339/// Returns the length in bytes of the whole frame.
340///
341/// # Returns
342///
343/// The encoded length, or 0 if `frame` is null.
344///
345/// # Safety
346///
347/// `frame` must be a live handle from a call that produced one, or null.
348#[no_mangle]
349pub unsafe extern "C" fn pamoja_mesh_frame_bytes_len(frame: *const PamojaMeshFrame) -> usize {
350    if frame.is_null() {
351        return 0;
352    }
353    (*frame).frame.as_bytes().len()
354}
355
356/// Returns the same frame with one hop spent, ready to forward.
357///
358/// # Arguments
359///
360/// * `frame` - the frame just received.
361/// * `out_frame` - receives the frame to forward.
362///
363/// # Returns
364///
365/// `true` when the frame still had a hop to spend, with `*out_frame` set to a
366/// handle the caller must release with [`pamoja_mesh_frame_free`], or `false` when
367/// its hops have run out and it must not be relayed further.
368///
369/// # Safety
370///
371/// `frame` must be a live handle from a call that produced one, or null, and
372/// `out_frame` must point to a writable `*mut PamojaMeshFrame`.
373#[no_mangle]
374pub unsafe extern "C" fn pamoja_mesh_frame_relayed(
375    frame: *const PamojaMeshFrame,
376    out_frame: *mut *mut PamojaMeshFrame,
377) -> bool {
378    if out_frame.is_null() {
379        return false;
380    }
381    let slot = &mut *out_frame;
382    *slot = ptr::null_mut();
383    if frame.is_null() {
384        return false;
385    }
386    match (*frame).frame.relayed() {
387        Some(forwarded) => {
388            *slot = Box::into_raw(Box::new(PamojaMeshFrame { frame: forwarded }));
389            true
390        }
391        None => false,
392    }
393}
394
395/// Releases a mesh frame handle.
396///
397/// Passing null is a no-op.
398///
399/// # Safety
400///
401/// `frame` must be a handle from a call that produced one and that has not already
402/// been freed, or null. After this call it must not be used again.
403#[no_mangle]
404pub unsafe extern "C" fn pamoja_mesh_frame_free(frame: *mut PamojaMeshFrame) {
405    if !frame.is_null() {
406        drop(Box::from_raw(frame));
407    }
408}
409
410/// Computes the CRC-16 a mesh frame carries.
411///
412/// # Arguments
413///
414/// * `data` - the bytes the checksum covers.
415/// * `data_len` - their length.
416///
417/// # Returns
418///
419/// The checksum, or 0 if `data` is null with a non-zero length.
420///
421/// # Safety
422///
423/// `data` must point to at least `data_len` readable bytes when that length is
424/// non-zero.
425#[no_mangle]
426pub unsafe extern "C" fn pamoja_mesh_crc16(data: *const u8, data_len: usize) -> u16 {
427    match read_bytes(data, data_len) {
428        Ok(bytes) => crc16(&bytes),
429        Err(_) => 0,
430    }
431}
432
433/// Creates an empty duplicate cache.
434///
435/// # Arguments
436///
437/// * `capacity` - how many recently seen packets to remember; pass
438///   [`PAMOJA_MESH_SEEN_DEFAULT_CAPACITY`] when there is no reason to choose. A
439///   capacity of zero remembers nothing, so every copy of a packet is relayed.
440///
441/// # Returns
442///
443/// A handle the caller must release with [`pamoja_mesh_seen_free`].
444#[no_mangle]
445pub extern "C" fn pamoja_mesh_seen_new(capacity: usize) -> *mut PamojaSeenCache {
446    Box::into_raw(Box::new(PamojaSeenCache {
447        seen: DynamicSeenCache::new(capacity),
448    }))
449}
450
451/// Reports whether a packet is currently remembered, without recording it.
452///
453/// # Arguments
454///
455/// * `cache` - the duplicate cache.
456/// * `src` - the address the packet came from.
457/// * `id` - the sequence number the packet carries.
458///
459/// # Returns
460///
461/// `true` if the packet has been seen recently, or `false` if `cache` is null.
462///
463/// # Safety
464///
465/// `cache` must be a live handle from [`pamoja_mesh_seen_new`], or null.
466#[no_mangle]
467pub unsafe extern "C" fn pamoja_mesh_seen_contains(
468    cache: *const PamojaSeenCache,
469    src: u32,
470    id: u16,
471) -> bool {
472    !cache.is_null() && (*cache).seen.contains((src, id))
473}
474
475/// Records a packet and reports whether it was new.
476///
477/// # Arguments
478///
479/// * `cache` - the duplicate cache.
480/// * `src` - the address the packet came from.
481/// * `id` - the sequence number the packet carries.
482///
483/// # Returns
484///
485/// `true` if the packet had not been seen, which is when a node should act on it
486/// and relay it, or `false` for a duplicate or a null cache.
487///
488/// # Safety
489///
490/// `cache` must be a live handle from [`pamoja_mesh_seen_new`], or null.
491#[no_mangle]
492pub unsafe extern "C" fn pamoja_mesh_seen_record(
493    cache: *mut PamojaSeenCache,
494    src: u32,
495    id: u16,
496) -> bool {
497    if cache.is_null() {
498        return false;
499    }
500    (*cache).seen.record((src, id))
501}
502
503/// Returns how many packets a duplicate cache remembers.
504///
505/// # Returns
506///
507/// The capacity it was created with, or 0 if `cache` is null.
508///
509/// # Safety
510///
511/// `cache` must be a live handle from [`pamoja_mesh_seen_new`], or null.
512#[no_mangle]
513pub unsafe extern "C" fn pamoja_mesh_seen_capacity(cache: *const PamojaSeenCache) -> usize {
514    if cache.is_null() {
515        return 0;
516    }
517    (*cache).seen.capacity()
518}
519
520/// Releases a duplicate cache handle.
521///
522/// Passing null is a no-op.
523///
524/// # Safety
525///
526/// `cache` must be a handle from [`pamoja_mesh_seen_new`] that has not already
527/// been freed, or null. After this call it must not be used again.
528#[no_mangle]
529pub unsafe extern "C" fn pamoja_mesh_seen_free(cache: *mut PamojaSeenCache) {
530    if !cache.is_null() {
531        drop(Box::from_raw(cache));
532    }
533}
534
535/// Runs a frame constructor over a borrowed buffer and hands back a handle.
536///
537/// # Safety
538///
539/// `bytes` must point to at least `len` readable bytes when that length is
540/// non-zero, and `out_frame` must point to a writable `*mut PamojaMeshFrame`.
541unsafe fn build(
542    bytes: *const u8,
543    len: usize,
544    out_frame: *mut *mut PamojaMeshFrame,
545    construct: impl FnOnce(&[u8]) -> Result<Frame, MeshError>,
546) -> PamojaStatus {
547    if out_frame.is_null() {
548        set_last_error("out_frame must not be null".to_owned());
549        return PamojaStatus::InvalidArgument;
550    }
551    let slot = &mut *out_frame;
552    *slot = ptr::null_mut();
553
554    let bytes = match read_bytes(bytes, len) {
555        Ok(bytes) => bytes,
556        Err(status) => return status,
557    };
558    match construct(&bytes) {
559        Ok(frame) => {
560            *slot = Box::into_raw(Box::new(PamojaMeshFrame { frame }));
561            PamojaStatus::Ok
562        }
563        Err(error) => failed(error),
564    }
565}
566
567/// Records a mesh error and classifies it.
568///
569/// # Arguments
570///
571/// * `error` - the failure the mesh crate reported.
572///
573/// # Returns
574///
575/// [`PamojaStatus::InvalidArgument`] when the caller asked for something no frame
576/// can hold, and [`PamojaStatus::Codec`] when a received frame could not be read.
577fn failed(error: MeshError) -> PamojaStatus {
578    set_last_error(error.to_string());
579    match error {
580        MeshError::PayloadTooLong | MeshError::FrameTooLong => PamojaStatus::InvalidArgument,
581        _ => PamojaStatus::Codec,
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    #[test]
590    fn the_constants_match_the_mesh_crate() {
591        assert_eq!(PAMOJA_MESH_FRAME_MAX, Frame::MAX_LEN);
592        assert_eq!(PAMOJA_MESH_HEADER_LEN, Frame::HEADER_LEN);
593        assert_eq!(PAMOJA_MESH_OVERHEAD, Frame::OVERHEAD);
594        assert_eq!(PAMOJA_MESH_PAYLOAD_MAX, Frame::MAX_PAYLOAD);
595        assert_eq!(PAMOJA_MESH_VERSION, Frame::VERSION);
596        assert_eq!(PAMOJA_MESH_DEFAULT_HOP_LIMIT, Frame::DEFAULT_HOP_LIMIT);
597        assert_eq!(PAMOJA_MESH_BROADCAST, pamoja_mesh::BROADCAST);
598    }
599
600    #[test]
601    fn a_broadcast_round_trips_through_the_boundary() {
602        let payload = b"level=high";
603        let mut frame = ptr::null_mut();
604        // Safety: the payload and out-pointer are both valid.
605        unsafe {
606            assert_eq!(
607                pamoja_mesh_frame_broadcast(
608                    0x1234_5678,
609                    1,
610                    payload.as_ptr(),
611                    payload.len(),
612                    &mut frame
613                ),
614                PamojaStatus::Ok
615            );
616            let on_air = std::slice::from_raw_parts(
617                pamoja_mesh_frame_bytes(frame),
618                pamoja_mesh_frame_bytes_len(frame),
619            )
620            .to_vec();
621            pamoja_mesh_frame_free(frame);
622
623            let mut received = ptr::null_mut();
624            assert_eq!(
625                pamoja_mesh_frame_parse(on_air.as_ptr(), on_air.len(), &mut received),
626                PamojaStatus::Ok
627            );
628            assert!(pamoja_mesh_frame_is_broadcast(received));
629            assert_eq!(pamoja_mesh_frame_src(received), 0x1234_5678);
630            assert_eq!(pamoja_mesh_frame_id(received), 1);
631            assert_eq!(pamoja_mesh_frame_version(received), PAMOJA_MESH_VERSION);
632            let recovered = std::slice::from_raw_parts(
633                pamoja_mesh_frame_payload(received),
634                pamoja_mesh_frame_payload_len(received),
635            );
636            assert_eq!(recovered, payload);
637            pamoja_mesh_frame_free(received);
638        }
639    }
640
641    #[test]
642    fn a_corrupt_frame_is_refused() {
643        let payload = b"reading";
644        let mut frame = ptr::null_mut();
645        // Safety: the payload and out-pointer are both valid.
646        unsafe {
647            assert_eq!(
648                pamoja_mesh_frame_new(1, 2, 7, payload.as_ptr(), payload.len(), &mut frame),
649                PamojaStatus::Ok
650            );
651            let mut on_air = std::slice::from_raw_parts(
652                pamoja_mesh_frame_bytes(frame),
653                pamoja_mesh_frame_bytes_len(frame),
654            )
655            .to_vec();
656            pamoja_mesh_frame_free(frame);
657            on_air[PAMOJA_MESH_HEADER_LEN] ^= 0xFF;
658
659            let mut received = ptr::null_mut();
660            assert_eq!(
661                pamoja_mesh_frame_parse(on_air.as_ptr(), on_air.len(), &mut received),
662                PamojaStatus::Codec
663            );
664            assert!(received.is_null());
665        }
666    }
667
668    #[test]
669    fn a_frame_stops_relaying_when_its_hops_run_out() {
670        let mut frame = ptr::null_mut();
671        // Safety: the out-pointers are valid.
672        unsafe {
673            assert_eq!(
674                pamoja_mesh_frame_broadcast(9, 1, ptr::null(), 0, &mut frame),
675                PamojaStatus::Ok
676            );
677            assert_eq!(pamoja_mesh_frame_payload_len(frame), 0);
678            assert!(pamoja_mesh_frame_payload(frame).is_null());
679            pamoja_mesh_frame_set_hop_limit(frame, 1);
680
681            let mut once = ptr::null_mut();
682            assert!(pamoja_mesh_frame_relayed(frame, &mut once));
683            assert_eq!(pamoja_mesh_frame_hop_limit(once), 0);
684
685            let mut twice = ptr::null_mut();
686            assert!(!pamoja_mesh_frame_relayed(once, &mut twice));
687            assert!(twice.is_null());
688
689            pamoja_mesh_frame_free(once);
690            pamoja_mesh_frame_free(frame);
691        }
692    }
693
694    #[test]
695    fn the_cache_recognises_a_packet_it_has_already_seen() {
696        let cache = pamoja_mesh_seen_new(PAMOJA_MESH_SEEN_DEFAULT_CAPACITY);
697        // Safety: the cache handle was just created.
698        unsafe {
699            assert!(!pamoja_mesh_seen_contains(cache, 0x42, 1));
700            assert!(pamoja_mesh_seen_record(cache, 0x42, 1));
701            assert!(pamoja_mesh_seen_contains(cache, 0x42, 1));
702            assert!(!pamoja_mesh_seen_record(cache, 0x42, 1));
703            assert!(pamoja_mesh_seen_record(cache, 0x42, 2));
704            assert_eq!(
705                pamoja_mesh_seen_capacity(cache),
706                PAMOJA_MESH_SEEN_DEFAULT_CAPACITY
707            );
708            pamoja_mesh_seen_free(cache);
709
710            // A cache sized by the caller evicts at the size it was given.
711            let small = pamoja_mesh_seen_new(2);
712            assert_eq!(pamoja_mesh_seen_capacity(small), 2);
713            assert!(pamoja_mesh_seen_record(small, 1, 1));
714            assert!(pamoja_mesh_seen_record(small, 1, 2));
715            assert!(pamoja_mesh_seen_record(small, 1, 3));
716            assert!(!pamoja_mesh_seen_contains(small, 1, 1));
717            pamoja_mesh_seen_free(small);
718        }
719    }
720
721    #[test]
722    fn the_checksum_matches_the_mesh_crate() {
723        let data = b"level=high";
724        // Safety: the buffer is valid for its length.
725        let checksum = unsafe { pamoja_mesh_crc16(data.as_ptr(), data.len()) };
726        assert_eq!(checksum, crc16(data));
727    }
728
729    #[test]
730    fn null_handles_are_tolerated() {
731        // Safety: every call below is documented to accept null.
732        unsafe {
733            assert_eq!(pamoja_mesh_frame_src(ptr::null()), 0);
734            assert_eq!(pamoja_mesh_frame_dst(ptr::null()), 0);
735            assert!(!pamoja_mesh_frame_is_broadcast(ptr::null()));
736            assert!(pamoja_mesh_frame_bytes(ptr::null()).is_null());
737            pamoja_mesh_frame_set_hop_limit(ptr::null_mut(), 3);
738            pamoja_mesh_frame_free(ptr::null_mut());
739            assert!(!pamoja_mesh_seen_record(ptr::null_mut(), 1, 1));
740            pamoja_mesh_seen_free(ptr::null_mut());
741        }
742    }
743}