From f65e0b97af9ec3cbc53ba7aadb9fd8af71e595b3 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 15 Nov 2025 18:57:22 -0800 Subject: [PATCH 1/7] Define interleaved writing as a trait This removes the "foreign" impl on ChunkBuilder. --- rbx_binary/src/chunk.rs | 24 ++++++++++++++++- rbx_binary/src/core.rs | 35 ++++++------------------- rbx_binary/src/serializer/state.rs | 4 +-- rbx_binary/src/tests/core_read_write.rs | 6 ++++- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/rbx_binary/src/chunk.rs b/rbx_binary/src/chunk.rs index 0c0fc837c..9b6effe84 100644 --- a/rbx_binary/src/chunk.rs +++ b/rbx_binary/src/chunk.rs @@ -5,7 +5,7 @@ use std::{ }; use crate::{ - core::{RbxReadExt, RbxWriteExt}, + core::{RbxReadExt, RbxWriteExt, RbxWriteInterleaved}, serializer::CompressionType, }; @@ -133,6 +133,28 @@ impl Write for ChunkBuilder { } } +impl RbxWriteInterleaved for ChunkBuilder { + fn write_interleaved_bytes(&mut self, values: I) -> io::Result<()> + where + I: IntoIterator, + ::IntoIter: ExactSizeIterator, + { + let values = values.into_iter(); + let values_len = values.len(); + let bytes_len = values_len * N; + + self.initialize_bytes_with(bytes_len, |buffer| { + for (i, bytes) in values.enumerate() { + for (b, byte) in IntoIterator::into_iter(bytes).enumerate() { + buffer[i + b * values_len] = byte; + } + } + }); + + Ok(()) + } +} + #[derive(Debug)] struct ChunkHeader { /// 4-byte short name for the chunk, like "INST" or "PRNT" diff --git a/rbx_binary/src/core.rs b/rbx_binary/src/core.rs index 523a9f31f..48b13879f 100644 --- a/rbx_binary/src/core.rs +++ b/rbx_binary/src/core.rs @@ -4,8 +4,6 @@ use rbx_reflection::{ ClassDescriptor, PropertyDescriptor, PropertyKind, PropertySerialization, ReflectionDatabase, }; -use crate::chunk::ChunkBuilder; - pub static FILE_MAGIC_HEADER: &[u8] = b"(&mut self, values: I) -> io::Result<()> + fn write_interleaved_bytes(&mut self, values: I) -> io::Result<()> where I: IntoIterator, - ::IntoIter: ExactSizeIterator, - { - let values = values.into_iter(); - let values_len = values.len(); - let bytes_len = values_len * N; - - let initialize_bytes = |buffer: &mut [u8]| { - for (i, bytes) in values.enumerate() { - for (b, byte) in IntoIterator::into_iter(bytes).enumerate() { - buffer[i + b * values_len] = byte; - } - } - }; - - self.initialize_bytes_with(bytes_len, initialize_bytes); - - Ok(()) - } + ::IntoIter: ExactSizeIterator; /// Writes all items from `values` into the buffer as a blob of interleaved /// bytes. Transformation is applied to the values as they're written. - pub fn write_interleaved_i32_array(&mut self, values: I) -> io::Result<()> + fn write_interleaved_i32_array(&mut self, values: I) -> io::Result<()> where I: IntoIterator, ::IntoIter: ExactSizeIterator, @@ -292,7 +273,7 @@ impl ChunkBuilder { /// Writes all items from `values` into the buffer as a blob of interleaved /// bytes. - pub fn write_interleaved_u32_array(&mut self, values: I) -> io::Result<()> + fn write_interleaved_u32_array(&mut self, values: I) -> io::Result<()> where I: IntoIterator, ::IntoIter: ExactSizeIterator, @@ -302,7 +283,7 @@ impl ChunkBuilder { /// Writes all items from `values` into the buffer as a blob of interleaved /// bytes. Rotation is applied to the values as they're written. - pub fn write_interleaved_f32_array(&mut self, values: I) -> io::Result<()> + fn write_interleaved_f32_array(&mut self, values: I) -> io::Result<()> where I: IntoIterator, ::IntoIter: ExactSizeIterator, @@ -317,7 +298,7 @@ impl ChunkBuilder { /// Writes all items from `values` into the buffer as a blob of interleaved /// bytes. The appropriate transformation and de-accumulation is done as /// values are written. - pub fn write_referent_array(&mut self, values: I) -> io::Result<()> + fn write_referent_array(&mut self, values: I) -> io::Result<()> where I: IntoIterator, ::IntoIter: ExactSizeIterator, @@ -334,7 +315,7 @@ impl ChunkBuilder { /// Writes all items from `values` into the buffer as a blob of interleaved /// bytes. Transformation is applied to the values as they're written. - pub fn write_interleaved_i64_array(&mut self, values: I) -> io::Result<()> + fn write_interleaved_i64_array(&mut self, values: I) -> io::Result<()> where I: IntoIterator, ::IntoIter: ExactSizeIterator, diff --git a/rbx_binary/src/serializer/state.rs b/rbx_binary/src/serializer/state.rs index 6e531cb95..830055d7e 100644 --- a/rbx_binary/src/serializer/state.rs +++ b/rbx_binary/src/serializer/state.rs @@ -24,8 +24,8 @@ use rbx_reflection::{ use crate::{ chunk::ChunkBuilder, core::{ - find_property_descriptors, PropertyDescriptors, RbxWriteExt, FILE_MAGIC_HEADER, - FILE_SIGNATURE, FILE_VERSION, + find_property_descriptors, PropertyDescriptors, RbxWriteExt, RbxWriteInterleaved, + FILE_MAGIC_HEADER, FILE_SIGNATURE, FILE_VERSION, }, types::Type, Serializer, diff --git a/rbx_binary/src/tests/core_read_write.rs b/rbx_binary/src/tests/core_read_write.rs index 486411cab..4fa17d390 100644 --- a/rbx_binary/src/tests/core_read_write.rs +++ b/rbx_binary/src/tests/core_read_write.rs @@ -1,4 +1,8 @@ -use crate::{chunk::ChunkBuilder, core::RbxReadExt, CompressionType}; +use crate::{ + chunk::ChunkBuilder, + core::{RbxReadExt, RbxWriteInterleaved}, + CompressionType, +}; #[test] fn read_interleaved_bytes() { From ca47a53ae7a2d65662b438848ec009b5c6e6a758 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sun, 16 Nov 2025 08:23:12 -0800 Subject: [PATCH 2/7] inline initialize_bytes_with --- rbx_binary/src/chunk.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/rbx_binary/src/chunk.rs b/rbx_binary/src/chunk.rs index 9b6effe84..acf672dee 100644 --- a/rbx_binary/src/chunk.rs +++ b/rbx_binary/src/chunk.rs @@ -78,13 +78,6 @@ impl ChunkBuilder { } } - /// Reserve bytes and use a closure to initialize them. - pub fn initialize_bytes_with(&mut self, len: usize, initialize_bytes: impl FnOnce(&mut [u8])) { - let current_len = self.buffer.len(); - self.buffer.extend(core::iter::repeat_n(0, len)); - initialize_bytes(&mut self.buffer[current_len..]); - } - /// Consume the chunk and write it to the given writer. pub fn dump(self, mut writer: W) -> io::Result<()> { writer.write_all(self.chunk_name)?; @@ -143,13 +136,17 @@ impl RbxWriteInterleaved for ChunkBuilder { let values_len = values.len(); let bytes_len = values_len * N; - self.initialize_bytes_with(bytes_len, |buffer| { - for (i, bytes) in values.enumerate() { - for (b, byte) in IntoIterator::into_iter(bytes).enumerate() { - buffer[i + b * values_len] = byte; - } + // Reserve space for new values + let current_len = self.buffer.len(); + self.buffer.extend(core::iter::repeat_n(0, bytes_len)); + + // Write new values + let buffer = &mut self.buffer[current_len..]; + for (i, bytes) in values.enumerate() { + for (b, byte) in IntoIterator::into_iter(bytes).enumerate() { + buffer[i + b * values_len] = byte; } - }); + } Ok(()) } From 2d491211529120e4090b87aefe370337b7c81f14 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 29 Nov 2025 10:26:41 -0800 Subject: [PATCH 3/7] impl RbxWriteInterleaved for Vec --- rbx_binary/src/chunk.rs | 18 +---------------- rbx_binary/src/core.rs | 26 +++++++++++++++++++++++++ rbx_binary/src/tests/core_read_write.rs | 16 +++------------ 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/rbx_binary/src/chunk.rs b/rbx_binary/src/chunk.rs index acf672dee..bcb505980 100644 --- a/rbx_binary/src/chunk.rs +++ b/rbx_binary/src/chunk.rs @@ -132,23 +132,7 @@ impl RbxWriteInterleaved for ChunkBuilder { I: IntoIterator, ::IntoIter: ExactSizeIterator, { - let values = values.into_iter(); - let values_len = values.len(); - let bytes_len = values_len * N; - - // Reserve space for new values - let current_len = self.buffer.len(); - self.buffer.extend(core::iter::repeat_n(0, bytes_len)); - - // Write new values - let buffer = &mut self.buffer[current_len..]; - for (i, bytes) in values.enumerate() { - for (b, byte) in IntoIterator::into_iter(bytes).enumerate() { - buffer[i + b * values_len] = byte; - } - } - - Ok(()) + self.buffer.write_interleaved_bytes(values) } } diff --git a/rbx_binary/src/core.rs b/rbx_binary/src/core.rs index 48b13879f..f2dd8a0e8 100644 --- a/rbx_binary/src/core.rs +++ b/rbx_binary/src/core.rs @@ -324,6 +324,32 @@ pub trait RbxWriteInterleaved { } } +impl RbxWriteInterleaved for Vec { + fn write_interleaved_bytes(&mut self, values: I) -> io::Result<()> + where + I: IntoIterator, + ::IntoIter: ExactSizeIterator, + { + let values = values.into_iter(); + let values_len = values.len(); + let bytes_len = values_len * N; + + // Reserve space for new values + let current_len = self.len(); + self.extend(core::iter::repeat_n(0, bytes_len)); + + // Write new values + let buffer = &mut self[current_len..]; + for (i, bytes) in values.enumerate() { + for (b, byte) in IntoIterator::into_iter(bytes).enumerate() { + buffer[i + b * values_len] = byte; + } + } + + Ok(()) + } +} + impl RbxWriteExt for W where W: Write {} /// Applies the 'zigzag' transformation done by Roblox to many `i32` values. diff --git a/rbx_binary/src/tests/core_read_write.rs b/rbx_binary/src/tests/core_read_write.rs index 4fa17d390..d2257517b 100644 --- a/rbx_binary/src/tests/core_read_write.rs +++ b/rbx_binary/src/tests/core_read_write.rs @@ -1,8 +1,4 @@ -use crate::{ - chunk::ChunkBuilder, - core::{RbxReadExt, RbxWriteInterleaved}, - CompressionType, -}; +use crate::core::{RbxReadExt, RbxWriteInterleaved}; #[test] fn read_interleaved_bytes() { @@ -68,14 +64,8 @@ fn write_interleaved_bytes() { 15, 15, 15, ]; - let mut chunk = ChunkBuilder::new(b"ASDF", CompressionType::None); - chunk.write_interleaved_bytes(input).unwrap(); - - let mut dump = Vec::new(); - chunk.dump(&mut dump).unwrap(); - - // the first 16 bytes are the chunk header - let result = &dump[16..]; + let mut result = Vec::new(); + result.write_interleaved_bytes(input).unwrap(); assert_eq!(result, expected); } From 1683d3d1ed4c591a73b09ea0c5e61bb49797a9d4 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 29 Nov 2025 10:53:06 -0800 Subject: [PATCH 4/7] unnecessary generic --- rbx_binary/src/serializer/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rbx_binary/src/serializer/state.rs b/rbx_binary/src/serializer/state.rs index 830055d7e..f29192d6d 100644 --- a/rbx_binary/src/serializer/state.rs +++ b/rbx_binary/src/serializer/state.rs @@ -1468,7 +1468,7 @@ impl<'dom, 'db: 'dom, W: Write> SerializerState<'dom, 'db, W> { } } - chunk.write_interleaved_bytes::<16, _>(blobs)?; + chunk.write_interleaved_bytes(blobs)?; } Type::SecurityCapabilities => { let mut capabilities = Vec::with_capacity(values.len()); From bc78a966c4010ded5d729d299d1a45c1698acdc0 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 29 Nov 2025 12:32:18 -0800 Subject: [PATCH 5/7] combine test contants --- rbx_binary/src/tests/core_read_write.rs | 80 ++++++++++--------------- rbx_binary/src/tests/mod.rs | 1 + 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/rbx_binary/src/tests/core_read_write.rs b/rbx_binary/src/tests/core_read_write.rs index d2257517b..28b408f64 100644 --- a/rbx_binary/src/tests/core_read_write.rs +++ b/rbx_binary/src/tests/core_read_write.rs @@ -1,32 +1,35 @@ use crate::core::{RbxReadExt, RbxWriteInterleaved}; +#[rustfmt::skip] +const BYTES_INTERLEAVED: &[u8] = &[ + 0, 0, 0, + 1, 1, 1, + 2, 2, 2, + 3, 3, 3, + 4, 4, 4, + 5, 5, 5, + 6, 6, 6, + 7, 7, 7, + 8, 8, 8, + 9, 9, 9, + 10, 10, 10, + 11, 11, 11, + 12, 12, 12, + 13, 13, 13, + 14, 14, 14, + 15, 15, 15, +]; + +const BYTES_UNINTERLEAVED: [[u8; 16]; 3] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], +]; + #[test] fn read_interleaved_bytes() { - #[rustfmt::skip] - let mut input: &[u8] = &[ - 0, 0, 0, - 1, 1, 1, - 2, 2, 2, - 3, 3, 3, - 4, 4, 4, - 5, 5, 5, - 6, 6, 6, - 7, 7, 7, - 8, 8, 8, - 9, 9, 9, - 10, 10, 10, - 11, 11, 11, - 12, 12, 12, - 13, 13, 13, - 14, 14, 14, - 15, 15, 15, - ]; - - let expected = &[ - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - ]; + let mut input = BYTES_INTERLEAVED; + let expected = BYTES_UNINTERLEAVED; let result: Vec<_> = input .read_interleaved_bytes::<16>(expected.len()) @@ -38,31 +41,8 @@ fn read_interleaved_bytes() { #[test] fn write_interleaved_bytes() { - let input: [[u8; 16]; 3] = [ - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - ]; - - #[rustfmt::skip] - let expected = &[ - 0, 0, 0, - 1, 1, 1, - 2, 2, 2, - 3, 3, 3, - 4, 4, 4, - 5, 5, 5, - 6, 6, 6, - 7, 7, 7, - 8, 8, 8, - 9, 9, 9, - 10, 10, 10, - 11, 11, 11, - 12, 12, 12, - 13, 13, 13, - 14, 14, 14, - 15, 15, 15, - ]; + let input = BYTES_UNINTERLEAVED; + let expected = BYTES_INTERLEAVED; let mut result = Vec::new(); result.write_interleaved_bytes(input).unwrap(); diff --git a/rbx_binary/src/tests/mod.rs b/rbx_binary/src/tests/mod.rs index 573e82529..2d4834336 100644 --- a/rbx_binary/src/tests/mod.rs +++ b/rbx_binary/src/tests/mod.rs @@ -1,3 +1,4 @@ +#[cfg(test)] mod core_read_write; mod models; mod places; From b8bacabdc36d92cd3ddfb0762a804a6b9cefaa7c Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 29 Nov 2025 12:37:24 -0800 Subject: [PATCH 6/7] make test cooler with no generics or allocations --- rbx_binary/src/tests/core_read_write.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rbx_binary/src/tests/core_read_write.rs b/rbx_binary/src/tests/core_read_write.rs index 28b408f64..2cb511b1f 100644 --- a/rbx_binary/src/tests/core_read_write.rs +++ b/rbx_binary/src/tests/core_read_write.rs @@ -31,12 +31,11 @@ fn read_interleaved_bytes() { let mut input = BYTES_INTERLEAVED; let expected = BYTES_UNINTERLEAVED; - let result: Vec<_> = input - .read_interleaved_bytes::<16>(expected.len()) - .unwrap() - .collect(); + let mut array_iter = input.read_interleaved_bytes(expected.len()).unwrap(); + let result = core::array::from_fn(|_| array_iter.next().unwrap()); - assert_eq!(result, expected) + assert_eq!(result, expected); + assert!(array_iter.next().is_none()); } #[test] From bba75722b5d893e084e810648c57b8a3c0ed20df Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 29 Nov 2025 13:07:33 -0800 Subject: [PATCH 7/7] `#[cfg(test)]` already defined in parent module --- rbx_binary/src/tests/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rbx_binary/src/tests/mod.rs b/rbx_binary/src/tests/mod.rs index 2d4834336..573e82529 100644 --- a/rbx_binary/src/tests/mod.rs +++ b/rbx_binary/src/tests/mod.rs @@ -1,4 +1,3 @@ -#[cfg(test)] mod core_read_write; mod models; mod places;