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