diff --git a/Cargo.lock b/Cargo.lock index 7052f14..982eb95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] @@ -182,7 +182,7 @@ dependencies = [ "pqcrypto-mldsa", "pqcrypto-mlkem", "pqcrypto-traits", - "rand_core", + "rand_core 0.6.4", "ssh-key", "sskr", "url", @@ -204,7 +204,7 @@ dependencies = [ "hkdf", "hmac", "pbkdf2", - "rand", + "rand 0.8.5", "secp256k1", "sha2", "thiserror", @@ -237,11 +237,11 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0343f6f8ac568563902079c13756e3444a82ef8cb9ed66fc6e3c212341b18e60" dependencies = [ - "getrandom", + "getrandom 0.2.15", "lazy_static", "num-traits", - "rand", - "rand_core", + "rand 0.8.5", + "rand_core 0.6.4", "rand_xoshiro", ] @@ -355,9 +355,8 @@ dependencies = [ name = "btp" version = "0.1.0" dependencies = [ - "anyhow", "consts", - "minicbor 0.24.4", + "rand 0.9.1", ] [[package]] @@ -530,7 +529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -542,7 +541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -704,7 +703,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -730,7 +729,7 @@ dependencies = [ "generic-array", "group", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -752,7 +751,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -990,7 +989,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -1012,7 +1023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1460,7 +1471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -1485,7 +1496,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -1596,7 +1607,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "rand_core", + "rand_core 0.6.4", "sha2", ] @@ -1671,7 +1682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -1762,7 +1773,7 @@ source = "git+https://github.com/Foundation-Devices/pqcrypto?rev=ebadf71214f67cb dependencies = [ "cc", "dunce", - "getrandom", + "getrandom 0.2.15", "libc", ] @@ -1830,7 +1841,7 @@ dependencies = [ "dcbor", "hex", "hkdf", - "rand_core", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1874,6 +1885,12 @@ 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 = "radium" version = "0.7.0" @@ -1887,8 +1904,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1898,7 +1925,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -1907,7 +1944,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", ] [[package]] @@ -1916,7 +1962,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -2005,7 +2051,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sha2", "signature", "spki", @@ -2073,7 +2119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes 0.14.0", - "rand", + "rand 0.8.5", "secp256k1-sys", ] @@ -2168,7 +2214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -2251,7 +2297,7 @@ dependencies = [ "p256", "p384", "p521", - "rand_core", + "rand_core 0.6.4", "rsa", "sec1", "sha1", @@ -2487,6 +2533,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2650,6 +2705,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "write16" version = "1.0.0" @@ -2678,7 +2742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "serde", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index a9c758f..085c151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ foundation-api = { path = "api" } foundation-abstracted = { path = "abstracted" } btp = { path = "btp" } quantum-link-macros = { path = "quantum-link-macros" } +rand = "0.9.1" [patch.crates-io] pqcrypto-traits = { git = "https://github.com/Foundation-Devices/pqcrypto", rev = "ebadf71214f67cb970242fa1053b4acb65767737" } diff --git a/abstracted/src/abstracted/abstract_bluetooth.rs b/abstracted/src/abstracted/abstract_bluetooth.rs index f6eaf0d..f8549db 100644 --- a/abstracted/src/abstracted/abstract_bluetooth.rs +++ b/abstracted/src/abstracted/abstract_bluetooth.rs @@ -43,8 +43,8 @@ pub trait AbstractBluetoothChannel { } } - let message = unchunker.data(); - Envelope::try_from_cbor_data(message.to_owned()) + let message = unchunker.data().expect("data is complete"); + Envelope::try_from_cbor_data(message) } async fn send_request_with_id( diff --git a/api/src/api/message.rs b/api/src/api/message.rs index 52eab73..97390c8 100644 --- a/api/src/api/message.rs +++ b/api/src/api/message.rs @@ -12,6 +12,12 @@ use flutter_rust_bridge::frb; use minicbor_derive::{Decode, Encode}; use quantum_link_macros::quantum_link; +#[quantum_link] +pub struct RawMessage { + #[n(0)] + pub payload: Vec, +} + #[quantum_link] pub struct EnvoyMessage { #[n(0)] diff --git a/btp/Cargo.toml b/btp/Cargo.toml index 9c20199..46de867 100644 --- a/btp/Cargo.toml +++ b/btp/Cargo.toml @@ -5,6 +5,5 @@ edition = "2021" homepage.workspace = true [dependencies] -minicbor = "0.24.2" -anyhow = "1.0.86" -consts = { git = "https://github.com/Foundation-Devices/prime-ble-firmware.git", features = ["dle"] } \ No newline at end of file +consts = { git = "https://github.com/Foundation-Devices/prime-ble-firmware.git", features = ["dle"] } +rand = { workspace = true } diff --git a/btp/src/lib.rs b/btp/src/lib.rs index fffe801..584592f 100644 --- a/btp/src/lib.rs +++ b/btp/src/lib.rs @@ -1,162 +1,252 @@ use consts::APP_MTU; -use std::collections::HashMap; +use rand::Rng; +#[cfg(test)] mod tests; -// TODO: is it possible to make this dynamic? -const CBOR_OVERHEAD: usize = 16; // 2 u32 + CBOR bytes header + padding -const CHUNK_SIZE: usize = APP_MTU - CBOR_OVERHEAD; +const HEADER_SIZE: usize = std::mem::size_of::
(); + +#[derive(Debug, Clone, Copy)] +struct Header { + message_id: u16, + index: u16, + total_chunks: u16, + data_len: u8, + is_last: bool, +} + +impl Header { + fn to_bytes(self) -> [u8; HEADER_SIZE] { + let mut bytes = [0; HEADER_SIZE]; + bytes[0..2].copy_from_slice(&self.message_id.to_be_bytes()); + bytes[2..4].copy_from_slice(&self.index.to_be_bytes()); + bytes[4..6].copy_from_slice(&self.total_chunks.to_be_bytes()); + bytes[6] = self.data_len; + bytes[7] = if self.is_last { 1 } else { 0 }; + bytes + } + + fn from_bytes(bytes: &[u8]) -> Option { + if bytes.len() < HEADER_SIZE { + return None; + } + let message_id = u16::from_be_bytes([bytes[0], bytes[1]]); + let index = u16::from_be_bytes([bytes[2], bytes[3]]); + let total_chunks = u16::from_be_bytes([bytes[4], bytes[5]]); + let data_len = bytes[6]; + let is_last = bytes[7] != 0; + Some(Self { + message_id, + index, + total_chunks, + data_len, + is_last, + }) + } +} pub struct Chunker<'a> { data: &'a [u8], - total_chunks: usize, - current_chunk: usize, + message_id: u16, + current_index: u16, + total_chunks: u16, + data_per_chunk: usize, } -impl Iterator for Chunker<'_> { +impl<'a> Iterator for Chunker<'a> { type Item = [u8; APP_MTU]; fn next(&mut self) -> Option { - if self.current_chunk >= self.total_chunks { + let start_idx = self.current_index as usize * self.data_per_chunk; + if start_idx >= self.data.len() { return None; } - let remaining_data = self.data.len() - self.current_chunk * CHUNK_SIZE; - let chunk_size = std::cmp::min(remaining_data, CHUNK_SIZE); - let chunk = &self.data[self.current_chunk * CHUNK_SIZE..][..chunk_size]; - let mut buffer = [0u8; APP_MTU]; - let mut encoder = minicbor::Encoder::new(&mut buffer[..]); - // Encode chunk index (m of n) and data - encoder - .u32(self.current_chunk as u32) - .unwrap() - .u32(self.total_chunks as u32) - .unwrap(); // m of n - encoder.bytes(chunk).unwrap(); + let end_idx = (start_idx + self.data_per_chunk).min(self.data.len()); + let chunk_data = &self.data[start_idx..end_idx]; + let is_last = end_idx >= self.data.len(); + + let header = Header { + message_id: self.message_id, + index: self.current_index, + total_chunks: self.total_chunks, + data_len: chunk_data.len() as u8, + is_last, + }; + + buffer[..HEADER_SIZE].copy_from_slice(&header.to_bytes()); + buffer[HEADER_SIZE..HEADER_SIZE + chunk_data.len()].copy_from_slice(chunk_data); + self.current_index += 1; - self.current_chunk += 1; Some(buffer) } } pub fn chunk(data: &[u8]) -> Chunker<'_> { - let total_chunks = (data.len() as f64 / CHUNK_SIZE as f64).ceil() as usize; + let message_id = rand::rng().random::(); + let data_per_chunk = APP_MTU - HEADER_SIZE; + let total_chunks = data.len().div_ceil(data_per_chunk) as u16; + Chunker { data, + message_id, + current_index: 0, total_chunks, - current_chunk: 0, + data_per_chunk, + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum DecodeError { + PacketTooSmall { size: usize }, + InvalidHeader, + WrongMessageId { expected: u16, received: u16 }, +} + +impl std::fmt::Display for DecodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DecodeError::PacketTooSmall { size } => write!(f, "Packet too small: {size} bytes"), + DecodeError::InvalidHeader => write!(f, "Invalid header"), + DecodeError::WrongMessageId { expected, received } => { + write!( + f, + "Wrong message ID: expected {expected}, received {received}" + ) + } + } + } +} + +impl std::error::Error for DecodeError {} + +const CHUNK_DATA_SIZE: usize = APP_MTU - HEADER_SIZE; + +#[derive(Clone, Copy)] +struct Chunk { + data: [u8; CHUNK_DATA_SIZE], + len: u8, +} + +impl Chunk { + fn as_slice(&self) -> &[u8] { + &self.data[..self.len as usize] } } -#[derive(Default)] pub struct Dechunker { - data: Vec, - pub seen: u32, + chunks: Vec>, + message_id: Option, + total_chunks: Option, is_complete: bool, - pub ooo_chunks: HashMap>, - pub m: u32, - pub n: u32, +} + +impl Default for Dechunker { + fn default() -> Self { + Self::new() + } } impl Dechunker { pub fn new() -> Self { - Self::default() + Self { + chunks: Vec::new(), + message_id: None, + total_chunks: None, + is_complete: false, + } } pub fn is_complete(&self) -> bool { self.is_complete } - pub fn data(&self) -> &Vec { - &self.data - } - pub fn clear(&mut self) { - self.ooo_chunks.clear(); - self.data.clear(); - self.seen = 0; + self.chunks.clear(); + self.message_id = None; + self.total_chunks = None; self.is_complete = false; } + pub fn progress(&self) -> f32 { - if self.n == 0 { - 0.0 - } else { - self.seen.min(self.n) as f32 / self.n as f32 + match self.total_chunks { + Some(total) if total > 0 => { + let received = self.chunks.iter().filter(|c| c.is_some()).count(); + received as f32 / total as f32 + } + _ => 0.0, } } -} -impl Dechunker { - pub fn receive(&mut self, data: &[u8]) -> anyhow::Result>> { - let mut decoder = minicbor::Decoder::new(data); - - let m = decoder.u32().map_err(|_| { - self.clear(); - anyhow::anyhow!("Invalid m value") - })?; - - let n = decoder.u32().map_err(|_| { - self.clear(); - anyhow::anyhow!("Invalid n value") - })?; - - if n == 0 { - self.clear(); - return Err(anyhow::anyhow!("n cannot be zero")); + pub fn receive(&mut self, data: &[u8]) -> Result>, DecodeError> { + let Some((header_data, chunk_data)) = data.split_at_checked(HEADER_SIZE) else { + return Err(DecodeError::PacketTooSmall { size: data.len() }); + }; + + let header = Header::from_bytes(header_data).ok_or(DecodeError::InvalidHeader)?; + + match self.message_id { + None => { + self.message_id = Some(header.message_id); + self.total_chunks = Some(header.total_chunks); + self.chunks.resize(header.total_chunks as usize, None); + } + Some(id) if id != header.message_id => { + return Err(DecodeError::WrongMessageId { + expected: id, + received: header.message_id, + }); + } + _ => {} } - if m >= n { - self.clear(); - return Err(anyhow::anyhow!("m must be less than n")); + let data_len = header.data_len as usize; + + // store chunk if not already received + // should this be an error? + if self.chunks[header.index as usize].is_none() { + let mut data = [0u8; CHUNK_DATA_SIZE]; + data[..data_len].copy_from_slice(&chunk_data[..data_len]); + self.chunks[header.index as usize] = Some(Chunk { + data, + len: data_len as u8, + }); } - self.m = m; - self.n = n; - - // Decode chunk data - let chunk_data = decoder.bytes().map_err(|_| { - self.clear(); - anyhow::anyhow!("Invalid chunk data") - })?; - - // Handle out-of-order chunks first - if m != self.seen { - self.ooo_chunks.insert(m, chunk_data.to_vec()); - - // Try to process any in-order chunks we might have now - while !self.ooo_chunks.is_empty() { - if let Some(&next_m) = self.ooo_chunks.keys().min() { - if next_m == self.seen { - let data = self.ooo_chunks.remove(&next_m).unwrap(); - self.data.extend(data); - self.seen += 1; - } else { - break; - } - } - } + if header.is_last { + self.is_complete = true; + } - return Ok(None); + // attempt to complete the message + if self.is_complete { + let data = self.data(); + return Ok(data); } - // Handle in-order chunk - self.data.extend_from_slice(chunk_data); - self.seen += 1; + Ok(None) + } - // Process any buffered OoO chunks that are now in-order - while let Some(data) = self.ooo_chunks.remove(&self.seen) { - self.data.extend(data); - self.seen += 1; + pub fn message_id(&self) -> Option { + self.message_id + } + + pub fn data(&self) -> Option> { + if !self.is_complete { + return None; } - // Check if transfer is complete - if self.seen == self.n { - self.is_complete = true; - return Ok(Some(&self.data)); + let mut result = Vec::new(); + let total = self.total_chunks? as usize; + + for i in 0..total { + match self.chunks.get(i).and_then(|chunk| chunk.as_ref()) { + Some(chunk) => result.extend_from_slice(chunk.as_slice()), + None => return None, + } } - Ok(None) + Some(result) } } diff --git a/btp/src/tests.rs b/btp/src/tests.rs index bec7a6f..27cb77a 100644 --- a/btp/src/tests.rs +++ b/btp/src/tests.rs @@ -1,50 +1,276 @@ -#[cfg(test)] use crate::{chunk, Dechunker, APP_MTU}; + #[test] fn end_to_end() { - // Example data - let data = b"This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor."; + let data = b" + This is some example data to be chunked.This is some example data to be chunked.This is some example data to be chunked. + This is some example data to be chunked.This is some example data to be chunked.This is some example data to be chunked. + This is some example data to be chunked.This is some example data to be chunked.This is some example data to be chunked. + This is some example data to be chunked.This is some example data to be chunked.This is some example data to be chunked. + ".to_vec(); - // Chunk the data - let chunked_data: Vec<[u8; APP_MTU]> = chunk(data).collect(); + let chunked_data: Vec<[u8; APP_MTU]> = chunk(&data).collect(); assert_eq!(chunked_data.len(), 3); - // Unchunk the data let mut unchunker = Dechunker::new(); for chunk in chunked_data { unchunker .receive(chunk.as_ref()) - .expect("TODO: panic message"); - if unchunker.is_complete() { - assert!(data.eq(unchunker.data().as_slice())); - } + .expect("Failed to receive chunk"); } + + assert_eq!(unchunker.data(), Some(data)); + assert!(unchunker.is_complete()); } #[test] fn end_to_end_ooo() { - // Example data - let data = b"This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor.This is some example data to be chunked using minicbor."; + let data = vec![0u8; 100000]; - // Chunk the data - let mut chunked_data: Vec<[u8; APP_MTU]> = chunk(data).collect(); + let mut chunked_data: Vec<[u8; APP_MTU]> = chunk(&data).collect(); - // Shuffle every other to simulate them being received out of order chunked_data.swap(0, 2); - chunked_data.swap(4, 3); - // Unchunk the data let mut dechunker = Dechunker::new(); - for chunk in chunked_data { - dechunker - .receive(chunk.as_ref()) - .expect("error receiving chunk"); - if dechunker.is_complete() { - assert!(data.eq(dechunker.data().as_slice())); + for chunk in chunked_data.iter() { + match dechunker.receive(chunk.as_ref()) { + Ok(result) => { + if let Some(reassembled) = result { + assert_eq!(reassembled.len(), data.len(),); + assert!(data.eq(&reassembled),); + } + } + Err(e) => panic!("Error receiving chunk: {e}"), + } + } + + assert_eq!(dechunker.data(), Some(data)); +} + +#[test] +fn test_single_chunk() { + let data = b"Small data".to_vec(); + let chunks: Vec<_> = chunk(&data).collect(); + + assert_eq!(chunks.len(), 1); + + let mut dechunker = Dechunker::new(); + let result = dechunker.receive(&chunks[0]).unwrap(); + + assert_eq!(result, Some(data)); + assert!(dechunker.is_complete()); +} + +#[test] +fn test_empty_data() { + let data = b""; + let chunks: Vec<_> = chunk(data).collect(); + assert_eq!(chunks.len(), 0, "Empty data should produce no chunks"); +} + +#[test] +fn test_exact_chunk_boundary() { + let data_per_chunk = APP_MTU - crate::HEADER_SIZE; + let data = vec![42u8; data_per_chunk * 3]; + + let chunks: Vec<_> = chunk(&data).collect(); + assert_eq!( + chunks.len(), + 3, + "Data exactly filling 3 chunks should produce 3 chunks" + ); + + let mut dechunker = Dechunker::new(); + for (i, chunk) in chunks.iter().enumerate() { + let result = dechunker.receive(chunk).unwrap(); + if i < chunks.len() - 1 { + assert!( + result.is_none(), + "non-last chunks should not complete the message" + ); + } else { + assert_eq!( + dechunker.data().as_ref(), + Some(&data), + "last chunk should complete the message" + ); } } + assert_eq!(dechunker.data(), Some(data)) +} + +#[test] +fn test_different_message_ids() { + let data1 = b"Message 1".to_vec(); + let data2 = b"Message 2".to_vec(); + + let chunks1: Vec<_> = chunk(&data1).collect(); + let chunks2: Vec<_> = chunk(&data2).collect(); + + let mut dechunker1 = Dechunker::new(); + let mut dechunker2 = Dechunker::new(); + + dechunker1.receive(&chunks1[0]).unwrap(); + + let result = dechunker1.receive(&chunks2[0]); + assert!( + matches!(result, Err(crate::DecodeError::WrongMessageId { .. })), + "Chunk from different message should be rejected" + ); + + let result = dechunker2.receive(&chunks2[0]).unwrap(); + assert!( + result.is_some(), + "Single chunk message should complete immediately" + ); + assert_eq!(result, Some(data2)); +} + +#[test] +fn test_progress_tracking() { + let data = vec![1u8; 10000]; + let chunks: Vec<_> = chunk(&data).collect(); + + let mut dechunker = Dechunker::new(); + + assert_eq!(dechunker.progress(), 0.0, "Initial progress should be 0"); + + let mut last_progress = 0.0; + for (i, chunk) in chunks.iter().enumerate() { + dechunker.receive(chunk).unwrap(); + + let current_progress = dechunker.progress(); + + assert!( + current_progress >= last_progress, + "Progress should never decrease" + ); + + if i == chunks.len() - 1 { + assert!( + (current_progress - 1.0).abs() < 0.01, + "Progress should be 1.0 when all chunks received" + ); + } + + last_progress = current_progress; + } + + assert_eq!(dechunker.data(), Some(data)); +} + +#[test] +fn test_duplicate_chunks() { + let data = b"Test duplicate handling".to_vec(); + let chunks: Vec<_> = chunk(&data).collect(); + + let mut dechunker = Dechunker::new(); + + dechunker.receive(&chunks[0]).unwrap(); + dechunker.receive(&chunks[0]).unwrap(); + + let result = dechunker.receive(&chunks[0]).unwrap(); + assert!( + result.is_some(), + "Duplicate chunks should not prevent completion" + ); + assert_eq!( + result.unwrap(), + data, + "Data should be correctly reassembled despite duplicates" + ); +} + +#[test] +fn test_missing_middle_chunk() { + let data = vec![1u8; 1000]; + let chunks: Vec<_> = chunk(&data).collect(); + + if chunks.len() < 3 { + return; + } + + let mut dechunker = Dechunker::new(); + + let middle = chunks.len() / 2; + + for (i, chunk) in chunks.iter().enumerate() { + if i != middle { + dechunker.receive(chunk).unwrap(); + } + } + + assert!( + dechunker.data().is_none(), + "Message should not complete with middle chunk still missing" + ); + + let result = dechunker.receive(&chunks[middle]).unwrap(); + + assert_eq!(result, Some(data)); + assert!(dechunker.is_complete()); +} + +#[test] +fn test_data_with_zeros() { + let mut data = vec![0u8; 500]; + data[100] = 1; + data[200] = 2; + data[300] = 3; + data[400] = 4; + + let chunks: Vec<_> = chunk(&data).collect(); + let mut dechunker = Dechunker::new(); + + for chunk in chunks { + if let Some(result) = dechunker.receive(&chunk).unwrap() { + assert_eq!( + result.len(), + data.len(), + "Data with zeros should maintain correct length" + ); + assert_eq!(result, data, "Data with zeros should be preserved exactly"); + } + } + + assert!( + dechunker.is_complete(), + "Dechunker should complete successfully with zero-containing data" + ); +} + +#[test] +fn test_reverse_order_decoding() { + let data = b" + This is some example data to be chunked.This is some example data to be chunked.This is some example data to be chunked. + This is some example data to be chunked.This is some example data to be chunked.This is some example data to be chunked. + This is some example data to be chunked.This is some example data to be chunked.This is some example data to be chunked. + This is some example data to be chunked.This is some example data to be chunked.This is some example data to be chunked. + ".to_vec(); + let chunks: Vec<_> = chunk(&data).collect(); + + let mut dechunker = Dechunker::new(); + + for chunk in chunks.iter().rev() { + let result = dechunker.receive(chunk).unwrap(); + + if result.is_some() { + assert_eq!( + result.unwrap(), + data, + "Data should be correctly reassembled" + ); + } + } + + assert!(dechunker.is_complete(), "Dechunker should be complete"); + assert_eq!( + dechunker.data(), + Some(data), + "Final data should match original" + ); }