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}