Skip to main content

kevy_bytes/
lib.rs

1//! `SmallBytes` — a 24-byte small-byte-string with inline-SSO optimization.
2//!
3//! Layout (**little-endian only**): a union of two 24-byte variants, distinguished
4//! by the byte at offset 23:
5//!
6//! - **Inline**: `[u8; 23]` data, then `u8` tag holding the inline length
7//!   (0..=22). The whole string lives in the value, no allocation.
8//! - **Heap (64-bit)**: `NonNull<u8>` ptr (8) + `usize` len (8) + `usize`
9//!   cap_and_tag (8). The high byte of `cap_and_tag` overlaps byte 23 of
10//!   the union and is fixed at `0xFF` (> 22) as the heap discriminator. The
11//!   low 56 bits hold the heap capacity (up to 72 PB).
12//! - **Heap (32-bit)**: `NonNull<u8>` ptr (4) + `u32` len (4) + `u32`
13//!   cap (4) + 11-byte pad, then `u8` tag fixed at `0xFF`. Same 24-byte
14//!   total, same discriminator byte at offset 23 — pointer / len fields
15//!   are 32-bit-native so a `wasm32-unknown-unknown` build picks up the
16//!   right size without shifting a `usize` past its bit width.
17//!
18//! The 64-bit layout is the one the kevy server runs on, and is locked
19//! against perf-affecting changes (cfg-gated 32-bit alternative lives
20//! alongside it without touching any 64-bit code path).
21//!
22//! This lets us store every byte string up to 22 bytes — covering the vast
23//! majority of Redis-style values — without any pointer-chase, while keeping
24//! `size_of::<SmallBytes>() == 24` (same as `Vec<u8>`). Used by `kevy-store`
25//! to make `Value::Str(SmallBytes)` fit alongside the boxed collection
26//! variants and keep `Entry` at 48 B.
27
28#![warn(missing_docs)]
29
30#[cfg(target_endian = "big")]
31compile_error!("kevy-bytes requires little-endian: heap-tag byte overlaps inline length byte");
32
33use std::alloc::{Layout, alloc, dealloc, handle_alloc_error};
34use std::cmp::Ordering;
35use std::fmt;
36use std::hash::{Hash, Hasher};
37use std::mem::{self, ManuallyDrop};
38use std::ptr::NonNull;
39use std::slice;
40
41const INLINE_CAP: usize = 23;
42const INLINE_LEN_MAX: u8 = (INLINE_CAP - 1) as u8;
43
44#[cfg(target_pointer_width = "64")]
45const TAG_HEAP_BIT: usize = 0xFFusize << 56;
46#[cfg(target_pointer_width = "64")]
47const CAP_MASK: usize = (1usize << 56) - 1;
48
49/// Heap-rep marker byte at offset 23. Used by the 32-bit `Heap::new` to
50/// set its dedicated `tag` field; the 64-bit path encodes the same byte
51/// implicitly via the high byte of `cap_and_tag`.
52#[cfg(target_pointer_width = "32")]
53const HEAP_TAG_BYTE: u8 = 0xFF;
54
55#[repr(C)]
56#[derive(Copy, Clone)]
57struct Inline {
58    data: [u8; INLINE_CAP],
59    /// 0..=22 = inline length. The heap rep sets this byte to 0xFF either via
60    /// the high byte of `Heap::cap_and_tag` (64-bit, little-endian overlap)
61    /// or as a dedicated `tag` field at offset 23 (32-bit).
62    tag: u8,
63}
64
65/// 64-bit Heap rep — `ptr|len|cap_and_tag` × usize. High byte of
66/// `cap_and_tag` shadows `Inline::tag` (LE) so the discriminator byte at
67/// offset 23 = `0xFF`. Locked layout: the kevy server runs here and the
68/// perf budget assumes this exact shape.
69#[cfg(target_pointer_width = "64")]
70#[repr(C)]
71#[derive(Copy, Clone)]
72struct Heap {
73    ptr: NonNull<u8>,
74    len: usize,
75    /// High byte = 0xFF (heap marker, shadows `Inline::tag`); low 56 bits =
76    /// capacity (from the source `Vec<u8>` or our own alloc; ≥ len).
77    cap_and_tag: usize,
78}
79
80/// 32-bit Heap rep — `ptr(4)|len(4)|cap(4)|pad(11)|tag(1)`. The dedicated
81/// `tag` byte at offset 23 (= `0xFF`) plays the role the 64-bit `cap_and_tag`
82/// high byte does, so the discriminator check at offset 23 stays identical
83/// across both layouts. Unlocks `wasm32-unknown-unknown` (Wave 3 #7) without
84/// touching the 64-bit hot path.
85#[cfg(target_pointer_width = "32")]
86#[repr(C)]
87#[derive(Copy, Clone)]
88struct Heap {
89    ptr: NonNull<u8>,
90    len: u32,
91    cap: u32,
92    _pad: [u8; 11],
93    tag: u8,
94}
95
96impl Heap {
97    /// Build a Heap rep tagging the discriminator byte to `0xFF`. cfg-gated
98    /// so each pointer-width hits its native fields without runtime cost.
99    #[cfg(target_pointer_width = "64")]
100    #[inline]
101    fn new(ptr: NonNull<u8>, len: usize, cap: usize) -> Self {
102        debug_assert!(cap <= CAP_MASK, "kevy-bytes: capacity exceeds 56-bit field");
103        Self {
104            ptr,
105            len,
106            cap_and_tag: TAG_HEAP_BIT | (cap & CAP_MASK),
107        }
108    }
109    #[cfg(target_pointer_width = "32")]
110    #[inline]
111    fn new(ptr: NonNull<u8>, len: usize, cap: usize) -> Self {
112        // On 32-bit, `Vec<u8>` is bounded by the 4 GiB address space, so
113        // any source `len`/`cap` already fits in `u32`. Debug-assert to
114        // catch unexpected callers.
115        debug_assert!(
116            len <= u32::MAX as usize && cap <= u32::MAX as usize,
117            "kevy-bytes: len/cap exceeds u32 on 32-bit platform"
118        );
119        Self {
120            ptr,
121            len: len as u32,
122            cap: cap as u32,
123            _pad: [0; 11],
124            tag: HEAP_TAG_BYTE,
125        }
126    }
127
128    /// Live capacity (always returned as `usize` regardless of underlying
129    /// field width).
130    #[cfg(target_pointer_width = "64")]
131    #[inline]
132    fn capacity(&self) -> usize {
133        self.cap_and_tag & CAP_MASK
134    }
135    #[cfg(target_pointer_width = "32")]
136    #[inline]
137    fn capacity(&self) -> usize {
138        self.cap as usize
139    }
140
141    /// Live length (always `usize`).
142    #[cfg(target_pointer_width = "64")]
143    #[inline]
144    fn length(&self) -> usize {
145        self.len
146    }
147    #[cfg(target_pointer_width = "32")]
148    #[inline]
149    fn length(&self) -> usize {
150        self.len as usize
151    }
152}
153
154/// A 24-byte owned byte string with inline small-string optimization.
155///
156/// Strings of up to 22 bytes live entirely inside the value (no allocation,
157/// no pointer chase); larger strings spill to a heap buffer. The
158/// discriminator is a single byte at offset 23 (the tag, which doubles as
159/// the inline length 0..=22 OR equals 0xFF when the heap variant is active).
160///
161/// See the crate root for layout details.
162#[repr(C)]
163pub union SmallBytes {
164    inline: Inline,
165    heap: Heap,
166}
167
168const _: () = {
169    assert!(mem::size_of::<SmallBytes>() == 24);
170    assert!(mem::align_of::<SmallBytes>() == mem::align_of::<usize>());
171};
172
173unsafe impl Send for SmallBytes {}
174unsafe impl Sync for SmallBytes {}
175
176impl SmallBytes {
177    /// Empty inline `SmallBytes` (zero allocation).
178    pub const fn new() -> Self {
179        Self {
180            inline: Inline {
181                data: [0; INLINE_CAP],
182                tag: 0,
183            },
184        }
185    }
186
187    /// Construct from a byte slice — inline if `bytes.len() <= 22`, else heap.
188    pub fn from_slice(bytes: &[u8]) -> Self {
189        if bytes.len() <= INLINE_LEN_MAX as usize {
190            let mut data = [0u8; INLINE_CAP];
191            // SAFETY: bytes.len() ≤ 22 ≤ data.len(); non-overlapping regions.
192            unsafe {
193                std::ptr::copy_nonoverlapping(bytes.as_ptr(), data.as_mut_ptr(), bytes.len());
194            }
195            Self {
196                inline: Inline {
197                    data,
198                    tag: bytes.len() as u8,
199                },
200            }
201        } else {
202            Self::alloc_heap(bytes)
203        }
204    }
205
206    /// Take ownership of a `Vec<u8>` — inline if `vec.len() <= 22`, else **reuse
207    /// the vec's allocation** (no copy on the heap path).
208    pub fn from_vec(vec: Vec<u8>) -> Self {
209        if vec.len() <= INLINE_LEN_MAX as usize {
210            Self::from_slice(&vec)
211        } else {
212            let mut v = ManuallyDrop::new(vec);
213            // SAFETY: len > 22 ⇒ cap > 0 ⇒ Vec has an allocation, so the pointer
214            // is non-null. Vec guarantees a non-null pointer for any allocated
215            // Vec (and a dangling-but-non-null for empty, which we don't hit here).
216            let ptr = unsafe { NonNull::new_unchecked(v.as_mut_ptr()) };
217            let len = v.len();
218            let cap = v.capacity();
219            Self {
220                heap: Heap::new(ptr, len, cap),
221            }
222        }
223    }
224
225    #[inline]
226    fn alloc_heap(bytes: &[u8]) -> Self {
227        let len = bytes.len();
228        // `len > 22` (caller has already taken the heap branch) and `len` is
229        // a slice length ⇒ ≤ `isize::MAX` ⇒ well below the `usize::MAX -
230        // (align - 1)` bound `from_size_align_unchecked` needs. u8's align is 1.
231        // SAFETY: see above.
232        let layout = unsafe { Layout::from_size_align_unchecked(len, 1) };
233        // SAFETY: layout.size() > 0 (caller's heap branch guarantees len > 22).
234        let raw = unsafe { alloc(layout) };
235        let ptr = match NonNull::new(raw) {
236            Some(p) => p,
237            None => handle_alloc_error(layout),
238        };
239        // SAFETY: alloc returned a writable region of `len` bytes; source is a
240        // disjoint slice.
241        unsafe {
242            std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.as_ptr(), len);
243        }
244        Self {
245            heap: Heap::new(ptr, len, len),
246        }
247    }
248
249    /// True when stored inline; the byte at index 23 is the deciding tag in
250    /// either rep, so the check is a single load + compare.
251    #[inline]
252    fn is_inline(&self) -> bool {
253        // SAFETY: byte 23 is always initialised — either as Inline::tag (0..=22)
254        // or as the high byte of Heap::cap_and_tag (= 0xFF). Reading it through
255        // the Inline view is valid in either case (the union is `repr(C)`).
256        unsafe { self.inline.tag <= INLINE_LEN_MAX }
257    }
258
259    /// Number of bytes stored.
260    #[inline]
261    pub fn len(&self) -> usize {
262        if self.is_inline() {
263            // SAFETY: just verified `inline.tag` ≤ 22.
264            unsafe { self.inline.tag as usize }
265        } else {
266            // SAFETY: tag > 22 ⇒ heap variant is active.
267            unsafe { self.heap.length() }
268        }
269    }
270
271    /// Whether `len() == 0`.
272    #[inline]
273    pub fn is_empty(&self) -> bool {
274        self.len() == 0
275    }
276
277    /// Bytes this value holds on the heap (0 when inline). Lets memory-accounting
278    /// callers (e.g. `maxmemory` enforcement) charge only the off-stack footprint
279    /// without re-deriving the inline-length threshold.
280    #[inline]
281    pub fn heap_bytes(&self) -> usize {
282        if self.is_inline() { 0 } else { self.len() }
283    }
284
285    /// Borrow the bytes (no allocation; same for inline and heap variants).
286    #[inline]
287    pub fn as_slice(&self) -> &[u8] {
288        if self.is_inline() {
289            // SAFETY: first `tag` bytes of `data` are valid (zero-init at construction).
290            unsafe {
291                slice::from_raw_parts(self.inline.data.as_ptr(), self.inline.tag as usize)
292            }
293        } else {
294            // SAFETY: heap variant active; ptr/len originate from a Vec or our own alloc.
295            unsafe { slice::from_raw_parts(self.heap.ptr.as_ptr(), self.heap.length()) }
296        }
297    }
298
299    /// Copy into a fresh `Vec<u8>` (clone semantics).
300    pub fn to_vec(&self) -> Vec<u8> {
301        self.as_slice().to_vec()
302    }
303
304    /// Consume self and return an owned `Vec<u8>`. The heap path reuses the
305    /// existing allocation; the inline path copies into a new vec.
306    pub fn into_vec(self) -> Vec<u8> {
307        if self.is_inline() {
308            self.as_slice().to_vec()
309            // self drops as inline — nothing to free.
310        } else {
311            // SAFETY: heap variant active.
312            let (ptr, len, cap) = unsafe {
313                (
314                    self.heap.ptr.as_ptr(),
315                    self.heap.length(),
316                    self.heap.capacity(),
317                )
318            };
319            // Skip our Drop to avoid double-free; Vec::from_raw_parts now owns it.
320            let _do_not_drop = ManuallyDrop::new(self);
321            // SAFETY: ptr/len/cap originated from either a Vec<u8> (from_vec)
322            // or our own `alloc(Layout::array::<u8>(cap))` (alloc_heap, where
323            // cap == len) — both meet Vec::from_raw_parts' requirements.
324            unsafe { Vec::from_raw_parts(ptr, len, cap) }
325        }
326    }
327}
328
329impl Default for SmallBytes {
330    fn default() -> Self {
331        Self::new()
332    }
333}
334
335impl Drop for SmallBytes {
336    fn drop(&mut self) {
337        if self.is_inline() {
338            return;
339        }
340        // SAFETY: heap variant active; layout matches the one used at alloc
341        // time (either from Vec — Vec uses `Layout::array::<u8>(cap)` — or our
342        // own alloc_heap which used the same layout).
343        unsafe {
344            let cap = self.heap.capacity();
345            let layout = Layout::array::<u8>(cap).expect("kevy-bytes: drop layout");
346            dealloc(self.heap.ptr.as_ptr(), layout);
347        }
348    }
349}
350
351impl Clone for SmallBytes {
352    /// Specialised clone that bypasses `as_slice → from_slice → alloc_heap`'s
353    /// two layered length checks. Inline variant is a bitwise union copy (no
354    /// branch through the slice path); heap variant goes straight to a single
355    /// `alloc + memcpy` keyed on the already-known heap length.
356    #[inline]
357    fn clone(&self) -> Self {
358        if self.is_inline() {
359            // SAFETY: `Inline` is `repr(C)` + `Copy`; bitwise copy is sound
360            // when the source is currently in the inline variant (the tag
361            // byte ≤ 22 is part of the bit pattern we're copying, so the
362            // discriminator stays correct).
363            unsafe { Self { inline: self.inline } }
364        } else {
365            // SAFETY: tag > 22 ⇒ heap variant is active.
366            unsafe { self.clone_heap() }
367        }
368    }
369}
370
371impl SmallBytes {
372    /// Heap-fast-path clone. Caller must have established that `self` is in
373    /// the heap variant.
374    ///
375    /// # Safety
376    /// `self.heap` must be the active union variant (i.e. `is_inline()` is
377    /// false). `self.heap.ptr` must point to `self.heap.len` valid bytes.
378    #[inline]
379    unsafe fn clone_heap(&self) -> Self {
380        // SAFETY (covers the three `self.heap.*` reads): caller asserts the
381        // heap variant is active.
382        let (src_ptr, len) = unsafe { (self.heap.ptr.as_ptr(), self.heap.length()) };
383        // `len > 22 ⇒ len > 0`, and the high bits are guarded by `CAP_MASK`
384        // never letting cap exceed 2^56, well below `isize::MAX`, so the
385        // unchecked layout is sound. Allocator alignment for `u8` is 1.
386        let layout = unsafe { Layout::from_size_align_unchecked(len, 1) };
387        // SAFETY: layout.size() > 0.
388        let raw = unsafe { alloc(layout) };
389        let ptr = match NonNull::new(raw) {
390            Some(p) => p,
391            None => handle_alloc_error(layout),
392        };
393        // SAFETY: src has `len` valid bytes; dst is freshly-allocated for `len`
394        // bytes; regions are disjoint.
395        unsafe { std::ptr::copy_nonoverlapping(src_ptr, ptr.as_ptr(), len) };
396        Self {
397            heap: Heap::new(ptr, len, len),
398        }
399    }
400}
401
402impl fmt::Debug for SmallBytes {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        // Match Vec<u8>'s Debug ("[1, 2, 3]" form).
405        f.debug_list().entries(self.as_slice().iter()).finish()
406    }
407}
408
409impl PartialEq for SmallBytes {
410    /// Specialised over the slice form (`as_slice == as_slice`) by branching
411    /// on variant **once** and reading the relevant length / pointer pair
412    /// directly. Same-variant cases (inline/inline + heap/heap, which are the
413    /// only ones produced by a single allocator) skip a redundant `as_slice`
414    /// dispatch on each side; the mixed case falls back to the slice form.
415    #[inline]
416    fn eq(&self, other: &Self) -> bool {
417        // SAFETY: byte 23 (`inline.tag`) is always a valid load in either
418        // variant — it's either the inline-length 0..=22 or 0xFF as the
419        // heap-discriminator overlap (see crate doc).
420        let self_tag = unsafe { self.inline.tag };
421        let other_tag = unsafe { other.inline.tag };
422        let self_inline = self_tag <= INLINE_LEN_MAX;
423        let other_inline = other_tag <= INLINE_LEN_MAX;
424        match (self_inline, other_inline) {
425            (true, true) => {
426                let len = self_tag as usize;
427                if len != other_tag as usize {
428                    return false;
429                }
430                // SAFETY: both in inline variant; first `len` bytes valid.
431                let a = unsafe {
432                    slice::from_raw_parts(self.inline.data.as_ptr(), len)
433                };
434                let b = unsafe {
435                    slice::from_raw_parts(other.inline.data.as_ptr(), len)
436                };
437                a == b
438            }
439            (false, false) => {
440                // SAFETY: both in heap variant.
441                let (a_len, b_len) =
442                    unsafe { (self.heap.length(), other.heap.length()) };
443                if a_len != b_len {
444                    return false;
445                }
446                // SAFETY: heap pointers + len are valid.
447                let a = unsafe {
448                    slice::from_raw_parts(self.heap.ptr.as_ptr(), a_len)
449                };
450                let b = unsafe {
451                    slice::from_raw_parts(other.heap.ptr.as_ptr(), b_len)
452                };
453                a == b
454            }
455            // Mixed inline/heap is unreachable from any safe constructor —
456            // a heap variant always carries len > 22, an inline always
457            // len ≤ 22, so two equal-length values land in the same arm
458            // (and non-equal-length comparisons short-circuit on
459            // `len != other_len` inside each arm).
460            _ => unreachable!(
461                "kevy-bytes invariant: a heap variant never carries len ≤ 22"
462            ),
463        }
464    }
465}
466impl Eq for SmallBytes {}
467
468impl PartialOrd for SmallBytes {
469    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
470        Some(self.cmp(other))
471    }
472}
473impl Ord for SmallBytes {
474    fn cmp(&self, other: &Self) -> Ordering {
475        self.as_slice().cmp(other.as_slice())
476    }
477}
478
479impl Hash for SmallBytes {
480    fn hash<H: Hasher>(&self, state: &mut H) {
481        self.as_slice().hash(state);
482    }
483}
484
485impl AsRef<[u8]> for SmallBytes {
486    fn as_ref(&self) -> &[u8] {
487        self.as_slice()
488    }
489}
490
491impl std::borrow::Borrow<[u8]> for SmallBytes {
492    fn borrow(&self) -> &[u8] {
493        self.as_slice()
494    }
495}
496
497/// `KevyHash` agrees with the byte-slice impl, so a `KevyMap<SmallBytes, V>`
498/// can be queried with `&[u8]` (via `Borrow<[u8]>`) and the hash matches.
499impl kevy_hash::KevyHash for SmallBytes {
500    #[inline]
501    fn kevy_hash(&self) -> u64 {
502        self.as_slice().kevy_hash()
503    }
504}
505
506impl From<&[u8]> for SmallBytes {
507    fn from(bytes: &[u8]) -> Self {
508        Self::from_slice(bytes)
509    }
510}
511
512impl From<Vec<u8>> for SmallBytes {
513    fn from(vec: Vec<u8>) -> Self {
514        Self::from_vec(vec)
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use kevy_hash::KevyHash as _;
522
523    #[test]
524    fn size_and_align() {
525        assert_eq!(mem::size_of::<SmallBytes>(), 24);
526        assert_eq!(mem::align_of::<SmallBytes>(), mem::align_of::<usize>());
527    }
528
529    #[test]
530    fn empty_is_inline() {
531        let s = SmallBytes::new();
532        assert!(s.is_inline());
533        assert_eq!(s.len(), 0);
534        assert!(s.is_empty());
535        assert_eq!(s.as_slice(), b"");
536    }
537
538    #[test]
539    fn inline_one_byte() {
540        let s = SmallBytes::from_slice(b"x");
541        assert!(s.is_inline());
542        assert_eq!(s.len(), 1);
543        assert_eq!(s.as_slice(), b"x");
544    }
545
546    #[test]
547    fn inline_at_boundary_22() {
548        let v: Vec<u8> = (0u8..22).collect();
549        let s = SmallBytes::from_slice(&v);
550        assert!(s.is_inline());
551        assert_eq!(s.len(), 22);
552        assert_eq!(s.as_slice(), v);
553    }
554
555    #[test]
556    fn heap_at_boundary_23() {
557        let v: Vec<u8> = (0u8..23).collect();
558        let s = SmallBytes::from_slice(&v);
559        assert!(!s.is_inline());
560        assert_eq!(s.len(), 23);
561        assert_eq!(s.as_slice(), v);
562    }
563
564    #[test]
565    fn heap_large() {
566        let v: Vec<u8> = (0..4096).map(|i| (i & 0xFF) as u8).collect();
567        let s = SmallBytes::from_slice(&v);
568        assert!(!s.is_inline());
569        assert_eq!(s.len(), 4096);
570        assert_eq!(s.as_slice(), v.as_slice());
571    }
572
573    #[test]
574    fn from_vec_inline() {
575        let s = SmallBytes::from_vec(vec![1u8, 2, 3]);
576        assert!(s.is_inline());
577        assert_eq!(s.as_slice(), &[1, 2, 3]);
578    }
579
580    #[test]
581    fn from_vec_heap_reuses_alloc() {
582        let mut v: Vec<u8> = (0u8..100).collect();
583        v.reserve(200);
584        let ptr_before = v.as_ptr();
585        let cap_before = v.capacity();
586        let s = SmallBytes::from_vec(v);
587        assert!(!s.is_inline());
588        // SAFETY: we know it's heap; peek to verify pointer reuse.
589        unsafe {
590            assert_eq!(s.heap.ptr.as_ptr() as *const u8, ptr_before);
591            assert_eq!(s.heap.capacity(), cap_before);
592        }
593    }
594
595    #[test]
596    fn into_vec_inline_copies() {
597        let s = SmallBytes::from_slice(b"hello");
598        let v = s.into_vec();
599        assert_eq!(v, b"hello");
600    }
601
602    #[test]
603    fn into_vec_heap_reuses_alloc() {
604        let original: Vec<u8> = (0u8..200).collect();
605        let ptr = original.as_ptr();
606        let cap = original.capacity();
607        let s = SmallBytes::from_vec(original);
608        let v = s.into_vec();
609        assert_eq!(v.as_ptr(), ptr);
610        assert_eq!(v.capacity(), cap);
611        assert_eq!(v.len(), 200);
612    }
613
614    #[test]
615    fn clone_inline() {
616        let s = SmallBytes::from_slice(b"abc");
617        let c = s.clone();
618        assert_eq!(s, c);
619        assert!(c.is_inline());
620    }
621
622    #[test]
623    fn clone_heap() {
624        let v: Vec<u8> = (0u8..50).collect();
625        let s = SmallBytes::from_slice(&v);
626        let c = s.clone();
627        assert_eq!(s, c);
628        assert!(!c.is_inline());
629    }
630
631    #[test]
632    fn eq_by_content() {
633        let a = SmallBytes::from_slice(b"short");
634        let b = SmallBytes::from_slice(b"short");
635        assert_eq!(a, b);
636        let c: Vec<u8> = (0u8..30).collect();
637        let d: Vec<u8> = (0u8..30).collect();
638        assert_eq!(SmallBytes::from_slice(&c), SmallBytes::from_slice(&d));
639    }
640
641    #[test]
642    fn ord_lex() {
643        let a = SmallBytes::from_slice(b"abc");
644        let b = SmallBytes::from_slice(b"abd");
645        assert!(a < b);
646    }
647
648    #[test]
649    fn debug_format_matches_slice() {
650        let s = SmallBytes::from_slice(&[1u8, 2, 3]);
651        let dbg = format!("{s:?}");
652        let exp = format!("{:?}", &[1u8, 2, 3][..]);
653        assert_eq!(dbg, exp);
654    }
655
656    #[test]
657    fn default_is_empty_inline() {
658        let s = SmallBytes::default();
659        assert!(s.is_inline());
660        assert_eq!(s.len(), 0);
661    }
662
663    #[test]
664    fn drop_heap_does_not_leak_or_double_free() {
665        // Loop a bunch to give miri/asan something to catch.
666        for n in [23usize, 64, 1024, 65536] {
667            let v: Vec<u8> = (0..n).map(|i| (i & 0xFF) as u8).collect();
668            let s = SmallBytes::from_slice(&v);
669            drop(s);
670        }
671    }
672
673    // ---- Effective coverage: trait impls + branch paths ---------------------
674
675    #[test]
676    fn eq_is_reflexive_and_symmetric_inline() {
677        let a = SmallBytes::from_slice(b"hi");
678        let b = SmallBytes::from_slice(b"hi");
679        let c = SmallBytes::from_slice(b"no");
680        assert_eq!(a, a);
681        assert_eq!(a, b);
682        assert_eq!(b, a);
683        assert_ne!(a, c);
684    }
685
686    #[test]
687    fn eq_is_reflexive_and_symmetric_heap() {
688        let v: Vec<u8> = (0u8..40).collect();
689        let a = SmallBytes::from_slice(&v);
690        let b = SmallBytes::from_slice(&v);
691        let mut w = v.clone();
692        w[0] = w[0].wrapping_add(1);
693        let c = SmallBytes::from_slice(&w);
694        assert_eq!(a, a);
695        assert_eq!(a, b);
696        assert_eq!(b, a);
697        assert_ne!(a, c);
698    }
699
700    #[test]
701    fn partial_cmp_matches_cmp_inline() {
702        let a = SmallBytes::from_slice(b"abc");
703        let b = SmallBytes::from_slice(b"abd");
704        assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Less));
705        assert_eq!(b.partial_cmp(&a), Some(std::cmp::Ordering::Greater));
706        assert_eq!(a.partial_cmp(&a), Some(std::cmp::Ordering::Equal));
707        // Same chain via the Ord impl directly.
708        assert_eq!(a.cmp(&b), std::cmp::Ordering::Less);
709        assert_eq!(a.cmp(&a), std::cmp::Ordering::Equal);
710    }
711
712    #[test]
713    fn hash_agrees_with_byte_slice() {
714        use std::collections::hash_map::DefaultHasher;
715        let v: Vec<u8> = (0u8..40).collect();
716        let s = SmallBytes::from_slice(&v);
717        let mut h_slice = DefaultHasher::new();
718        v.as_slice().hash(&mut h_slice);
719        let mut h_sb = DefaultHasher::new();
720        s.hash(&mut h_sb);
721        // Same byte stream into the Hasher (Hash for [u8] writes len + bytes;
722        // ours delegates to as_slice so it matches).
723        assert_eq!(h_slice.finish(), h_sb.finish());
724    }
725
726    #[test]
727    fn kevy_hash_agrees_with_byte_slice() {
728        let v: Vec<u8> = (0u8..40).collect();
729        let s = SmallBytes::from_slice(&v);
730        assert_eq!(
731            s.kevy_hash(),
732            v.as_slice().kevy_hash(),
733            "KevyHash impl must agree with &[u8] so a KevyMap<SmallBytes, V> can be queried by Borrow<[u8]>"
734        );
735        let small = SmallBytes::from_slice(b"foo");
736        assert_eq!(small.kevy_hash(), (b"foo" as &[u8]).kevy_hash());
737    }
738
739    #[test]
740    fn as_ref_is_zero_copy_view() {
741        let s = SmallBytes::from_slice(b"abcdef");
742        let r: &[u8] = s.as_ref();
743        assert_eq!(r, b"abcdef");
744        // Same slice address as as_slice (the impl delegates to as_slice).
745        assert!(std::ptr::eq(r.as_ptr(), s.as_slice().as_ptr()));
746    }
747
748    #[test]
749    fn borrow_lookup_works_in_collection() {
750        use std::collections::HashMap;
751        let mut m: HashMap<SmallBytes, i32> = HashMap::new();
752        m.insert(SmallBytes::from_slice(b"key1"), 1);
753        m.insert(SmallBytes::from_slice(b"key2"), 2);
754        // Look up by &[u8] thanks to Borrow<[u8]>.
755        assert_eq!(m.get(b"key1".as_slice()), Some(&1));
756        assert_eq!(m.get(b"key2".as_slice()), Some(&2));
757        assert_eq!(m.get(b"none".as_slice()), None);
758    }
759
760    #[test]
761    fn from_byte_slice_round_trip() {
762        let a: SmallBytes = (&b"short"[..]).into();
763        assert_eq!(a.as_slice(), b"short");
764        let v: Vec<u8> = (0u8..40).collect();
765        let b: SmallBytes = v.as_slice().into();
766        assert_eq!(b.as_slice(), v.as_slice());
767        assert!(!b.is_inline());
768    }
769
770    #[test]
771    fn from_vec_dispatches_inline_or_heap() {
772        // ≤ 22 → inline (copies)
773        let inline_src: SmallBytes = vec![1u8, 2, 3].into();
774        assert!(inline_src.is_inline());
775        assert_eq!(inline_src.as_slice(), &[1, 2, 3]);
776        // > 22 → heap (reuses alloc; verified by from_vec_heap_reuses_alloc)
777        let v: Vec<u8> = (0u8..30).collect();
778        let heap_src: SmallBytes = v.clone().into();
779        assert!(!heap_src.is_inline());
780        assert_eq!(heap_src.as_slice(), v.as_slice());
781    }
782
783    #[test]
784    fn clone_heap_keeps_data_and_is_independent() {
785        // Cloned heap value must allocate a separate buffer (no shared
786        // pointer), so dropping the source doesn't invalidate the clone.
787        let v: Vec<u8> = (0u8..50).collect();
788        let src = SmallBytes::from_slice(&v);
789        let dup = src.clone();
790        // SAFETY: both in heap variant by len > 22.
791        unsafe {
792            assert_ne!(
793                src.heap.ptr.as_ptr(),
794                dup.heap.ptr.as_ptr(),
795                "clone must allocate a fresh buffer"
796            );
797        }
798        drop(src);
799        // dup remains valid.
800        assert_eq!(dup.as_slice(), v.as_slice());
801    }
802
803    #[test]
804    fn drop_inline_is_noop() {
805        // Just exercise the inline path of Drop (the `if self.is_inline()
806        // { return }` early-return); miri checks no UB.
807        for &n in &[0usize, 1, 5, 22] {
808            let s = SmallBytes::from_slice(&vec![b'x'; n]);
809            assert!(s.is_inline());
810            drop(s);
811        }
812    }
813
814    #[test]
815    fn into_vec_zero_size_path() {
816        // Empty (inline) → into_vec returns empty Vec without panic.
817        let s = SmallBytes::new();
818        let v = s.into_vec();
819        assert!(v.is_empty());
820    }
821
822    #[test]
823    fn to_vec_copies_inline_and_heap() {
824        let inline = SmallBytes::from_slice(b"hi");
825        assert_eq!(inline.to_vec(), b"hi");
826        let v: Vec<u8> = (0u8..30).collect();
827        let heap = SmallBytes::from_slice(&v);
828        let copy = heap.to_vec();
829        assert_eq!(copy, v);
830        // to_vec returns an owned independent Vec; heap can be modified
831        // via subsequent operations without affecting the returned Vec.
832        // (Just verify equality after going through .to_vec.)
833        assert_eq!(heap.as_slice(), v.as_slice());
834    }
835
836    // ===== alloc-count test =====
837    //
838    // The whole point of SmallBytes' SSO is "no heap alloc when payload ≤ 22
839    // bytes". We can prove it by swapping in a counting allocator and asserting
840    // the inline path produces ZERO Allocator::alloc calls. A heap-bound payload
841    // produces at least one. Wrapping the system allocator (not replacing it
842    // wholesale with a fake) keeps the test compatible with Rust's std types
843    // that the tests themselves use.
844    //
845    // Concurrency: the global allocator is shared by EVERY thread in the test
846    // process, and `cargo test` runs ~30 unrelated tests in this crate in
847    // parallel. A simple global flag would attribute their allocs to our
848    // measurement window. We instead key the recording on a thread-local so
849    // only the test thread *currently inside* `measure_allocs` counts.
850
851    use std::alloc::{GlobalAlloc, Layout, System};
852    use std::cell::Cell;
853
854    struct CountingAlloc {
855        inner: System,
856    }
857
858    thread_local! {
859        // `const { Cell::new(...) }` is lazily-zero-init at thread spawn — no
860        // heap alloc — so the allocator itself can safely consult them.
861        static THREAD_RECORDING: Cell<bool> = const { Cell::new(false) };
862        static THREAD_ALLOC_CALLS: Cell<usize> = const { Cell::new(0) };
863    }
864
865    unsafe impl GlobalAlloc for CountingAlloc {
866        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
867            // `try_with` so if the TLS is being destroyed (process teardown)
868            // we still serve the alloc instead of panicking.
869            let _ = THREAD_RECORDING.try_with(|r| {
870                if r.get() {
871                    let _ = THREAD_ALLOC_CALLS.try_with(|c| c.set(c.get() + 1));
872                }
873            });
874            // SAFETY: forwarding to the system allocator with the same layout.
875            unsafe { self.inner.alloc(layout) }
876        }
877        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
878            // SAFETY: forwarding to the system allocator with the same layout.
879            unsafe { self.inner.dealloc(ptr, layout) }
880        }
881    }
882
883    #[global_allocator]
884    static COUNTING: CountingAlloc = CountingAlloc { inner: System };
885
886    fn measure_allocs<F: FnOnce()>(f: F) -> usize {
887        THREAD_ALLOC_CALLS.with(|c| c.set(0));
888        THREAD_RECORDING.with(|r| r.set(true));
889        f();
890        THREAD_RECORDING.with(|r| r.set(false));
891        THREAD_ALLOC_CALLS.with(|c| c.get())
892    }
893
894    #[test]
895    fn inline_payload_does_not_allocate() {
896        // Warm + capture: every inline-sized SmallBytes constructor + access
897        // must produce zero heap allocations. `INLINE_LEN_MAX` is the max
898        // payload length the inline variant can hold (one byte of the
899        // INLINE_CAP-byte buffer is the length+discriminant tag).
900        let max_inline = INLINE_LEN_MAX as usize;
901        let allocs = measure_allocs(|| {
902            for n in 0..=max_inline {
903                let s = SmallBytes::from_slice(&[0u8; INLINE_CAP][..n]);
904                std::hint::black_box(&s);
905                std::hint::black_box(s.as_slice());
906                std::hint::black_box(s.len());
907                let c = s.clone(); // Clone of an inline value is also alloc-free.
908                std::hint::black_box(&c);
909                drop(c);
910                drop(s);
911            }
912        });
913        assert_eq!(
914            allocs, 0,
915            "expected SSO inline path to be alloc-free, got {allocs} allocs"
916        );
917    }
918
919    #[test]
920    fn heap_payload_does_allocate() {
921        // Control: payload just over the inline cap MUST allocate. If this
922        // is 0 either SSO bumped its cap silently or the counter is broken —
923        // either way the inline-zero assertion above is meaningless.
924        let max_inline = INLINE_LEN_MAX as usize;
925        let allocs = measure_allocs(|| {
926            let s = SmallBytes::from_slice(&[7u8; INLINE_CAP + 8][..max_inline + 1]);
927            std::hint::black_box(&s);
928            drop(s);
929        });
930        assert!(
931            allocs >= 1,
932            "expected the heap path to allocate at least once, got {allocs}"
933        );
934    }
935}