This is an automated email from the ASF dual-hosted git repository.

gkoszyk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new f762e4f2b feat(server): implement TwoHalves buffer (#2944)
f762e4f2b is described below

commit f762e4f2b3afcb8e6b1a20ce86ec7484b637d096
Author: Grzegorz Koszyk <[email protected]>
AuthorDate: Fri Mar 20 19:20:43 2026 +0100

    feat(server): implement TwoHalves buffer (#2944)
    
    This PR introduces an `TwoHalves` buffer, that uses `Vec<u8>` instead of
    `Bytes` as it's underlying storage.
    
    TwoHalves splits the underlying storage into two parts (mutable and
    non-mutable one). The non-mutable part uses thread-safe reference
    counting for cheap sharing (Bytes like), where the mutable one is copied
    over on each clone.
    
    In the initial state both the mutable and non-mutable parts point to the
    same allocation using exactly the same control block, the diverge
    happens on first `clone` when the mutable part gets copied over and
    re-allocated entirely.
---
 Cargo.lock            |   7 +
 Cargo.toml            |   1 +
 DEPENDENCIES.md       |   1 +
 core/iobuf/Cargo.toml |  25 +++
 core/iobuf/src/lib.rs | 604 ++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 638 insertions(+)

diff --git a/Cargo.lock b/Cargo.lock
index 711b658df..4a091845a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5846,6 +5846,13 @@ dependencies = [
  "rustix 1.1.4",
 ]
 
+[[package]]
+name = "iobuf"
+version = "0.1.0"
+dependencies = [
+ "aligned-vec",
+]
+
 [[package]]
 name = "ipconfig"
 version = "0.3.2"
diff --git a/Cargo.toml b/Cargo.toml
index bd6fb22a4..2abd1fe87 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,6 +45,7 @@ members = [
     "core/consensus",
     "core/harness_derive",
     "core/integration",
+    "core/iobuf",
     "core/journal",
     "core/message_bus",
     "core/metadata",
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 95c211adc..804404fb9 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -486,6 +486,7 @@ interpolate_name: 0.2.4, "MIT",
 inventory: 0.3.22, "Apache-2.0 OR MIT",
 io-uring: 0.7.11, "Apache-2.0 OR MIT",
 io_uring_buf_ring: 0.2.3, "MIT",
+iobuf: 0.1.0, "Apache-2.0",
 ipconfig: 0.3.2, "Apache-2.0 OR MIT",
 ipnet: 2.12.0, "Apache-2.0 OR MIT",
 iri-string: 0.7.10, "Apache-2.0 OR MIT",
diff --git a/core/iobuf/Cargo.toml b/core/iobuf/Cargo.toml
new file mode 100644
index 000000000..62a8b8cc1
--- /dev/null
+++ b/core/iobuf/Cargo.toml
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[package]
+name = "iobuf"
+version = "0.1.0"
+edition = "2024"
+license = "Apache-2.0"
+
+[dependencies]
+aligned-vec = "0.6"
diff --git a/core/iobuf/src/lib.rs b/core/iobuf/src/lib.rs
new file mode 100644
index 000000000..4b9006672
--- /dev/null
+++ b/core/iobuf/src/lib.rs
@@ -0,0 +1,604 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::mem::ManuallyDrop;
+use std::ptr::NonNull;
+use std::slice;
+use std::sync::atomic::{AtomicUsize, Ordering, fence};
+
+use aligned_vec::{AVec, ConstAlign};
+
+#[derive(Debug)]
+pub struct Owned<const ALIGN: usize = 4096> {
+    inner: AVec<u8, ConstAlign<ALIGN>>,
+}
+
+impl<const ALIGN: usize> From<AVec<u8, ConstAlign<ALIGN>>> for Owned<ALIGN> {
+    fn from(vec: AVec<u8, ConstAlign<ALIGN>>) -> Self {
+        Self { inner: vec }
+    }
+}
+
+impl<const ALIGN: usize> From<Owned<ALIGN>> for AVec<u8, ConstAlign<ALIGN>> {
+    fn from(value: Owned<ALIGN>) -> Self {
+        value.inner
+    }
+}
+
+impl<const ALIGN: usize> Owned<ALIGN> {
+    pub fn as_slice(&self) -> &[u8] {
+        &self.inner
+    }
+
+    pub fn as_mut_slice(&mut self) -> &mut [u8] {
+        &mut self.inner
+    }
+
+    /// Split `Owned` buffer into two halves
+    ///
+    /// # Panics
+    /// Panics if `split_at > self.len()`.
+    pub fn split_at(self, split_at: usize) -> TwoHalves<ALIGN> {
+        assert!(split_at <= self.inner.len());
+
+        // Take ownership of the AVec's allocation. After this, we are 
responsible
+        // for deallocating via `AVec::from_raw_parts` or equivalent.
+        let (ptr, _, len, capacity) = self.inner.into_raw_parts();
+
+        // SAFETY: both pointers are constructed from the same `Inner` 
allocation, the split_at bounds are validated.
+        // The control block captures original `Inner` metadata to allow 
reconstructing the original frame for merging/dropping.
+        // The ptr provenance rules are maintained by the use of `NonNull` 
apis.
+        let base: NonNull<u8> = unsafe { NonNull::new_unchecked(ptr) };
+        let tail = unsafe { NonNull::new_unchecked(ptr.add(split_at)) };
+        let ctrlb = ControlBlock::new(base, len, capacity);
+        // We need to increment the ref_count as the resulting halves will 
both point to the same control block.
+        unsafe { ctrlb.as_ref().ref_count.fetch_add(1, Ordering::Relaxed) };
+
+        TwoHalves {
+            inner: (
+                Extent {
+                    ptr: base,
+                    len: split_at,
+                    ctrlb,
+                    _pad: 0,
+                },
+                Extent {
+                    ptr: tail,
+                    len: len - split_at,
+                    ctrlb,
+                    _pad: 0,
+                },
+            ),
+        }
+    }
+}
+
+pub struct TwoHalves<const ALIGN: usize> {
+    inner: (Extent<ALIGN>, Extent<ALIGN>),
+}
+
+// SAFETY: `TwoHalves` can be sent across threads as long as the caller 
ensures that the head half is not shared between
+// threads (e.g. by cloning, which creates a new head half),
+// and that the tail half is only shared immutably (e.g. by cloning, which 
shares the tail half immutably).
+unsafe impl<const ALIGN: usize> Send for TwoHalves<ALIGN> {}
+
+impl<const ALIGN: usize> TwoHalves<ALIGN> {
+    pub fn head(&self) -> &[u8] {
+        self.inner.0.as_slice()
+    }
+
+    pub fn head_mut(&mut self) -> &mut [u8] {
+        // SAFETY: We are accessing the head half mutably, this is the only 
correct operation, as the head is not shared between clones,
+        // instead it gets copied.
+        unsafe { self.inner.0.as_mut_slice() }
+    }
+
+    pub fn tail(&self) -> &[u8] {
+        self.inner.1.as_slice()
+    }
+
+    pub fn split_point(&self) -> usize {
+        self.inner.0.len
+    }
+
+    pub fn total_len(&self) -> usize {
+        self.inner.0.len + self.inner.1.len
+    }
+
+    pub fn try_merge(self) -> Result<Owned<ALIGN>, Self> {
+        let ctrlb_eq = std::ptr::addr_eq(self.inner.0.ctrlb.as_ptr(), 
self.inner.1.ctrlb.as_ptr());
+        // SAFETY: `inner.1.ctrlb` points to a live control block while `self` 
is alive.
+        let ref_count = unsafe {
+            self.inner
+                .1
+                .ctrlb
+                .as_ref()
+                .ref_count
+                .load(Ordering::Acquire)
+        };
+        // When ctrlb_eq, both extents share the same control block with 
refcount 2.
+        // When !ctrlb_eq (after clone), the tail has its own refcount.
+        let is_unique = if ctrlb_eq {
+            ref_count == 2
+        } else {
+            ref_count == 1
+        };
+
+        if !is_unique {
+            return Err(self);
+        }
+
+        // Transfer ownership to prevent Extent::drop from running.
+        // SAFETY: We read the inner tuple out of ManuallyDrop, which won't 
run the compiler-generated drop.
+        let this = ManuallyDrop::new(self);
+        let (head, tail) = unsafe { std::ptr::read(&this.inner) };
+        let split_at = head.len;
+
+        // SAFETY: `tail.ctrlb` is unique at this point (is_unique checked 
above),
+        // If `head.ctrlb != tail.ctrlb`, the head owns a standalone allocation
+        // that must be released after copying.
+        unsafe {
+            if !ctrlb_eq {
+                let dst_ctrlb = tail.ctrlb.as_ref();
+
+                // We are patching up the original allocation, with the 
current head data, so that the resulting `Owned` has correct content.
+                let dst = slice::from_raw_parts_mut(dst_ctrlb.base.as_ptr(), 
split_at);
+                dst.copy_from_slice(head.as_slice());
+            }
+
+            // Dropping the head in `ctrlb_eq` case, should decrease the 
refcount to 1, so it's safe to reuse tail control block.
+            // In case when head was it's own allocation, we guarantee that 
it's always unique.
+            drop(head);
+            let tail_ctrlb = tail.ctrlb;
+            // Prevent tail Drop from running, we're taking ownership of the 
control block.
+            std::mem::forget(tail);
+
+            let ctrlb = reclaim_unique_control_block(tail_ctrlb);
+            // SAFETY: `ctrlb.base,capacity` were captured from an `AVec<u8>` 
allocation and
+            // are now exclusively owned by this path.
+            let inner = AVec::from_raw_parts(ctrlb.base.as_ptr(), ALIGN, 
ctrlb.len, ctrlb.capacity);
+            Ok(Owned { inner })
+        }
+    }
+}
+
+impl<const ALIGN: usize> Clone for TwoHalves<ALIGN> {
+    fn clone(&self) -> Self {
+        Self {
+            inner: (
+                Extent::<ALIGN>::copy_from_slice(self.head()),
+                self.inner.1.clone(),
+            ),
+        }
+    }
+}
+
+impl<const ALIGN: usize> std::fmt::Debug for TwoHalves<ALIGN> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("TwoHalves")
+            .field("split_point", &self.split_point())
+            .field("head_len", &self.inner.0.len)
+            .field("tail_len", &self.inner.1.len)
+            .field("halves_alias", &(self.inner.0.ctrlb == self.inner.1.ctrlb))
+            .finish()
+    }
+}
+
+#[derive(Clone)]
+pub struct Frozen<const ALIGN: usize> {
+    inner: Extent<ALIGN>,
+}
+
+impl<const ALIGN: usize> From<Owned<ALIGN>> for Frozen<ALIGN> {
+    fn from(value: Owned<ALIGN>) -> Self {
+        let inner = value.inner;
+        let (ptr, _, len, capacity) = inner.into_raw_parts();
+
+        // SAFETY: The `Owned` buffer is guaranteed to have a valid 
allocation, and we are taking ownership of it, so it's safe to construct the 
control block and extent.
+        let base: NonNull<u8> = unsafe { NonNull::new_unchecked(ptr) };
+        let ctrlb = ControlBlock::new(base, len, capacity);
+        Self {
+            inner: Extent {
+                ptr: base,
+                len,
+                ctrlb,
+                _pad: 0,
+            },
+        }
+    }
+}
+
+impl<const ALIGN: usize> Frozen<ALIGN> {
+    pub fn as_slice(&self) -> &[u8] {
+        self.inner.as_slice()
+    }
+}
+
+#[repr(C, align(64))]
+struct ControlBlock {
+    ref_count: AtomicUsize,
+    base: NonNull<u8>,
+    len: usize,
+    capacity: usize,
+}
+
+impl ControlBlock {
+    fn new(base: NonNull<u8>, len: usize, capacity: usize) -> NonNull<Self> {
+        let ctrl = Box::new(ControlBlock {
+            ref_count: AtomicUsize::new(1),
+            base,
+            len,
+            capacity,
+        });
+        // SAFETY: Box::into_raw returns a valid pointer
+        unsafe { NonNull::new_unchecked(Box::into_raw(ctrl)) }
+    }
+}
+
+struct Extent<const ALIGN: usize> {
+    ptr: NonNull<u8>,
+    len: usize,
+    ctrlb: NonNull<ControlBlock>,
+    // Padded to 32 bytes in order to avoid false sharing when used by 
`TwoHalves`
+    // If `Extent` would be smaller than 32 bytes, two `Extent`s that are 
adjacent in memory
+    // could potentially share the same cache line + some extra
+    // that extra could lead to false sharing, in case of invalidation of extra
+    _pad: usize,
+}
+
+impl<const ALIGN: usize> Drop for Extent<ALIGN> {
+    fn drop(&mut self) {
+        // SAFETY: `self.ctrlb` points to a live control block while `self` is 
alive.
+        unsafe { release_control_block_w_allocation::<ALIGN>(self.ctrlb) }
+    }
+}
+
+impl<const ALIGN: usize> Extent<ALIGN> {
+    fn as_slice(&self) -> &[u8] {
+        // SAFETY: ptr and len describe a valid allocation
+        unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
+    }
+
+    unsafe fn as_mut_slice(&mut self) -> &mut [u8] {
+        // SAFETY: caller guarantees exclusive access
+        unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
+    }
+
+    fn copy_from_slice(src: &[u8]) -> Self {
+        let mut v: AVec<u8, ConstAlign<ALIGN>> = AVec::new(ALIGN);
+        v.extend_from_slice(src);
+
+        let (ptr, _, len, capacity) = v.into_raw_parts();
+        let data = unsafe { NonNull::new_unchecked(ptr) };
+
+        let ctrlb = ControlBlock::new(data, len, capacity);
+
+        Extent {
+            ptr: data,
+            len,
+            ctrlb,
+            _pad: 0,
+        }
+    }
+}
+
+impl<const ALIGN: usize> Clone for Extent<ALIGN> {
+    fn clone(&self) -> Self {
+        // SAFETY: `self.ctrlb` points to a live control block while `self` is 
alive.
+        unsafe {
+            self.ctrlb
+                .as_ref()
+                .ref_count
+                .fetch_add(1, Ordering::Relaxed);
+        }
+        Self {
+            ptr: self.ptr,
+            len: self.len,
+            ctrlb: self.ctrlb,
+            _pad: 0,
+        }
+    }
+}
+
+unsafe fn release_control_block_w_allocation<const ALIGN: usize>(ctrlb: 
NonNull<ControlBlock>) {
+    // SAFETY: ctrlb is valid per function preconditions
+    let old = unsafe { ctrlb.as_ref() }
+        .ref_count
+        .fetch_sub(1, Ordering::Release);
+    debug_assert!(old > 0, "control block refcount underflow");
+
+    if old != 1 {
+        return;
+    }
+
+    // This fence is needed to prevent reordering of use of the data and
+    // deletion of the data. Because it is marked `Release`, the decreasing
+    // of the reference count synchronizes with this `Acquire` fence. This
+    // means that use of the data happens before decreasing the reference
+    // count, which happens before this fence, which happens before the
+    // deletion of the data.
+    //
+    // As explained in the [Boost documentation][1],
+    //
+    // > It is important to enforce any possible access to the object in one
+    // > thread (through an existing reference) to *happen before* deleting
+    // > the object in a different thread. This is achieved by a "release"
+    // > operation after dropping a reference (any access to the object
+    // > through this reference must obviously happened before), and an
+    // > "acquire" operation before deleting the object.
+    //
+    // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html)
+    //
+    fence(Ordering::Acquire);
+
+    // SAFETY: refcount is zero, we have exclusive ownership
+    let ctrlb = unsafe { Box::from_raw(ctrlb.as_ptr()) };
+
+    // SAFETY: `ctrlb.base`, `ctrlb.len` and `ctrlb.capacity` were captured 
from an `AVec`
+    // allocation. We reconstruct the AVec and let it deallocate properly.
+    let _ = unsafe {
+        AVec::<u8, ConstAlign<ALIGN>>::from_raw_parts(
+            ctrlb.base.as_ptr(),
+            ALIGN,
+            ctrlb.len,
+            ctrlb.capacity,
+        )
+    };
+}
+
+unsafe fn reclaim_unique_control_block(ctrlb: NonNull<ControlBlock>) -> 
ControlBlock {
+    assert_eq!(
+        // SAFETY: caller guarantees `ctrlb` points to a live control block.
+        unsafe { ctrlb.as_ref() }.ref_count.load(Ordering::Acquire),
+        1
+    );
+
+    // SAFETY: caller guarantees uniqueness, so ownership of the control block 
can be reclaimed directly.
+    unsafe { *Box::from_raw(ctrlb.as_ptr()) }
+}
+
+// TODO: Better tests & miri.
+#[cfg(test)]
+mod tests {
+    use super::Owned;
+    use aligned_vec::AVec;
+    use aligned_vec::ConstAlign;
+
+    fn make_owned(data: &[u8]) -> Owned {
+        let mut v: AVec<u8, ConstAlign<4096>> = AVec::new(4096);
+        v.extend_from_slice(data);
+        v.into()
+    }
+
+    #[test]
+    fn split_exposes_head_and_tail() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let mut buffer = owned.split_at(2);
+
+        assert_eq!(buffer.head(), &[1, 2]);
+        assert_eq!(buffer.tail(), &[3, 4, 5]);
+        assert_eq!(buffer.split_point(), 2);
+        assert_eq!(buffer.total_len(), 5);
+
+        buffer.head_mut().copy_from_slice(&[9, 8]);
+        assert_eq!(buffer.head(), &[9, 8]);
+        assert_eq!(buffer.tail(), &[3, 4, 5]);
+    }
+
+    #[test]
+    fn clone_copies_head_and_shares_tail() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let mut original = owned.split_at(2);
+        let mut cloned = original.clone();
+
+        original.head_mut().copy_from_slice(&[9, 9]);
+        cloned.head_mut().copy_from_slice(&[7, 7]);
+
+        assert_eq!(original.head(), &[9, 9]);
+        assert_eq!(cloned.head(), &[7, 7]);
+        assert_eq!(original.tail(), &[3, 4, 5]);
+        assert_eq!(cloned.tail(), &[3, 4, 5]);
+    }
+
+    #[test]
+    fn try_merge_reuses_original_frame_when_unique() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let mut buffer = owned.split_at(2);
+        buffer.head_mut().copy_from_slice(&[8, 9]);
+
+        let merged: AVec<u8, ConstAlign<4096>> = 
buffer.try_merge().unwrap().into();
+        assert_eq!(merged.as_slice(), &[8, 9, 3, 4, 5]);
+    }
+
+    #[test]
+    fn try_merge_fails_while_tail_is_shared() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let buffer = owned.split_at(2);
+        let clone = buffer.clone();
+
+        // Merge fails because tail is shared
+        let buffer = buffer.try_merge().unwrap_err();
+
+        drop(clone);
+
+        // Now merge succeeds
+        let merged: AVec<u8, ConstAlign<4096>> = 
buffer.try_merge().unwrap().into();
+        assert_eq!(merged.as_slice(), &[1, 2, 3, 4, 5]);
+    }
+
+    #[test]
+    fn merge_after_cloned_head_mutation_writes_back_to_original_frame() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let buffer = owned.split_at(2);
+        let mut clone = buffer.clone();
+
+        drop(buffer);
+
+        clone.head_mut().copy_from_slice(&[4, 2]);
+
+        let merged: AVec<u8, ConstAlign<4096>> = 
clone.try_merge().unwrap().into();
+        assert_eq!(merged.as_slice(), &[4, 2, 3, 4, 5]);
+    }
+
+    #[test]
+    fn zero_length_splits_work() {
+        let owned = make_owned(&[1, 2, 3]);
+        let left_empty = owned.split_at(0);
+        assert_eq!(left_empty.head(), &[]);
+        assert_eq!(left_empty.tail(), &[1, 2, 3]);
+
+        let owned = make_owned(&[1, 2, 3]);
+        let right_empty = owned.split_at(3);
+        assert_eq!(right_empty.head(), &[1, 2, 3]);
+        assert_eq!(right_empty.tail(), &[]);
+    }
+
+    #[test]
+    fn clone_of_clone_shares_tail() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let original = owned.split_at(2);
+        let clone1 = original.clone();
+        let _clone2 = clone1.clone();
+
+        // All clones share tail, so merge should fail
+        assert!(original.try_merge().is_err());
+    }
+
+    #[test]
+    fn owned_as_slice_returns_correct_data() {
+        let owned = make_owned(&[10, 20, 30, 40, 50]);
+        assert_eq!(owned.as_slice(), &[10, 20, 30, 40, 50]);
+    }
+
+    #[test]
+    fn owned_as_slice_empty_buffer() {
+        let owned = make_owned(&[]);
+        assert_eq!(owned.as_slice(), &[]);
+    }
+
+    #[test]
+    fn owned_as_mut_slice_allows_modification() {
+        let mut owned = make_owned(&[1, 2, 3, 4, 5]);
+        let slice = owned.as_mut_slice();
+        slice[0] = 100;
+        slice[4] = 200;
+
+        assert_eq!(owned.as_slice(), &[100, 2, 3, 4, 200]);
+    }
+
+    #[test]
+    fn owned_as_mut_slice_full_overwrite() {
+        let mut owned = make_owned(&[1, 2, 3]);
+        owned.as_mut_slice().copy_from_slice(&[7, 8, 9]);
+
+        assert_eq!(owned.as_slice(), &[7, 8, 9]);
+    }
+
+    #[test]
+    fn owned_modifications_persist_after_split() {
+        let mut owned = make_owned(&[1, 2, 3, 4, 5]);
+        owned.as_mut_slice()[0] = 99;
+        owned.as_mut_slice()[4] = 88;
+
+        let buffer = owned.split_at(2);
+        assert_eq!(buffer.head(), &[99, 2]);
+        assert_eq!(buffer.tail(), &[3, 4, 88]);
+    }
+
+    #[test]
+    fn two_halves_head_returns_correct_slice() {
+        let owned = make_owned(&[10, 20, 30, 40, 50]);
+        let buffer = owned.split_at(3);
+
+        assert_eq!(buffer.head(), &[10, 20, 30]);
+    }
+
+    #[test]
+    fn two_halves_tail_returns_correct_slice() {
+        let owned = make_owned(&[10, 20, 30, 40, 50]);
+        let buffer = owned.split_at(3);
+
+        assert_eq!(buffer.tail(), &[40, 50]);
+    }
+
+    #[test]
+    fn two_halves_head_mut_allows_modification() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let mut buffer = owned.split_at(3);
+
+        buffer.head_mut()[0] = 100;
+        buffer.head_mut()[2] = 200;
+
+        assert_eq!(buffer.head(), &[100, 2, 200]);
+        assert_eq!(buffer.tail(), &[4, 5]);
+    }
+
+    #[test]
+    fn two_halves_head_mut_full_overwrite() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let mut buffer = owned.split_at(3);
+
+        buffer.head_mut().copy_from_slice(&[7, 8, 9]);
+
+        assert_eq!(buffer.head(), &[7, 8, 9]);
+        assert_eq!(buffer.tail(), &[4, 5]);
+    }
+
+    #[test]
+    fn two_halves_head_empty_slice() {
+        let owned = make_owned(&[1, 2, 3]);
+        let buffer = owned.split_at(0);
+
+        assert_eq!(buffer.head(), &[]);
+        assert_eq!(buffer.tail(), &[1, 2, 3]);
+    }
+
+    #[test]
+    fn two_halves_tail_empty_slice() {
+        let owned = make_owned(&[1, 2, 3]);
+        let buffer = owned.split_at(3);
+
+        assert_eq!(buffer.head(), &[1, 2, 3]);
+        assert_eq!(buffer.tail(), &[]);
+    }
+
+    #[test]
+    fn two_halves_head_mut_does_not_affect_tail() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let mut buffer = owned.split_at(2);
+
+        let original_tail: Vec<u8> = buffer.tail().to_vec();
+        buffer.head_mut().copy_from_slice(&[99, 99]);
+
+        assert_eq!(buffer.tail(), original_tail.as_slice());
+    }
+
+    #[test]
+    fn two_halves_cloned_head_mut_independent() {
+        let owned = make_owned(&[1, 2, 3, 4, 5]);
+        let mut original = owned.split_at(2);
+        let mut cloned = original.clone();
+
+        original.head_mut().copy_from_slice(&[10, 20]);
+        cloned.head_mut().copy_from_slice(&[30, 40]);
+
+        assert_eq!(original.head(), &[10, 20]);
+        assert_eq!(cloned.head(), &[30, 40]);
+        // Tail is shared, both should see the same data
+        assert_eq!(original.tail(), cloned.tail());
+    }
+}

Reply via email to