From 5cf04d7a977d1f9eb42fc23ec6191aa025d542d0 Mon Sep 17 00:00:00 2001 From: widberg Date: Fri, 27 Jun 2025 18:14:51 -0400 Subject: [PATCH 1/3] Add align_size_to field-level attribute --- .../ui/invalid_keyword_struct_field.stderr | 2 +- binrw/tests/ui/non_blocking_errors.stderr | 4 +-- .../binrw/backtrace/syntax_highlighting.rs | 3 ++- .../src/binrw/codegen/read_options/struct.rs | 25 +++++++++++++---- .../codegen/write_options/struct_field.rs | 27 +++++++++++++++---- binrw_derive/src/binrw/parser/attrs.rs | 1 + .../src/binrw/parser/field_level_attrs.rs | 4 +++ binrw_derive/src/binrw/parser/keywords.rs | 1 + 8 files changed, 53 insertions(+), 14 deletions(-) diff --git a/binrw/tests/ui/invalid_keyword_struct_field.stderr b/binrw/tests/ui/invalid_keyword_struct_field.stderr index aff77145..0cc9b2e0 100644 --- a/binrw/tests/ui/invalid_keyword_struct_field.stderr +++ b/binrw/tests/ui/invalid_keyword_struct_field.stderr @@ -1,4 +1,4 @@ -error: expected one of: `big`, `little`, `is_big`, `is_little`, `map`, `try_map`, `repr`, `map_stream`, `magic`, `args`, `args_raw`, `calc`, `try_calc`, `default`, `ignore`, `parse_with`, `count`, `offset`, `if`, `restore_position`, `try`, `temp`, `assert`, `err_context`, `pad_before`, `pad_after`, `align_before`, `align_after`, `seek_before`, `pad_size_to`, `dbg` +error: expected one of: `big`, `little`, `is_big`, `is_little`, `map`, `try_map`, `repr`, `map_stream`, `magic`, `args`, `args_raw`, `calc`, `try_calc`, `default`, `ignore`, `parse_with`, `count`, `offset`, `if`, `restore_position`, `try`, `temp`, `assert`, `err_context`, `pad_before`, `pad_after`, `align_before`, `align_after`, `seek_before`, `pad_size_to`, `align_size_to`, `dbg` --> tests/ui/invalid_keyword_struct_field.rs:5:10 | 5 | #[br(invalid_struct_field_keyword)] diff --git a/binrw/tests/ui/non_blocking_errors.stderr b/binrw/tests/ui/non_blocking_errors.stderr index 606ec8b5..48890f0f 100644 --- a/binrw/tests/ui/non_blocking_errors.stderr +++ b/binrw/tests/ui/non_blocking_errors.stderr @@ -4,13 +4,13 @@ error: expected one of: `stream`, `big`, `little`, `is_big`, `is_little`, `map`, 6 | #[br(invalid_keyword_struct)] | ^^^^^^^^^^^^^^^^^^^^^^ -error: expected one of: `big`, `little`, `is_big`, `is_little`, `map`, `try_map`, `repr`, `map_stream`, `magic`, `args`, `args_raw`, `calc`, `try_calc`, `default`, `ignore`, `parse_with`, `count`, `offset`, `if`, `restore_position`, `try`, `temp`, `assert`, `err_context`, `pad_before`, `pad_after`, `align_before`, `align_after`, `seek_before`, `pad_size_to`, `dbg` +error: expected one of: `big`, `little`, `is_big`, `is_little`, `map`, `try_map`, `repr`, `map_stream`, `magic`, `args`, `args_raw`, `calc`, `try_calc`, `default`, `ignore`, `parse_with`, `count`, `offset`, `if`, `restore_position`, `try`, `temp`, `assert`, `err_context`, `pad_before`, `pad_after`, `align_before`, `align_after`, `seek_before`, `pad_size_to`, `align_size_to`, `dbg` --> tests/ui/non_blocking_errors.rs:8:10 | 8 | #[br(invalid_keyword_struct_field_a)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: expected one of: `big`, `little`, `is_big`, `is_little`, `map`, `try_map`, `repr`, `map_stream`, `magic`, `args`, `args_raw`, `calc`, `try_calc`, `default`, `ignore`, `parse_with`, `count`, `offset`, `if`, `restore_position`, `try`, `temp`, `assert`, `err_context`, `pad_before`, `pad_after`, `align_before`, `align_after`, `seek_before`, `pad_size_to`, `dbg` +error: expected one of: `big`, `little`, `is_big`, `is_little`, `map`, `try_map`, `repr`, `map_stream`, `magic`, `args`, `args_raw`, `calc`, `try_calc`, `default`, `ignore`, `parse_with`, `count`, `offset`, `if`, `restore_position`, `try`, `temp`, `assert`, `err_context`, `pad_before`, `pad_after`, `align_before`, `align_after`, `seek_before`, `pad_size_to`, `align_size_to`, `dbg` --> tests/ui/non_blocking_errors.rs:10:10 | 10 | #[br(invalid_keyword_struct_field_b)] diff --git a/binrw_derive/src/binrw/backtrace/syntax_highlighting.rs b/binrw_derive/src/binrw/backtrace/syntax_highlighting.rs index 1c31585e..5db95dc1 100644 --- a/binrw_derive/src/binrw/backtrace/syntax_highlighting.rs +++ b/binrw_derive/src/binrw/backtrace/syntax_highlighting.rs @@ -192,7 +192,8 @@ fn visit_expr_attributes(field: &StructField, visitor: &mut Visitor) { align_before, align_after, seek_before, - pad_size_to + pad_size_to, + align_size_to ); if let Some(condition) = field.if_cond.clone() { diff --git a/binrw_derive/src/binrw/codegen/read_options/struct.rs b/binrw_derive/src/binrw/codegen/read_options/struct.rs index b20b0aba..09ef49d9 100644 --- a/binrw_derive/src/binrw/codegen/read_options/struct.rs +++ b/binrw_derive/src/binrw/codegen/read_options/struct.rs @@ -237,6 +237,8 @@ impl<'field> FieldGenerator<'field> { let dbg_align_before = dbg_space("align_before", &at, self.field.align_before.as_ref()); let dbg_pad_size_to = dbg_space("pad_size_to", &at, self.field.pad_size_to.as_ref()); let dbg_pad_after = dbg_space("pad_after", &at, self.field.pad_after.as_ref()); + let dbg_align_size_to = + dbg_space("align_size_to", &at, self.field.align_size_to.as_ref()); let dbg_align_after = dbg_space("align_after", &at, self.field.align_after.as_ref()); self.out = quote! {{ @@ -250,6 +252,7 @@ impl<'field> FieldGenerator<'field> { ); #dbg_pad_size_to #dbg_pad_after + #dbg_align_size_to #dbg_align_after #TEMP }}; @@ -620,6 +623,13 @@ fn generate_seek_after(reader_var: &TokenStream, field: &StructField) -> TokenSt .pad_after .as_ref() .map(|value| map_pad(reader_var, value)); + let align_size_to = field.align_size_to.as_ref().map(|alignment| { + quote! {{ + let align = (#alignment) as i64; + let size = (#SEEK_TRAIT::stream_position(#reader_var)? - #POS) as i64; + #SEEK_TRAIT::seek(#reader_var, #SEEK_FROM::Current((align - (size % align)) % align))?; + }} + }); let align_after = field .align_after .as_ref() @@ -628,6 +638,7 @@ fn generate_seek_after(reader_var: &TokenStream, field: &StructField) -> TokenSt quote! { #pad_size_to #pad_after + #align_size_to #align_after } } @@ -646,11 +657,15 @@ fn generate_seek_before(reader_var: &TokenStream, field: &StructField) -> TokenS .align_before .as_ref() .map(|value| map_align(reader_var, value)); - let pad_size_to_before = field.pad_size_to.as_ref().map(|_| { - quote! { - let #POS = #SEEK_TRAIT::stream_position(#reader_var)?; - } - }); + let pad_size_to_before = field + .pad_size_to + .as_ref() + .or(field.align_size_to.as_ref()) + .map(|_| { + quote! { + let #POS = #SEEK_TRAIT::stream_position(#reader_var)?; + } + }); quote! { #seek_before diff --git a/binrw_derive/src/binrw/codegen/write_options/struct_field.rs b/binrw_derive/src/binrw/codegen/write_options/struct_field.rs index 67be6d71..6ef78004 100644 --- a/binrw_derive/src/binrw/codegen/write_options/struct_field.rs +++ b/binrw_derive/src/binrw/codegen/write_options/struct_field.rs @@ -359,6 +359,18 @@ fn pad_after(writer_var: &TokenStream, field: &StructField) -> TokenStream { #WRITE_ZEROES(#writer_var, (#padding) as u64)?; } }); + let align_size_to = field.align_size_to.as_ref().map(|alignment| { + quote! {{ + let align = (#alignment) as u64; + let after_pos = #SEEK_TRAIT::stream_position(#writer_var)?; + if let Some(size) = after_pos.checked_sub(#BEFORE_POS) { + let rem = size % align; + if rem != 0 { + #WRITE_ZEROES(#writer_var, align - rem)?; + } + } + }} + }); let align_after = field.align_after.as_ref().map(|alignment| { quote! {{ let pos = #SEEK_TRAIT::stream_position(#writer_var)?; @@ -378,6 +390,7 @@ fn pad_after(writer_var: &TokenStream, field: &StructField) -> TokenStream { quote! { #pad_size_to #pad_after + #align_size_to #align_after #restore_position } @@ -407,11 +420,15 @@ fn pad_before(writer_var: &TokenStream, field: &StructField) -> TokenStream { } }} }); - let pad_size_to_before = field.pad_size_to.as_ref().map(|_| { - quote! { - let #BEFORE_POS = #SEEK_TRAIT::stream_position(#writer_var)?; - } - }); + let pad_size_to_before = field + .pad_size_to + .as_ref() + .or(field.align_size_to.as_ref()) + .map(|_| { + quote! { + let #BEFORE_POS = #SEEK_TRAIT::stream_position(#writer_var)?; + } + }); let store_position = field.restore_position.map(|()| { quote! { let #SAVED_POSITION = #SEEK_TRAIT::stream_position(#writer_var)?; diff --git a/binrw_derive/src/binrw/parser/attrs.rs b/binrw_derive/src/binrw/parser/attrs.rs index d53615be..5ce37b86 100644 --- a/binrw_derive/src/binrw/parser/attrs.rs +++ b/binrw_derive/src/binrw/parser/attrs.rs @@ -7,6 +7,7 @@ use syn::{Expr, FieldValue, Token}; pub(super) type AlignAfter = MetaExpr; pub(super) type AlignBefore = MetaExpr; +pub(super) type AlignSizeTo = MetaExpr; pub(super) type Args = MetaEnclosedList; pub(super) type ArgsRaw = MetaExpr; pub(super) type AssertLike = MetaList; diff --git a/binrw_derive/src/binrw/parser/field_level_attrs.rs b/binrw_derive/src/binrw/parser/field_level_attrs.rs index 0e6076b0..ef51f130 100644 --- a/binrw_derive/src/binrw/parser/field_level_attrs.rs +++ b/binrw_derive/src/binrw/parser/field_level_attrs.rs @@ -56,6 +56,8 @@ attr_struct! { pub(crate) seek_before: Option, #[from(RW:PadSizeTo)] pub(crate) pad_size_to: Option, + #[from(RW:AlignSizeTo)] + pub(crate) align_size_to: Option, #[from(RO:Debug)] // TODO is this really RO? pub(crate) debug: Option<()>, } @@ -128,6 +130,7 @@ impl StructField { align_after, seek_before, pad_size_to, + align_size_to, magic ) } @@ -239,6 +242,7 @@ impl FromField for StructField { align_after: <_>::default(), seek_before: <_>::default(), pad_size_to: <_>::default(), + align_size_to: <_>::default(), #[cfg(feature = "verbose-backtrace")] keyword_spans: <_>::default(), err_context: <_>::default(), diff --git a/binrw_derive/src/binrw/parser/keywords.rs b/binrw_derive/src/binrw/parser/keywords.rs index 4a4af2ba..4671727f 100644 --- a/binrw_derive/src/binrw/parser/keywords.rs +++ b/binrw_derive/src/binrw/parser/keywords.rs @@ -9,6 +9,7 @@ macro_rules! define_keywords { define_keywords! { align_after, align_before, + align_size_to, args, args_raw, assert, From dafe56c80a8209777b79ff484edbac896e4d308f Mon Sep 17 00:00:00 2001 From: widberg Date: Fri, 27 Jun 2025 18:54:47 -0400 Subject: [PATCH 2/3] Add align_size_to tests --- binrw/tests/dbg.rs | 7 ++++++- binrw/tests/derive/struct.rs | 13 +++++++++++++ binrw/tests/derive/write/padding.rs | 13 +++++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/binrw/tests/dbg.rs b/binrw/tests/dbg.rs index dcb883af..f72d13f5 100644 --- a/binrw/tests/dbg.rs +++ b/binrw/tests/dbg.rs @@ -20,12 +20,14 @@ fn dbg() { last: u8, #[br(dbg)] terminator: u8, + #[br(dbg, align_size_to = 2)] + after: u8, } // 🥴 if let Some("1") = option_env!("BINRW_IN_CHILD_PROC") { Test::read(&mut Cursor::new( - b"\0\0\xff\xff\0\0\0\x04\xff\xff\0\x0e\xff\xed\xff\xff\x42\0\0\0\x69", + b"\0\0\xff\xff\0\0\0\x04\xff\xff\0\x0e\xff\xed\xff\xff\x42\0\0\0\x69\x25\0", )) .unwrap(); } else { @@ -55,12 +57,15 @@ fn dbg() { "[{file}:{offset_2} | offset 0x10] last = 0x42\n", "[{file}:{offset_2} | pad_size_to 0x4]\n", "[{file}:{offset_3} | offset 0x14] terminator = 0x69\n", + "[{file}:{offset_4} | offset 0x15] after = 0x25\n", + "[{file}:{offset_4} | align_size_to 0x2]\n", ), file = core::file!(), offset_0 = if cfg!(nightly) { 16 } else { 11 }, offset_1 = if cfg!(nightly) { 18 } else { 11 }, offset_2 = if cfg!(nightly) { 20 } else { 11 }, offset_3 = if cfg!(nightly) { 22 } else { 11 }, + offset_4 = if cfg!(nightly) { 24 } else { 11 }, ) ); } diff --git a/binrw/tests/derive/struct.rs b/binrw/tests/derive/struct.rs index 1d8f7fd1..b0215dab 100644 --- a/binrw/tests/derive/struct.rs +++ b/binrw/tests/derive/struct.rs @@ -679,6 +679,19 @@ fn pad_size_to() { assert_eq!(result, Test { a: 1, b: 2 }); } +#[test] +fn align_size_to() { + #[derive(BinRead, Debug, PartialEq)] + struct Test { + #[br(align_size_to = 3)] + a: u32, + b: u8, + } + + let result = Test::read_le(&mut Cursor::new(b"\x01\0\0\0\0\0\x02")).unwrap(); + assert_eq!(result, Test { a: 1, b: 2 }); +} + #[test] fn parse_with_default_args() { #[derive(Clone)] diff --git a/binrw/tests/derive/write/padding.rs b/binrw/tests/derive/write/padding.rs index 8ce88aea..1520fbf7 100644 --- a/binrw/tests/derive/write/padding.rs +++ b/binrw/tests/derive/write/padding.rs @@ -25,12 +25,16 @@ fn padding_round_trip() { #[brw(pad_size_to = 0x6_u32)] z: u32, + + #[brw(align_size_to = 0x3_u32)] + w: u32, } let data = &[ /* pad_before: */ 0, 0, /* x */ 1, /* align: */ 0, 0, 0, 0, 0, /* align_before: (none)*/ /* y */ 2, /* pad_after: */ 0, 0, 0, /* z */ 0, - 0xab, 0xcd, 0xef, /* pad_size_to */ 0, 0, + 0xab, 0xcd, 0xef, /* pad_size_to */ 0, 0, /* w */ 0x25, + /* align_size_to */ 0, 0, 0, 0, 0, ]; let test: Test = Cursor::new(data).read_be().unwrap(); @@ -53,12 +57,16 @@ fn padding_one_way() { #[brw(pad_size_to = 0x6_u32)] z: u32, + + #[brw(align_size_to = 0x3_u32)] + w: u32, } let data = &[ /* pad_before: */ 0, 0, /* x */ 1, /* align: */ 0, 0, 0, 0, 0, /* align_before: (none)*/ /* y */ 2, /* pad_after: */ 0, 0, 0, /* z */ 0xef, - 0xcd, 0xab, 0, /* pad_size_to */ 0, 0, + 0xcd, 0xab, 0, /* pad_size_to */ 0, 0, /* w */ 0x25, /* align_size_to */ 0, + 0, 0, 0, 0, ]; let mut x = Cursor::new(Vec::new()); @@ -67,6 +75,7 @@ fn padding_one_way() { x: 1, y: 2, z: 0xabcdef, + w: 0x25, } .write_options(&mut x, Endian::Little, ()) .unwrap(); From dd3a0e60878c28fd7e00f14f1acc2485249a678d Mon Sep 17 00:00:00 2001 From: widberg Date: Fri, 27 Jun 2025 18:49:55 -0400 Subject: [PATCH 3/3] Add align_size_to docs --- binrw/doc/attribute.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/binrw/doc/attribute.md b/binrw/doc/attribute.md index 334ce5b3..79cb6f7a 100644 --- a/binrw/doc/attribute.md +++ b/binrw/doc/attribute.md @@ -92,6 +92,7 @@ Glossary of directives in binrw attributes (`#[br]`, `#[bw]`, `#[brw]`). |-----|-----------|----------|------------ | rw | [`align_after`](#padding-and-alignment) | field | Aligns the readerwriter to the Nth byte after a field. | rw | [`align_before`](#padding-and-alignment) | field | Aligns the readerwriter to the Nth byte before a field. +| rw | [`align_size_to`](#padding-and-alignment) | field | Ensures the readerwriter is always advanced by a multiple of N bytes. | rw | [`args`](#arguments) | field | Passes arguments to another binrw object. | rw | [`args_raw`](#arguments) | field | Like `args`, but specifies a single variable containing the arguments. | rw | [`assert`](#assert) | struct, field, non-unit enum, data variant | Asserts that a condition is true. Can be used multiple times. @@ -2121,6 +2122,25 @@ read the string and `pad_size_to(256)` will ensure the reader skips whatever padding, if any, remains. If the string is longer than 256 bytes, no padding will be skipped. +--- + +The `align_size_to` directive will ensure that the +readerwriter has advanced a multiple of the number of bytes given after the field has been +readwritten: + +
+ +```text +#[br(align_size_to = $size:expr)] or #[br(align_size_to($size:expr))] +``` +
+
+ +```text +#[bw(align_size_to = $size:expr)] or #[bw(align_size_to($size:expr))] +``` +
+ Any (earlier only, when reading)earlier field or [import](#arguments) can be referenced by the expressions in any of these directives.