From 9ea632c571aae5bca2ae44fddd68f141830e918a Mon Sep 17 00:00:00 2001 From: Emmanuel Thompson Date: Sat, 17 Oct 2026 21:35:46 -0400 Subject: [PATCH 1/9] test: align no-bits padding expectations --- tests/test_attributes/test_padding/test_pad_bytes_after.rs | 2 +- tests/test_attributes/test_padding/test_pad_bytes_before.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_attributes/test_padding/test_pad_bytes_after.rs b/tests/test_attributes/test_padding/test_pad_bytes_after.rs index c2039f4f..05930c44 100644 --- a/tests/test_attributes/test_padding/test_pad_bytes_after.rs +++ b/tests/test_attributes/test_padding/test_pad_bytes_after.rs @@ -29,7 +29,7 @@ fn test_pad_bytes_after() { #[test] #[cfg(feature = "descriptive-errors")] -#[should_panic(expected = "Incomplete(NeedSize { bits: 8 })")] +#[should_panic(expected = "Incomplete(NeedSize { bits: 24 })")] fn test_pad_bytes_after_not_enough() { #[derive(PartialEq, Debug, DekuRead, DekuWrite)] struct TestStruct { diff --git a/tests/test_attributes/test_padding/test_pad_bytes_before.rs b/tests/test_attributes/test_padding/test_pad_bytes_before.rs index 5c25c1cd..10d5526b 100644 --- a/tests/test_attributes/test_padding/test_pad_bytes_before.rs +++ b/tests/test_attributes/test_padding/test_pad_bytes_before.rs @@ -29,7 +29,7 @@ fn test_pad_bytes_before() { #[test] #[cfg(feature = "descriptive-errors")] -#[should_panic(expected = "Incomplete(NeedSize { bits: 8 })")] +#[should_panic(expected = "Incomplete(NeedSize { bits: 16 })")] fn test_pad_bytes_before_not_enough() { #[derive(PartialEq, Debug, DekuRead, DekuWrite)] struct TestStruct { From 2293ec9f7a9fff24e4e2cde8c5361bb2551c9bde Mon Sep 17 00:00:00 2001 From: Emmanuel Thompson Date: Sat, 17 Oct 2026 21:36:11 -0400 Subject: [PATCH 2/9] fix: resolve crate paths in container readers --- deku-derive/src/macros/deku_read.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deku-derive/src/macros/deku_read.rs b/deku-derive/src/macros/deku_read.rs index 71c88cf0..093fb9db 100644 --- a/deku-derive/src/macros/deku_read.rs +++ b/deku-derive/src/macros/deku_read.rs @@ -113,7 +113,7 @@ fn emit_struct(input: &DekuData) -> Result { let from_reader_body = quote! { use core::convert::TryFrom; use ::#crate_::DekuReader as _; - let __deku_reader = &mut deku::reader::Reader::new(__deku_input.0); + let __deku_reader = &mut ::#crate_::reader::Reader::new(__deku_input.0); if __deku_input.1 != 0 { __deku_reader.skip_bits(__deku_input.1, ::#crate_::ctx::Order::default())?; } @@ -127,7 +127,7 @@ fn emit_struct(input: &DekuData) -> Result { use core::convert::TryFrom; use ::#crate_::DekuReader as _; let mut __deku_cursor = #crate_::no_std_io::Cursor::new(__deku_input.0); - let mut __deku_reader = &mut deku::reader::Reader::new(&mut __deku_cursor); + let mut __deku_reader = &mut ::#crate_::reader::Reader::new(&mut __deku_cursor); if __deku_input.1 != 0 { __deku_reader.skip_bits(__deku_input.1, ::#crate_::ctx::Order::default())?; } @@ -140,7 +140,7 @@ fn emit_struct(input: &DekuData) -> Result { (__deku_reader.bits_read - (__deku_reader.bits_read % 8)) / 8 }; let Some(rest) = __deku_input.0.get(idx..) else { - return Err(deku::DekuError::Incomplete(deku::prelude::NeedSize::new(8 * (idx - __deku_input.0.len())))); + return Err(::#crate_::DekuError::Incomplete(::#crate_::prelude::NeedSize::new(8 * (idx - __deku_input.0.len())))); }; Ok(((rest, __deku_reader.bits_read % 8), __deku_value)) }; @@ -400,7 +400,7 @@ fn emit_enum(input: &DekuData) -> Result { let from_reader_body = quote! { use core::convert::TryFrom; use ::#crate_::DekuReader as _; - let __deku_reader = &mut deku::reader::Reader::new(__deku_input.0); + let __deku_reader = &mut ::#crate_::reader::Reader::new(__deku_input.0); if __deku_input.1 != 0 { __deku_reader.skip_bits(__deku_input.1, ::#crate_::ctx::Order::default())?; } @@ -414,7 +414,7 @@ fn emit_enum(input: &DekuData) -> Result { use core::convert::TryFrom; use ::#crate_::DekuReader as _; let mut __deku_cursor = #crate_::no_std_io::Cursor::new(__deku_input.0); - let mut __deku_reader = &mut deku::reader::Reader::new(&mut __deku_cursor); + let mut __deku_reader = &mut ::#crate_::reader::Reader::new(&mut __deku_cursor); if __deku_input.1 != 0 { __deku_reader.skip_bits(__deku_input.1, ::#crate_::ctx::Order::default())?; } @@ -427,7 +427,7 @@ fn emit_enum(input: &DekuData) -> Result { (__deku_reader.bits_read - (__deku_reader.bits_read % 8)) / 8 }; let Some(rest) = __deku_input.0.get(idx..) else { - return Err(deku::DekuError::Incomplete(deku::prelude::NeedSize::new(8 * (idx - __deku_input.0.len())))); + return Err(::#crate_::DekuError::Incomplete(::#crate_::prelude::NeedSize::new(8 * (idx - __deku_input.0.len())))); }; Ok(((rest, __deku_reader.bits_read % 8), __deku_value)) }; From 4dd4c880974d37847d3ed0f3aec15653bc0ade21 Mon Sep 17 00:00:00 2001 From: Emmanuel Thompson Date: Sat, 17 Oct 2026 21:37:32 -0400 Subject: [PATCH 3/9] fix: route generated logging through deku --- Cargo.toml | 2 +- deku-derive/src/macros/deku_read.rs | 4 ++-- deku-derive/src/macros/deku_write.rs | 6 +++--- src/lib.rs | 4 ++++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79409927..a9be55ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ descriptive-errors = ["alloc"] [dependencies] deku_derive = { version = "^0.20.3", path = "deku-derive", default-features = false} bitvec = { version = "1.1.0", default-features = false, optional = true } -log = { version = "0.4.28", optional = true } +log = { version = "0.4.28", optional = true, default-features = false } no_std_io = { version = "0.9.4", default-features = false, package = "no_std_io2" } rustversion = "1.0.22" diff --git a/deku-derive/src/macros/deku_read.rs b/deku-derive/src/macros/deku_read.rs index 093fb9db..d2ef1698 100644 --- a/deku-derive/src/macros/deku_read.rs +++ b/deku-derive/src/macros/deku_read.rs @@ -684,7 +684,7 @@ fn emit_bit_run_read( }; let trace_field_log = if cfg!(feature = "logging") { let field_ident_str = field_ident.to_string(); - quote! { log::trace!("Reading: {}.{}", #ident, #field_ident_str); } + quote! { ::#crate_::log::trace!("Reading: {}.{}", #ident, #field_ident_str); } } else { quote! {} }; @@ -894,7 +894,7 @@ fn emit_field_read( let trace_field_log = if cfg!(feature = "logging") { quote! { - log::trace!("Reading: {}.{}", #ident, #field_ident_str); + ::#crate_::log::trace!("Reading: {}.{}", #ident, #field_ident_str); } } else { quote! {} diff --git a/deku-derive/src/macros/deku_write.rs b/deku-derive/src/macros/deku_write.rs index 66ca862c..d5696e91 100644 --- a/deku-derive/src/macros/deku_write.rs +++ b/deku-derive/src/macros/deku_write.rs @@ -535,7 +535,7 @@ fn emit_bit_run_write( if cfg!(feature = "logging") { let field_ident_str = field_ident.to_string(); - traces.extend(quote! { log::trace!("Writing: {}.{}", #ident, #field_ident_str); }); + traces.extend(quote! { ::#crate_::log::trace!("Writing: {}.{}", #ident, #field_ident_str); }); } let value = quote! { (*(#object_prefix #field_ident) as u64) }; @@ -822,7 +822,7 @@ fn emit_field_write( let trace_field_log = if cfg!(feature = "logging") { quote! { - log::trace!("Writing: {}.{}", #ident, #field_ident_str); + ::#crate_::log::trace!("Writing: {}.{}", #ident, #field_ident_str); } } else { quote! {} @@ -899,7 +899,7 @@ fn emit_field_write( let skipping_log = if cfg!(feature = "logging") { quote! { - log::trace!("skipping"); + ::#crate_::log::trace!("skipping"); } } else { quote! {} diff --git a/src/lib.rs b/src/lib.rs index bda1b5ce..67528aeb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -543,6 +543,10 @@ use ::bitvec::view::BitViewSized; pub use deku_derive::*; +#[doc(hidden)] +#[cfg(feature = "logging")] +pub use log; + pub mod attributes; pub mod ctx; pub mod error; From dfd5051602802ae3aa52af2824bda69fd7bd3809 Mon Sep 17 00:00:00 2001 From: Emmanuel Thompson Date: Sat, 17 Oct 2026 21:37:55 -0400 Subject: [PATCH 4/9] fix: validate read_all conflicts without bits --- deku-derive/src/lib.rs | 58 +++++++++++++++++-- .../cases/read_all_count_conflict_no_bits.rs | 9 +++ .../read_all_count_conflict_no_bits.stderr | 7 +++ ...ead_all_count_conflict_no_bits.stderr.beta | 7 +++ ...ead_all_count_conflict_no_bits.stderr.msrv | 7 +++ tests/test_compile/mod.rs | 14 +++-- 6 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 tests/test_compile/cases/read_all_count_conflict_no_bits.rs create mode 100644 tests/test_compile/cases/read_all_count_conflict_no_bits.stderr create mode 100644 tests/test_compile/cases/read_all_count_conflict_no_bits.stderr.beta create mode 100644 tests/test_compile/cases/read_all_count_conflict_no_bits.stderr.msrv diff --git a/deku-derive/src/lib.rs b/deku-derive/src/lib.rs index 65a32d33..a84e3326 100644 --- a/deku-derive/src/lib.rs +++ b/deku-derive/src/lib.rs @@ -57,7 +57,6 @@ enum Id { Int(syn::LitInt), Bool(syn::LitBool), } - impl Display for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.to_token_stream().to_string()) @@ -715,7 +714,7 @@ struct FieldData { /// a predicate to decide when to stop reading elements into the container until: Option, - /// read until `reader.end()` + /// read until `reader.try_end()` read_all: bool, /// apply a function to the field after it's read @@ -1006,11 +1005,21 @@ impl FieldData { } // Validate usage of read_all - #[cfg(feature = "bits")] + let bits_read_set = { + #[cfg(feature = "bits")] + { + data.bits_read.is_some() + } + #[cfg(not(feature = "bits"))] + { + false + } + }; if data.read_all && (data.until.is_some() || data.count.is_some() - || (data.bits_read.is_some() || data.bytes_read.is_some())) + || bits_read_set + || data.bytes_read.is_some()) { return Err(cerror( data.read_all.span(), @@ -1306,7 +1315,7 @@ struct DekuFieldReceiver { #[darling(default = "default_res_opt", map = "map_litstr_as_tokenstream")] until: Result, ReplacementError>, - /// read until `reader.end()` + /// read until `reader.try_end()` #[darling(default)] read_all: bool, @@ -1600,6 +1609,7 @@ mod tests { use super::*; + #[cfg(feature = "bits")] #[rstest(input, // Valid struct case::struct_empty(r#"struct Test {}"#), @@ -1653,4 +1663,42 @@ mod tests { res_reader.unwrap(); res_writer.unwrap(); } + + #[cfg(not(feature = "bits"))] + #[rstest( + input, + case::struct_empty(r#"struct Test {}"#), + case::struct_unnamed(r#"struct Test(u8, u8);"#), + case::struct_bytes( + r#" + struct Test { + #[deku(bytes = 4)] + field_a: u32, + #[deku(endian = "little")] + field_b: u16, + }"# + ), + case::enum_empty(r#"#[deku(id_type = "u8")] enum Test {}"#), + case::enum_without_bit_fields( + r#" + #[deku(id_type = "u8")] + enum Test { + #[deku(id = "1")] + A, + #[deku(id = "2")] + B(u8), + }"# + ), + case::invalid_endian(r#"struct Test(#[endian=big] u8);"#) + )] + fn test_macro_without_bits(input: &str) { + let parsed = parse_str(input).unwrap(); + + let data = DekuData::from_input(parsed).unwrap(); + let res_reader = data.emit_reader_checked(); + let res_writer = data.emit_writer_checked(); + + res_reader.unwrap(); + res_writer.unwrap(); + } } diff --git a/tests/test_compile/cases/read_all_count_conflict_no_bits.rs b/tests/test_compile/cases/read_all_count_conflict_no_bits.rs new file mode 100644 index 00000000..d39fed26 --- /dev/null +++ b/tests/test_compile/cases/read_all_count_conflict_no_bits.rs @@ -0,0 +1,9 @@ +use deku::prelude::*; + +#[derive(DekuRead)] +struct Packet { + #[deku(read_all, count = "1")] + payload: Vec, +} + +fn main() {} diff --git a/tests/test_compile/cases/read_all_count_conflict_no_bits.stderr b/tests/test_compile/cases/read_all_count_conflict_no_bits.stderr new file mode 100644 index 00000000..f3e54d9a --- /dev/null +++ b/tests/test_compile/cases/read_all_count_conflict_no_bits.stderr @@ -0,0 +1,7 @@ +error: conflicting: `read_all` cannot be used with `until`, `count`, `bits_read`, or `bytes_read` + --> tests/test_compile/cases/read_all_count_conflict_no_bits.rs:3:10 + | +3 | #[derive(DekuRead)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `DekuRead` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/test_compile/cases/read_all_count_conflict_no_bits.stderr.beta b/tests/test_compile/cases/read_all_count_conflict_no_bits.stderr.beta new file mode 100644 index 00000000..f3e54d9a --- /dev/null +++ b/tests/test_compile/cases/read_all_count_conflict_no_bits.stderr.beta @@ -0,0 +1,7 @@ +error: conflicting: `read_all` cannot be used with `until`, `count`, `bits_read`, or `bytes_read` + --> tests/test_compile/cases/read_all_count_conflict_no_bits.rs:3:10 + | +3 | #[derive(DekuRead)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `DekuRead` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/test_compile/cases/read_all_count_conflict_no_bits.stderr.msrv b/tests/test_compile/cases/read_all_count_conflict_no_bits.stderr.msrv new file mode 100644 index 00000000..f3e54d9a --- /dev/null +++ b/tests/test_compile/cases/read_all_count_conflict_no_bits.stderr.msrv @@ -0,0 +1,7 @@ +error: conflicting: `read_all` cannot be used with `until`, `count`, `bits_read`, or `bytes_read` + --> tests/test_compile/cases/read_all_count_conflict_no_bits.rs:3:10 + | +3 | #[derive(DekuRead)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `DekuRead` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/test_compile/mod.rs b/tests/test_compile/mod.rs index 3d6d6762..b13b84e2 100644 --- a/tests/test_compile/mod.rs +++ b/tests/test_compile/mod.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "bits")] use std::{env, fs, path::PathBuf}; #[test] @@ -13,12 +12,20 @@ fn test_compile() { t.compile_fail("tests/test_compile/cases/*.rs"); } -#[cfg(feature = "bits")] +#[test] +#[cfg(all(feature = "std", not(feature = "bits")))] +fn test_read_all_conflict_without_bits() { + let profile = env::var("DEKU_TRYBUILD_PROFILE").unwrap_or_else(|_| "stable".to_owned()); + let _snapshots = install_snapshots(&profile); + + let t = trybuild::TestCases::new(); + t.compile_fail("tests/test_compile/cases/read_all_count_conflict_no_bits.rs"); +} + struct SnapshotGuard { originals: Vec<(PathBuf, Vec)>, } -#[cfg(feature = "bits")] impl Drop for SnapshotGuard { fn drop(&mut self) { for (path, contents) in &self.originals { @@ -27,7 +34,6 @@ impl Drop for SnapshotGuard { } } -#[cfg(feature = "bits")] fn install_snapshots(profile: &str) -> SnapshotGuard { let source_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/test_compile/cases"); let mut originals = Vec::new(); From 41f8f69dbb4aef83225fe6fcbe644343107192f4 Mon Sep 17 00:00:00 2001 From: Emmanuel Thompson Date: Sat, 17 Oct 2026 21:38:10 -0400 Subject: [PATCH 5/9] fix: return an error for no-bits offsets --- src/reader.rs | 5 ++++- tests/test_no_bits_offset.rs | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/test_no_bits_offset.rs diff --git a/src/reader.rs b/src/reader.rs index 8c589aed..c3893a3a 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -216,7 +216,10 @@ impl Reader { #[cfg(not(feature = "bits"))] { if amt > 0 { - panic!("requires deku feature: bits"); + return Err(crate::deku_error!( + DekuError::InvalidParam, + "requires deku feature: bits" + )); } } Ok(()) diff --git a/tests/test_no_bits_offset.rs b/tests/test_no_bits_offset.rs new file mode 100644 index 00000000..8c672f22 --- /dev/null +++ b/tests/test_no_bits_offset.rs @@ -0,0 +1,24 @@ +#![cfg(not(feature = "bits"))] + +use deku::prelude::*; + +#[derive(DekuRead)] +struct Packet { + value: u8, +} + +#[test] +fn nonzero_byte_offset_returns_an_error_without_bits() { + let result = Packet::from_bytes((&[0][..], 1)); + + assert!(matches!(result, Err(DekuError::InvalidParam(_)))); +} + +#[test] +fn direct_bit_skips_return_an_error_without_bits() { + let mut reader = Reader::new(no_std_io::io::Cursor::new([0u8])); + + let result = reader.skip_bits(1, deku::ctx::Order::Msb0); + + assert!(matches!(result, Err(DekuError::InvalidParam(_)))); +} From 3603bad4417f4f311b1c038a31ae15c623aa86da Mon Sep 17 00:00:00 2001 From: Emmanuel Thompson Date: Sat, 17 Oct 2026 21:39:06 -0400 Subject: [PATCH 6/9] fix: propagate reader errors without panicking --- CHANGELOG.md | 4 + deku-derive/src/lib.rs | 4 +- deku-derive/src/macros/deku_write.rs | 4 +- src/attributes.rs | 8 +- src/ctx.rs | 6 +- src/impls/hashmap.rs | 4 +- src/impls/hashset.rs | 4 +- src/impls/vec.rs | 30 ++++- src/reader.rs | 164 +++++++++++++++++++++------ 9 files changed, 180 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 709afd48..f9e56754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Breaking + +- Remove `Reader::end()` in favor of `Reader::try_end()`, which propagates I/O errors + ### Fixed - `Writer`: an `Lsb0` write that ends on a byte boundary no longer reorders the diff --git a/deku-derive/src/lib.rs b/deku-derive/src/lib.rs index a84e3326..bef9f6fe 100644 --- a/deku-derive/src/lib.rs +++ b/deku-derive/src/lib.rs @@ -714,7 +714,7 @@ struct FieldData { /// a predicate to decide when to stop reading elements into the container until: Option, - /// read until `reader.try_end()` + /// read until `reader.try_end()` returns `Ok(true)` read_all: bool, /// apply a function to the field after it's read @@ -1315,7 +1315,7 @@ struct DekuFieldReceiver { #[darling(default = "default_res_opt", map = "map_litstr_as_tokenstream")] until: Result, ReplacementError>, - /// read until `reader.try_end()` + /// read until `reader.try_end()` returns `Ok(true)` #[darling(default)] read_all: bool, diff --git a/deku-derive/src/macros/deku_write.rs b/deku-derive/src/macros/deku_write.rs index d5696e91..1c4efd6a 100644 --- a/deku-derive/src/macros/deku_write.rs +++ b/deku-derive/src/macros/deku_write.rs @@ -535,7 +535,9 @@ fn emit_bit_run_write( if cfg!(feature = "logging") { let field_ident_str = field_ident.to_string(); - traces.extend(quote! { ::#crate_::log::trace!("Writing: {}.{}", #ident, #field_ident_str); }); + traces.extend( + quote! { ::#crate_::log::trace!("Writing: {}.{}", #ident, #field_ident_str); }, + ); } let value = quote! { (*(#object_prefix #field_ident) as u64) }; diff --git a/src/attributes.rs b/src/attributes.rs index e264ac5d..fb851180 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -51,7 +51,7 @@ enum DekuEnum { | [bits_read](#bits_read) | field | Set the field representing the number of bits to read into a container | [bytes_read](#bytes_read) | field | Set the field representing the number of bytes to read into a container | [until](#until) | field | Set a predicate returning when to stop reading elements into a container -| [read_all](#read_all) | field | Read until [reader.end()] returns `true` +| [read_all](#read_all) | field | Read until [reader.try_end()] returns `Ok(true)` | [update](#update) | field | Apply code over the field when `.update()` is called | [temp](#temp) | field | Read the field but exclude it from the struct/enum | [temp_value](#temp_value) | field | Write the field but exclude it from the struct/enum @@ -1149,7 +1149,9 @@ assert_eq!( ``` # read_all -Read values into the container until [reader.end()] returns `true`. +Read values into the container until [reader.try_end()] returns `Ok(true)`. I/O errors from the +end probe propagate. EOF before the next element ends the collection successfully. Errors while +decoding an element, including EOF after only part of an element, propagate. Example: ```rust @@ -2289,5 +2291,5 @@ assert_eq!(data, value); # fn main() {} ``` -[reader.end()]: crate::reader::Reader::end() +[reader.try_end()]: crate::reader::Reader::try_end() */ diff --git a/src/ctx.rs b/src/ctx.rs index 34830b85..ebe60065 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -106,7 +106,8 @@ pub enum Limit bool> { /// Read until a given quantity of bits have been read BitSize(BitSize), - /// Read until `reader.end()` is true. Used for `read_all` attribute. + /// Read until [`crate::reader::Reader::try_end`] returns `Ok(true)`. Errors from the end probe + /// propagate. Used for the `read_all` attribute. End, } @@ -149,7 +150,8 @@ impl FnMut(&'a T) -> bool> Limit { } impl Limit bool> { - /// Read until `reader.end()` is true + /// Construct a limit that reads until [`crate::reader::Reader::try_end`] returns `Ok(true)`. + /// Errors from the end probe propagate. #[inline] pub fn end() -> Self { Self::End diff --git a/src/impls/hashmap.rs b/src/impls/hashmap.rs index 9ebff2e1..394ba97d 100644 --- a/src/impls/hashmap.rs +++ b/src/impls/hashmap.rs @@ -56,7 +56,7 @@ where let mut res = HashMap::with_capacity_and_hasher(capacity.unwrap_or(0), S::default()); loop { - if reader.end() { + if reader.try_end()? { break; } let val = <(K, V)>::from_reader_with_ctx(reader, ctx)?; @@ -170,7 +170,7 @@ where ) } - // Read until `reader.end()` is true + // Read until `reader.try_end()` returns `Ok(true)` Limit::End => from_reader_with_ctx_hashmap_to_end(reader, None, inner_ctx), } } diff --git a/src/impls/hashset.rs b/src/impls/hashset.rs index 1c9eecfe..667496be 100644 --- a/src/impls/hashset.rs +++ b/src/impls/hashset.rs @@ -54,7 +54,7 @@ where let mut res = HashSet::with_capacity_and_hasher(capacity.unwrap_or(0), S::default()); loop { - if reader.end() { + if reader.try_end()? { break; } let val = ::from_reader_with_ctx(reader, ctx)?; @@ -155,7 +155,7 @@ where ) } - // Read until `reader.end()` is true + // Read until `reader.try_end()` returns `Ok(true)` Limit::End => from_reader_with_ctx_hashset_to_end(reader, None, inner_ctx), } } diff --git a/src/impls/vec.rs b/src/impls/vec.rs index 3da8ad87..b5af64df 100644 --- a/src/impls/vec.rs +++ b/src/impls/vec.rs @@ -99,7 +99,7 @@ where let mut res = capacity.map_or_else(Vec::new, Vec::with_capacity); loop { - if reader.end() { + if reader.try_end()? { break; } let val = ::from_reader_with_ctx(reader, ctx)?; @@ -236,11 +236,37 @@ mod tests { use rstest::rstest; use std::io::Cursor; - #[cfg(feature = "bits")] use crate::reader::Reader; use super::*; + struct ErrorReader; + + impl no_std_io::io::Read for ErrorReader { + fn read(&mut self, _buf: &mut [u8]) -> no_std_io::io::Result { + Err(no_std_io::io::ErrorKind::Other.into()) + } + } + + impl no_std_io::io::Seek for ErrorReader { + fn seek(&mut self, _pos: no_std_io::io::SeekFrom) -> no_std_io::io::Result { + Ok(0) + } + } + + #[test] + fn read_all_propagates_errors_from_end_probe() { + let mut reader = Reader::new(ErrorReader); + let limit = Limit:: bool>::end(); + + let result = Vec::::from_reader_with_ctx(&mut reader, (limit, ())); + + assert_eq!( + result, + Err(crate::DekuError::Io(no_std_io::io::ErrorKind::Other)) + ); + } + #[cfg(feature = "bits")] #[rstest(input, limit, expected, expected_rest_bits, expected_rest_bytes, case::count_0([0xAA].as_ref(), 0.into(), vec![], bits![u8, Msb0;], &[0xaa]), diff --git a/src/reader.rs b/src/reader.rs index c3893a3a..59353678 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -148,31 +148,38 @@ impl Reader { alloc::vec![] } - /// Return true if we are at the end of a reader and there are no cached bits in the reader. - /// Since this uses [Read] internally, this will return true when [Read] returns [ErrorKind::UnexpectedEof]. + /// Probe for the end of the reader while preserving any available byte for later reads through + /// this `Reader`. /// - /// The byte that was read will be internally buffered and will *not* be included in the `bits_read` count. + /// Returns `Ok(true)` when the probe reaches [`ErrorKind::UnexpectedEof`]. Returns `Ok(false)` + /// if a byte or bits are already buffered, or if a byte is available from the underlying + /// reader. When the probe reads a byte, it buffers it for later reads through this `Reader`; + /// the underlying reader advances, but `bits_read` does not. Reading from the underlying + /// reader directly or calling [`into_inner`](Self::into_inner) bypasses that buffered byte. + /// Other I/O errors are returned as `Err(DekuError::Io(error.kind()))`. #[inline] - pub fn end(&mut self) -> bool { + pub fn try_end(&mut self) -> Result { if self.leftover.is_some() { #[cfg(feature = "logging")] log::trace!("not end"); - false + Ok(false) } else { let mut buf = [0; 1]; - if let Err(e) = self.inner.read_exact(&mut buf) - && e.kind() == ErrorKind::UnexpectedEof - { - #[cfg(feature = "logging")] - log::trace!("end"); - return true; - } - - #[cfg(feature = "logging")] - log::trace!("not end: read {buf:02x?}"); + match self.inner.read_exact(&mut buf) { + Ok(()) => { + #[cfg(feature = "logging")] + log::trace!("not end: read {buf:02x?}"); - self.leftover = Some(Leftover::Byte(buf[0])); - false + self.leftover = Some(Leftover::Byte(buf[0])); + Ok(false) + } + Err(e) if e.kind() == ErrorKind::UnexpectedEof => { + #[cfg(feature = "logging")] + log::trace!("end"); + Ok(true) + } + Err(e) => Err(DekuError::Io(e.kind())), + } } } @@ -316,10 +323,11 @@ impl Reader { let mut iter = dst[..end].rchunks_exact_mut(8); for slot in iter.by_ref() { let mut buf: [u8; 1] = [0u8]; - if let Err(e) = self.inner.read_exact(&mut buf) - && e.kind() == ErrorKind::UnexpectedEof - { - return Err(DekuError::Incomplete(NeedSize::new(dst.len()))); + if let Err(e) = self.inner.read_exact(&mut buf) { + if e.kind() == ErrorKind::UnexpectedEof { + return Err(DekuError::Incomplete(NeedSize::new(dst.len()))); + } + return Err(DekuError::Io(e.kind())); } slot.store_be(buf[0]); } @@ -329,10 +337,11 @@ impl Reader { let mut iter = dst[start..end].chunks_exact_mut(8); for slot in iter.by_ref() { let mut buf: [u8; 1] = [0u8]; - if let Err(e) = self.inner.read_exact(&mut buf) - && e.kind() == ErrorKind::UnexpectedEof - { - return Err(DekuError::Incomplete(NeedSize::new(dst.len()))); + if let Err(e) = self.inner.read_exact(&mut buf) { + if e.kind() == ErrorKind::UnexpectedEof { + return Err(DekuError::Incomplete(NeedSize::new(dst.len()))); + } + return Err(DekuError::Io(e.kind())); } slot.store_be(buf[0]); } @@ -495,6 +504,16 @@ impl Reader { #[cfg(feature = "logging")] log::trace!("read_bytes: requesting {amt} bytes"); + if buf.len() < amt { + return Err(crate::deku_error!( + DekuError::InvalidParam, + "buffer is too small" + )); + } + if amt == 0 { + return Ok(ReaderRet::Bytes); + } + if self.leftover.is_none() { if let Err(e) = self.inner.read_exact(&mut buf[..amt]) { if e.kind() == ErrorKind::UnexpectedEof { @@ -596,6 +615,10 @@ impl Reader { #[cfg(feature = "logging")] log::trace!("read_bytes_const: requesting {N} bytes"); + if N == 0 { + return Ok(ReaderRet::Bytes); + } + if self.leftover.is_none() { if let Err(e) = self.inner.read_exact(buf) { if e.kind() == ErrorKind::UnexpectedEof { @@ -659,6 +682,10 @@ impl Reader { #[cfg(not(feature = "bits"))] let _ = order; + if N == 0 { + return Ok(()); + } + if self.leftover.is_none() { if let Err(e) = self.inner.read_exact(buf) { if e.kind() == ErrorKind::UnexpectedEof { @@ -746,37 +773,106 @@ impl Reader { mod tests { use super::*; use hexlit::hex; - use no_std_io::io::Cursor; + use no_std_io::io::{Cursor, Read, Seek, SeekFrom}; + + struct ErrorReader; + + impl Read for ErrorReader { + fn read(&mut self, _buf: &mut [u8]) -> no_std_io::io::Result { + Err(ErrorKind::Other.into()) + } + } + + impl Seek for ErrorReader { + fn seek(&mut self, _pos: SeekFrom) -> no_std_io::io::Result { + Ok(0) + } + } + + #[test] + fn try_end_propagates_arbitrary_io_errors() { + let mut reader = Reader::new(ErrorReader); + + assert_eq!(reader.try_end(), Err(DekuError::Io(ErrorKind::Other))); + assert!(reader.leftover.is_none()); + } + + #[test] + fn zero_length_reads_are_noops_with_byte_leftover() { + let mut cursor = Cursor::new([0xaa]); + let mut reader = Reader::new(&mut cursor); + assert!(!reader.try_end().unwrap()); + + let mut empty = []; + assert!(reader.read_bytes(0, &mut empty, Order::Msb0).is_ok()); + + let mut empty_const = []; + assert!( + reader + .read_bytes_const::<0>(&mut empty_const, Order::Msb0) + .is_ok() + ); + assert!( + reader + .read_bytes_const_into::<0>(&mut empty_const, Order::Msb0) + .is_ok() + ); + assert!(matches!(reader.leftover, Some(Leftover::Byte(0xaa)))); + } + + #[test] + fn short_byte_destinations_return_an_error() { + let mut cursor = Cursor::new([0xaa, 0xbb]); + let mut reader = Reader::new(&mut cursor); + let mut buf = [0; 1]; + + let result = reader.read_bytes(2, &mut buf, Order::Msb0); + + assert!(matches!(result, Err(DekuError::InvalidParam(_)))); + } + + #[test] + #[cfg(feature = "bits")] + fn bit_reads_propagate_arbitrary_io_errors() { + for order in [Order::Msb0, Order::Lsb0] { + let mut reader = Reader::new(ErrorReader); + let mut bits = bitarr!(u8, Msb0; 0; 8); + + let result = reader.read_bits_into(&mut bits, order); + + assert_eq!(result, Err(DekuError::Io(ErrorKind::Other))); + } + } #[test] #[cfg(all(feature = "alloc", feature = "bits"))] - fn test_end() { + fn test_try_end() { let input = hex!("aabb"); let mut cursor = Cursor::new(input); let mut reader = Reader::new(&mut cursor); - assert!(!reader.end()); + assert!(!reader.try_end().unwrap()); let mut buf = [0; 2]; let _ = reader.read_bytes_const::<2>(&mut buf, Order::Lsb0).unwrap(); - assert!(reader.end()); + assert!(reader.try_end().unwrap()); assert_eq!(reader.bits_read, 8 * 2); let input = hex!("aa"); let mut cursor = Cursor::new(input); let mut reader = Reader::new(&mut cursor); - assert!(!reader.end()); + assert!(!reader.try_end().unwrap()); let _ = reader.read_bits(4, Order::Lsb0).unwrap(); - assert!(!reader.end()); + assert!(!reader.try_end().unwrap()); let _ = reader.read_bits(4, Order::Lsb0).unwrap(); - assert!(reader.end()); + assert!(reader.try_end().unwrap()); assert_eq!(reader.bits_read, 8); let input = hex!("aa"); let mut cursor = Cursor::new(input); let mut reader = Reader::new(&mut cursor); - assert!(!reader.end()); + assert!(!reader.try_end().unwrap()); let mut buf = [0; 1]; let _ = reader.read_bytes(1, &mut buf, Order::Lsb0).unwrap(); - assert!(reader.end()); + assert!(reader.try_end().unwrap()); assert_eq!(reader.bits_read, 8); } From cb1b5edfedd1c6a34d6369814171e5ff54a7f34d Mon Sep 17 00:00:00 2001 From: Emmanuel Thompson Date: Sat, 17 Oct 2026 21:39:40 -0400 Subject: [PATCH 7/9] fix: preserve DekuError display without std --- src/error.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/error.rs b/src/error.rs index ef2e7ce1..47d810b1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -144,7 +144,6 @@ impl From for DekuError { } } -#[cfg(feature = "std")] impl core::fmt::Display for DekuError { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match self { @@ -158,7 +157,7 @@ impl core::fmt::Display for DekuError { DekuError::InvalidParam(err) => write!(f, "Invalid param error: {err}"), DekuError::Assertion(err) => write!(f, "{err}"), DekuError::IdVariantNotFound => write!(f, "Could not resolve `id` for variant"), - DekuError::Io(e) => write!(f, "io errorr: {e:?}"), + DekuError::Io(e) => write!(f, "io error: {e:?}"), } } } @@ -166,13 +165,6 @@ impl core::fmt::Display for DekuError { #[cfg(not(feature = "std"))] impl core::error::Error for DekuError {} -#[cfg(not(feature = "std"))] -impl core::fmt::Display for DekuError { - fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - Ok(()) - } -} - #[cfg(not(feature = "std"))] impl From for no_std_io::io::Error { fn from(error: DekuError) -> Self { @@ -219,3 +211,25 @@ impl From for DekuError { DekuError::Io(value.kind()) } } + +#[cfg(all(test, feature = "alloc"))] +mod tests { + use super::*; + + #[test] + fn display_includes_parse_error_details() { + #[cfg(feature = "descriptive-errors")] + let error = DekuError::Parse(alloc::borrow::Cow::Borrowed("details")); + #[cfg(not(feature = "descriptive-errors"))] + let error = DekuError::Parse("details"); + + assert_eq!(alloc::format!("{error}"), "Parse error: details"); + } + + #[test] + fn display_formats_io_errors_consistently() { + let error = DekuError::Io(ErrorKind::Other); + + assert_eq!(alloc::format!("{error}"), "io error: Other"); + } +} From 8602c7af9881dbe0e8c280f3486ae2d3f98f9b6a Mon Sep 17 00:00:00 2001 From: Emmanuel Thompson Date: Sat, 17 Oct 2026 21:40:39 -0400 Subject: [PATCH 8/9] test: cover padding expectations per feature set --- .../test_attributes/test_padding/test_pad_bytes_after.rs | 9 ++++++++- .../test_padding/test_pad_bytes_before.rs | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/test_attributes/test_padding/test_pad_bytes_after.rs b/tests/test_attributes/test_padding/test_pad_bytes_after.rs index 05930c44..857844af 100644 --- a/tests/test_attributes/test_padding/test_pad_bytes_after.rs +++ b/tests/test_attributes/test_padding/test_pad_bytes_after.rs @@ -29,7 +29,14 @@ fn test_pad_bytes_after() { #[test] #[cfg(feature = "descriptive-errors")] -#[should_panic(expected = "Incomplete(NeedSize { bits: 24 })")] +#[cfg_attr( + feature = "bits", + should_panic(expected = "Incomplete(NeedSize { bits: 8 })") +)] +#[cfg_attr( + not(feature = "bits"), + should_panic(expected = "Incomplete(NeedSize { bits: 24 })") +)] fn test_pad_bytes_after_not_enough() { #[derive(PartialEq, Debug, DekuRead, DekuWrite)] struct TestStruct { diff --git a/tests/test_attributes/test_padding/test_pad_bytes_before.rs b/tests/test_attributes/test_padding/test_pad_bytes_before.rs index 10d5526b..b36d1a53 100644 --- a/tests/test_attributes/test_padding/test_pad_bytes_before.rs +++ b/tests/test_attributes/test_padding/test_pad_bytes_before.rs @@ -29,7 +29,14 @@ fn test_pad_bytes_before() { #[test] #[cfg(feature = "descriptive-errors")] -#[should_panic(expected = "Incomplete(NeedSize { bits: 16 })")] +#[cfg_attr( + feature = "bits", + should_panic(expected = "Incomplete(NeedSize { bits: 8 })") +)] +#[cfg_attr( + not(feature = "bits"), + should_panic(expected = "Incomplete(NeedSize { bits: 16 })") +)] fn test_pad_bytes_before_not_enough() { #[derive(PartialEq, Debug, DekuRead, DekuWrite)] struct TestStruct { From e322a127e20f5a95df5cf84c15e973985f060e64 Mon Sep 17 00:00:00 2001 From: Emmanuel Thompson Date: Sat, 17 Oct 2026 21:43:56 -0400 Subject: [PATCH 9/9] test: cover the complete workspace feature matrix --- Cargo.toml | 1 + Justfile | 21 +++++---- deku-derive/src/lib.rs | 41 ++--------------- scripts/feature-matrix.sh | 28 +++++++++++ tests/renamed_dependency/Cargo.toml | 8 ++++ tests/renamed_dependency/src/lib.rs | 46 +++++++++++++++++++ .../test_padding/test_pad_bytes_after.rs | 27 +++++++---- .../test_padding/test_pad_bytes_before.rs | 27 +++++++---- tests/test_compile/mod.rs | 5 ++ tests/test_no_bits_offset.rs | 1 + 10 files changed, 138 insertions(+), 67 deletions(-) create mode 100644 scripts/feature-matrix.sh create mode 100644 tests/renamed_dependency/Cargo.toml create mode 100644 tests/renamed_dependency/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index a9be55ad..fe7bed83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ bench = false members = [ "deku-derive" ] +exclude = ["tests/renamed_dependency"] [features] default = ["std", "bits", "descriptive-errors"] diff --git a/Justfile b/Justfile index 35526b26..d78c9dea 100644 --- a/Justfile +++ b/Justfile @@ -8,8 +8,7 @@ pipeline := env_var_or_default("DEKU_TOOLCHAIN", "stable") toolchain := if pipeline == "msrv" { msrv } else if pipeline == "stable" { stable } else if pipeline == "beta" { beta } else { pipeline } trybuild_profile := if pipeline == "msrv" { "msrv" } else if pipeline == "beta" { "beta" } else { "stable" } -# Non-default feature combinations exercised by the test and coverage matrix. -feature_matrix := "std alloc descriptive-errors bits logging bits,alloc" +# Non-default feature combinations are generated by scripts/feature-matrix.sh. default: @just --list @@ -21,12 +20,14 @@ test: #!/usr/bin/env bash set -euo pipefail export DEKU_TRYBUILD_PROFILE="{{trybuild_profile}}" - cargo +{{toolchain}} test --all - cargo +{{toolchain}} test --all-features - cargo +{{toolchain}} test --no-default-features - for features in {{feature_matrix}}; do - cargo +{{toolchain}} test --no-default-features --features="${features}" + cargo +{{toolchain}} test --workspace + cargo +{{toolchain}} test --workspace --all-features + cargo +{{toolchain}} test --workspace --no-default-features + feature_matrix="$(bash scripts/feature-matrix.sh)" + for features in $feature_matrix; do + cargo +{{toolchain}} test --workspace --no-default-features --features="${features}" done + cargo +{{toolchain}} test --manifest-path tests/renamed_dependency/Cargo.toml miri-test: #!/usr/bin/env bash @@ -35,7 +36,8 @@ miri-test: cargo +{{nightly}} miri test --workspace cargo +{{nightly}} miri test --workspace --all-features cargo +{{nightly}} miri test --workspace --no-default-features - for features in {{feature_matrix}}; do + feature_matrix="$(bash scripts/feature-matrix.sh)" + for features in $feature_matrix; do cargo +{{nightly}} miri test --workspace --no-default-features --features="${features}" done @@ -104,7 +106,8 @@ coverage: cargo +{{stable}} llvm-cov --workspace --no-report cargo +{{stable}} llvm-cov --workspace --all-features --no-report cargo +{{stable}} llvm-cov --package deku --no-default-features --no-report - for features in {{feature_matrix}}; do + feature_matrix="$(bash scripts/feature-matrix.sh)" + for features in $feature_matrix; do cargo +{{stable}} llvm-cov --package deku --no-default-features --features="${features}" --no-report done diff --git a/deku-derive/src/lib.rs b/deku-derive/src/lib.rs index bef9f6fe..c9201241 100644 --- a/deku-derive/src/lib.rs +++ b/deku-derive/src/lib.rs @@ -1604,9 +1604,12 @@ pub fn deku_derive( #[cfg(test)] mod tests { + #[cfg(feature = "bits")] use rstest::rstest; + #[cfg(feature = "bits")] use syn::parse_str; + #[cfg(feature = "bits")] use super::*; #[cfg(feature = "bits")] @@ -1663,42 +1666,4 @@ mod tests { res_reader.unwrap(); res_writer.unwrap(); } - - #[cfg(not(feature = "bits"))] - #[rstest( - input, - case::struct_empty(r#"struct Test {}"#), - case::struct_unnamed(r#"struct Test(u8, u8);"#), - case::struct_bytes( - r#" - struct Test { - #[deku(bytes = 4)] - field_a: u32, - #[deku(endian = "little")] - field_b: u16, - }"# - ), - case::enum_empty(r#"#[deku(id_type = "u8")] enum Test {}"#), - case::enum_without_bit_fields( - r#" - #[deku(id_type = "u8")] - enum Test { - #[deku(id = "1")] - A, - #[deku(id = "2")] - B(u8), - }"# - ), - case::invalid_endian(r#"struct Test(#[endian=big] u8);"#) - )] - fn test_macro_without_bits(input: &str) { - let parsed = parse_str(input).unwrap(); - - let data = DekuData::from_input(parsed).unwrap(); - let res_reader = data.emit_reader_checked(); - let res_writer = data.emit_writer_checked(); - - res_reader.unwrap(); - res_writer.unwrap(); - } } diff --git a/scripts/feature-matrix.sh b/scripts/feature-matrix.sh new file mode 100644 index 00000000..0770448f --- /dev/null +++ b/scripts/feature-matrix.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# The base states are the distinct std/alloc/descriptive-errors combinations +# after accounting for std and descriptive-errors both implying alloc. +bases=("" alloc std descriptive-errors std,descriptive-errors) +modifiers=("" bits logging bits,logging) + +for base in "${bases[@]}"; do + for modifier in "${modifiers[@]}"; do + # `just test` already runs no-default-features, default features, and + # all-features, so omit those three configurations from this matrix. + if [[ -z "$base" && -z "$modifier" ]]; then + continue + fi + if [[ "$base" == "std,descriptive-errors" && ( "$modifier" == "bits" || "$modifier" == "bits,logging" ) ]]; then + continue + fi + + if [[ -n "$base" && -n "$modifier" ]]; then + printf '%s,%s\n' "$base" "$modifier" + elif [[ -n "$base" ]]; then + printf '%s\n' "$base" + else + printf '%s\n' "$modifier" + fi + done +done diff --git a/tests/renamed_dependency/Cargo.toml b/tests/renamed_dependency/Cargo.toml new file mode 100644 index 00000000..45d928ea --- /dev/null +++ b/tests/renamed_dependency/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "deku-renamed-dependency-test" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +renamed_deku = { package = "deku", path = "../..", default-features = false, features = ["std", "bits", "logging"] } diff --git a/tests/renamed_dependency/src/lib.rs b/tests/renamed_dependency/src/lib.rs new file mode 100644 index 00000000..30db517a --- /dev/null +++ b/tests/renamed_dependency/src/lib.rs @@ -0,0 +1,46 @@ +use renamed_deku::prelude::*; + +#[derive(DekuRead, DekuWrite)] +#[deku(endian = "big")] +struct Packet { + value: u8, +} + +#[test] +fn generated_reader_and_writer_support_a_renamed_dependency_with_logging() { + let (_, packet) = Packet::from_bytes((&[0x42][..], 0)).unwrap(); + assert_eq!(packet.value, 0x42); + assert_eq!(packet.to_bytes().unwrap(), [0x42]); +} + +#[derive(Debug, PartialEq, DekuRead, DekuWrite)] +#[deku(id_type = "u8")] +enum MessageKind { + #[deku(id = "1")] + Ping, + #[deku(id = "2")] + Pong, +} + +#[test] +fn generated_enum_reader_and_writer_support_a_renamed_dependency() { + let (_, kind) = MessageKind::from_bytes((&[0x02][..], 0)).unwrap(); + assert_eq!(kind, MessageKind::Pong); + assert_eq!(kind.to_bytes().unwrap(), [0x02]); +} + +#[derive(Debug, PartialEq, DekuRead, DekuWrite)] +#[deku(endian = "big")] +struct Flags { + #[deku(bits = "3")] + mode: u8, + #[deku(bits = "5")] + count: u8, +} + +#[test] +fn generated_bit_run_logging_supports_a_renamed_dependency() { + let (_, flags) = Flags::from_bytes((&[0b101_10010][..], 0)).unwrap(); + assert_eq!(flags, Flags { mode: 5, count: 18 }); + assert_eq!(flags.to_bytes().unwrap(), [0b101_10010]); +} diff --git a/tests/test_attributes/test_padding/test_pad_bytes_after.rs b/tests/test_attributes/test_padding/test_pad_bytes_after.rs index 857844af..6aea43ac 100644 --- a/tests/test_attributes/test_padding/test_pad_bytes_after.rs +++ b/tests/test_attributes/test_padding/test_pad_bytes_after.rs @@ -27,17 +27,8 @@ fn test_pad_bytes_after() { assert_eq!(vec![0xaa, 0x00, 0x00, 0xdd], ret_write); } -#[test] #[cfg(feature = "descriptive-errors")] -#[cfg_attr( - feature = "bits", - should_panic(expected = "Incomplete(NeedSize { bits: 8 })") -)] -#[cfg_attr( - not(feature = "bits"), - should_panic(expected = "Incomplete(NeedSize { bits: 24 })") -)] -fn test_pad_bytes_after_not_enough() { +fn read_pad_bytes_after_not_enough() { #[derive(PartialEq, Debug, DekuRead, DekuWrite)] struct TestStruct { field_a: u8, @@ -50,6 +41,22 @@ fn test_pad_bytes_after_not_enough() { let _ret_read = TestStruct::try_from(data.as_slice()).unwrap(); } +// With bits enabled, padding is skipped by seeking, so the input length check reports the deficit. +#[test] +#[cfg(all(feature = "descriptive-errors", feature = "bits"))] +#[should_panic(expected = "Incomplete(NeedSize { bits: 8 })")] +fn test_pad_bytes_after_not_enough_with_bits() { + read_pad_bytes_after_not_enough(); +} + +// Without bits, the padding read reports the full three-byte request on EOF. +#[test] +#[cfg(all(feature = "descriptive-errors", not(feature = "bits")))] +#[should_panic(expected = "Incomplete(NeedSize { bits: 24 })")] +fn test_pad_bytes_after_not_enough_without_bits() { + read_pad_bytes_after_not_enough(); +} + // TODO: add cfg test with updated msg for not(bits) #[cfg(all(feature = "bits", feature = "descriptive-errors"))] #[test] diff --git a/tests/test_attributes/test_padding/test_pad_bytes_before.rs b/tests/test_attributes/test_padding/test_pad_bytes_before.rs index b36d1a53..fb1fbab7 100644 --- a/tests/test_attributes/test_padding/test_pad_bytes_before.rs +++ b/tests/test_attributes/test_padding/test_pad_bytes_before.rs @@ -27,17 +27,8 @@ fn test_pad_bytes_before() { assert_eq!(vec![0xaa, 0x00, 0x00, 0xdd], ret_write); } -#[test] #[cfg(feature = "descriptive-errors")] -#[cfg_attr( - feature = "bits", - should_panic(expected = "Incomplete(NeedSize { bits: 8 })") -)] -#[cfg_attr( - not(feature = "bits"), - should_panic(expected = "Incomplete(NeedSize { bits: 16 })") -)] -fn test_pad_bytes_before_not_enough() { +fn read_pad_bytes_before_not_enough() { #[derive(PartialEq, Debug, DekuRead, DekuWrite)] struct TestStruct { field_a: u8, @@ -50,6 +41,22 @@ fn test_pad_bytes_before_not_enough() { let _ret_read = TestStruct::try_from(data.as_slice()).unwrap(); } +// With bits enabled, padding is skipped by seeking, so the next field reports EOF. +#[test] +#[cfg(all(feature = "descriptive-errors", feature = "bits"))] +#[should_panic(expected = "Incomplete(NeedSize { bits: 8 })")] +fn test_pad_bytes_before_not_enough_with_bits() { + read_pad_bytes_before_not_enough(); +} + +// Without bits, the padding read reports the full two-byte request on EOF. +#[test] +#[cfg(all(feature = "descriptive-errors", not(feature = "bits")))] +#[should_panic(expected = "Incomplete(NeedSize { bits: 16 })")] +fn test_pad_bytes_before_not_enough_without_bits() { + read_pad_bytes_before_not_enough(); +} + // TODO: add cfg test with updated msg for not(bits) #[cfg(all(feature = "bits", feature = "descriptive-errors"))] #[test] diff --git a/tests/test_compile/mod.rs b/tests/test_compile/mod.rs index b13b84e2..3f9e49d7 100644 --- a/tests/test_compile/mod.rs +++ b/tests/test_compile/mod.rs @@ -1,3 +1,4 @@ +#[cfg(any(feature = "bits", feature = "std"))] use std::{env, fs, path::PathBuf}; #[test] @@ -14,6 +15,7 @@ fn test_compile() { #[test] #[cfg(all(feature = "std", not(feature = "bits")))] +#[cfg_attr(miri, ignore)] fn test_read_all_conflict_without_bits() { let profile = env::var("DEKU_TRYBUILD_PROFILE").unwrap_or_else(|_| "stable".to_owned()); let _snapshots = install_snapshots(&profile); @@ -22,10 +24,12 @@ fn test_read_all_conflict_without_bits() { t.compile_fail("tests/test_compile/cases/read_all_count_conflict_no_bits.rs"); } +#[cfg(any(feature = "bits", feature = "std"))] struct SnapshotGuard { originals: Vec<(PathBuf, Vec)>, } +#[cfg(any(feature = "bits", feature = "std"))] impl Drop for SnapshotGuard { fn drop(&mut self) { for (path, contents) in &self.originals { @@ -34,6 +38,7 @@ impl Drop for SnapshotGuard { } } +#[cfg(any(feature = "bits", feature = "std"))] fn install_snapshots(profile: &str) -> SnapshotGuard { let source_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/test_compile/cases"); let mut originals = Vec::new(); diff --git a/tests/test_no_bits_offset.rs b/tests/test_no_bits_offset.rs index 8c672f22..4fd4ee17 100644 --- a/tests/test_no_bits_offset.rs +++ b/tests/test_no_bits_offset.rs @@ -3,6 +3,7 @@ use deku::prelude::*; #[derive(DekuRead)] +#[allow(dead_code)] struct Packet { value: u8, }