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