diff --git a/Cargo.lock b/Cargo.lock index 1841268..c754b4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -58,18 +67,10 @@ version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ + "arbitrary", "serde_core", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.12.1" @@ -189,15 +190,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -284,21 +276,22 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "generic-array", - "typenum", + "hybrid-array", ] [[package]] -name = "crypto-common" -version = "0.2.2" +name = "derive_arbitrary" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ - "hybrid-array", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -309,25 +302,15 @@ dependencies = [ "rand", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", + "block-buffer", "const-oid", - "crypto-common 0.2.2", + "crypto-common", ] [[package]] @@ -434,25 +417,16 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" -version = "0.2.17" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "wasip2", ] [[package]] @@ -528,12 +502,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.186" @@ -566,30 +534,33 @@ dependencies = [ [[package]] name = "mavlink" -version = "0.16.2" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cad9bd7a9d546da9d25e692ac0a276f0fb7df4b412ae06a07ec473ac90cb588" +checksum = "ef539c358c31f69d47816dac709eebbcb733166ae1bcdeeb4463383c042e3e7c" dependencies = [ + "arbitrary", "bitflags 2.13.0", "mavlink-bindgen", "mavlink-core", "num-derive", "num-traits", + "rand", "serde", "serde_arrays", ] [[package]] name = "mavlink-bindgen" -version = "0.16.2" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70320e411d3daf25576e94b919dc1787bfe7b0384076ddca2cf3b7ccb120da1e" +checksum = "055c957f9fe541226e5b6f0a8aef09b81b9ac865bfc2d083bd6440ed1692602f" dependencies = [ + "arbitrary", "crc-any", - "lazy_static", "proc-macro2", "quick-xml", "quote", + "rand", "regex", "thiserror", ] @@ -606,7 +577,7 @@ dependencies = [ "log", "mavlink", "rand", - "sha2 0.11.0", + "sha2", "thiserror", "tokio", "tokio-stream", @@ -616,18 +587,20 @@ dependencies = [ [[package]] name = "mavlink-core" -version = "0.16.2" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfff2f6e0215164e3f3c26168a028cdfd0b24eddf1744dc81f8b3dbdb28f45ab" +checksum = "b31cc9f930c7edce0c1933659d405a330a28346cdf087bd283a56a6d72eb3d85" dependencies = [ + "arbitrary", "async-trait", "byteorder", "crc-any", "futures", + "rand", "serde", "serde_arrays", "serialport", - "sha2 0.10.9", + "sha2", "tokio", "tokio-serial", ] @@ -795,9 +768,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] @@ -811,22 +784,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" -version = "0.8.6" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -834,9 +812,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom", ] @@ -990,17 +968,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha2" version = "0.11.0" @@ -1008,8 +975,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -1204,12 +1171,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" @@ -1226,6 +1187,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -1378,6 +1348,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "zerocopy" version = "0.8.52" diff --git a/Cargo.toml b/Cargo.toml index 1966806..5493a7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,9 @@ bench = false # opt-level = 3 [dependencies] -bytes = "1.10" +bytes = "1.12" log = "0.4" -mavlink = { version = "0.16.1", default-features = false, features = ["std", "ardupilotmega", "signing"] } +mavlink = { version = "0.18.0", default-features = false, features = ["std", "dialect-ardupilotmega", "mav2-message-signing"] } sha2 = "0.11" thiserror = "2.0" tokio-util = { version = "0.7", features = ["codec"] } @@ -33,8 +33,8 @@ anyhow = "1.0" criterion = { version = "0.5", features = ["async_tokio"] } dev-utils = { path = "dev_utils" } futures = "0.3" -mavlink = { version = "0.16.1", default-features = false, features = ["std", "ardupilotmega", "tokio-1", "signing"] } -rand = "0.8" +mavlink = { version = "0.18.0", default-features = false, features = ["std", "dialect-ardupilotmega", "mav2-message-signing", "arbitrary"] } +rand = "0.9" tokio = { version = "1", features = ["full"] } tokio-stream = "0.1" tokio-util = "0.7" diff --git a/benches/bench.rs b/benches/bench.rs index f5816c2..ab26a74 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -1,6 +1,8 @@ +use std::hint::black_box; + use criterion::{ - black_box, criterion_group, criterion_main, AxisScale, BenchmarkId, Criterion, - PlotConfiguration, Throughput, + criterion_group, criterion_main, AxisScale, BenchmarkId, Criterion, PlotConfiguration, + Throughput, }; use mavlink::Message; use mavlink_codec::{codec::MavlinkCodec, v2::V2Packet}; @@ -8,19 +10,76 @@ use rand::{prelude::StdRng, SeedableRng}; use tokio_stream::StreamExt; use tokio_util::codec::{Decoder, FramedRead}; +#[cfg(feature = "bench-c-reference")] +mod c_reference { + use std::{ffi::c_int, mem::MaybeUninit, ptr}; + + pub struct BenchState(*mut std::ffi::c_void); + + extern "C" { + fn mavlink_codec_bench_state_new() -> *mut std::ffi::c_void; + fn mavlink_codec_bench_state_reset(state: *mut std::ffi::c_void); + fn mavlink_codec_bench_state_free(state: *mut std::ffi::c_void); + fn mavlink_codec_bench_state_decode( + state: *mut std::ffi::c_void, + data: *const u8, + len: usize, + ) -> c_int; + fn mavlink_codec_bench_state_last_message( + state: *const std::ffi::c_void, + ) -> *const std::ffi::c_void; + } + + impl BenchState { + pub fn new() -> Self { + let state = unsafe { mavlink_codec_bench_state_new() }; + assert!(!state.is_null()); + Self(state) + } + + pub fn reset(&mut self) { + unsafe { mavlink_codec_bench_state_reset(self.0) }; + } + + pub fn decode(&mut self, data: &[u8]) -> c_int { + unsafe { mavlink_codec_bench_state_decode(self.0, data.as_ptr(), data.len()) } + } + + pub fn last_message_word(&self) -> u64 { + let msg = unsafe { mavlink_codec_bench_state_last_message(self.0) }; + let mut word = MaybeUninit::::uninit(); + unsafe { + ptr::copy_nonoverlapping(msg.cast(), word.as_mut_ptr(), 1); + word.assume_init() + } + } + } + + impl Drop for BenchState { + fn drop(&mut self) { + unsafe { mavlink_codec_bench_state_free(self.0) }; + } + } +} + +const SIGNING_KEY: [u8; mavlink_codec::signing::SECRET_KEY_SIZE] = [ + 0x00, 0x01, 0xf2, 0xe3, 0xd4, 0xc5, 0xb6, 0xa7, 0x98, 0x00, 0x70, 0x76, 0x34, 0x32, 0x00, 0x16, + 0x22, 0x42, 0x00, 0xcc, 0xff, 0x7a, 0x00, 0x52, 0x75, 0x73, 0x74, 0x00, 0x4d, 0x41, 0x56, 0xb3, +]; + fn add_random_v2_message(buf: &mut Vec, rng: &mut StdRng) { use rand::Rng; - use mavlink::ardupilotmega::*; + use mavlink::dialects::ardupilotmega::*; let header = mavlink::MavHeader { - system_id: rng.gen_range(1..255), - component_id: rng.gen_range(1..255), - sequence: rng.gen_range(0..255), + system_id: rng.random_range(1..255), + component_id: rng.random_range(1..255), + sequence: rng.random_range(0..255), }; loop { - let message_id = rng.gen_range(0..2 ^ 24); + let message_id = rng.random_range(0..2 ^ 24); if let Some(data) = MavMessage::default_message_from_id(message_id) { if mavlink::write_v2_msg(buf, header, &data).is_ok() { break; @@ -29,6 +88,33 @@ fn add_random_v2_message(buf: &mut Vec, rng: &mut StdRng) { } } +fn add_random_signed_v2_message( + buf: &mut Vec, + rng: &mut StdRng, + signing: &mavlink::SigningData, +) { + use rand::Rng; + + use mavlink::dialects::ardupilotmega::*; + + let header = mavlink::MavHeader { + system_id: rng.random_range(1..255), + component_id: rng.random_range(1..255), + sequence: rng.random_range(0..255), + }; + + loop { + let message_id = rng.random_range(0..2 ^ 24); + if let Some(data) = MavMessage::default_message_from_id(message_id) { + let mut raw = mavlink::MAVLinkV2MessageRaw::new(); + raw.serialize_message_for_signing(header, &data); + signing.sign_message(&mut raw); + buf.extend_from_slice(raw.raw_bytes()); + break; + }; + } +} + fn benchmark_decode(c: &mut Criterion) { let seed = 42; println!("Using seed {seed:?}"); @@ -60,18 +146,25 @@ fn benchmark_decode(c: &mut Criterion) { |b, &messages_count| { let buf = buf.clone(); - b.to_async(&rt).iter(|| async { - let mut reader = mavlink::peek_reader::PeekReader::new(&buf[..]); + b.to_async(&rt).iter_batched( + || { + let reader = mavlink::peek_reader::PeekReader::new(&buf[..]); - for _ in 0..messages_count { - let _msg = black_box( - mavlink::read_v2_raw_message::( - &mut reader, - ) - .unwrap(), - ); - } - }) + reader + }, + |mut reader| async move { + for _ in 0..messages_count { + let _msg = black_box( + mavlink::read_v2_raw_message::< + mavlink::dialects::ardupilotmega::MavMessage, + _, + >(&mut reader) + .unwrap(), + ); + } + }, + criterion::BatchSize::SmallInput, + ) }, ); @@ -81,20 +174,26 @@ fn benchmark_decode(c: &mut Criterion) { |b, &messages_count| { let buf = buf.clone(); - b.to_async(&rt).iter(|| async { - let mut reader = mavlink::async_peek_reader::AsyncPeekReader::new(&buf[..]); + b.to_async(&rt).iter_batched( + || { + let reader = mavlink::async_peek_reader::AsyncPeekReader::new(&buf[..]); - for _ in 0..messages_count { - let _msg = black_box( - mavlink::read_v2_raw_message_async::< - mavlink::ardupilotmega::MavMessage, - _, - >(&mut reader) - .await - .unwrap(), - ); - } - }) + reader + }, + |mut reader| async move { + for _ in 0..messages_count { + let _msg = black_box( + mavlink::read_v2_raw_message_async::< + mavlink::dialects::ardupilotmega::MavMessage, + _, + >(&mut reader) + .await + .unwrap(), + ); + } + }, + criterion::BatchSize::SmallInput, + ) }, ); @@ -104,24 +203,22 @@ fn benchmark_decode(c: &mut Criterion) { |b, &messages_count| { let buf = buf.clone(); // Reset buffer each time -<<<<<<< HEAD - b.to_async(&rt).iter(|| async { - let mut buf = bytes::BytesMut::from(buf.as_slice()); - let mut codec = - MavlinkCodec::::default(); -======= b.to_async(&rt).iter_batched( || { let buf = bytes::BytesMut::from(buf.as_slice()); let codec = MavlinkCodec::::default( ); ->>>>>>> d7c4204 (src: codec: Add MAVLink2 signature verification) - for _ in 0..messages_count { - let _msg = black_box(codec.decode(&mut buf).unwrap().unwrap()); - } - }) + (buf, codec) + }, + |(mut buf, mut codec)| async move { + for _ in 0..messages_count { + let _msg = black_box(codec.decode(&mut buf).unwrap().unwrap()); + } + }, + criterion::BatchSize::SmallInput, + ) }, ); @@ -129,25 +226,199 @@ fn benchmark_decode(c: &mut Criterion) { BenchmarkId::new("decoder-framed.next", messages_count), messages_count, |b, &messages_count| { -<<<<<<< HEAD - let buf = buf.clone(); -======= b.to_async(&rt).iter_batched( || { let codec = MavlinkCodec::::default( ); let framed = FramedRead::new(buf.as_slice(), codec); ->>>>>>> d7c4204 (src: codec: Add MAVLink2 signature verification) - b.to_async(&rt).iter(|| async { - let codec = MavlinkCodec::::default(); - let mut framed = FramedRead::new(buf.as_slice(), codec); + framed + }, + |mut framed| async move { + for _ in 0..messages_count { + let _msg = black_box(framed.next().await.unwrap().unwrap()); + } + }, + criterion::BatchSize::SmallInput, + ); + }, + ); + + #[cfg(feature = "bench-c-reference")] + group.bench_with_input( + BenchmarkId::new("c_library_v2", messages_count), + messages_count, + |b, &messages_count| { + let buf = buf.clone(); + + b.iter_batched( + || { + let mut state = c_reference::BenchState::new(); + state.reset(); + state + }, + |mut state| { + let count = state.decode(&buf); + black_box(count); + black_box(state.last_message_word()); + }, + criterion::BatchSize::SmallInput, + ); + }, + ); + } + + group.finish(); +} + +fn benchmark_decode_signed(c: &mut Criterion) { + let seed = 42; + println!("Using seed {seed:?}"); + let mut rng: StdRng = SeedableRng::seed_from_u64(seed); + + let mut group = c.benchmark_group("decode_signed"); + group.confidence_level(0.95).sample_size(100); + + let plot_config = PlotConfiguration::default().summary_scale(AxisScale::Logarithmic); + + group.plot_config(plot_config); + + let messages_counts = vec![1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000]; + + let rt = tokio::runtime::Runtime::new().unwrap(); + + for messages_count in &messages_counts { + group.throughput(Throughput::Elements(*messages_count)); + + let mut buf: Vec = + Vec::with_capacity(V2Packet::MAX_PACKET_SIZE * *messages_count as usize); + let signing = mavlink::SigningData::from_config(mavlink::SigningConfig::new( + SIGNING_KEY, + 0, + true, + false, + )); + for _ in 0..*messages_count { + add_random_signed_v2_message(&mut buf, &mut rng, &signing); + } + + group.bench_with_input( + BenchmarkId::new("rust-mavlink", messages_count), + messages_count, + |b, &messages_count| { + let buf = buf.clone(); + + b.to_async(&rt).iter_batched( + || { + let reader = mavlink::peek_reader::PeekReader::new(&buf[..]); + let signing = mavlink::SigningData::from_config( + mavlink::SigningConfig::new(SIGNING_KEY, 0, false, false), + ); + + (reader, signing) + }, + |(mut reader, signing)| async move { + for _ in 0..messages_count { + let msg = mavlink::read_v2_raw_message::< + mavlink::dialects::ardupilotmega::MavMessage, + _, + >(&mut reader) + .unwrap(); + black_box(signing.verify_signature(&msg)); + } + }, + criterion::BatchSize::SmallInput, + ) + }, + ); + + group.bench_with_input( + BenchmarkId::new("rust-mavlink-async", messages_count), + messages_count, + |b, &messages_count| { + let buf = buf.clone(); + + b.to_async(&rt).iter_batched( + || { + let reader = mavlink::async_peek_reader::AsyncPeekReader::new(&buf[..]); + let signing = mavlink::SigningData::from_config( + mavlink::SigningConfig::new(SIGNING_KEY, 0, false, false), + ); + + (reader, signing) + }, + |(mut reader, signing)| async move { + for _ in 0..messages_count { + let msg = mavlink::read_v2_raw_message_async::< + mavlink::dialects::ardupilotmega::MavMessage, + _, + >(&mut reader) + .await + .unwrap(); + black_box(signing.verify_signature(&msg)); + } + }, + criterion::BatchSize::SmallInput, + ) + }, + ); + + group.bench_with_input( + BenchmarkId::new("decoder-decode", messages_count), + messages_count, + |b, &messages_count| { + let buf = buf.clone(); + + b.to_async(&rt).iter_batched( + || { + let buf = bytes::BytesMut::from(buf.as_slice()); + let codec = MavlinkCodec::::with_signing( + mavlink_codec::signing::SigningData::new( + mavlink_codec::signing::SigningConfig { + secret_key: SIGNING_KEY, + allow_unsigned: false, + }, + ), + ); + + (buf, codec) + }, + |(mut buf, mut codec)| async move { + for _ in 0..messages_count { + let _msg = black_box(codec.decode(&mut buf).unwrap().unwrap()); + } + }, + criterion::BatchSize::SmallInput, + ) + }, + ); + + group.bench_with_input( + BenchmarkId::new("decoder-framed.next", messages_count), + messages_count, + |b, &messages_count| { + b.to_async(&rt).iter_batched( + || { + let codec = MavlinkCodec::::with_signing( + mavlink_codec::signing::SigningData::new( + mavlink_codec::signing::SigningConfig { + secret_key: SIGNING_KEY, + allow_unsigned: false, + }, + ), + ); + let framed = FramedRead::new(buf.as_slice(), codec); - for _ in 0..messages_count { - let _msg = black_box(framed.next().await.unwrap().unwrap()); - } - }) + framed + }, + |mut framed| async move { + for _ in 0..messages_count { + let _msg = black_box(framed.next().await.unwrap().unwrap()); + } + }, + criterion::BatchSize::SmallInput, + ); }, ); } @@ -155,5 +426,5 @@ fn benchmark_decode(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, benchmark_decode); +criterion_group!(benches, benchmark_decode, benchmark_decode_signed); criterion_main!(benches); diff --git a/benches/compatibility_bench.rs b/benches/compatibility_bench.rs index 34caf77..1f2e376 100644 --- a/benches/compatibility_bench.rs +++ b/benches/compatibility_bench.rs @@ -139,7 +139,7 @@ fn benchmark_mavlink_compatibility_v2(c: &mut Criterion) { fn try_from_first_implementation_v1( value: V1Packet, ) -> Result { - use mavlink::ardupilotmega::MavMessage; + use mavlink::dialects::ardupilotmega::MavMessage; let mut reader = mavlink::peek_reader::PeekReader::new(value.as_slice()); let message = mavlink::read_v1_raw_message::(&mut reader); @@ -149,7 +149,7 @@ fn try_from_first_implementation_v1( fn try_from_first_implementation_v2( value: V2Packet, ) -> Result { - use mavlink::ardupilotmega::MavMessage; + use mavlink::dialects::ardupilotmega::MavMessage; let mut reader = mavlink::peek_reader::PeekReader::new(value.as_slice()); let message = mavlink::read_v2_raw_message::(&mut reader); diff --git a/dev_utils/Cargo.toml b/dev_utils/Cargo.toml index 447d40e..6485c08 100644 --- a/dev_utils/Cargo.toml +++ b/dev_utils/Cargo.toml @@ -6,5 +6,5 @@ version = "0.1.0" edition = "2021" [dependencies] -mavlink = { version = "0.16.1", features = ["std", "ardupilotmega", "tokio-1"] } -rand = "0.8" +mavlink = { version = "0.18.0", features = ["std", "dialect-ardupilotmega", "tokio", "arbitrary"] } +rand = "0.9" diff --git a/dev_utils/src/lib.rs b/dev_utils/src/lib.rs index 5e08f0e..ea978c0 100644 --- a/dev_utils/src/lib.rs +++ b/dev_utils/src/lib.rs @@ -1,25 +1,71 @@ -use std::io::Write; +use std::{io::Write, sync::OnceLock}; use mavlink::{MAVLinkV1MessageRaw, MAVLinkV2MessageRaw}; use rand::{prelude::StdRng, Rng}; +pub fn all_message_ids() -> &'static [(&'static str, u32)] { + static IDS: OnceLock> = OnceLock::new(); + + IDS.get_or_init(|| { + use mavlink::{dialects::ardupilotmega::MavMessage, Message}; + + MavMessage::all_ids() + .iter() + .map(|&id| { + let msg = MavMessage::default_message_from_id(id) + .unwrap_or_else(|| panic!("dialect lists id {id} but has no default message")); + (msg.message_name(), id) + }) + .collect() + }) + .as_slice() +} + +pub fn create_random_v1_message_from_id(rng: &mut StdRng, id: u32) -> Option { + use mavlink::{dialects::ardupilotmega::MavMessage, Message}; + + let message_data = MavMessage::random_message_from_id(id, rng)?; + let header = random_header(rng); + let mut raw = MAVLinkV1MessageRaw::new(); + raw.serialize_message(header, &message_data); + Some(raw) +} + +pub fn create_random_v2_message_from_id(rng: &mut StdRng, id: u32) -> Option { + use mavlink::{dialects::ardupilotmega::MavMessage, Message}; + + let message_data = MavMessage::random_message_from_id(id, rng)?; + let header = random_header(rng); + let mut raw = MAVLinkV2MessageRaw::new(); + raw.serialize_message(header, &message_data); + Some(raw) +} + +fn random_header(rng: &mut StdRng) -> mavlink::MavHeader { + mavlink::MavHeader { + system_id: rng.random_range(1..255), + component_id: rng.random_range(1..255), + sequence: rng.random_range(0..255), + } +} + pub fn add_random_v1_message(buf: &mut Vec, rng: &mut StdRng) { let raw_v1_message = create_random_v1_raw_message(rng); - buf.write(raw_v1_message.raw_bytes()).unwrap(); + buf.write_all(raw_v1_message.raw_bytes()).unwrap(); } pub fn create_random_v1_raw_message(rng: &mut StdRng) -> MAVLinkV1MessageRaw { - use mavlink::{ardupilotmega::*, Message}; + use mavlink::{dialects::ardupilotmega::*, Message}; let header = mavlink::MavHeader { - system_id: rng.gen_range(1..255), - component_id: rng.gen_range(1..255), - sequence: rng.gen_range(0..255), + system_id: rng.random_range(1..255), + component_id: rng.random_range(1..255), + sequence: rng.random_range(0..255), }; loop { - let message_id = rng.gen_range(0..2 ^ 24); + let message_id = rng.random_range(0..2 ^ 24); if let Some(message_data) = MavMessage::default_message_from_id(message_id) { let mut raw_v1_message = MAVLinkV1MessageRaw::new(); @@ -33,20 +79,20 @@ pub fn create_random_v1_raw_message(rng: &mut StdRng) -> MAVLinkV1MessageRaw { pub fn add_random_v2_message(buf: &mut Vec, rng: &mut StdRng) { let raw_v2_message = create_random_v2_raw_message(rng); - buf.write(raw_v2_message.raw_bytes()).unwrap(); + buf.write_all(raw_v2_message.raw_bytes()).unwrap(); } pub fn create_random_v2_raw_message(rng: &mut StdRng) -> MAVLinkV2MessageRaw { - use mavlink::{ardupilotmega::*, Message}; + use mavlink::{dialects::ardupilotmega::*, Message}; let header = mavlink::MavHeader { - system_id: rng.gen_range(1..255), - component_id: rng.gen_range(1..255), - sequence: rng.gen_range(0..255), + system_id: rng.random_range(1..255), + component_id: rng.random_range(1..255), + sequence: rng.random_range(0..255), }; loop { - let message_id = rng.gen_range(0..2 ^ 24); + let message_id = rng.random_range(0..2 ^ 24); if let Some(message_data) = MavMessage::default_message_from_id(message_id) { let mut raw_v2_message = MAVLinkV2MessageRaw::new(); @@ -63,7 +109,7 @@ pub fn chunk_buffer_randomly(buf: &[u8], rng: &mut StdRng, min: usize, max: usiz let mut start = 0; while remaining > 0 { - let chunk_size = rng.gen_range(min..=max).min(remaining); + let chunk_size = rng.random_range(min..=max).min(remaining); let end = start + chunk_size; chunks.push(buf[start..end].to_vec()); start = end; diff --git a/src/codec.rs b/src/codec.rs index 70fbf9f..7040e39 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -518,7 +518,9 @@ impl< pub fn get_extra_crc(msgid: u32) -> Option { use mavlink::Message; - Some(mavlink::ardupilotmega::MavMessage::extra_crc(msgid)) + Some(mavlink::dialects::ardupilotmega::MavMessage::extra_crc( + msgid, + )) } /// Returns whether `msgid` exists in the compiled dialect. @@ -530,14 +532,15 @@ pub fn get_extra_crc(msgid: u32) -> Option { pub fn is_known_msgid(msgid: u32) -> bool { use mavlink::Message; - mavlink::ardupilotmega::MavMessage::default_message_from_id(msgid).is_some() + mavlink::dialects::ardupilotmega::MavMessage::default_message_from_id(msgid).is_some() } #[cfg(test)] mod test_encode { use super::*; use mavlink::{ - ardupilotmega::MavMessage, MAVLinkV1MessageRaw, MAVLinkV2MessageRaw, MavHeader, Message, + dialects::ardupilotmega::MavMessage, MAVLinkV1MessageRaw, MAVLinkV2MessageRaw, MavHeader, + Message, }; #[test] @@ -596,7 +599,8 @@ mod test_encode { mod test_decode { use super::*; use mavlink::{ - ardupilotmega::MavMessage, MAVLinkV1MessageRaw, MAVLinkV2MessageRaw, MavHeader, Message, + dialects::ardupilotmega::MavMessage, MAVLinkV1MessageRaw, MAVLinkV2MessageRaw, MavHeader, + Message, }; #[test] diff --git a/src/rust_mavlink_compatibility.rs b/src/rust_mavlink_compatibility.rs index 1cb8406..b0e1cee 100644 --- a/src/rust_mavlink_compatibility.rs +++ b/src/rust_mavlink_compatibility.rs @@ -18,7 +18,7 @@ impl TryFrom for mavlink::MAVLinkV1MessageRaw { type Error = mavlink::error::MessageReadError; /// A convenient rust-mavlink compatibility layer - /// warning: this has a bad performance because we don't have access to the mutable internal buffer of rust-mavlink's raw messages fn try_from(value: Packet) -> Result { + /// warning: this has a bad performance because we don't have access to the mutable internal buffer of rust-mavlink's raw messages fn try_from(value: Packet) -> Result { match value { Packet::V1(v1_packet) => mavlink::MAVLinkV1MessageRaw::try_from(v1_packet), @@ -34,7 +34,7 @@ impl TryFrom for mavlink::MAVLinkV2MessageRaw { type Error = mavlink::error::MessageReadError; /// A convenient rust-mavlink compatibility layer - /// warning: this has a bad performance because we don't have access to the mutable internal buffer of rust-mavlink's raw messages fn try_from(value: Packet) -> Result { + /// warning: this has a bad performance because we don't have access to the mutable internal buffer of rust-mavlink's raw messages fn try_from(value: Packet) -> Result { match value { Packet::V1(_) => Err(mavlink::error::MessageReadError::Io(std::io::Error::new( diff --git a/src/v1.rs b/src/v1.rs index 38e012c..3171c20 100644 --- a/src/v1.rs +++ b/src/v1.rs @@ -247,7 +247,9 @@ mod test { #[test] fn test_v1packet_from_raw_v1_message() { - use mavlink::{ardupilotmega::MavMessage, MAVLinkV1MessageRaw, MavHeader, Message}; + use mavlink::{ + dialects::ardupilotmega::MavMessage, MAVLinkV1MessageRaw, MavHeader, Message, + }; let raw_v1_message = { let header = MavHeader { diff --git a/src/v2.rs b/src/v2.rs index 6652ac4..c4c3e00 100644 --- a/src/v2.rs +++ b/src/v2.rs @@ -474,7 +474,9 @@ mod test { #[test] fn test_v2packet_from_raw_v2_message() { - use mavlink::{ardupilotmega::MavMessage, MAVLinkV2MessageRaw, MavHeader, Message}; + use mavlink::{ + dialects::ardupilotmega::MavMessage, MAVLinkV2MessageRaw, MavHeader, Message, + }; let raw_v2_message = { let header = MavHeader { @@ -512,7 +514,9 @@ mod test { #[test] fn test_raw_v2_message_from_v2packet() { - use mavlink::{ardupilotmega::MavMessage, MAVLinkV2MessageRaw, MavHeader, Message}; + use mavlink::{ + dialects::ardupilotmega::MavMessage, MAVLinkV2MessageRaw, MavHeader, Message, + }; let raw_v2_message_original = { let header = MavHeader { diff --git a/tests/arbitrary_compatibility_test.rs b/tests/arbitrary_compatibility_test.rs new file mode 100644 index 0000000..7a5ac13 --- /dev/null +++ b/tests/arbitrary_compatibility_test.rs @@ -0,0 +1,193 @@ +//! Cross-checks our codec against rust-mavlink over the entire ardupilotmega dialect. +//! +//! For every message id in the compiled dialect we generate several random-payload frames +//! (via rust-mavlink's `arbitrary` support), then feed the exact same on-wire bytes into both +//! our `MavlinkCodec` and rust-mavlink's raw-message reader, asserting the two implementations +//! agree on acceptance and on every decoded field. + +use bytes::BytesMut; +use rand::{prelude::StdRng, SeedableRng}; +use tokio_util::codec::Decoder; + +use dev_utils::{ + all_message_ids, create_random_v1_message_from_id, create_random_v2_message_from_id, +}; +use mavlink::dialects::ardupilotmega::MavMessage; +use mavlink_codec::codec::MavlinkCodec; + +const SEED: u64 = 42; +const ITERATIONS_PER_MESSAGE: usize = 32; + +#[test] +fn v2_all_messages_match_rust_mavlink() { + let mut rng: StdRng = SeedableRng::seed_from_u64(SEED); + let mut compared = 0usize; + + for &(name, id) in all_message_ids() { + for _ in 0..ITERATIONS_PER_MESSAGE { + let raw = create_random_v2_message_from_id(&mut rng, id) + .unwrap_or_else(|| panic!("dialect is missing v2 message {name} (id {id})")); + let wire = raw.raw_bytes(); + + // Our codec decodes the frame we just serialized. + let mut codec = + MavlinkCodec::::default(); + let mut buf = BytesMut::with_capacity(wire.len()); + buf.extend_from_slice(wire); + let our_packet = match codec.decode(&mut buf) { + Ok(Some(Ok(packet))) => packet, + other => panic!("our codec rejected v2 {name} (id {id}): {other:?}"), + }; + assert!( + buf.is_empty(), + "our codec left {} trailing bytes for v2 {name} (id {id})", + buf.len() + ); + + // rust-mavlink parses the same bytes. + let mut reader = mavlink::peek_reader::PeekReader::new(wire); + let their = mavlink::read_v2_raw_message::(&mut reader) + .unwrap_or_else(|e| panic!("rust-mavlink rejected v2 {name} (id {id}): {e:?}")); + + assert_eq!( + our_packet.as_slice(), + wire, + "v2 {name} (id {id}): our frame bytes diverged from the serialized frame" + ); + assert_eq!( + our_packet.as_slice(), + their.raw_bytes(), + "v2 {name} (id {id}): our frame bytes diverged from rust-mavlink" + ); + assert_eq!( + our_packet.message_id(), + id, + "v2 {name} (id {id}): message id" + ); + assert_eq!( + our_packet.message_id(), + their.message_id(), + "v2 {name} (id {id}): message id vs rust-mavlink" + ); + assert_eq!( + *our_packet.system_id(), + their.system_id(), + "v2 {name} (id {id}): system id" + ); + assert_eq!( + *our_packet.component_id(), + their.component_id(), + "v2 {name} (id {id}): component id" + ); + assert_eq!( + *our_packet.sequence(), + their.sequence(), + "v2 {name} (id {id}): sequence" + ); + assert_eq!( + our_packet.payload(), + their.payload(), + "v2 {name} (id {id}): payload" + ); + assert_eq!( + our_packet.checksum(), + their.checksum(), + "v2 {name} (id {id}): checksum" + ); + + compared += 1; + } + } + + println!( + "Compared {compared} v2 frames across {} messages", + all_message_ids().len() + ); +} + +#[test] +fn v1_all_messages_match_rust_mavlink() { + let mut rng: StdRng = SeedableRng::seed_from_u64(SEED); + let mut compared = 0usize; + + for &(name, id) in all_message_ids() { + // MAVLink 1 carries the message id in a single byte, so ids above 255 are unrepresentable. + if id > u8::MAX as u32 { + continue; + } + + for _ in 0..ITERATIONS_PER_MESSAGE { + let raw = create_random_v1_message_from_id(&mut rng, id) + .unwrap_or_else(|| panic!("dialect is missing v1 message {name} (id {id})")); + let wire = raw.raw_bytes(); + + let mut codec = + MavlinkCodec::::default(); + let mut buf = BytesMut::with_capacity(wire.len()); + buf.extend_from_slice(wire); + let our_packet = match codec.decode(&mut buf) { + Ok(Some(Ok(packet))) => packet, + other => panic!("our codec rejected v1 {name} (id {id}): {other:?}"), + }; + assert!( + buf.is_empty(), + "our codec left {} trailing bytes for v1 {name} (id {id})", + buf.len() + ); + + let mut reader = mavlink::peek_reader::PeekReader::new(wire); + let their = mavlink::read_v1_raw_message::(&mut reader) + .unwrap_or_else(|e| panic!("rust-mavlink rejected v1 {name} (id {id}): {e:?}")); + + assert_eq!( + our_packet.as_slice(), + wire, + "v1 {name} (id {id}): our frame bytes diverged from the serialized frame" + ); + assert_eq!( + our_packet.as_slice(), + their.raw_bytes(), + "v1 {name} (id {id}): our frame bytes diverged from rust-mavlink" + ); + assert_eq!( + our_packet.message_id(), + id, + "v1 {name} (id {id}): message id" + ); + assert_eq!( + our_packet.message_id(), + their.message_id() as u32, + "v1 {name} (id {id}): message id vs rust-mavlink" + ); + assert_eq!( + *our_packet.system_id(), + their.system_id(), + "v1 {name} (id {id}): system id" + ); + assert_eq!( + *our_packet.component_id(), + their.component_id(), + "v1 {name} (id {id}): component id" + ); + assert_eq!( + *our_packet.sequence(), + their.sequence(), + "v1 {name} (id {id}): sequence" + ); + assert_eq!( + our_packet.payload(), + their.payload(), + "v1 {name} (id {id}): payload" + ); + assert_eq!( + our_packet.checksum(), + their.checksum(), + "v1 {name} (id {id}): checksum" + ); + + compared += 1; + } + } + + println!("Compared {compared} v1 frames"); +} diff --git a/tests/chuncked_decode_test.rs b/tests/chuncked_decode_test.rs index 2627261..1627c1b 100644 --- a/tests/chuncked_decode_test.rs +++ b/tests/chuncked_decode_test.rs @@ -31,7 +31,7 @@ async fn chuncked_decode_v1() { // False-marker recovery is exercised by the exploit suite, not here. for _ in 0..100 { let trash: u8 = loop { - let b: u8 = rng.gen(); + let b: u8 = rng.random(); if b != 0xFD && b != 0xFE { break b; } @@ -117,7 +117,7 @@ async fn chuncked_decode_v2() { // False-marker recovery is exercised by the exploit suite, not here. for _ in 0..100 { let trash: u8 = loop { - let b: u8 = rng.gen(); + let b: u8 = rng.random(); if b != 0xFD && b != 0xFE { break b; } diff --git a/tests/exploits/common.rs b/tests/exploits/common.rs index 19bf96f..289b09b 100644 --- a/tests/exploits/common.rs +++ b/tests/exploits/common.rs @@ -2,8 +2,8 @@ use bytes::BytesMut; use mavlink::{ - ardupilotmega::MavMessage, MAVLinkV1MessageRaw, MAVLinkV2MessageRaw, MavHeader, Message, - SigningConfig, SigningData, + dialects::ardupilotmega::MavMessage, MAVLinkV1MessageRaw, MAVLinkV2MessageRaw, MavHeader, + Message, SigningConfig, SigningData, }; use mavlink_codec::{codec::MavlinkCodec, error::DecoderError, Packet}; use tokio_util::codec::Decoder; @@ -51,7 +51,7 @@ pub fn build_signed_v2(msgid: u32, sysid: u8, compid: u8, seq: u8) -> Vec { sequence: seq, }; let msg = MavMessage::default_message_from_id(msgid) - .or_else(|_| MavMessage::default_message_from_id(0)) + .or_else(|| MavMessage::default_message_from_id(0)) .unwrap(); let mut raw = MAVLinkV2MessageRaw::new(); raw.serialize_message_for_signing(header, &msg); @@ -67,7 +67,7 @@ pub fn build_valid_v2(msgid: u32, sysid: u8, compid: u8, seq: u8) -> Vec { sequence: seq, }; let msg = MavMessage::default_message_from_id(msgid) - .or_else(|_| MavMessage::default_message_from_id(0)) + .or_else(|| MavMessage::default_message_from_id(0)) .unwrap(); let mut raw = MAVLinkV2MessageRaw::new(); raw.serialize_message(header, &msg); @@ -81,7 +81,7 @@ pub fn build_valid_v1(msgid: u32, sysid: u8, compid: u8, seq: u8) -> Vec { sequence: seq, }; let msg = MavMessage::default_message_from_id(msgid) - .or_else(|_| MavMessage::default_message_from_id(0)) + .or_else(|| MavMessage::default_message_from_id(0)) .unwrap(); let mut raw = MAVLinkV1MessageRaw::new(); raw.serialize_message(header, &msg); diff --git a/tests/exploits/desync_liveness/leading_garbage.rs b/tests/exploits/desync_liveness/leading_garbage.rs index d90ceae..5e4b13d 100644 --- a/tests/exploits/desync_liveness/leading_garbage.rs +++ b/tests/exploits/desync_liveness/leading_garbage.rs @@ -17,7 +17,7 @@ fn leading_garbage_skipped_valid_frame_decodes() { let mut garbage = Vec::with_capacity(100); for _ in 0..100 { let b: u8 = loop { - let b = rng.gen(); + let b = rng.random(); if b != 0xFD && b != 0xFE { break b; } diff --git a/tests/exploits/desync_liveness/mod.rs b/tests/exploits/desync_liveness/mod.rs index c1f3b4d..b082ad7 100644 --- a/tests/exploits/desync_liveness/mod.rs +++ b/tests/exploits/desync_liveness/mod.rs @@ -21,7 +21,7 @@ mod length_overskip; mod rogue_stx; mod stx_storm; -use mavlink::{ardupilotmega::MavMessage, calculate_crc, Message}; +use mavlink::{calculate_crc, dialects::ardupilotmega::MavMessage, Message}; use crate::common::build_v2_frame; diff --git a/tests/exploits/parser_dos/forward_progress.rs b/tests/exploits/parser_dos/forward_progress.rs index 750d412..189751b 100644 --- a/tests/exploits/parser_dos/forward_progress.rs +++ b/tests/exploits/parser_dos/forward_progress.rs @@ -175,7 +175,7 @@ fn interleaved_valid_frames_and_garbage_strict_v2_liveness() { if i + 1 < FRAME_COUNT { for _ in 0..GARBAGE_PER_GAP { let b: u8 = loop { - let b = rng.gen(); + let b = rng.random(); if b != 0xFD && b != 0xFE { break b; }