Skip to main content

pamoja_ffi/
mavlink.rs

1//! The C ABI for the MAVLink wire protocol.
2//!
3//! MAVLink is the language drones speak, so talking to a PX4 or ArduPilot
4//! autopilot means putting exactly the right bytes on the wire and trusting the
5//! bytes that come back. This is that byte layer: assembling and parsing v1 and
6//! v2 frames, the CRC-16/MCRF4XX checksum every frame carries, the per-message
7//! `CRC_EXTRA` seed that catches a frame whose shape does not match what the
8//! receiver expects, and MAVLink 2 signing.
9//!
10//! # Any dialect, not only the common one
11//!
12//! A receiver has to know a message's `CRC_EXTRA` before it can check the frame
13//! carrying it. The common dialect's seeds are built in, but a vehicle running a
14//! vendor or private dialect uses ids this build has never heard of. Two things
15//! keep those reachable: [`pamoja_mavlink_message_crc_extra`] derives a seed from
16//! a message definition the way the specification does, and a
17//! [`PamojaMavlinkDialect`] table carries the results, taking precedence over the
18//! built-in registry. Nothing here is limited to the ids this crate happens to
19//! type.
20//!
21//! This module moves a message's bytes. [`mavlink_schema`](crate::mavlink_schema)
22//! is the layer above, which gives those bytes named fields, and
23//! [`mavlink_protocol`](crate::mavlink_protocol) the one above that, which turns
24//! single messages into the mission, command, and offboard exchanges.
25
26use pamoja_mavlink::dialect::{crc_extra as common_crc_extra, RawMessage};
27use pamoja_mavlink::{
28    crc16_mcrf4xx, message_crc_extra, signing, Frame, Header, MavlinkError, Parser, Signer,
29    Verifier, Version, MAX_PAYLOAD, SIGNATURE_LEN,
30};
31
32use crate::{read_bytes, read_str, set_last_error, PamojaStatus};
33
34/// The start marker of a v1 frame.
35pub const PAMOJA_MAVLINK_MAGIC_V1: u8 = pamoja_mavlink::MAGIC_V1;
36/// The start marker of a v2 frame.
37pub const PAMOJA_MAVLINK_MAGIC_V2: u8 = pamoja_mavlink::MAGIC_V2;
38/// The incompatibility flag that marks a v2 frame as signed.
39pub const PAMOJA_MAVLINK_IFLAG_SIGNED: u8 = pamoja_mavlink::IFLAG_SIGNED;
40/// The largest payload a frame can carry, in bytes.
41pub const PAMOJA_MAVLINK_MAX_PAYLOAD: usize = MAX_PAYLOAD;
42/// The largest frame, in bytes, header, checksum and signature included.
43pub const PAMOJA_MAVLINK_MAX_FRAME: usize = pamoja_mavlink::MAX_FRAME;
44/// The length of a v2 signature block, in bytes.
45pub const PAMOJA_MAVLINK_SIGNATURE_LEN: usize = SIGNATURE_LEN;
46/// The length of a signing key, in bytes.
47pub const PAMOJA_MAVLINK_KEY_LEN: usize = signing::KEY_LEN;
48/// The default window a verifier accepts a timestamp within, in microseconds.
49pub const PAMOJA_MAVLINK_DEFAULT_TIMESTAMP_WINDOW: u64 = signing::DEFAULT_TIMESTAMP_WINDOW;
50/// The Unix time MAVLink counts signing timestamps from, in seconds.
51pub const PAMOJA_MAVLINK_EPOCH_OFFSET_SECS: u64 = signing::MAVLINK_EPOCH_OFFSET_SECS;
52
53/// The original wire format, with a six-byte header.
54pub const PAMOJA_MAVLINK_VERSION_V1: u8 = 1;
55/// The current wire format, with a 24-bit message id, flags, and optional signing.
56pub const PAMOJA_MAVLINK_VERSION_V2: u8 = 2;
57
58/// The addressing fields a sender stamps on every frame.
59///
60/// A frame says who sent it, a system and a component, and where it sits in that
61/// sender's stream, so a receiver can tell a dropped frame from a quiet link.
62#[repr(C)]
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct PamojaMavlinkHeader {
65    /// The sending system's id.
66    pub system_id: u8,
67    /// The sending component's id.
68    pub component_id: u8,
69    /// The sender's sequence number, which wraps at 256.
70    pub sequence: u8,
71}
72
73impl From<PamojaMavlinkHeader> for Header {
74    fn from(header: PamojaMavlinkHeader) -> Self {
75        Header::new(header.system_id, header.component_id, header.sequence)
76    }
77}
78
79/// One field of a message definition, as the `CRC_EXTRA` derivation reads it.
80///
81/// The seed folds in each field's type name and field name in wire order, plus
82/// the element count for an array field, which is what makes it catch a peer
83/// whose idea of the message shape differs.
84#[repr(C)]
85#[derive(Clone, Copy, Debug)]
86pub struct PamojaMavlinkField {
87    /// The field's type name as the dialect writes it, such as `uint8_t`.
88    pub type_name: *const std::os::raw::c_char,
89    /// The field's name as the dialect writes it, such as `custom_mode`.
90    pub field_name: *const std::os::raw::c_char,
91    /// The element count for an array field, or `0` for a scalar.
92    pub array_len: u8,
93}
94
95/// Maps a MAVLink error onto the matching status code.
96///
97/// # Arguments
98///
99/// * `error` - the error the wire layer returned.
100///
101/// # Returns
102///
103/// The status, with the message left in the last-error slot.
104pub(crate) fn status_of(error: MavlinkError) -> PamojaStatus {
105    set_last_error(error.to_string());
106    match error {
107        MavlinkError::FrameTooShort
108        | MavlinkError::BadMagic(_)
109        | MavlinkError::Truncated
110        | MavlinkError::CrcMismatch { .. }
111        | MavlinkError::UnknownMessage(_)
112        | MavlinkError::BadPayload => PamojaStatus::Codec,
113        MavlinkError::PayloadTooLong
114        | MavlinkError::UnknownField
115        | MavlinkError::UnknownFieldType
116        | MavlinkError::DuplicateField
117        | MavlinkError::FieldTypeMismatch
118        | MavlinkError::FieldIndexOutOfRange
119        | MavlinkError::ValueOutOfRange => PamojaStatus::InvalidArgument,
120        MavlinkError::Unsigned | MavlinkError::BadSignature | MavlinkError::ReplayedTimestamp => {
121            PamojaStatus::Auth
122        }
123        MavlinkError::Closed => PamojaStatus::Closed,
124        _ => PamojaStatus::Other,
125    }
126}
127
128/// Returns the CRC-16/MCRF4XX checksum of a byte string.
129///
130/// This is the checksum every MAVLink frame carries, exposed because a host that
131/// implements part of the protocol itself still needs the same arithmetic.
132///
133/// # Arguments
134///
135/// * `bytes` - the data to checksum.
136/// * `bytes_len` - how many bytes `bytes` holds.
137///
138/// # Returns
139///
140/// The checksum, or `0` if `bytes` is null with a non-zero length.
141///
142/// # Safety
143///
144/// When `bytes_len` is non-zero, `bytes` must point to at least `bytes_len`
145/// readable bytes.
146#[no_mangle]
147pub unsafe extern "C" fn pamoja_mavlink_crc16_mcrf4xx(bytes: *const u8, bytes_len: usize) -> u16 {
148    match read_bytes(bytes, bytes_len) {
149        Ok(bytes) => crc16_mcrf4xx(&bytes),
150        Err(_) => 0,
151    }
152}
153
154/// Derives the `CRC_EXTRA` seed of a message from its definition.
155///
156/// This is what makes a dialect this build has never seen usable: given a
157/// message's name and its fields in wire order, the seed comes out the same as
158/// the one the dialect publishes, and a frame carrying that message then checks
159/// like any other.
160///
161/// # Arguments
162///
163/// * `name` - the message name, such as `HEARTBEAT`.
164/// * `fields` - the base fields in wire order; extension fields are excluded from
165///   the seed and must not be listed.
166/// * `field_count` - how many fields `fields` holds.
167/// * `out_crc_extra` - set to the seed on success.
168///
169/// # Returns
170///
171/// [`PamojaStatus::Ok`] on success.
172///
173/// # Errors
174///
175/// Returns [`PamojaStatus::InvalidArgument`] if any pointer is null or any name
176/// is not valid UTF-8.
177///
178/// # Safety
179///
180/// `name` must be a valid null-terminated string, `fields` must point to
181/// `field_count` readable entries whose own pointers are valid null-terminated
182/// strings, and `out_crc_extra` must point at writable storage for one byte.
183#[no_mangle]
184pub unsafe extern "C" fn pamoja_mavlink_message_crc_extra(
185    name: *const std::os::raw::c_char,
186    fields: *const PamojaMavlinkField,
187    field_count: usize,
188    out_crc_extra: *mut u8,
189) -> PamojaStatus {
190    if out_crc_extra.is_null() || (field_count != 0 && fields.is_null()) {
191        set_last_error("fields and out_crc_extra must not be null".to_owned());
192        return PamojaStatus::InvalidArgument;
193    }
194    let Some(name) = read_str(name, "name") else {
195        return PamojaStatus::InvalidArgument;
196    };
197
198    let described = std::slice::from_raw_parts(fields, field_count);
199    let mut parsed: Vec<(&str, &str, u8)> = Vec::with_capacity(field_count);
200    for field in described {
201        let (Some(type_name), Some(field_name)) = (
202            read_str(field.type_name, "a field type"),
203            read_str(field.field_name, "a field name"),
204        ) else {
205            return PamojaStatus::InvalidArgument;
206        };
207        parsed.push((type_name, field_name, field.array_len));
208    }
209
210    *out_crc_extra = message_crc_extra(name, &parsed);
211    PamojaStatus::Ok
212}
213
214/// Returns the `CRC_EXTRA` the common dialect publishes for a message id.
215///
216/// # Arguments
217///
218/// * `msgid` - the message id to look up.
219/// * `out_crc_extra` - set to the seed on success.
220///
221/// # Returns
222///
223/// [`PamojaStatus::Ok`] on success.
224///
225/// # Errors
226///
227/// Returns [`PamojaStatus::InvalidArgument`] if `out_crc_extra` is null, and
228/// [`PamojaStatus::Unsupported`] if the id is outside the common dialect, which
229/// is what a [`PamojaMavlinkDialect`] table is for.
230///
231/// # Safety
232///
233/// `out_crc_extra` must point at writable storage for one byte.
234#[no_mangle]
235pub unsafe extern "C" fn pamoja_mavlink_known_crc_extra(
236    msgid: u32,
237    out_crc_extra: *mut u8,
238) -> PamojaStatus {
239    if out_crc_extra.is_null() {
240        set_last_error("out_crc_extra must not be null".to_owned());
241        return PamojaStatus::InvalidArgument;
242    }
243    let Some(crc) = common_crc_extra(msgid) else {
244        set_last_error(format!("message {msgid} is not in the common dialect"));
245        return PamojaStatus::Unsupported;
246    };
247    *out_crc_extra = crc;
248    PamojaStatus::Ok
249}
250
251/// The `CRC_EXTRA` seeds of a dialect beyond the common one.
252///
253/// A handle the caller must release with [`pamoja_mavlink_dialect_free`].
254/// Entries added here are consulted before the built-in common-dialect registry,
255/// so a private dialect may also override an id the common one defines.
256pub struct PamojaMavlinkDialect {
257    seeds: Vec<(u32, u8)>,
258}
259
260impl PamojaMavlinkDialect {
261    /// Returns the seed for a message id, preferring this table.
262    ///
263    /// # Arguments
264    ///
265    /// * `msgid` - the message id to look up.
266    ///
267    /// # Returns
268    ///
269    /// The seed, or `None` if neither this table nor the common dialect has one.
270    fn crc_extra(&self, msgid: u32) -> Option<u8> {
271        self.seeds
272            .iter()
273            .find(|(id, _)| *id == msgid)
274            .map(|(_, crc)| *crc)
275            .or_else(|| common_crc_extra(msgid))
276    }
277}
278
279/// Looks a message id up in an optional dialect table, then the common dialect.
280///
281/// # Arguments
282///
283/// * `dialect` - the table to prefer, or null for the common dialect alone.
284/// * `msgid` - the message id to look up.
285///
286/// # Returns
287///
288/// The seed, or `None` if neither has one.
289///
290/// # Safety
291///
292/// `dialect` must be a live dialect handle, or null.
293unsafe fn lookup(dialect: *const PamojaMavlinkDialect, msgid: u32) -> Option<u8> {
294    match dialect.as_ref() {
295        Some(dialect) => dialect.crc_extra(msgid),
296        None => common_crc_extra(msgid),
297    }
298}
299
300/// Creates an empty dialect table.
301///
302/// # Returns
303///
304/// A handle the caller must release with [`pamoja_mavlink_dialect_free`].
305#[no_mangle]
306pub extern "C" fn pamoja_mavlink_dialect_new() -> *mut PamojaMavlinkDialect {
307    Box::into_raw(Box::new(PamojaMavlinkDialect { seeds: Vec::new() }))
308}
309
310/// Adds or replaces the `CRC_EXTRA` seed for a message id.
311///
312/// # Arguments
313///
314/// * `dialect` - the table to extend.
315/// * `msgid` - the message id.
316/// * `crc_extra` - the seed, usually from
317///   [`pamoja_mavlink_message_crc_extra`].
318///
319/// # Returns
320///
321/// [`PamojaStatus::Ok`] on success.
322///
323/// # Errors
324///
325/// Returns [`PamojaStatus::InvalidArgument`] if `dialect` is null.
326///
327/// # Safety
328///
329/// `dialect` must be a live dialect handle.
330#[no_mangle]
331pub unsafe extern "C" fn pamoja_mavlink_dialect_add(
332    dialect: *mut PamojaMavlinkDialect,
333    msgid: u32,
334    crc_extra: u8,
335) -> PamojaStatus {
336    let Some(dialect) = dialect.as_mut() else {
337        set_last_error("dialect must not be null".to_owned());
338        return PamojaStatus::InvalidArgument;
339    };
340    match dialect.seeds.iter_mut().find(|(id, _)| *id == msgid) {
341        Some(entry) => entry.1 = crc_extra,
342        None => dialect.seeds.push((msgid, crc_extra)),
343    }
344    PamojaStatus::Ok
345}
346
347/// Returns the seed a dialect resolves a message id to.
348///
349/// # Arguments
350///
351/// * `dialect` - the table to search, or null for the common dialect alone.
352/// * `msgid` - the message id to look up.
353/// * `out_crc_extra` - set to the seed on success.
354///
355/// # Returns
356///
357/// [`PamojaStatus::Ok`] on success.
358///
359/// # Errors
360///
361/// Returns [`PamojaStatus::InvalidArgument`] if `out_crc_extra` is null, and
362/// [`PamojaStatus::Unsupported`] if neither the table nor the common dialect
363/// knows the id.
364///
365/// # Safety
366///
367/// `dialect` must be a live dialect handle or null, and `out_crc_extra` must
368/// point at writable storage for one byte.
369#[no_mangle]
370pub unsafe extern "C" fn pamoja_mavlink_dialect_crc_extra(
371    dialect: *const PamojaMavlinkDialect,
372    msgid: u32,
373    out_crc_extra: *mut u8,
374) -> PamojaStatus {
375    if out_crc_extra.is_null() {
376        set_last_error("out_crc_extra must not be null".to_owned());
377        return PamojaStatus::InvalidArgument;
378    }
379    let Some(crc) = lookup(dialect, msgid) else {
380        set_last_error(format!("no dialect here defines message {msgid}"));
381        return PamojaStatus::Unsupported;
382    };
383    *out_crc_extra = crc;
384    PamojaStatus::Ok
385}
386
387/// Releases a dialect table.
388///
389/// # Arguments
390///
391/// * `dialect` - the handle to release; null is ignored.
392///
393/// # Safety
394///
395/// `dialect` must have come from [`pamoja_mavlink_dialect_new`] and must not be
396/// used afterwards.
397#[no_mangle]
398pub unsafe extern "C" fn pamoja_mavlink_dialect_free(dialect: *mut PamojaMavlinkDialect) {
399    if !dialect.is_null() {
400        drop(Box::from_raw(dialect));
401    }
402}
403
404/// One MAVLink frame, assembled or received.
405///
406/// A handle the caller must release with [`pamoja_mavlink_frame_free`].
407pub struct PamojaMavlinkFrame {
408    inner: Frame,
409}
410
411impl PamojaMavlinkFrame {
412    /// Returns the frame this handle wraps, for another module in this crate to read.
413    pub(crate) fn frame(&self) -> &Frame {
414        &self.inner
415    }
416
417    /// Moves a frame onto the heap and hands the caller its handle.
418    pub(crate) fn into_handle(inner: Frame) -> *mut Self {
419        Box::into_raw(Box::new(Self { inner }))
420    }
421}
422
423/// Assembles a frame carrying a message.
424///
425/// # Arguments
426///
427/// * `version` - [`PAMOJA_MAVLINK_VERSION_V1`] or [`PAMOJA_MAVLINK_VERSION_V2`].
428/// * `header` - the addressing fields to stamp on the frame.
429/// * `msgid` - the message id; a v1 frame only carries ids below 256.
430/// * `payload` - the message payload.
431/// * `payload_len` - how many bytes `payload` holds.
432/// * `crc_extra` - the seed for this message id.
433/// * `out_frame` - set to the frame handle on success, and to null otherwise.
434///
435/// # Returns
436///
437/// [`PamojaStatus::Ok`] on success.
438///
439/// # Errors
440///
441/// Returns [`PamojaStatus::InvalidArgument`] if `out_frame` is null, the version
442/// is neither constant, or the payload does not fit a frame.
443///
444/// # Safety
445///
446/// `payload` must point to `payload_len` readable bytes when the length is
447/// non-zero, and `out_frame` must point at writable storage for one pointer.
448#[no_mangle]
449pub unsafe extern "C" fn pamoja_mavlink_frame_encode(
450    version: u8,
451    header: PamojaMavlinkHeader,
452    msgid: u32,
453    payload: *const u8,
454    payload_len: usize,
455    crc_extra: u8,
456    out_frame: *mut *mut PamojaMavlinkFrame,
457) -> PamojaStatus {
458    if out_frame.is_null() {
459        set_last_error("out_frame must not be null".to_owned());
460        return PamojaStatus::InvalidArgument;
461    }
462    let slot = &mut *out_frame;
463    *slot = std::ptr::null_mut();
464
465    let payload = match read_bytes(payload, payload_len) {
466        Ok(payload) => payload,
467        Err(status) => return status,
468    };
469
470    let built = match version {
471        PAMOJA_MAVLINK_VERSION_V1 => Frame::encode_v1(header.into(), msgid, &payload, crc_extra),
472        PAMOJA_MAVLINK_VERSION_V2 => Frame::encode_v2(header.into(), msgid, &payload, crc_extra),
473        other => {
474            set_last_error(format!("{other} is not a MAVLink version"));
475            return PamojaStatus::InvalidArgument;
476        }
477    };
478    match built {
479        Ok(frame) => {
480            *slot = PamojaMavlinkFrame::into_handle(frame);
481            PamojaStatus::Ok
482        }
483        Err(error) => status_of(error),
484    }
485}
486
487/// Parses one frame, checking it against a known `CRC_EXTRA`.
488///
489/// # Arguments
490///
491/// * `bytes` - the frame as received.
492/// * `bytes_len` - how many bytes `bytes` holds.
493/// * `crc_extra` - the seed for the message the frame carries.
494/// * `out_frame` - set to the frame handle on success, and to null otherwise.
495///
496/// # Returns
497///
498/// [`PamojaStatus::Ok`] on success.
499///
500/// # Errors
501///
502/// Returns [`PamojaStatus::InvalidArgument`] if `out_frame` is null, and
503/// [`PamojaStatus::Codec`] if the bytes are not a whole frame or the checksum
504/// does not match, which is what rejects a frame mangled in transit.
505///
506/// # Safety
507///
508/// `bytes` must point to `bytes_len` readable bytes when the length is non-zero,
509/// and `out_frame` must point at writable storage for one pointer.
510#[no_mangle]
511pub unsafe extern "C" fn pamoja_mavlink_frame_parse(
512    bytes: *const u8,
513    bytes_len: usize,
514    crc_extra: u8,
515    out_frame: *mut *mut PamojaMavlinkFrame,
516) -> PamojaStatus {
517    if out_frame.is_null() {
518        set_last_error("out_frame must not be null".to_owned());
519        return PamojaStatus::InvalidArgument;
520    }
521    let slot = &mut *out_frame;
522    *slot = std::ptr::null_mut();
523
524    let bytes = match read_bytes(bytes, bytes_len) {
525        Ok(bytes) => bytes,
526        Err(status) => return status,
527    };
528    match Frame::parse(&bytes, crc_extra) {
529        Ok(frame) => {
530            *slot = PamojaMavlinkFrame::into_handle(frame);
531            PamojaStatus::Ok
532        }
533        Err(error) => status_of(error),
534    }
535}
536
537/// Parses one frame, looking its `CRC_EXTRA` up as it goes.
538///
539/// This is what a receiver holding many message types uses: the id comes out of
540/// the frame, and the seed comes from the dialect table or the common registry.
541///
542/// # Arguments
543///
544/// * `bytes` - the frame as received.
545/// * `bytes_len` - how many bytes `bytes` holds.
546/// * `dialect` - the dialect to prefer, or null for the common one alone.
547/// * `out_frame` - set to the frame handle on success, and to null otherwise.
548///
549/// # Returns
550///
551/// [`PamojaStatus::Ok`] on success.
552///
553/// # Errors
554///
555/// Returns [`PamojaStatus::InvalidArgument`] if `out_frame` is null, and
556/// [`PamojaStatus::Codec`] if the bytes are not a whole frame, the checksum does
557/// not match, or no dialect here knows the message id.
558///
559/// # Safety
560///
561/// `bytes` must point to `bytes_len` readable bytes when the length is non-zero,
562/// `dialect` must be a live dialect handle or null, and `out_frame` must point at
563/// writable storage for one pointer.
564#[no_mangle]
565pub unsafe extern "C" fn pamoja_mavlink_frame_parse_known(
566    bytes: *const u8,
567    bytes_len: usize,
568    dialect: *const PamojaMavlinkDialect,
569    out_frame: *mut *mut PamojaMavlinkFrame,
570) -> PamojaStatus {
571    if out_frame.is_null() {
572        set_last_error("out_frame must not be null".to_owned());
573        return PamojaStatus::InvalidArgument;
574    }
575    let slot = &mut *out_frame;
576    *slot = std::ptr::null_mut();
577
578    let bytes = match read_bytes(bytes, bytes_len) {
579        Ok(bytes) => bytes,
580        Err(status) => return status,
581    };
582    match Frame::parse_with(&bytes, |msgid| lookup(dialect, msgid)) {
583        Ok(frame) => {
584            *slot = PamojaMavlinkFrame::into_handle(frame);
585            PamojaStatus::Ok
586        }
587        Err(error) => status_of(error),
588    }
589}
590
591/// Releases a frame.
592///
593/// # Arguments
594///
595/// * `frame` - the handle to release; null is ignored.
596///
597/// # Safety
598///
599/// `frame` must have come from one of the frame constructors and must not be used
600/// afterwards.
601#[no_mangle]
602pub unsafe extern "C" fn pamoja_mavlink_frame_free(frame: *mut PamojaMavlinkFrame) {
603    if !frame.is_null() {
604        drop(Box::from_raw(frame));
605    }
606}
607
608/// Returns the wire format a frame uses.
609///
610/// # Arguments
611///
612/// * `frame` - the frame to read.
613///
614/// # Returns
615///
616/// [`PAMOJA_MAVLINK_VERSION_V1`] or [`PAMOJA_MAVLINK_VERSION_V2`], or `0` if
617/// `frame` is null.
618///
619/// # Safety
620///
621/// `frame` must be a live frame handle, or null.
622#[no_mangle]
623pub unsafe extern "C" fn pamoja_mavlink_frame_version(frame: *const PamojaMavlinkFrame) -> u8 {
624    match frame.as_ref() {
625        Some(frame) => match frame.inner.version() {
626            Version::V1 => PAMOJA_MAVLINK_VERSION_V1,
627            Version::V2 => PAMOJA_MAVLINK_VERSION_V2,
628        },
629        None => 0,
630    }
631}
632
633/// Returns the addressing fields a frame carries.
634///
635/// # Arguments
636///
637/// * `frame` - the frame to read.
638/// * `out_header` - set to the header on success.
639///
640/// # Returns
641///
642/// [`PamojaStatus::Ok`] on success.
643///
644/// # Errors
645///
646/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null.
647///
648/// # Safety
649///
650/// `frame` must be a live frame handle and `out_header` must point at writable
651/// storage for one [`PamojaMavlinkHeader`].
652#[no_mangle]
653pub unsafe extern "C" fn pamoja_mavlink_frame_header(
654    frame: *const PamojaMavlinkFrame,
655    out_header: *mut PamojaMavlinkHeader,
656) -> PamojaStatus {
657    let (Some(frame), false) = (frame.as_ref(), out_header.is_null()) else {
658        set_last_error("frame and out_header must not be null".to_owned());
659        return PamojaStatus::InvalidArgument;
660    };
661    *out_header = PamojaMavlinkHeader {
662        system_id: frame.inner.system_id(),
663        component_id: frame.inner.component_id(),
664        sequence: frame.inner.sequence(),
665    };
666    PamojaStatus::Ok
667}
668
669/// Returns the id of the message a frame carries.
670///
671/// # Arguments
672///
673/// * `frame` - the frame to read.
674///
675/// # Returns
676///
677/// The message id, or `0` if `frame` is null.
678///
679/// # Safety
680///
681/// `frame` must be a live frame handle, or null.
682#[no_mangle]
683pub unsafe extern "C" fn pamoja_mavlink_frame_message_id(frame: *const PamojaMavlinkFrame) -> u32 {
684    frame.as_ref().map_or(0, |frame| frame.inner.message_id())
685}
686
687/// Returns the incompatibility flags a v2 frame declares.
688///
689/// # Arguments
690///
691/// * `frame` - the frame to read.
692///
693/// # Returns
694///
695/// The flags, or `0` for a v1 frame or a null handle.
696///
697/// # Safety
698///
699/// `frame` must be a live frame handle, or null.
700#[no_mangle]
701pub unsafe extern "C" fn pamoja_mavlink_frame_incompat_flags(
702    frame: *const PamojaMavlinkFrame,
703) -> u8 {
704    frame
705        .as_ref()
706        .map_or(0, |frame| frame.inner.incompat_flags())
707}
708
709/// Reports whether a frame carries a signature.
710///
711/// A signature only says the frame was signed, not that the signature is good;
712/// [`pamoja_mavlink_verifier_verify`] decides that.
713///
714/// # Arguments
715///
716/// * `frame` - the frame to read.
717///
718/// # Returns
719///
720/// `1` if the frame is signed, `0` otherwise.
721///
722/// # Safety
723///
724/// `frame` must be a live frame handle, or null.
725#[no_mangle]
726pub unsafe extern "C" fn pamoja_mavlink_frame_is_signed(frame: *const PamojaMavlinkFrame) -> u8 {
727    frame
728        .as_ref()
729        .map_or(0, |frame| u8::from(frame.inner.is_signed()))
730}
731
732/// Returns a pointer to a frame's payload.
733///
734/// The pointer is valid until the frame is released.
735///
736/// # Arguments
737///
738/// * `frame` - the frame to read.
739/// * `out_len` - set to the payload length in bytes.
740///
741/// # Returns
742///
743/// A pointer to the payload, or null if either argument is null.
744///
745/// # Safety
746///
747/// `frame` must be a live frame handle and `out_len` must point at writable
748/// storage for one length.
749#[no_mangle]
750pub unsafe extern "C" fn pamoja_mavlink_frame_payload(
751    frame: *const PamojaMavlinkFrame,
752    out_len: *mut usize,
753) -> *const u8 {
754    let (Some(frame), false) = (frame.as_ref(), out_len.is_null()) else {
755        set_last_error("frame and out_len must not be null".to_owned());
756        return std::ptr::null();
757    };
758    let payload = frame.inner.payload();
759    *out_len = payload.len();
760    payload.as_ptr()
761}
762
763/// Returns a pointer to a frame's bytes, ready to put on the wire.
764///
765/// The pointer is valid until the frame is released.
766///
767/// # Arguments
768///
769/// * `frame` - the frame to read.
770/// * `out_len` - set to the frame length in bytes.
771///
772/// # Returns
773///
774/// A pointer to the frame, or null if either argument is null.
775///
776/// # Safety
777///
778/// `frame` must be a live frame handle and `out_len` must point at writable
779/// storage for one length.
780#[no_mangle]
781pub unsafe extern "C" fn pamoja_mavlink_frame_bytes(
782    frame: *const PamojaMavlinkFrame,
783    out_len: *mut usize,
784) -> *const u8 {
785    let (Some(frame), false) = (frame.as_ref(), out_len.is_null()) else {
786        set_last_error("frame and out_len must not be null".to_owned());
787        return std::ptr::null();
788    };
789    let bytes = frame.inner.as_bytes();
790    *out_len = bytes.len();
791    bytes.as_ptr()
792}
793
794/// Copies a frame's signature block out.
795///
796/// # Arguments
797///
798/// * `frame` - the frame to read.
799/// * `out_signature` - filled with [`PAMOJA_MAVLINK_SIGNATURE_LEN`] bytes on
800///   success.
801///
802/// # Returns
803///
804/// [`PamojaStatus::Ok`] on success.
805///
806/// # Errors
807///
808/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null, and
809/// [`PamojaStatus::Unsupported`] if the frame carries no signature.
810///
811/// # Safety
812///
813/// `frame` must be a live frame handle and `out_signature` must point at writable
814/// storage for [`PAMOJA_MAVLINK_SIGNATURE_LEN`] bytes.
815#[no_mangle]
816pub unsafe extern "C" fn pamoja_mavlink_frame_signature(
817    frame: *const PamojaMavlinkFrame,
818    out_signature: *mut u8,
819) -> PamojaStatus {
820    let (Some(frame), false) = (frame.as_ref(), out_signature.is_null()) else {
821        set_last_error("frame and out_signature must not be null".to_owned());
822        return PamojaStatus::InvalidArgument;
823    };
824    let Some(signature) = frame.inner.signature() else {
825        set_last_error("this frame is not signed".to_owned());
826        return PamojaStatus::Unsupported;
827    };
828    std::ptr::copy_nonoverlapping(signature.as_ptr(), out_signature, SIGNATURE_LEN);
829    PamojaStatus::Ok
830}
831
832/// A streaming frame parser, and the frames it has completed.
833///
834/// A handle the caller must release with [`pamoja_mavlink_parser_free`].
835pub struct PamojaMavlinkParser {
836    parser: Parser,
837    ready: std::collections::VecDeque<Frame>,
838}
839
840/// Creates a parser with an empty buffer.
841///
842/// # Returns
843///
844/// A handle the caller must release with [`pamoja_mavlink_parser_free`].
845#[no_mangle]
846pub extern "C" fn pamoja_mavlink_parser_new() -> *mut PamojaMavlinkParser {
847    Box::into_raw(Box::new(PamojaMavlinkParser {
848        parser: Parser::new(),
849        ready: std::collections::VecDeque::new(),
850    }))
851}
852
853/// Feeds bytes off a link into the parser.
854///
855/// Whatever a serial port or socket delivers can be pushed as it arrives, however
856/// it is split. Frames that complete are queued for
857/// [`pamoja_mavlink_parser_next`]. Noise between frames is skipped rather than
858/// reported, which is what lets a parser join a stream already in progress.
859///
860/// # Arguments
861///
862/// * `parser` - the parser to feed.
863/// * `bytes` - the bytes just read off the link.
864/// * `bytes_len` - how many bytes `bytes` holds.
865/// * `dialect` - the dialect to prefer, or null for the common one alone.
866///
867/// # Returns
868///
869/// [`PamojaStatus::Ok`] on success.
870///
871/// # Errors
872///
873/// Returns [`PamojaStatus::InvalidArgument`] if `parser` is null.
874///
875/// # Safety
876///
877/// `parser` must be a live parser handle, `bytes` must point to `bytes_len`
878/// readable bytes when the length is non-zero, and `dialect` must be a live
879/// dialect handle or null.
880#[no_mangle]
881pub unsafe extern "C" fn pamoja_mavlink_parser_push(
882    parser: *mut PamojaMavlinkParser,
883    bytes: *const u8,
884    bytes_len: usize,
885    dialect: *const PamojaMavlinkDialect,
886) -> PamojaStatus {
887    let Some(parser) = parser.as_mut() else {
888        set_last_error("parser must not be null".to_owned());
889        return PamojaStatus::InvalidArgument;
890    };
891    let bytes = match read_bytes(bytes, bytes_len) {
892        Ok(bytes) => bytes,
893        Err(status) => return status,
894    };
895    let lookup = |msgid: u32| lookup(dialect, msgid);
896    for byte in bytes {
897        if let Some(frame) = parser.parser.push_byte(byte, &lookup) {
898            parser.ready.push_back(frame);
899        }
900    }
901    PamojaStatus::Ok
902}
903
904/// Takes the next completed frame out of the parser.
905///
906/// # Arguments
907///
908/// * `parser` - the parser to drain.
909/// * `out_frame` - set to the frame handle, or to null when none is waiting.
910///
911/// # Returns
912///
913/// [`PamojaStatus::Ok`] whether or not a frame was waiting; a null `out_frame`
914/// means the parser needs more bytes.
915///
916/// # Errors
917///
918/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null.
919///
920/// # Safety
921///
922/// `parser` must be a live parser handle and `out_frame` must point at writable
923/// storage for one pointer.
924#[no_mangle]
925pub unsafe extern "C" fn pamoja_mavlink_parser_next(
926    parser: *mut PamojaMavlinkParser,
927    out_frame: *mut *mut PamojaMavlinkFrame,
928) -> PamojaStatus {
929    if out_frame.is_null() {
930        set_last_error("out_frame must not be null".to_owned());
931        return PamojaStatus::InvalidArgument;
932    }
933    let slot = &mut *out_frame;
934    *slot = std::ptr::null_mut();
935
936    let Some(parser) = parser.as_mut() else {
937        set_last_error("parser must not be null".to_owned());
938        return PamojaStatus::InvalidArgument;
939    };
940    if let Some(frame) = parser.ready.pop_front() {
941        *slot = PamojaMavlinkFrame::into_handle(frame);
942    }
943    PamojaStatus::Ok
944}
945
946/// Returns how many completed frames are waiting to be taken.
947///
948/// # Arguments
949///
950/// * `parser` - the parser to inspect.
951///
952/// # Returns
953///
954/// The number of frames waiting, or `0` if `parser` is null.
955///
956/// # Safety
957///
958/// `parser` must be a live parser handle, or null.
959#[no_mangle]
960pub unsafe extern "C" fn pamoja_mavlink_parser_pending(
961    parser: *const PamojaMavlinkParser,
962) -> usize {
963    parser.as_ref().map_or(0, |parser| parser.ready.len())
964}
965
966/// Releases a parser.
967///
968/// # Arguments
969///
970/// * `parser` - the handle to release; null is ignored.
971///
972/// # Safety
973///
974/// `parser` must have come from [`pamoja_mavlink_parser_new`] and must not be
975/// used afterwards.
976#[no_mangle]
977pub unsafe extern "C" fn pamoja_mavlink_parser_free(parser: *mut PamojaMavlinkParser) {
978    if !parser.is_null() {
979        drop(Box::from_raw(parser));
980    }
981}
982
983/// Converts Unix time into the timestamp MAVLink signing counts in.
984///
985/// # Arguments
986///
987/// * `unix_micros` - the time in microseconds since the Unix epoch.
988///
989/// # Returns
990///
991/// The MAVLink signing timestamp, in units of ten microseconds since 2015.
992#[no_mangle]
993pub extern "C" fn pamoja_mavlink_timestamp_from_unix_micros(unix_micros: u64) -> u64 {
994    signing::timestamp_from_unix_micros(unix_micros)
995}
996
997/// A signing key and the monotonic timestamp that goes with it.
998///
999/// A handle the caller must release with [`pamoja_mavlink_signer_free`].
1000pub struct PamojaMavlinkSigner {
1001    inner: Signer,
1002}
1003
1004/// Creates a signer.
1005///
1006/// # Arguments
1007///
1008/// * `key` - the shared signing key, [`PAMOJA_MAVLINK_KEY_LEN`] bytes.
1009/// * `link_id` - which link this sender signs on, so two links from one system
1010///   do not look like replays of each other.
1011/// * `timestamp` - the timestamp to start from, usually from
1012///   [`pamoja_mavlink_timestamp_from_unix_micros`].
1013/// * `out_signer` - set to the signer handle on success, and to null otherwise.
1014///
1015/// # Returns
1016///
1017/// [`PamojaStatus::Ok`] on success.
1018///
1019/// # Errors
1020///
1021/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null.
1022///
1023/// # Safety
1024///
1025/// `key` must point to [`PAMOJA_MAVLINK_KEY_LEN`] readable bytes and
1026/// `out_signer` must point at writable storage for one pointer.
1027#[no_mangle]
1028pub unsafe extern "C" fn pamoja_mavlink_signer_new(
1029    key: *const u8,
1030    link_id: u8,
1031    timestamp: u64,
1032    out_signer: *mut *mut PamojaMavlinkSigner,
1033) -> PamojaStatus {
1034    if out_signer.is_null() || key.is_null() {
1035        set_last_error("key and out_signer must not be null".to_owned());
1036        return PamojaStatus::InvalidArgument;
1037    }
1038    let slot = &mut *out_signer;
1039    let mut bytes = [0u8; signing::KEY_LEN];
1040    std::ptr::copy_nonoverlapping(key, bytes.as_mut_ptr(), signing::KEY_LEN);
1041    *slot = Box::into_raw(Box::new(PamojaMavlinkSigner {
1042        inner: Signer::new(bytes, link_id, timestamp),
1043    }));
1044    PamojaStatus::Ok
1045}
1046
1047/// Signs a message into a v2 frame.
1048///
1049/// Each call advances the signer's timestamp, which is what makes a replayed
1050/// frame detectable.
1051///
1052/// # Arguments
1053///
1054/// * `signer` - the signer to use.
1055/// * `header` - the addressing fields to stamp on the frame.
1056/// * `msgid` - the message id.
1057/// * `payload` - the message payload.
1058/// * `payload_len` - how many bytes `payload` holds.
1059/// * `crc_extra` - the seed for this message id.
1060/// * `out_frame` - set to the signed frame on success, and to null otherwise.
1061///
1062/// # Returns
1063///
1064/// [`PamojaStatus::Ok`] on success.
1065///
1066/// # Errors
1067///
1068/// Returns [`PamojaStatus::InvalidArgument`] if `signer` or `out_frame` is null,
1069/// or the payload does not fit a frame.
1070///
1071/// # Safety
1072///
1073/// `signer` must be a live signer handle, `payload` must point to `payload_len`
1074/// readable bytes when the length is non-zero, and `out_frame` must point at
1075/// writable storage for one pointer.
1076#[no_mangle]
1077pub unsafe extern "C" fn pamoja_mavlink_signer_sign(
1078    signer: *mut PamojaMavlinkSigner,
1079    header: PamojaMavlinkHeader,
1080    msgid: u32,
1081    payload: *const u8,
1082    payload_len: usize,
1083    crc_extra: u8,
1084    out_frame: *mut *mut PamojaMavlinkFrame,
1085) -> PamojaStatus {
1086    if out_frame.is_null() {
1087        set_last_error("out_frame must not be null".to_owned());
1088        return PamojaStatus::InvalidArgument;
1089    }
1090    let slot = &mut *out_frame;
1091    *slot = std::ptr::null_mut();
1092
1093    let Some(signer) = signer.as_mut() else {
1094        set_last_error("signer must not be null".to_owned());
1095        return PamojaStatus::InvalidArgument;
1096    };
1097    let payload = match read_bytes(payload, payload_len) {
1098        Ok(payload) => payload,
1099        Err(status) => return status,
1100    };
1101    match signer.inner.sign(header.into(), msgid, &payload, crc_extra) {
1102        Ok(frame) => {
1103            *slot = PamojaMavlinkFrame::into_handle(frame);
1104            PamojaStatus::Ok
1105        }
1106        Err(error) => status_of(error),
1107    }
1108}
1109
1110/// Returns the link a signer signs on.
1111///
1112/// # Arguments
1113///
1114/// * `signer` - the signer to read.
1115///
1116/// # Returns
1117///
1118/// The link id, or `0` if `signer` is null.
1119///
1120/// # Safety
1121///
1122/// `signer` must be a live signer handle, or null.
1123#[no_mangle]
1124pub unsafe extern "C" fn pamoja_mavlink_signer_link_id(signer: *const PamojaMavlinkSigner) -> u8 {
1125    signer.as_ref().map_or(0, |signer| signer.inner.link_id())
1126}
1127
1128/// Releases a signer.
1129///
1130/// # Arguments
1131///
1132/// * `signer` - the handle to release; null is ignored.
1133///
1134/// # Safety
1135///
1136/// `signer` must have come from [`pamoja_mavlink_signer_new`] and must not be
1137/// used afterwards.
1138#[no_mangle]
1139pub unsafe extern "C" fn pamoja_mavlink_signer_free(signer: *mut PamojaMavlinkSigner) {
1140    if !signer.is_null() {
1141        drop(Box::from_raw(signer));
1142    }
1143}
1144
1145/// A signing key and the timestamps it has already accepted.
1146///
1147/// A handle the caller must release with [`pamoja_mavlink_verifier_free`].
1148pub struct PamojaMavlinkVerifier {
1149    inner: Verifier,
1150}
1151
1152/// Creates a verifier.
1153///
1154/// # Arguments
1155///
1156/// * `key` - the shared signing key, [`PAMOJA_MAVLINK_KEY_LEN`] bytes.
1157/// * `out_verifier` - set to the handle on success, and to null otherwise.
1158///
1159/// # Returns
1160///
1161/// [`PamojaStatus::Ok`] on success.
1162///
1163/// # Errors
1164///
1165/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null.
1166///
1167/// # Safety
1168///
1169/// `key` must point to [`PAMOJA_MAVLINK_KEY_LEN`] readable bytes and
1170/// `out_verifier` must point at writable storage for one pointer.
1171#[no_mangle]
1172pub unsafe extern "C" fn pamoja_mavlink_verifier_new(
1173    key: *const u8,
1174    out_verifier: *mut *mut PamojaMavlinkVerifier,
1175) -> PamojaStatus {
1176    if out_verifier.is_null() || key.is_null() {
1177        set_last_error("key and out_verifier must not be null".to_owned());
1178        return PamojaStatus::InvalidArgument;
1179    }
1180    let slot = &mut *out_verifier;
1181    let mut bytes = [0u8; signing::KEY_LEN];
1182    std::ptr::copy_nonoverlapping(key, bytes.as_mut_ptr(), signing::KEY_LEN);
1183    *slot = Box::into_raw(Box::new(PamojaMavlinkVerifier {
1184        inner: Verifier::new(bytes),
1185    }));
1186    PamojaStatus::Ok
1187}
1188
1189/// Sets how far a timestamp may run ahead of the last one accepted.
1190///
1191/// A wider window tolerates a noisier link; a narrower one narrows the chance of
1192/// a replay landing inside it.
1193///
1194/// # Arguments
1195///
1196/// * `verifier` - the verifier to set.
1197/// * `window` - the window in timestamp units, ten microseconds each.
1198///
1199/// # Returns
1200///
1201/// [`PamojaStatus::Ok`] on success.
1202///
1203/// # Errors
1204///
1205/// Returns [`PamojaStatus::InvalidArgument`] if `verifier` is null.
1206///
1207/// # Safety
1208///
1209/// `verifier` must be a live verifier handle.
1210#[no_mangle]
1211pub unsafe extern "C" fn pamoja_mavlink_verifier_set_window(
1212    verifier: *mut PamojaMavlinkVerifier,
1213    window: u64,
1214) -> PamojaStatus {
1215    let Some(verifier) = verifier.as_mut() else {
1216        set_last_error("verifier must not be null".to_owned());
1217        return PamojaStatus::InvalidArgument;
1218    };
1219    let held = std::mem::replace(&mut verifier.inner, Verifier::new([0u8; signing::KEY_LEN]));
1220    verifier.inner = held.with_window(window);
1221    PamojaStatus::Ok
1222}
1223
1224/// Checks a frame's signature and its place in the timestamp sequence.
1225///
1226/// # Arguments
1227///
1228/// * `verifier` - the verifier to use.
1229/// * `frame` - the frame to check.
1230///
1231/// # Returns
1232///
1233/// [`PamojaStatus::Ok`] if the frame is authentic and not a replay.
1234///
1235/// # Errors
1236///
1237/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null, and
1238/// [`PamojaStatus::Auth`] if the frame is unsigned, the signature does not match
1239/// the key, or the timestamp has been seen before.
1240///
1241/// # Safety
1242///
1243/// `verifier` must be a live verifier handle and `frame` must be a live frame
1244/// handle.
1245#[no_mangle]
1246pub unsafe extern "C" fn pamoja_mavlink_verifier_verify(
1247    verifier: *mut PamojaMavlinkVerifier,
1248    frame: *const PamojaMavlinkFrame,
1249) -> PamojaStatus {
1250    let (Some(verifier), Some(frame)) = (verifier.as_mut(), frame.as_ref()) else {
1251        set_last_error("verifier and frame must not be null".to_owned());
1252        return PamojaStatus::InvalidArgument;
1253    };
1254    match verifier.inner.verify(&frame.inner) {
1255        Ok(()) => PamojaStatus::Ok,
1256        Err(error) => status_of(error),
1257    }
1258}
1259
1260/// Releases a verifier.
1261///
1262/// # Arguments
1263///
1264/// * `verifier` - the handle to release; null is ignored.
1265///
1266/// # Safety
1267///
1268/// `verifier` must have come from [`pamoja_mavlink_verifier_new`] and must not be
1269/// used afterwards.
1270#[no_mangle]
1271pub unsafe extern "C" fn pamoja_mavlink_verifier_free(verifier: *mut PamojaMavlinkVerifier) {
1272    if !verifier.is_null() {
1273        drop(Box::from_raw(verifier));
1274    }
1275}
1276
1277/// Assembles a v2 frame carrying a message this build does not type.
1278///
1279/// This is the escape hatch a private dialect needs: supply the id, the payload,
1280/// and the seed, and the frame is built and checked like any other.
1281///
1282/// # Arguments
1283///
1284/// * `header` - the addressing fields to stamp on the frame.
1285/// * `msgid` - the message id.
1286/// * `crc_extra` - the seed for this message id.
1287/// * `payload` - the message payload.
1288/// * `payload_len` - how many bytes `payload` holds.
1289/// * `out_frame` - set to the frame handle on success, and to null otherwise.
1290///
1291/// # Returns
1292///
1293/// [`PamojaStatus::Ok`] on success.
1294///
1295/// # Errors
1296///
1297/// Returns [`PamojaStatus::InvalidArgument`] if `out_frame` is null or the
1298/// payload does not fit a frame.
1299///
1300/// # Safety
1301///
1302/// `payload` must point to `payload_len` readable bytes when the length is
1303/// non-zero, and `out_frame` must point at writable storage for one pointer.
1304#[no_mangle]
1305pub unsafe extern "C" fn pamoja_mavlink_raw_message_to_frame(
1306    header: PamojaMavlinkHeader,
1307    msgid: u32,
1308    crc_extra: u8,
1309    payload: *const u8,
1310    payload_len: usize,
1311    out_frame: *mut *mut PamojaMavlinkFrame,
1312) -> PamojaStatus {
1313    if out_frame.is_null() {
1314        set_last_error("out_frame must not be null".to_owned());
1315        return PamojaStatus::InvalidArgument;
1316    }
1317    let slot = &mut *out_frame;
1318    *slot = std::ptr::null_mut();
1319
1320    let payload = match read_bytes(payload, payload_len) {
1321        Ok(payload) => payload,
1322        Err(status) => return status,
1323    };
1324    let raw = RawMessage {
1325        msgid,
1326        crc_extra,
1327        payload: &payload,
1328    };
1329    match raw.to_frame(header.into()) {
1330        Ok(frame) => {
1331            *slot = PamojaMavlinkFrame::into_handle(frame);
1332            PamojaStatus::Ok
1333        }
1334        Err(error) => status_of(error),
1335    }
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use super::*;
1341    use std::ffi::CString;
1342    use std::ptr;
1343
1344    /// The HEARTBEAT payload an onboard controller announces itself with.
1345    const HEARTBEAT: [u8; 9] = [0, 0, 0, 0, 18, 0, 0, 4, 3];
1346
1347    #[test]
1348    fn a_frame_round_trips_through_the_boundary() {
1349        unsafe {
1350            let header = PamojaMavlinkHeader {
1351                system_id: 1,
1352                component_id: 1,
1353                sequence: 7,
1354            };
1355            let mut frame = ptr::null_mut();
1356            assert_eq!(
1357                pamoja_mavlink_frame_encode(
1358                    PAMOJA_MAVLINK_VERSION_V2,
1359                    header,
1360                    0,
1361                    HEARTBEAT.as_ptr(),
1362                    HEARTBEAT.len(),
1363                    50,
1364                    &mut frame
1365                ),
1366                PamojaStatus::Ok
1367            );
1368
1369            let mut len = 0;
1370            let bytes = pamoja_mavlink_frame_bytes(frame, &mut len);
1371            let wire = std::slice::from_raw_parts(bytes, len).to_vec();
1372
1373            let mut received = ptr::null_mut();
1374            assert_eq!(
1375                pamoja_mavlink_frame_parse(wire.as_ptr(), wire.len(), 50, &mut received),
1376                PamojaStatus::Ok
1377            );
1378            assert_eq!(pamoja_mavlink_frame_message_id(received), 0);
1379            assert_eq!(pamoja_mavlink_frame_version(received), 2);
1380            assert_eq!(pamoja_mavlink_frame_is_signed(received), 0);
1381
1382            let mut got = PamojaMavlinkHeader {
1383                system_id: 0,
1384                component_id: 0,
1385                sequence: 0,
1386            };
1387            assert_eq!(
1388                pamoja_mavlink_frame_header(received, &mut got),
1389                PamojaStatus::Ok
1390            );
1391            assert_eq!(got, header);
1392
1393            pamoja_mavlink_frame_free(frame);
1394            pamoja_mavlink_frame_free(received);
1395        }
1396    }
1397
1398    #[test]
1399    fn a_frame_mangled_in_transit_is_refused() {
1400        unsafe {
1401            let header = PamojaMavlinkHeader {
1402                system_id: 1,
1403                component_id: 1,
1404                sequence: 0,
1405            };
1406            let mut frame = ptr::null_mut();
1407            assert_eq!(
1408                pamoja_mavlink_frame_encode(
1409                    PAMOJA_MAVLINK_VERSION_V2,
1410                    header,
1411                    0,
1412                    HEARTBEAT.as_ptr(),
1413                    HEARTBEAT.len(),
1414                    50,
1415                    &mut frame
1416                ),
1417                PamojaStatus::Ok
1418            );
1419            let mut len = 0;
1420            let bytes = pamoja_mavlink_frame_bytes(frame, &mut len);
1421            let mut wire = std::slice::from_raw_parts(bytes, len).to_vec();
1422            pamoja_mavlink_frame_free(frame);
1423
1424            wire[12] ^= 0xFF;
1425            let mut received = ptr::null_mut();
1426            assert_eq!(
1427                pamoja_mavlink_frame_parse(wire.as_ptr(), wire.len(), 50, &mut received),
1428                PamojaStatus::Codec
1429            );
1430            assert!(received.is_null());
1431        }
1432    }
1433
1434    #[test]
1435    fn the_parser_finds_frames_split_across_reads_and_skips_noise() {
1436        unsafe {
1437            let header = PamojaMavlinkHeader {
1438                system_id: 2,
1439                component_id: 1,
1440                sequence: 3,
1441            };
1442            let mut frame = ptr::null_mut();
1443            pamoja_mavlink_frame_encode(
1444                PAMOJA_MAVLINK_VERSION_V2,
1445                header,
1446                0,
1447                HEARTBEAT.as_ptr(),
1448                HEARTBEAT.len(),
1449                50,
1450                &mut frame,
1451            );
1452            let mut len = 0;
1453            let bytes = pamoja_mavlink_frame_bytes(frame, &mut len);
1454            let wire = std::slice::from_raw_parts(bytes, len).to_vec();
1455            pamoja_mavlink_frame_free(frame);
1456
1457            let parser = pamoja_mavlink_parser_new();
1458
1459            // Noise, then the frame split mid-payload, the way a serial port
1460            // actually delivers it.
1461            let noise = [0x11u8, 0x22, 0x33];
1462            pamoja_mavlink_parser_push(parser, noise.as_ptr(), noise.len(), ptr::null());
1463            pamoja_mavlink_parser_push(parser, wire.as_ptr(), 5, ptr::null());
1464            assert_eq!(pamoja_mavlink_parser_pending(parser), 0);
1465            pamoja_mavlink_parser_push(parser, wire.as_ptr().add(5), wire.len() - 5, ptr::null());
1466            assert_eq!(pamoja_mavlink_parser_pending(parser), 1);
1467
1468            let mut received = ptr::null_mut();
1469            assert_eq!(
1470                pamoja_mavlink_parser_next(parser, &mut received),
1471                PamojaStatus::Ok
1472            );
1473            assert!(!received.is_null());
1474            assert_eq!(pamoja_mavlink_frame_message_id(received), 0);
1475            pamoja_mavlink_frame_free(received);
1476
1477            // Draining an empty parser is not an error; it means feed it more.
1478            let mut empty = ptr::null_mut();
1479            assert_eq!(
1480                pamoja_mavlink_parser_next(parser, &mut empty),
1481                PamojaStatus::Ok
1482            );
1483            assert!(empty.is_null());
1484
1485            pamoja_mavlink_parser_free(parser);
1486        }
1487    }
1488
1489    #[test]
1490    fn a_private_dialect_is_parsed_once_its_seed_is_known() {
1491        unsafe {
1492            // An id no common dialect defines, with a seed derived from its own
1493            // definition the way the specification does.
1494            let name = CString::new("PRIVATE_STATUS").expect("name");
1495            let type_name = CString::new("uint32_t").expect("type");
1496            let field_name = CString::new("uptime").expect("field");
1497            let fields = [PamojaMavlinkField {
1498                type_name: type_name.as_ptr(),
1499                field_name: field_name.as_ptr(),
1500                array_len: 0,
1501            }];
1502            let mut seed = 0;
1503            assert_eq!(
1504                pamoja_mavlink_message_crc_extra(name.as_ptr(), fields.as_ptr(), 1, &mut seed),
1505                PamojaStatus::Ok
1506            );
1507
1508            let header = PamojaMavlinkHeader {
1509                system_id: 9,
1510                component_id: 1,
1511                sequence: 0,
1512            };
1513            let payload = 42u32.to_le_bytes();
1514            let mut frame = ptr::null_mut();
1515            assert_eq!(
1516                pamoja_mavlink_raw_message_to_frame(
1517                    header,
1518                    50_000,
1519                    seed,
1520                    payload.as_ptr(),
1521                    payload.len(),
1522                    &mut frame
1523                ),
1524                PamojaStatus::Ok
1525            );
1526            let mut len = 0;
1527            let bytes = pamoja_mavlink_frame_bytes(frame, &mut len);
1528            let wire = std::slice::from_raw_parts(bytes, len).to_vec();
1529            pamoja_mavlink_frame_free(frame);
1530
1531            // The common registry alone cannot check it.
1532            let mut refused = ptr::null_mut();
1533            assert_eq!(
1534                pamoja_mavlink_frame_parse_known(
1535                    wire.as_ptr(),
1536                    wire.len(),
1537                    ptr::null(),
1538                    &mut refused
1539                ),
1540                PamojaStatus::Codec
1541            );
1542            assert!(refused.is_null());
1543
1544            // Told the seed, it parses like any other frame.
1545            let dialect = pamoja_mavlink_dialect_new();
1546            assert_eq!(
1547                pamoja_mavlink_dialect_add(dialect, 50_000, seed),
1548                PamojaStatus::Ok
1549            );
1550            let mut received = ptr::null_mut();
1551            assert_eq!(
1552                pamoja_mavlink_frame_parse_known(wire.as_ptr(), wire.len(), dialect, &mut received),
1553                PamojaStatus::Ok
1554            );
1555            assert_eq!(pamoja_mavlink_frame_message_id(received), 50_000);
1556
1557            // MAVLink 2 drops trailing zero bytes, so a four-byte field whose
1558            // value fits in one arrives as one byte; a decoder zero-extends it.
1559            let mut payload_len = 0;
1560            let payload = pamoja_mavlink_frame_payload(received, &mut payload_len);
1561            assert_eq!(std::slice::from_raw_parts(payload, payload_len), [42]);
1562
1563            pamoja_mavlink_frame_free(received);
1564            pamoja_mavlink_dialect_free(dialect);
1565        }
1566    }
1567
1568    #[test]
1569    fn the_common_registry_answers_for_the_ids_it_knows() {
1570        unsafe {
1571            let mut seed = 0;
1572            assert_eq!(
1573                pamoja_mavlink_known_crc_extra(0, &mut seed),
1574                PamojaStatus::Ok
1575            );
1576            assert_eq!(seed, 50, "HEARTBEAT");
1577            assert_eq!(
1578                pamoja_mavlink_known_crc_extra(9999, &mut seed),
1579                PamojaStatus::Unsupported
1580            );
1581        }
1582    }
1583
1584    #[test]
1585    fn a_signed_frame_verifies_once_and_a_replay_is_refused() {
1586        unsafe {
1587            let key = [7u8; PAMOJA_MAVLINK_KEY_LEN];
1588            let mut signer = ptr::null_mut();
1589            assert_eq!(
1590                pamoja_mavlink_signer_new(key.as_ptr(), 1, 1_000, &mut signer),
1591                PamojaStatus::Ok
1592            );
1593            assert_eq!(pamoja_mavlink_signer_link_id(signer), 1);
1594
1595            let header = PamojaMavlinkHeader {
1596                system_id: 1,
1597                component_id: 1,
1598                sequence: 0,
1599            };
1600            let mut frame = ptr::null_mut();
1601            assert_eq!(
1602                pamoja_mavlink_signer_sign(
1603                    signer,
1604                    header,
1605                    0,
1606                    HEARTBEAT.as_ptr(),
1607                    HEARTBEAT.len(),
1608                    50,
1609                    &mut frame
1610                ),
1611                PamojaStatus::Ok
1612            );
1613            assert_eq!(pamoja_mavlink_frame_is_signed(frame), 1);
1614
1615            let mut signature = [0u8; PAMOJA_MAVLINK_SIGNATURE_LEN];
1616            assert_eq!(
1617                pamoja_mavlink_frame_signature(frame, signature.as_mut_ptr()),
1618                PamojaStatus::Ok
1619            );
1620            assert_eq!(signature[0], 1, "the link id leads the block");
1621
1622            let mut verifier = ptr::null_mut();
1623            assert_eq!(
1624                pamoja_mavlink_verifier_new(key.as_ptr(), &mut verifier),
1625                PamojaStatus::Ok
1626            );
1627            assert_eq!(
1628                pamoja_mavlink_verifier_verify(verifier, frame),
1629                PamojaStatus::Ok
1630            );
1631            assert_eq!(
1632                pamoja_mavlink_verifier_verify(verifier, frame),
1633                PamojaStatus::Auth,
1634                "the same timestamp a second time is a replay"
1635            );
1636
1637            // A different key is a different sender.
1638            let mut stranger = ptr::null_mut();
1639            pamoja_mavlink_verifier_new([9u8; PAMOJA_MAVLINK_KEY_LEN].as_ptr(), &mut stranger);
1640            assert_eq!(
1641                pamoja_mavlink_verifier_verify(stranger, frame),
1642                PamojaStatus::Auth
1643            );
1644
1645            pamoja_mavlink_verifier_free(stranger);
1646            pamoja_mavlink_verifier_free(verifier);
1647            pamoja_mavlink_frame_free(frame);
1648            pamoja_mavlink_signer_free(signer);
1649        }
1650    }
1651
1652    #[test]
1653    fn an_unsigned_frame_has_no_signature_to_report() {
1654        unsafe {
1655            let header = PamojaMavlinkHeader {
1656                system_id: 1,
1657                component_id: 1,
1658                sequence: 0,
1659            };
1660            let mut frame = ptr::null_mut();
1661            pamoja_mavlink_frame_encode(
1662                PAMOJA_MAVLINK_VERSION_V2,
1663                header,
1664                0,
1665                HEARTBEAT.as_ptr(),
1666                HEARTBEAT.len(),
1667                50,
1668                &mut frame,
1669            );
1670            let mut signature = [0u8; PAMOJA_MAVLINK_SIGNATURE_LEN];
1671            assert_eq!(
1672                pamoja_mavlink_frame_signature(frame, signature.as_mut_ptr()),
1673                PamojaStatus::Unsupported
1674            );
1675            pamoja_mavlink_frame_free(frame);
1676        }
1677    }
1678
1679    #[test]
1680    fn the_checksum_matches_the_catalogue_check_value() {
1681        unsafe {
1682            let data = b"123456789";
1683            assert_eq!(
1684                pamoja_mavlink_crc16_mcrf4xx(data.as_ptr(), data.len()),
1685                crc16_mcrf4xx(data)
1686            );
1687        }
1688    }
1689}