From 5c44b7d054b9e335d2988043746b50bcb190c6ef Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 8 Sep 2026 13:12:46 -0400 Subject: [PATCH 1/6] fix(buffer): address allocator review comments with focused ZST fixes Signed-off-by: Nicholas Gates --- vortex-buffer/src/allocation.rs | 69 +++++++++++++--- vortex-buffer/src/buffer.rs | 37 ++++++--- vortex-buffer/src/buffer_mut.rs | 31 ++++++- vortex-buffer/tests/zst.rs | 139 ++++++++++++++++++++++++++++++++ 4 files changed, 253 insertions(+), 23 deletions(-) create mode 100644 vortex-buffer/tests/zst.rs diff --git a/vortex-buffer/src/allocation.rs b/vortex-buffer/src/allocation.rs index 2523587253c..bd66eed99e4 100644 --- a/vortex-buffer/src/allocation.rs +++ b/vortex-buffer/src/allocation.rs @@ -27,8 +27,15 @@ pub trait BufferAllocator: Allocator + Debug + Send + Sync + 'static {} impl BufferAllocator for A where A: Allocator + Debug + Send + Sync + 'static {} /// A shared reference to a buffer allocator. +/// +/// The static allocator does not need shared ownership, so it is stored without an [`Arc`]. This +/// makes cloning the common static allocator a simple value copy. #[derive(Clone)] -pub struct BufferAllocatorRef(Option>); +pub struct BufferAllocatorRef( + // `None` selects the static allocator without allocating or updating an Arc reference count. + // `Some` keeps a custom allocator alive for as long as its buffers need it. + Option>, +); impl BufferAllocatorRef { /// Wrap an allocator in a shared reference. @@ -335,8 +342,6 @@ impl Drop for Allocation { pub(crate) trait BufferOwner: Send + Sync + 'static { fn as_ptr(&self) -> *const u8; - - fn len(&self) -> usize; } impl BufferOwner for T @@ -346,10 +351,6 @@ where fn as_ptr(&self) -> *const u8 { self.as_ref().as_ptr() } - - fn len(&self) -> usize { - self.as_ref().len() - } } pub(crate) enum BufferBacking { @@ -386,9 +387,13 @@ mod tests { use allocator_api2::alloc::AllocError; use allocator_api2::alloc::Allocator; use allocator_api2::alloc::Global; + use rstest::rstest; + use vortex_error::VortexResult; + use vortex_error::vortex_err; use crate::Alignment; use crate::BufferAllocatorRef; + use crate::BufferMut; #[derive(Clone, Debug, Default)] struct TrackingAllocator { @@ -462,15 +467,18 @@ mod tests { assert_eq!(state.deallocations.load(Ordering::Relaxed), 1); } - #[test] - fn buffer_growth_uses_allocator_grow() { + #[rstest] + fn buffer_growth_uses_allocator_grow(#[values(4, 64, 4096)] alignment: usize) { let allocator = TrackingAllocator::default(); let state = Arc::clone(&allocator.state); - let mut buffer = BufferAllocatorRef::new(allocator).with_capacity::(1); + let alignment = Alignment::new(alignment); + let mut buffer = + BufferAllocatorRef::new(allocator).with_capacity_aligned::(1, alignment); let initial_capacity = buffer.capacity(); buffer.extend(std::iter::repeat_n(7, initial_capacity)); buffer.push(u32::MAX); + assert!(alignment.is_ptr_aligned(buffer.as_ptr())); assert_eq!(&buffer[..initial_capacity], vec![7; initial_capacity]); assert_eq!(buffer[initial_capacity], u32::MAX); @@ -498,4 +506,45 @@ mod tests { assert_eq!(state.allocations.load(Ordering::Relaxed), 1); assert_eq!(state.grows.load(Ordering::Relaxed), 0); } + + #[test] + fn zero_sized_buffers_do_not_allocate() -> VortexResult<()> { + let allocator = TrackingAllocator::default(); + let state = Arc::clone(&allocator.state); + let allocator = BufferAllocatorRef::new(allocator); + let mut buffer = BufferMut::<()>::zeroed_in(3, allocator.clone()); + buffer.push(()); + let buffer = buffer.freeze(); + let copy = buffer.clone().into_mut(); + assert!(copy.allocator().ptr_eq(&allocator)); + let mut buffer = buffer + .try_into_mut() + .map_err(|_| vortex_err!("unique buffer"))?; + buffer.push(()); + assert_eq!(buffer.len(), 5); + assert!(buffer.allocator().ptr_eq(&allocator)); + drop((copy, buffer)); + assert_eq!(state.allocations.load(Ordering::Relaxed), 0); + assert_eq!(state.grows.load(Ordering::Relaxed), 0); + assert_eq!(state.deallocations.load(Ordering::Relaxed), 0); + Ok(()) + } + + #[test] + fn shared_into_mut_preserves_allocator() { + let allocator = TrackingAllocator::default(); + let state = Arc::clone(&allocator.state); + let allocator = BufferAllocatorRef::new(allocator); + let original = allocator.copy_from([1u32, 2, 3]).freeze(); + let mut copy = original.clone().into_mut(); + assert!(copy.allocator().ptr_eq(&allocator)); + copy[0] = 42; + assert_eq!(original.as_slice(), [1, 2, 3]); + assert_eq!(copy.as_slice(), [42, 2, 3]); + assert_eq!(state.allocations.load(Ordering::Relaxed), 2); + drop(copy); + assert_eq!(state.deallocations.load(Ordering::Relaxed), 1); + drop(original); + assert_eq!(state.deallocations.load(Ordering::Relaxed), 2); + } } diff --git a/vortex-buffer/src/buffer.rs b/vortex-buffer/src/buffer.rs index 3b48889fa8d..dc12887428e 100644 --- a/vortex-buffer/src/buffer.rs +++ b/vortex-buffer/src/buffer.rs @@ -30,9 +30,13 @@ use crate::trusted_len::TrustedLen; /// An immutable buffer of items of `T`. #[derive(Clone)] pub struct Buffer { + /// The first element in this view; may dangle for an empty buffer or zero-sized `T`. pub(crate) ptr: NonNull, + /// The number of initialized `T` values visible from `ptr`. pub(crate) length: usize, + /// The minimum alignment promised for `ptr` and preserved by aligned slices. pub(crate) alignment: Alignment, + /// Shared ownership of the storage containing `ptr`, if any; `Buffer::empty` has no backing. pub(crate) backing: Option>, } @@ -100,9 +104,8 @@ impl Buffer { } } - fn from_owner(owner: impl crate::BufferOwner, alignment: Alignment) -> Self { + fn from_owner(owner: impl crate::BufferOwner, length: usize, alignment: Alignment) -> Self { let owner: Box = Box::new(owner); - let length = owner.len() / size_of::(); let ptr = if length == 0 { empty_ptr() } else { @@ -275,8 +278,12 @@ impl Buffer { /// ## Panics /// /// Panics if the buffer is not aligned to the given alignment, if the length is not a multiple - /// of the size of `T`, or if the given alignment is not aligned to that of `T`. + /// of the size of `T`, if `T` is zero-sized, or if the given alignment is not aligned to that of `T`. pub fn from_byte_buffer_aligned(buffer: ByteBuffer, alignment: Alignment) -> Self { + assert!( + size_of::() != 0, + "cannot infer a zero-sized element count from bytes" + ); if !alignment.is_aligned_to(Alignment::of::()) { vortex_panic!( "Alignment {} must be compatible with the scalar type's alignment {}", @@ -307,8 +314,12 @@ impl Buffer { /// ## Panics /// /// Panics if the buffer is not aligned to the size of `T`, or the length is not a multiple of - /// the size of `T`. + /// the size of `T`, or if `T` is zero-sized. pub fn from_bytes_aligned(bytes: Bytes, alignment: Alignment) -> Self { + assert!( + size_of::() != 0, + "cannot infer a zero-sized element count from bytes" + ); if !alignment.is_aligned_to(Alignment::of::()) { vortex_panic!( "Alignment {} must be compatible with the scalar type's alignment {}", @@ -559,7 +570,7 @@ impl Buffer { let subset_end = subset_start .checked_add(size_of_val(subset)) .vortex_expect("slice_ref address overflow"); - if subset_start < start || subset_end > end { + if subset.len() > self.len() || subset_start < start || subset_end > end { vortex_panic!("slice_ref subset must be contained in the buffer"); } @@ -631,7 +642,9 @@ impl Buffer { match Arc::try_unwrap(backing) { Ok(BufferBacking::Owned(allocation)) => { let offset = ptr.addr().get() - allocation.ptr().addr().get(); - let capacity = if allocation.size() == 0 { + let capacity = if size_of::() == 0 { + usize::MAX + } else if allocation.size() == 0 { 0 } else { (allocation.size() - offset) / size_of::() @@ -810,10 +823,6 @@ impl crate::BufferOwner for Wrapper { fn as_ptr(&self) -> *const u8 { self.0.as_ptr().cast() } - - fn len(&self) -> usize { - self.0.len() * size_of::() - } } impl From> for Buffer @@ -824,7 +833,7 @@ where let length = value.len(); let alignment = Alignment::of::(); if std::mem::needs_drop::() { - Self::from_owner(Wrapper(value), alignment) + Self::from_owner(Wrapper(value), length, alignment) } else { Self::from_allocation(Allocation::from_vec(value), 0, length, alignment) } @@ -1151,9 +1160,15 @@ mod test { let Ok(mut sliced) = sliced.try_into_mut() else { panic!("uniquely owned slice should become mutable") }; + let ptr = sliced.as_ptr(); let capacity = sliced.capacity(); sliced.push_n(0, capacity - sliced.len()); assert_eq!(sliced.len(), capacity); + assert_eq!(sliced.as_ptr(), ptr); + sliced.push(42); + assert_eq!(&sliced[..32], (64u32..96).collect::>()); + assert_eq!(&sliced[32..capacity], vec![0; capacity - 32]); + assert_eq!(sliced[capacity], 42); } #[test] diff --git a/vortex-buffer/src/buffer_mut.rs b/vortex-buffer/src/buffer_mut.rs index 27c3dad3532..05cd6585daa 100644 --- a/vortex-buffer/src/buffer_mut.rs +++ b/vortex-buffer/src/buffer_mut.rs @@ -24,11 +24,17 @@ use crate::trusted_len::TrustedLen; /// A mutable buffer that maintains a runtime-defined alignment through resizing operations. pub struct BufferMut { + /// The owned allocation, including any bytes before `ptr` used for alignment. pub(crate) allocation: Allocation, + /// The first element, aligned to `alignment`; it may dangle for an empty or zero-sized buffer. pub(crate) ptr: std::ptr::NonNull, + /// The number of initialized `T` values starting at `ptr`. pub(crate) length: usize, + /// The number of `T` values that fit from `ptr`; this is `usize::MAX` for zero-sized `T`. pub(crate) capacity: usize, + /// The minimum alignment maintained for `ptr` across reallocations. pub(crate) alignment: Alignment, + /// Marks the buffer as logically owning values of `T` despite storing an erased allocation. pub(crate) _marker: std::marker::PhantomData, } @@ -132,7 +138,7 @@ impl BufferMut { // SAFETY: the allocation includes enough padding to reach this aligned pointer. let ptr = unsafe { allocation.ptr().add(offset).cast() }; let capacity = if size_of::() == 0 { - capacity + usize::MAX } else { (allocation.size() - offset) / size_of::() }; @@ -227,7 +233,7 @@ impl BufferMut { // SAFETY: the allocation includes enough padding to reach this aligned pointer. let ptr = unsafe { allocation.ptr().add(offset).cast() }; let capacity = if size_of::() == 0 { - len + usize::MAX } else { (allocation.size() - offset) / size_of::() }; @@ -837,6 +843,12 @@ impl BufferMut { /// We use the lower bound hint on the iterator to manually write data, and then we continue to /// push items normally past the lower bound. fn extend_iter(&mut self, mut iter: impl Iterator) { + // Pointer differences cannot count zero-sized elements. + if size_of::() == 0 { + iter.for_each(|item| self.push(item)); + return; + } + // Since we do not know the length of the iterator, we can only guess how much memory we // need to reserve. Note that these hints may be inaccurate. let (lower_bound, _) = iter.size_hint(); @@ -887,6 +899,12 @@ impl BufferMut { /// The caller guarantees that the iterator will have a trusted upper bound, which allows the /// implementation to reserve all of the memory needed up front. pub fn extend_trusted>(&mut self, iter: I) { + // Pointer differences cannot count zero-sized elements. + if size_of::() == 0 { + iter.for_each(|item| self.push(item)); + return; + } + let (_, upper_bound) = iter.size_hint(); self.reserve( upper_bound @@ -1028,6 +1046,15 @@ mod test { assert_eq!(buffer.capacity(), capacity * 2); } + #[test] + fn zero_sized_elements_grow() { + let mut buffer = BufferMut::<()>::empty(); + assert_eq!(buffer.capacity(), usize::MAX); + buffer.push(()); + buffer.push(()); + assert_eq!(buffer.len(), 2); + } + #[test] fn static_growth_copies_live_data() { let mut buffer = BufferMut::::with_capacity(1); diff --git a/vortex-buffer/tests/zst.rs b/vortex-buffer/tests/zst.rs new file mode 100644 index 00000000000..af228654ef9 --- /dev/null +++ b/vortex-buffer/tests/zst.rs @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#![cfg(test)] + +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +use bytes::Bytes; +use rstest::rstest; +use vortex_buffer::Alignment; +use vortex_buffer::Buffer; +use vortex_buffer::BufferMut; +use vortex_buffer::ByteBuffer; +use vortex_error::VortexResult; +use vortex_error::vortex_err; + +#[repr(align(64))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct AlignedZst; + +#[rstest] +#[case(BufferMut::empty())] +#[case(BufferMut::with_capacity(4))] +#[case(BufferMut::zeroed(3))] +#[case(BufferMut::copy_from([AlignedZst; 2]))] +#[case(BufferMut::from_iter([AlignedZst; 2]))] +#[case(BufferMut::from_trusted_len_iter([AlignedZst; 2].into_iter()))] +fn zero_sized_elements_grow(#[case] mut buffer: BufferMut) { + let initial_len = buffer.len(); + assert_eq!(buffer.capacity(), usize::MAX); + buffer.reserve(100); + buffer.push(AlignedZst); + buffer.push_n(AlignedZst, 2); + buffer.extend_from_slice(&[AlignedZst; 3]); + buffer.extend([AlignedZst; 4]); + buffer.extend_trusted([AlignedZst; 5].into_iter()); + assert_eq!(buffer.as_slice(), vec![AlignedZst; initial_len + 15]); + assert_eq!(buffer.spare_capacity_mut().len(), usize::MAX - buffer.len()); + assert!(Alignment::of::().is_ptr_aligned(buffer.as_ptr())); + buffer.truncate(2); + let clone = buffer.clone(); + buffer.clear(); + assert_eq!(clone.len(), 2); + assert_eq!(buffer.capacity(), usize::MAX); + assert!(buffer.is_empty()); +} + +#[rstest] +#[case(Buffer::empty())] +#[case(Buffer::from(vec![AlignedZst; 4]))] +#[case(BufferMut::zeroed(4).freeze())] +fn zero_sized_freeze_thaw(#[case] buffer: Buffer) -> VortexResult<()> { + let len = buffer.len(); + let mut mutable = buffer + .try_into_mut() + .map_err(|_| vortex_err!("unique buffer"))?; + assert_eq!(mutable.len(), len); + assert_eq!(mutable.capacity(), usize::MAX); + mutable.push(AlignedZst); + let frozen = mutable.freeze(); + let view = frozen.slice(1..); + let mut shared_copy = frozen.clone().into_mut(); + shared_copy.push(AlignedZst); + assert_eq!(frozen.len(), len + 1); + assert_eq!(shared_copy.len(), len + 2); + drop(frozen); + let mut unique_view = view + .try_into_mut() + .map_err(|_| vortex_err!("unique slice"))?; + assert_eq!(unique_view.len(), len); + assert_eq!(unique_view.capacity(), usize::MAX); + unique_view.push(AlignedZst); + assert_eq!(unique_view.len(), len + 1); + Ok(()) +} + +#[test] +fn zero_sized_vec_retains_drop_glue() { + static DROPS: AtomicUsize = AtomicUsize::new(0); + #[repr(align(64))] + struct DropZst; + impl Drop for DropZst { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::Relaxed); + } + } + + let buffer = Buffer::from(vec![DropZst, DropZst, DropZst]); + assert_eq!(buffer.len(), 3); + assert!(Alignment::of::().is_ptr_aligned(buffer.as_ptr())); + let view = buffer.slice(1..); + drop(buffer); + assert_eq!(view.len(), 2); + assert_eq!(DROPS.load(Ordering::Relaxed), 0); + drop(view); + assert_eq!(DROPS.load(Ordering::Relaxed), 3); +} + +#[test] +fn zero_sized_byte_views_are_empty() { + let buffer = BufferMut::::zeroed(3); + let frozen = buffer.clone().freeze(); + assert!(frozen.as_bytes().is_empty()); + assert!(frozen.clone().into_bytes().is_empty()); + assert!(frozen.into_byte_buffer().is_empty()); + let mut bytes = buffer.into_byte_buffer(); + assert!(bytes.is_empty()); + assert_eq!(bytes.capacity(), 0); + bytes.push(42); + assert_eq!(bytes.as_slice(), [42]); +} + +#[test] +#[should_panic(expected = "buffer capacity overflow")] +fn zero_sized_reserve_checks_length_overflow() { + let mut buffer = BufferMut::<()>::zeroed(1); + buffer.reserve(usize::MAX); +} + +#[test] +#[should_panic(expected = "slice_ref subset must be contained in the buffer")] +fn zero_sized_slice_ref_rejects_longer_subset() { + let original = Buffer::<()>::zeroed(4); + let short = original.slice(..2); + short.slice_ref(original.as_slice()); +} + +#[rstest] +#[case(false)] +#[case(true)] +#[should_panic(expected = "cannot infer a zero-sized element count from bytes")] +fn bytes_cannot_determine_zero_sized_length(#[case] use_byte_buffer: bool) { + if use_byte_buffer { + Buffer::<()>::from_byte_buffer(ByteBuffer::empty()); + } else { + Buffer::<()>::from_bytes_aligned(Bytes::new(), Alignment::of::<()>()); + } +} From a62c85b63547c1f10ac29909b7d69357e7b5a085 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 8 Sep 2026 13:24:11 -0400 Subject: [PATCH 2/6] fix(buffer): restrict owner construction to byte buffers Signed-off-by: Nicholas Gates --- vortex-buffer/src/allocation.rs | 16 +----- vortex-buffer/src/buffer.rs | 88 +++++++++++++++++++++++---------- vortex-buffer/src/memmap2.rs | 3 +- 3 files changed, 65 insertions(+), 42 deletions(-) diff --git a/vortex-buffer/src/allocation.rs b/vortex-buffer/src/allocation.rs index bd66eed99e4..d351a75ba8b 100644 --- a/vortex-buffer/src/allocation.rs +++ b/vortex-buffer/src/allocation.rs @@ -4,6 +4,7 @@ //! Allocator-backed storage for Vortex buffers. use std::alloc::Layout; +use std::any::Any; use std::fmt; use std::fmt::Debug; use std::mem::ManuallyDrop; @@ -340,26 +341,13 @@ impl Drop for Allocation { } } -pub(crate) trait BufferOwner: Send + Sync + 'static { - fn as_ptr(&self) -> *const u8; -} - -impl BufferOwner for T -where - T: AsRef<[u8]> + Send + Sync + 'static, -{ - fn as_ptr(&self) -> *const u8 { - self.as_ref().as_ptr() - } -} - pub(crate) enum BufferBacking { Owned(Allocation), Bytes(bytes::Bytes), #[cfg(feature = "arrow")] Arrow(arrow_buffer::Buffer), External { - _owner: Box, + _owner: Box, }, } diff --git a/vortex-buffer/src/buffer.rs b/vortex-buffer/src/buffer.rs index dc12887428e..cae07cf9ada 100644 --- a/vortex-buffer/src/buffer.rs +++ b/vortex-buffer/src/buffer.rs @@ -104,21 +104,6 @@ impl Buffer { } } - fn from_owner(owner: impl crate::BufferOwner, length: usize, alignment: Alignment) -> Self { - let owner: Box = Box::new(owner); - let ptr = if length == 0 { - empty_ptr() - } else { - NonNull::new(owner.as_ptr().cast_mut().cast()).vortex_expect("owner pointer is null") - }; - Self { - ptr, - length, - alignment, - backing: Some(Arc::new(BufferBacking::External { _owner: owner })), - } - } - fn from_bytes(bytes: Bytes, alignment: Alignment) -> Self { let length = bytes.len() / size_of::(); if length == 0 { @@ -815,16 +800,6 @@ impl FromIterator for Buffer { } } -// Helper struct that preserves drop glue for non-native Vec elements. -#[repr(transparent)] -struct Wrapper(Vec); - -impl crate::BufferOwner for Wrapper { - fn as_ptr(&self) -> *const u8 { - self.0.as_ptr().cast() - } -} - impl From> for Buffer where T: Send + Sync + 'static, @@ -833,13 +808,38 @@ where let length = value.len(); let alignment = Alignment::of::(); if std::mem::needs_drop::() { - Self::from_owner(Wrapper(value), length, alignment) + // Keep the typed owner so its elements are dropped, including zero-sized elements. + Self { + ptr: NonNull::new(value.as_ptr().cast_mut()) + .vortex_expect("a Vec always has a non-null pointer"), + length, + alignment, + backing: Some(Arc::new(BufferBacking::External { + _owner: Box::new(value), + })), + } } else { Self::from_allocation(Allocation::from_vec(value), 0, length, alignment) } } } +impl ByteBuffer { + /// Takes zero-copy ownership of a byte slice, retaining its owner until the last view is dropped. + /// + /// The buffer's length comes from the owner's byte slice. Typed buffers must instead be + /// constructed from typed values or through the checked byte-buffer conversion APIs. + /// + /// ```compile_fail + /// use vortex_buffer::Buffer; + /// + /// let buffer = Buffer::::from_owner(vec![0u8; 4]); + /// ``` + pub fn from_owner(owner: impl AsRef<[u8]> + Send + 'static) -> Self { + Self::from(Bytes::from_owner(owner)) + } +} + impl From for ByteBuffer { fn from(bytes: Bytes) -> Self { Self::from_bytes(bytes, Alignment::of::()) @@ -1103,6 +1103,42 @@ mod test { assert_eq!(buffer.allocation.alignment(), align_of::()); } + #[test] + fn byte_owner_preserves_slice_and_lifetime() { + struct Owner { + values: Vec, + drops: Arc, + } + + impl AsRef<[u8]> for Owner { + fn as_ref(&self) -> &[u8] { + &self.values[1..4] + } + } + + impl Drop for Owner { + fn drop(&mut self) { + self.drops.fetch_add(1, Ordering::Relaxed); + } + } + + let drops = Arc::new(AtomicUsize::new(0)); + let owner = Owner { + values: vec![0, 1, 2, 3, 4], + drops: Arc::clone(&drops), + }; + let ptr = owner.as_ref().as_ptr(); + let buffer = ByteBuffer::from_owner(owner); + assert_eq!(buffer.as_ptr(), ptr); + assert_eq!(buffer.as_slice(), [1, 2, 3]); + let view = buffer.slice(1..); + drop(buffer); + assert_eq!(drops.load(Ordering::Relaxed), 0); + assert_eq!(view.as_slice(), [2, 3]); + drop(view); + assert_eq!(drops.load(Ordering::Relaxed), 1); + } + #[test] fn bytes_round_trip_reuses_owner() { let bytes = Bytes::from_static(&[1, 2, 3, 4]); diff --git a/vortex-buffer/src/memmap2.rs b/vortex-buffer/src/memmap2.rs index bac4f7b7716..91efcdab6bc 100644 --- a/vortex-buffer/src/memmap2.rs +++ b/vortex-buffer/src/memmap2.rs @@ -1,13 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use bytes::Bytes; use memmap2::Mmap; use crate::ByteBuffer; impl From for ByteBuffer { fn from(value: Mmap) -> Self { - ByteBuffer::from(Bytes::from_owner(value)) + ByteBuffer::from_owner(value) } } From 7fc03f3777c1b1995fbfc17fadc7ee7a972623d3 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 8 Sep 2026 14:29:55 -0400 Subject: [PATCH 3/6] refactor(buffer): remove redundant byte owner wrapper Signed-off-by: Nicholas Gates --- vortex-buffer/src/buffer.rs | 18 +----------------- vortex-buffer/src/memmap2.rs | 3 ++- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/vortex-buffer/src/buffer.rs b/vortex-buffer/src/buffer.rs index cae07cf9ada..a44f24e73e8 100644 --- a/vortex-buffer/src/buffer.rs +++ b/vortex-buffer/src/buffer.rs @@ -824,22 +824,6 @@ where } } -impl ByteBuffer { - /// Takes zero-copy ownership of a byte slice, retaining its owner until the last view is dropped. - /// - /// The buffer's length comes from the owner's byte slice. Typed buffers must instead be - /// constructed from typed values or through the checked byte-buffer conversion APIs. - /// - /// ```compile_fail - /// use vortex_buffer::Buffer; - /// - /// let buffer = Buffer::::from_owner(vec![0u8; 4]); - /// ``` - pub fn from_owner(owner: impl AsRef<[u8]> + Send + 'static) -> Self { - Self::from(Bytes::from_owner(owner)) - } -} - impl From for ByteBuffer { fn from(bytes: Bytes) -> Self { Self::from_bytes(bytes, Alignment::of::()) @@ -1128,7 +1112,7 @@ mod test { drops: Arc::clone(&drops), }; let ptr = owner.as_ref().as_ptr(); - let buffer = ByteBuffer::from_owner(owner); + let buffer = ByteBuffer::from(Bytes::from_owner(owner)); assert_eq!(buffer.as_ptr(), ptr); assert_eq!(buffer.as_slice(), [1, 2, 3]); let view = buffer.slice(1..); diff --git a/vortex-buffer/src/memmap2.rs b/vortex-buffer/src/memmap2.rs index 91efcdab6bc..bac4f7b7716 100644 --- a/vortex-buffer/src/memmap2.rs +++ b/vortex-buffer/src/memmap2.rs @@ -1,12 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use bytes::Bytes; use memmap2::Mmap; use crate::ByteBuffer; impl From for ByteBuffer { fn from(value: Mmap) -> Self { - ByteBuffer::from_owner(value) + ByteBuffer::from(Bytes::from_owner(value)) } } From 8d9db98c6784d69d6642b9f363ee1060dfc7d3ab Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 8 Sep 2026 14:45:05 -0400 Subject: [PATCH 4/6] fix(buffer): preserve iterator code generation for non-ZSTs Signed-off-by: Nicholas Gates --- vortex-buffer/src/buffer_mut.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vortex-buffer/src/buffer_mut.rs b/vortex-buffer/src/buffer_mut.rs index 05cd6585daa..701a4610fb8 100644 --- a/vortex-buffer/src/buffer_mut.rs +++ b/vortex-buffer/src/buffer_mut.rs @@ -845,7 +845,9 @@ impl BufferMut { fn extend_iter(&mut self, mut iter: impl Iterator) { // Pointer differences cannot count zero-sized elements. if size_of::() == 0 { - iter.for_each(|item| self.push(item)); + for item in iter { + self.push(item); + } return; } @@ -901,7 +903,9 @@ impl BufferMut { pub fn extend_trusted>(&mut self, iter: I) { // Pointer differences cannot count zero-sized elements. if size_of::() == 0 { - iter.for_each(|item| self.push(item)); + for item in iter { + self.push(item); + } return; } From d4eb3e84224a531bb33433448a5b802481450b4c Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 8 Sep 2026 15:08:02 -0400 Subject: [PATCH 5/6] fix(buffer): preserve ZST counts in owned iterators Signed-off-by: Nicholas Gates --- vortex-buffer/src/buffer.rs | 14 ++++++++++++-- vortex-buffer/tests/zst.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/vortex-buffer/src/buffer.rs b/vortex-buffer/src/buffer.rs index a44f24e73e8..c45d31f32ff 100644 --- a/vortex-buffer/src/buffer.rs +++ b/vortex-buffer/src/buffer.rs @@ -882,7 +882,7 @@ fn empty_ptr() -> NonNull { /// Owned iterator over a [`Buffer`]. pub struct BufferIterator { - // Keep the buffer alive for the duration of the iteration. + // Keep the buffer alive; its length also counts the remaining zero-sized elements. _buffer: Buffer, ptr: *const T, end: *const T, @@ -898,6 +898,11 @@ impl Iterator for BufferIterator { #[inline] fn next(&mut self) -> Option { + if size_of::() == 0 { + let value = self._buffer.as_slice().last().copied()?; + self._buffer.length -= 1; + return Some(value); + } if self.ptr == self.end { None } else { @@ -910,7 +915,12 @@ impl Iterator for BufferIterator { #[inline] fn size_hint(&self) -> (usize, Option) { - let remaining = unsafe { self.end.offset_from(self.ptr) } as usize; + let remaining = if size_of::() == 0 { + self._buffer.length + } else { + // SAFETY: both cursors belong to the buffer and T is not zero-sized. + (unsafe { self.end.offset_from(self.ptr) }) as usize + }; (remaining, Some(remaining)) } } diff --git a/vortex-buffer/tests/zst.rs b/vortex-buffer/tests/zst.rs index af228654ef9..4a243f372bb 100644 --- a/vortex-buffer/tests/zst.rs +++ b/vortex-buffer/tests/zst.rs @@ -137,3 +137,35 @@ fn bytes_cannot_determine_zero_sized_length(#[case] use_byte_buffer: bool) { Buffer::<()>::from_bytes_aligned(Bytes::new(), Alignment::of::<()>()); } } + +#[rstest] +#[case(0)] +#[case(1)] +#[case(4)] +fn zero_sized_owned_iterator(#[case] len: usize) { + let mut iter = Buffer::::zeroed(len).into_iter(); + assert_eq!(iter.size_hint(), (len, Some(len))); + for remaining in (0..len).rev() { + assert_eq!(iter.next(), Some(AlignedZst)); + assert_eq!(iter.len(), remaining); + } + assert_eq!(iter.next(), None); + assert_eq!(iter.next(), None); + assert_eq!(iter.size_hint(), (0, Some(0))); +} + +#[test] +fn zero_sized_owned_iterator_retains_count_when_collected() { + let buffer = Buffer::::zeroed(5).slice(1..); + let buffer = Buffer::from_trusted_len_iter(buffer.into_iter()); + assert_eq!(buffer.len(), 4); + assert_eq!(buffer.into_iter().collect::>(), vec![AlignedZst; 4]); +} + +#[test] +fn zero_sized_owned_iterator_supports_maximum_length() { + let mut iter = Buffer::::zeroed(usize::MAX).into_iter(); + assert_eq!(iter.len(), usize::MAX); + assert_eq!(iter.next(), Some(AlignedZst)); + assert_eq!(iter.len(), usize::MAX - 1); +} From 2a3f5177043cc74c2e935391b78862854240989f Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 8 Sep 2026 15:20:31 -0400 Subject: [PATCH 6/6] fix(buffer): reject zero-sized elements at compile time Signed-off-by: Nicholas Gates --- vortex-buffer/src/allocation.rs | 9 +- vortex-buffer/src/buffer.rs | 63 ++++++------ vortex-buffer/src/buffer_mut.rs | 55 ++++------ vortex-buffer/tests/zst.rs | 171 -------------------------------- 4 files changed, 57 insertions(+), 241 deletions(-) delete mode 100644 vortex-buffer/tests/zst.rs diff --git a/vortex-buffer/src/allocation.rs b/vortex-buffer/src/allocation.rs index d351a75ba8b..69d9d4c0021 100644 --- a/vortex-buffer/src/allocation.rs +++ b/vortex-buffer/src/allocation.rs @@ -496,20 +496,19 @@ mod tests { } #[test] - fn zero_sized_buffers_do_not_allocate() -> VortexResult<()> { + fn empty_buffers_preserve_allocator_without_allocating() -> VortexResult<()> { let allocator = TrackingAllocator::default(); let state = Arc::clone(&allocator.state); let allocator = BufferAllocatorRef::new(allocator); - let mut buffer = BufferMut::<()>::zeroed_in(3, allocator.clone()); - buffer.push(()); + let buffer = BufferMut::::zeroed_in(0, allocator.clone()); let buffer = buffer.freeze(); let copy = buffer.clone().into_mut(); assert!(copy.allocator().ptr_eq(&allocator)); let mut buffer = buffer .try_into_mut() .map_err(|_| vortex_err!("unique buffer"))?; - buffer.push(()); - assert_eq!(buffer.len(), 5); + buffer.reserve(0); + assert!(buffer.is_empty()); assert!(buffer.allocator().ptr_eq(&allocator)); drop((copy, buffer)); assert_eq!(state.allocations.load(Ordering::Relaxed), 0); diff --git a/vortex-buffer/src/buffer.rs b/vortex-buffer/src/buffer.rs index c45d31f32ff..73d15991f0b 100644 --- a/vortex-buffer/src/buffer.rs +++ b/vortex-buffer/src/buffer.rs @@ -28,9 +28,32 @@ use crate::debug::TruncatedDebug; use crate::trusted_len::TrustedLen; /// An immutable buffer of items of `T`. +/// +/// Zero-sized element types are rejected at compile time when constructing a buffer. +/// +/// ```compile_fail +/// use vortex_buffer::Buffer; +/// let _ = Buffer::<()>::empty(); +/// ``` +/// +/// ```compile_fail +/// use vortex_buffer::Buffer; +/// let _ = Buffer::from(vec![(); 3]); +/// ``` +/// +/// ```compile_fail +/// use vortex_buffer::{Buffer, ByteBuffer}; +/// let _ = Buffer::<()>::from_byte_buffer(ByteBuffer::empty()); +/// ``` +/// +/// ```compile_fail +/// use bytes::Bytes; +/// use vortex_buffer::{Alignment, Buffer}; +/// let _ = Buffer::<()>::from_bytes_aligned(Bytes::new(), Alignment::none()); +/// ``` #[derive(Clone)] pub struct Buffer { - /// The first element in this view; may dangle for an empty buffer or zero-sized `T`. + /// The first element in this view; may dangle for an empty buffer. pub(crate) ptr: NonNull, /// The number of initialized `T` values visible from `ptr`. pub(crate) length: usize, @@ -216,6 +239,7 @@ impl Buffer { /// /// This does not allocate. Empty buffers use an aligned dangling pointer. pub fn empty_aligned(alignment: Alignment) -> Self { + const { assert!(size_of::() != 0, "ZSTs are not supported") }; if !alignment.is_aligned_to(Alignment::of::()) { vortex_panic!( "Alignment {} must align to the scalar type's alignment {}", @@ -263,12 +287,9 @@ impl Buffer { /// ## Panics /// /// Panics if the buffer is not aligned to the given alignment, if the length is not a multiple - /// of the size of `T`, if `T` is zero-sized, or if the given alignment is not aligned to that of `T`. + /// of the size of `T`, or if the given alignment is not aligned to that of `T`. pub fn from_byte_buffer_aligned(buffer: ByteBuffer, alignment: Alignment) -> Self { - assert!( - size_of::() != 0, - "cannot infer a zero-sized element count from bytes" - ); + const { assert!(size_of::() != 0, "ZSTs are not supported") }; if !alignment.is_aligned_to(Alignment::of::()) { vortex_panic!( "Alignment {} must be compatible with the scalar type's alignment {}", @@ -299,12 +320,9 @@ impl Buffer { /// ## Panics /// /// Panics if the buffer is not aligned to the size of `T`, or the length is not a multiple of - /// the size of `T`, or if `T` is zero-sized. + /// the size of `T`. pub fn from_bytes_aligned(bytes: Bytes, alignment: Alignment) -> Self { - assert!( - size_of::() != 0, - "cannot infer a zero-sized element count from bytes" - ); + const { assert!(size_of::() != 0, "ZSTs are not supported") }; if !alignment.is_aligned_to(Alignment::of::()) { vortex_panic!( "Alignment {} must be compatible with the scalar type's alignment {}", @@ -555,7 +573,7 @@ impl Buffer { let subset_end = subset_start .checked_add(size_of_val(subset)) .vortex_expect("slice_ref address overflow"); - if subset.len() > self.len() || subset_start < start || subset_end > end { + if subset_start < start || subset_end > end { vortex_panic!("slice_ref subset must be contained in the buffer"); } @@ -627,9 +645,7 @@ impl Buffer { match Arc::try_unwrap(backing) { Ok(BufferBacking::Owned(allocation)) => { let offset = ptr.addr().get() - allocation.ptr().addr().get(); - let capacity = if size_of::() == 0 { - usize::MAX - } else if allocation.size() == 0 { + let capacity = if allocation.size() == 0 { 0 } else { (allocation.size() - offset) / size_of::() @@ -805,10 +821,11 @@ where T: Send + Sync + 'static, { fn from(value: Vec) -> Self { + const { assert!(size_of::() != 0, "ZSTs are not supported") }; let length = value.len(); let alignment = Alignment::of::(); if std::mem::needs_drop::() { - // Keep the typed owner so its elements are dropped, including zero-sized elements. + // Keep the typed owner so its elements are dropped. Self { ptr: NonNull::new(value.as_ptr().cast_mut()) .vortex_expect("a Vec always has a non-null pointer"), @@ -882,7 +899,7 @@ fn empty_ptr() -> NonNull { /// Owned iterator over a [`Buffer`]. pub struct BufferIterator { - // Keep the buffer alive; its length also counts the remaining zero-sized elements. + // Keep the buffer alive for the duration of the iteration. _buffer: Buffer, ptr: *const T, end: *const T, @@ -898,11 +915,6 @@ impl Iterator for BufferIterator { #[inline] fn next(&mut self) -> Option { - if size_of::() == 0 { - let value = self._buffer.as_slice().last().copied()?; - self._buffer.length -= 1; - return Some(value); - } if self.ptr == self.end { None } else { @@ -915,12 +927,7 @@ impl Iterator for BufferIterator { #[inline] fn size_hint(&self) -> (usize, Option) { - let remaining = if size_of::() == 0 { - self._buffer.length - } else { - // SAFETY: both cursors belong to the buffer and T is not zero-sized. - (unsafe { self.end.offset_from(self.ptr) }) as usize - }; + let remaining = unsafe { self.end.offset_from(self.ptr) } as usize; (remaining, Some(remaining)) } } diff --git a/vortex-buffer/src/buffer_mut.rs b/vortex-buffer/src/buffer_mut.rs index 701a4610fb8..71e27c4f8ce 100644 --- a/vortex-buffer/src/buffer_mut.rs +++ b/vortex-buffer/src/buffer_mut.rs @@ -23,14 +23,26 @@ use crate::debug::TruncatedDebug; use crate::trusted_len::TrustedLen; /// A mutable buffer that maintains a runtime-defined alignment through resizing operations. +/// +/// Zero-sized element types are rejected at compile time when constructing a buffer. +/// +/// ```compile_fail +/// use vortex_buffer::BufferMut; +/// let _ = BufferMut::<()>::empty(); +/// ``` +/// +/// ```compile_fail +/// use vortex_buffer::BufferMut; +/// let _ = BufferMut::<()>::zeroed(3); +/// ``` pub struct BufferMut { /// The owned allocation, including any bytes before `ptr` used for alignment. pub(crate) allocation: Allocation, - /// The first element, aligned to `alignment`; it may dangle for an empty or zero-sized buffer. + /// The first element, aligned to `alignment`; it may dangle for an empty buffer. pub(crate) ptr: std::ptr::NonNull, /// The number of initialized `T` values starting at `ptr`. pub(crate) length: usize, - /// The number of `T` values that fit from `ptr`; this is `usize::MAX` for zero-sized `T`. + /// The number of `T` values that fit from `ptr`. pub(crate) capacity: usize, /// The minimum alignment maintained for `ptr` across reallocations. pub(crate) alignment: Alignment, @@ -106,6 +118,7 @@ impl BufferMut { preferred_alignment: Option, allocator: BufferAllocatorRef, ) -> Self { + const { assert!(size_of::() != 0, "ZSTs are not supported") }; let actual = max( alignment, preferred_alignment.unwrap_or(Alignment::of::()), @@ -137,11 +150,7 @@ impl BufferMut { let offset = allocation.ptr().as_ptr().align_offset(actual.as_usize()); // SAFETY: the allocation includes enough padding to reach this aligned pointer. let ptr = unsafe { allocation.ptr().add(offset).cast() }; - let capacity = if size_of::() == 0 { - usize::MAX - } else { - (allocation.size() - offset) / size_of::() - }; + let capacity = (allocation.size() - offset) / size_of::(); Self { allocation, ptr, @@ -210,6 +219,7 @@ impl BufferMut { preferred_alignment: Option, allocator: BufferAllocatorRef, ) -> Self { + const { assert!(size_of::() != 0, "ZSTs are not supported") }; let preferred_alignment = preferred_alignment.unwrap_or(Alignment::of::()); let actual_alignment = max(preferred_alignment, alignment); let size = len @@ -232,11 +242,7 @@ impl BufferMut { .align_offset(actual_alignment.as_usize()); // SAFETY: the allocation includes enough padding to reach this aligned pointer. let ptr = unsafe { allocation.ptr().add(offset).cast() }; - let capacity = if size_of::() == 0 { - usize::MAX - } else { - (allocation.size() - offset) / size_of::() - }; + let capacity = (allocation.size() - offset) / size_of::(); Self { allocation, ptr, @@ -843,14 +849,6 @@ impl BufferMut { /// We use the lower bound hint on the iterator to manually write data, and then we continue to /// push items normally past the lower bound. fn extend_iter(&mut self, mut iter: impl Iterator) { - // Pointer differences cannot count zero-sized elements. - if size_of::() == 0 { - for item in iter { - self.push(item); - } - return; - } - // Since we do not know the length of the iterator, we can only guess how much memory we // need to reserve. Note that these hints may be inaccurate. let (lower_bound, _) = iter.size_hint(); @@ -901,14 +899,6 @@ impl BufferMut { /// The caller guarantees that the iterator will have a trusted upper bound, which allows the /// implementation to reserve all of the memory needed up front. pub fn extend_trusted>(&mut self, iter: I) { - // Pointer differences cannot count zero-sized elements. - if size_of::() == 0 { - for item in iter { - self.push(item); - } - return; - } - let (_, upper_bound) = iter.size_hint(); self.reserve( upper_bound @@ -1050,15 +1040,6 @@ mod test { assert_eq!(buffer.capacity(), capacity * 2); } - #[test] - fn zero_sized_elements_grow() { - let mut buffer = BufferMut::<()>::empty(); - assert_eq!(buffer.capacity(), usize::MAX); - buffer.push(()); - buffer.push(()); - assert_eq!(buffer.len(), 2); - } - #[test] fn static_growth_copies_live_data() { let mut buffer = BufferMut::::with_capacity(1); diff --git a/vortex-buffer/tests/zst.rs b/vortex-buffer/tests/zst.rs deleted file mode 100644 index 4a243f372bb..00000000000 --- a/vortex-buffer/tests/zst.rs +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -#![cfg(test)] - -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; - -use bytes::Bytes; -use rstest::rstest; -use vortex_buffer::Alignment; -use vortex_buffer::Buffer; -use vortex_buffer::BufferMut; -use vortex_buffer::ByteBuffer; -use vortex_error::VortexResult; -use vortex_error::vortex_err; - -#[repr(align(64))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct AlignedZst; - -#[rstest] -#[case(BufferMut::empty())] -#[case(BufferMut::with_capacity(4))] -#[case(BufferMut::zeroed(3))] -#[case(BufferMut::copy_from([AlignedZst; 2]))] -#[case(BufferMut::from_iter([AlignedZst; 2]))] -#[case(BufferMut::from_trusted_len_iter([AlignedZst; 2].into_iter()))] -fn zero_sized_elements_grow(#[case] mut buffer: BufferMut) { - let initial_len = buffer.len(); - assert_eq!(buffer.capacity(), usize::MAX); - buffer.reserve(100); - buffer.push(AlignedZst); - buffer.push_n(AlignedZst, 2); - buffer.extend_from_slice(&[AlignedZst; 3]); - buffer.extend([AlignedZst; 4]); - buffer.extend_trusted([AlignedZst; 5].into_iter()); - assert_eq!(buffer.as_slice(), vec![AlignedZst; initial_len + 15]); - assert_eq!(buffer.spare_capacity_mut().len(), usize::MAX - buffer.len()); - assert!(Alignment::of::().is_ptr_aligned(buffer.as_ptr())); - buffer.truncate(2); - let clone = buffer.clone(); - buffer.clear(); - assert_eq!(clone.len(), 2); - assert_eq!(buffer.capacity(), usize::MAX); - assert!(buffer.is_empty()); -} - -#[rstest] -#[case(Buffer::empty())] -#[case(Buffer::from(vec![AlignedZst; 4]))] -#[case(BufferMut::zeroed(4).freeze())] -fn zero_sized_freeze_thaw(#[case] buffer: Buffer) -> VortexResult<()> { - let len = buffer.len(); - let mut mutable = buffer - .try_into_mut() - .map_err(|_| vortex_err!("unique buffer"))?; - assert_eq!(mutable.len(), len); - assert_eq!(mutable.capacity(), usize::MAX); - mutable.push(AlignedZst); - let frozen = mutable.freeze(); - let view = frozen.slice(1..); - let mut shared_copy = frozen.clone().into_mut(); - shared_copy.push(AlignedZst); - assert_eq!(frozen.len(), len + 1); - assert_eq!(shared_copy.len(), len + 2); - drop(frozen); - let mut unique_view = view - .try_into_mut() - .map_err(|_| vortex_err!("unique slice"))?; - assert_eq!(unique_view.len(), len); - assert_eq!(unique_view.capacity(), usize::MAX); - unique_view.push(AlignedZst); - assert_eq!(unique_view.len(), len + 1); - Ok(()) -} - -#[test] -fn zero_sized_vec_retains_drop_glue() { - static DROPS: AtomicUsize = AtomicUsize::new(0); - #[repr(align(64))] - struct DropZst; - impl Drop for DropZst { - fn drop(&mut self) { - DROPS.fetch_add(1, Ordering::Relaxed); - } - } - - let buffer = Buffer::from(vec![DropZst, DropZst, DropZst]); - assert_eq!(buffer.len(), 3); - assert!(Alignment::of::().is_ptr_aligned(buffer.as_ptr())); - let view = buffer.slice(1..); - drop(buffer); - assert_eq!(view.len(), 2); - assert_eq!(DROPS.load(Ordering::Relaxed), 0); - drop(view); - assert_eq!(DROPS.load(Ordering::Relaxed), 3); -} - -#[test] -fn zero_sized_byte_views_are_empty() { - let buffer = BufferMut::::zeroed(3); - let frozen = buffer.clone().freeze(); - assert!(frozen.as_bytes().is_empty()); - assert!(frozen.clone().into_bytes().is_empty()); - assert!(frozen.into_byte_buffer().is_empty()); - let mut bytes = buffer.into_byte_buffer(); - assert!(bytes.is_empty()); - assert_eq!(bytes.capacity(), 0); - bytes.push(42); - assert_eq!(bytes.as_slice(), [42]); -} - -#[test] -#[should_panic(expected = "buffer capacity overflow")] -fn zero_sized_reserve_checks_length_overflow() { - let mut buffer = BufferMut::<()>::zeroed(1); - buffer.reserve(usize::MAX); -} - -#[test] -#[should_panic(expected = "slice_ref subset must be contained in the buffer")] -fn zero_sized_slice_ref_rejects_longer_subset() { - let original = Buffer::<()>::zeroed(4); - let short = original.slice(..2); - short.slice_ref(original.as_slice()); -} - -#[rstest] -#[case(false)] -#[case(true)] -#[should_panic(expected = "cannot infer a zero-sized element count from bytes")] -fn bytes_cannot_determine_zero_sized_length(#[case] use_byte_buffer: bool) { - if use_byte_buffer { - Buffer::<()>::from_byte_buffer(ByteBuffer::empty()); - } else { - Buffer::<()>::from_bytes_aligned(Bytes::new(), Alignment::of::<()>()); - } -} - -#[rstest] -#[case(0)] -#[case(1)] -#[case(4)] -fn zero_sized_owned_iterator(#[case] len: usize) { - let mut iter = Buffer::::zeroed(len).into_iter(); - assert_eq!(iter.size_hint(), (len, Some(len))); - for remaining in (0..len).rev() { - assert_eq!(iter.next(), Some(AlignedZst)); - assert_eq!(iter.len(), remaining); - } - assert_eq!(iter.next(), None); - assert_eq!(iter.next(), None); - assert_eq!(iter.size_hint(), (0, Some(0))); -} - -#[test] -fn zero_sized_owned_iterator_retains_count_when_collected() { - let buffer = Buffer::::zeroed(5).slice(1..); - let buffer = Buffer::from_trusted_len_iter(buffer.into_iter()); - assert_eq!(buffer.len(), 4); - assert_eq!(buffer.into_iter().collect::>(), vec![AlignedZst; 4]); -} - -#[test] -fn zero_sized_owned_iterator_supports_maximum_length() { - let mut iter = Buffer::::zeroed(usize::MAX).into_iter(); - assert_eq!(iter.len(), usize::MAX); - assert_eq!(iter.next(), Some(AlignedZst)); - assert_eq!(iter.len(), usize::MAX - 1); -}