1#![warn(missing_docs)]
29
30#[cfg(target_endian = "big")]
31compile_error!("kevy-bytes requires little-endian: heap-tag byte overlaps inline length byte");
32
33mod traits;
34
35use std::alloc::{Layout, alloc, dealloc, handle_alloc_error};
36use std::mem::{self, ManuallyDrop};
37use std::ptr::NonNull;
38use std::slice;
39
40const INLINE_CAP: usize = 23;
41const INLINE_LEN_MAX: u8 = (INLINE_CAP - 1) as u8;
42
43#[cfg(target_pointer_width = "64")]
44const TAG_HEAP_BIT: usize = 0xFFusize << 56;
45#[cfg(target_pointer_width = "64")]
46const CAP_MASK: usize = (1usize << 56) - 1;
47
48#[cfg(target_pointer_width = "32")]
52const HEAP_TAG_BYTE: u8 = 0xFF;
53
54#[repr(C)]
55#[derive(Copy, Clone)]
56struct Inline {
57 data: [u8; INLINE_CAP],
58 tag: u8,
62}
63
64#[cfg(target_pointer_width = "64")]
69#[repr(C)]
70#[derive(Copy, Clone)]
71struct Heap {
72 ptr: NonNull<u8>,
73 len: usize,
74 cap_and_tag: usize,
77}
78
79#[cfg(target_pointer_width = "32")]
85#[repr(C)]
86#[derive(Copy, Clone)]
87struct Heap {
88 ptr: NonNull<u8>,
89 len: u32,
90 cap: u32,
91 _pad: [u8; 11],
92 tag: u8,
93}
94
95impl Heap {
96 #[cfg(target_pointer_width = "64")]
99 #[inline]
100 fn new(ptr: NonNull<u8>, len: usize, cap: usize) -> Self {
101 debug_assert!(cap <= CAP_MASK, "kevy-bytes: capacity exceeds 56-bit field");
102 Self {
103 ptr,
104 len,
105 cap_and_tag: TAG_HEAP_BIT | (cap & CAP_MASK),
106 }
107 }
108 #[cfg(target_pointer_width = "32")]
109 #[inline]
110 fn new(ptr: NonNull<u8>, len: usize, cap: usize) -> Self {
111 debug_assert!(
115 len <= u32::MAX as usize && cap <= u32::MAX as usize,
116 "kevy-bytes: len/cap exceeds u32 on 32-bit platform"
117 );
118 Self {
119 ptr,
120 len: len as u32,
121 cap: cap as u32,
122 _pad: [0; 11],
123 tag: HEAP_TAG_BYTE,
124 }
125 }
126
127 #[cfg(target_pointer_width = "64")]
130 #[inline]
131 fn capacity(&self) -> usize {
132 self.cap_and_tag & CAP_MASK
133 }
134 #[cfg(target_pointer_width = "32")]
135 #[inline]
136 fn capacity(&self) -> usize {
137 self.cap as usize
138 }
139
140 #[cfg(target_pointer_width = "64")]
142 #[inline]
143 fn length(&self) -> usize {
144 self.len
145 }
146 #[cfg(target_pointer_width = "32")]
147 #[inline]
148 fn length(&self) -> usize {
149 self.len as usize
150 }
151}
152
153#[repr(C)]
162pub union SmallBytes {
163 inline: Inline,
164 heap: Heap,
165}
166
167const _: () = {
168 assert!(mem::size_of::<SmallBytes>() == 24);
169 assert!(mem::align_of::<SmallBytes>() == mem::align_of::<usize>());
170};
171
172unsafe impl Send for SmallBytes {}
173unsafe impl Sync for SmallBytes {}
174
175impl SmallBytes {
176 pub const fn new() -> Self {
178 Self {
179 inline: Inline {
180 data: [0; INLINE_CAP],
181 tag: 0,
182 },
183 }
184 }
185
186 pub fn from_slice(bytes: &[u8]) -> Self {
188 if bytes.len() <= INLINE_LEN_MAX as usize {
189 let mut data = [0u8; INLINE_CAP];
190 unsafe {
192 std::ptr::copy_nonoverlapping(bytes.as_ptr(), data.as_mut_ptr(), bytes.len());
193 }
194 Self {
195 inline: Inline {
196 data,
197 tag: bytes.len() as u8,
198 },
199 }
200 } else {
201 Self::alloc_heap(bytes)
202 }
203 }
204
205 pub fn from_vec(vec: Vec<u8>) -> Self {
208 if vec.len() <= INLINE_LEN_MAX as usize {
209 Self::from_slice(&vec)
210 } else {
211 let mut v = ManuallyDrop::new(vec);
212 let ptr = unsafe { NonNull::new_unchecked(v.as_mut_ptr()) };
216 let len = v.len();
217 let cap = v.capacity();
218 Self {
219 heap: Heap::new(ptr, len, cap),
220 }
221 }
222 }
223
224 #[inline]
225 fn alloc_heap(bytes: &[u8]) -> Self {
226 let len = bytes.len();
227 let layout = unsafe { Layout::from_size_align_unchecked(len, 1) };
232 let raw = unsafe { alloc(layout) };
234 let ptr = match NonNull::new(raw) {
235 Some(p) => p,
236 None => handle_alloc_error(layout),
237 };
238 unsafe {
241 std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.as_ptr(), len);
242 }
243 Self {
244 heap: Heap::new(ptr, len, len),
245 }
246 }
247
248 #[inline]
251 fn is_inline(&self) -> bool {
252 unsafe { self.inline.tag <= INLINE_LEN_MAX }
256 }
257
258 #[inline]
260 pub fn len(&self) -> usize {
261 if self.is_inline() {
262 unsafe { self.inline.tag as usize }
264 } else {
265 unsafe { self.heap.length() }
267 }
268 }
269
270 #[inline]
272 pub fn is_empty(&self) -> bool {
273 self.len() == 0
274 }
275
276 #[inline]
280 pub fn heap_bytes(&self) -> usize {
281 if self.is_inline() { 0 } else { self.len() }
282 }
283
284 #[inline]
286 pub fn as_slice(&self) -> &[u8] {
287 if self.is_inline() {
288 unsafe {
290 slice::from_raw_parts(self.inline.data.as_ptr(), self.inline.tag as usize)
291 }
292 } else {
293 unsafe { slice::from_raw_parts(self.heap.ptr.as_ptr(), self.heap.length()) }
295 }
296 }
297
298 pub fn to_vec(&self) -> Vec<u8> {
300 self.as_slice().to_vec()
301 }
302
303 pub fn into_vec(self) -> Vec<u8> {
306 if self.is_inline() {
307 self.as_slice().to_vec()
308 } else {
310 let (ptr, len, cap) = unsafe {
312 (
313 self.heap.ptr.as_ptr(),
314 self.heap.length(),
315 self.heap.capacity(),
316 )
317 };
318 let _do_not_drop = ManuallyDrop::new(self);
320 unsafe { Vec::from_raw_parts(ptr, len, cap) }
324 }
325 }
326}
327
328impl Default for SmallBytes {
329 fn default() -> Self {
330 Self::new()
331 }
332}
333
334impl Drop for SmallBytes {
335 fn drop(&mut self) {
336 if self.is_inline() {
337 return;
338 }
339 unsafe {
343 let cap = self.heap.capacity();
344 let layout = Layout::array::<u8>(cap).expect("kevy-bytes: drop layout");
345 dealloc(self.heap.ptr.as_ptr(), layout);
346 }
347 }
348}
349
350impl Clone for SmallBytes {
351 #[inline]
356 fn clone(&self) -> Self {
357 if self.is_inline() {
358 unsafe { Self { inline: self.inline } }
363 } else {
364 unsafe { self.clone_heap() }
366 }
367 }
368}
369
370impl SmallBytes {
371 #[inline]
378 unsafe fn clone_heap(&self) -> Self {
379 let (src_ptr, len) = unsafe { (self.heap.ptr.as_ptr(), self.heap.length()) };
382 let layout = unsafe { Layout::from_size_align_unchecked(len, 1) };
386 let raw = unsafe { alloc(layout) };
388 let ptr = match NonNull::new(raw) {
389 Some(p) => p,
390 None => handle_alloc_error(layout),
391 };
392 unsafe { std::ptr::copy_nonoverlapping(src_ptr, ptr.as_ptr(), len) };
395 Self {
396 heap: Heap::new(ptr, len, len),
397 }
398 }
399}
400
401impl PartialEq for SmallBytes {
408 #[inline]
414 fn eq(&self, other: &Self) -> bool {
415 let self_tag = unsafe { self.inline.tag };
419 let other_tag = unsafe { other.inline.tag };
420 let self_inline = self_tag <= INLINE_LEN_MAX;
421 let other_inline = other_tag <= INLINE_LEN_MAX;
422 match (self_inline, other_inline) {
423 (true, true) => {
424 let len = self_tag as usize;
425 if len != other_tag as usize {
426 return false;
427 }
428 let a = unsafe {
430 slice::from_raw_parts(self.inline.data.as_ptr(), len)
431 };
432 let b = unsafe {
433 slice::from_raw_parts(other.inline.data.as_ptr(), len)
434 };
435 a == b
436 }
437 (false, false) => {
438 let (a_len, b_len) =
440 unsafe { (self.heap.length(), other.heap.length()) };
441 if a_len != b_len {
442 return false;
443 }
444 let a = unsafe {
446 slice::from_raw_parts(self.heap.ptr.as_ptr(), a_len)
447 };
448 let b = unsafe {
449 slice::from_raw_parts(other.heap.ptr.as_ptr(), b_len)
450 };
451 a == b
452 }
453 _ => unreachable!(
459 "kevy-bytes invariant: a heap variant never carries len ≤ 22"
460 ),
461 }
462 }
463}
464impl Eq for SmallBytes {}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use kevy_hash::KevyHash as _;
470 use std::hash::{Hash, Hasher};
471
472 #[test]
473 fn size_and_align() {
474 assert_eq!(mem::size_of::<SmallBytes>(), 24);
475 assert_eq!(mem::align_of::<SmallBytes>(), mem::align_of::<usize>());
476 }
477
478 #[test]
479 fn empty_is_inline() {
480 let s = SmallBytes::new();
481 assert!(s.is_inline());
482 assert_eq!(s.len(), 0);
483 assert!(s.is_empty());
484 assert_eq!(s.as_slice(), b"");
485 }
486
487 #[test]
488 fn inline_one_byte() {
489 let s = SmallBytes::from_slice(b"x");
490 assert!(s.is_inline());
491 assert_eq!(s.len(), 1);
492 assert_eq!(s.as_slice(), b"x");
493 }
494
495 #[test]
496 fn inline_at_boundary_22() {
497 let v: Vec<u8> = (0u8..22).collect();
498 let s = SmallBytes::from_slice(&v);
499 assert!(s.is_inline());
500 assert_eq!(s.len(), 22);
501 assert_eq!(s.as_slice(), v);
502 }
503
504 #[test]
505 fn heap_at_boundary_23() {
506 let v: Vec<u8> = (0u8..23).collect();
507 let s = SmallBytes::from_slice(&v);
508 assert!(!s.is_inline());
509 assert_eq!(s.len(), 23);
510 assert_eq!(s.as_slice(), v);
511 }
512
513 #[test]
514 fn heap_large() {
515 let v: Vec<u8> = (0..4096).map(|i| (i & 0xFF) as u8).collect();
516 let s = SmallBytes::from_slice(&v);
517 assert!(!s.is_inline());
518 assert_eq!(s.len(), 4096);
519 assert_eq!(s.as_slice(), v.as_slice());
520 }
521
522 #[test]
523 fn from_vec_inline() {
524 let s = SmallBytes::from_vec(vec![1u8, 2, 3]);
525 assert!(s.is_inline());
526 assert_eq!(s.as_slice(), &[1, 2, 3]);
527 }
528
529 #[test]
530 fn from_vec_heap_reuses_alloc() {
531 let mut v: Vec<u8> = (0u8..100).collect();
532 v.reserve(200);
533 let ptr_before = v.as_ptr();
534 let cap_before = v.capacity();
535 let s = SmallBytes::from_vec(v);
536 assert!(!s.is_inline());
537 unsafe {
539 assert_eq!(s.heap.ptr.as_ptr() as *const u8, ptr_before);
540 assert_eq!(s.heap.capacity(), cap_before);
541 }
542 }
543
544 #[test]
545 fn into_vec_inline_copies() {
546 let s = SmallBytes::from_slice(b"hello");
547 let v = s.into_vec();
548 assert_eq!(v, b"hello");
549 }
550
551 #[test]
552 fn into_vec_heap_reuses_alloc() {
553 let original: Vec<u8> = (0u8..200).collect();
554 let ptr = original.as_ptr();
555 let cap = original.capacity();
556 let s = SmallBytes::from_vec(original);
557 let v = s.into_vec();
558 assert_eq!(v.as_ptr(), ptr);
559 assert_eq!(v.capacity(), cap);
560 assert_eq!(v.len(), 200);
561 }
562
563 #[test]
564 fn clone_inline() {
565 let s = SmallBytes::from_slice(b"abc");
566 let c = s.clone();
567 assert_eq!(s, c);
568 assert!(c.is_inline());
569 }
570
571 #[test]
572 fn clone_heap() {
573 let v: Vec<u8> = (0u8..50).collect();
574 let s = SmallBytes::from_slice(&v);
575 let c = s.clone();
576 assert_eq!(s, c);
577 assert!(!c.is_inline());
578 }
579
580 #[test]
581 fn eq_by_content() {
582 let a = SmallBytes::from_slice(b"short");
583 let b = SmallBytes::from_slice(b"short");
584 assert_eq!(a, b);
585 let c: Vec<u8> = (0u8..30).collect();
586 let d: Vec<u8> = (0u8..30).collect();
587 assert_eq!(SmallBytes::from_slice(&c), SmallBytes::from_slice(&d));
588 }
589
590 #[test]
591 fn ord_lex() {
592 let a = SmallBytes::from_slice(b"abc");
593 let b = SmallBytes::from_slice(b"abd");
594 assert!(a < b);
595 }
596
597 #[test]
598 fn debug_format_matches_slice() {
599 let s = SmallBytes::from_slice(&[1u8, 2, 3]);
600 let dbg = format!("{s:?}");
601 let exp = format!("{:?}", &[1u8, 2, 3][..]);
602 assert_eq!(dbg, exp);
603 }
604
605 #[test]
606 fn default_is_empty_inline() {
607 let s = SmallBytes::default();
608 assert!(s.is_inline());
609 assert_eq!(s.len(), 0);
610 }
611
612 #[test]
613 fn drop_heap_does_not_leak_or_double_free() {
614 for n in [23usize, 64, 1024, 65536] {
616 let v: Vec<u8> = (0..n).map(|i| (i & 0xFF) as u8).collect();
617 let s = SmallBytes::from_slice(&v);
618 drop(s);
619 }
620 }
621
622 #[test]
625 fn eq_is_reflexive_and_symmetric_inline() {
626 let a = SmallBytes::from_slice(b"hi");
627 let b = SmallBytes::from_slice(b"hi");
628 let c = SmallBytes::from_slice(b"no");
629 assert_eq!(a, a);
630 assert_eq!(a, b);
631 assert_eq!(b, a);
632 assert_ne!(a, c);
633 }
634
635 #[test]
636 fn eq_is_reflexive_and_symmetric_heap() {
637 let v: Vec<u8> = (0u8..40).collect();
638 let a = SmallBytes::from_slice(&v);
639 let b = SmallBytes::from_slice(&v);
640 let mut w = v.clone();
641 w[0] = w[0].wrapping_add(1);
642 let c = SmallBytes::from_slice(&w);
643 assert_eq!(a, a);
644 assert_eq!(a, b);
645 assert_eq!(b, a);
646 assert_ne!(a, c);
647 }
648
649 #[test]
650 fn partial_cmp_matches_cmp_inline() {
651 let a = SmallBytes::from_slice(b"abc");
652 let b = SmallBytes::from_slice(b"abd");
653 assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Less));
654 assert_eq!(b.partial_cmp(&a), Some(std::cmp::Ordering::Greater));
655 assert_eq!(a.partial_cmp(&a), Some(std::cmp::Ordering::Equal));
656 assert_eq!(a.cmp(&b), std::cmp::Ordering::Less);
658 assert_eq!(a.cmp(&a), std::cmp::Ordering::Equal);
659 }
660
661 #[test]
662 fn hash_agrees_with_byte_slice() {
663 use std::collections::hash_map::DefaultHasher;
664 let v: Vec<u8> = (0u8..40).collect();
665 let s = SmallBytes::from_slice(&v);
666 let mut h_slice = DefaultHasher::new();
667 v.as_slice().hash(&mut h_slice);
668 let mut h_sb = DefaultHasher::new();
669 s.hash(&mut h_sb);
670 assert_eq!(h_slice.finish(), h_sb.finish());
673 }
674
675 #[test]
676 fn kevy_hash_agrees_with_byte_slice() {
677 let v: Vec<u8> = (0u8..40).collect();
678 let s = SmallBytes::from_slice(&v);
679 assert_eq!(
680 s.kevy_hash(),
681 v.as_slice().kevy_hash(),
682 "KevyHash impl must agree with &[u8] so a KevyMap<SmallBytes, V> can be queried by Borrow<[u8]>"
683 );
684 let small = SmallBytes::from_slice(b"foo");
685 assert_eq!(small.kevy_hash(), (b"foo" as &[u8]).kevy_hash());
686 }
687
688 #[test]
689 fn as_ref_is_zero_copy_view() {
690 let s = SmallBytes::from_slice(b"abcdef");
691 let r: &[u8] = s.as_ref();
692 assert_eq!(r, b"abcdef");
693 assert!(std::ptr::eq(r.as_ptr(), s.as_slice().as_ptr()));
695 }
696
697 #[test]
698 fn borrow_lookup_works_in_collection() {
699 use std::collections::HashMap;
700 let mut m: HashMap<SmallBytes, i32> = HashMap::new();
701 m.insert(SmallBytes::from_slice(b"key1"), 1);
702 m.insert(SmallBytes::from_slice(b"key2"), 2);
703 assert_eq!(m.get(b"key1".as_slice()), Some(&1));
705 assert_eq!(m.get(b"key2".as_slice()), Some(&2));
706 assert_eq!(m.get(b"none".as_slice()), None);
707 }
708
709 #[test]
710 fn from_byte_slice_round_trip() {
711 let a: SmallBytes = (&b"short"[..]).into();
712 assert_eq!(a.as_slice(), b"short");
713 let v: Vec<u8> = (0u8..40).collect();
714 let b: SmallBytes = v.as_slice().into();
715 assert_eq!(b.as_slice(), v.as_slice());
716 assert!(!b.is_inline());
717 }
718
719 #[test]
720 fn from_vec_dispatches_inline_or_heap() {
721 let inline_src: SmallBytes = vec![1u8, 2, 3].into();
723 assert!(inline_src.is_inline());
724 assert_eq!(inline_src.as_slice(), &[1, 2, 3]);
725 let v: Vec<u8> = (0u8..30).collect();
727 let heap_src: SmallBytes = v.clone().into();
728 assert!(!heap_src.is_inline());
729 assert_eq!(heap_src.as_slice(), v.as_slice());
730 }
731
732 #[test]
733 fn clone_heap_keeps_data_and_is_independent() {
734 let v: Vec<u8> = (0u8..50).collect();
737 let src = SmallBytes::from_slice(&v);
738 let dup = src.clone();
739 unsafe {
741 assert_ne!(
742 src.heap.ptr.as_ptr(),
743 dup.heap.ptr.as_ptr(),
744 "clone must allocate a fresh buffer"
745 );
746 }
747 drop(src);
748 assert_eq!(dup.as_slice(), v.as_slice());
750 }
751
752 #[test]
753 fn drop_inline_is_noop() {
754 for &n in &[0usize, 1, 5, 22] {
757 let s = SmallBytes::from_slice(&vec![b'x'; n]);
758 assert!(s.is_inline());
759 drop(s);
760 }
761 }
762
763 #[test]
764 fn into_vec_zero_size_path() {
765 let s = SmallBytes::new();
767 let v = s.into_vec();
768 assert!(v.is_empty());
769 }
770
771 #[test]
772 fn to_vec_copies_inline_and_heap() {
773 let inline = SmallBytes::from_slice(b"hi");
774 assert_eq!(inline.to_vec(), b"hi");
775 let v: Vec<u8> = (0u8..30).collect();
776 let heap = SmallBytes::from_slice(&v);
777 let copy = heap.to_vec();
778 assert_eq!(copy, v);
779 assert_eq!(heap.as_slice(), v.as_slice());
783 }
784
785 use std::alloc::{GlobalAlloc, Layout, System};
801 use std::cell::Cell;
802
803 struct CountingAlloc {
804 inner: System,
805 }
806
807 thread_local! {
808 static THREAD_RECORDING: Cell<bool> = const { Cell::new(false) };
811 static THREAD_ALLOC_CALLS: Cell<usize> = const { Cell::new(0) };
812 }
813
814 unsafe impl GlobalAlloc for CountingAlloc {
815 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
816 let _ = THREAD_RECORDING.try_with(|r| {
819 if r.get() {
820 let _ = THREAD_ALLOC_CALLS.try_with(|c| c.set(c.get() + 1));
821 }
822 });
823 unsafe { self.inner.alloc(layout) }
825 }
826 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
827 unsafe { self.inner.dealloc(ptr, layout) }
829 }
830 }
831
832 #[global_allocator]
833 static COUNTING: CountingAlloc = CountingAlloc { inner: System };
834
835 fn measure_allocs<F: FnOnce()>(f: F) -> usize {
836 THREAD_ALLOC_CALLS.with(|c| c.set(0));
837 THREAD_RECORDING.with(|r| r.set(true));
838 f();
839 THREAD_RECORDING.with(|r| r.set(false));
840 THREAD_ALLOC_CALLS.with(|c| c.get())
841 }
842
843 #[test]
844 fn inline_payload_does_not_allocate() {
845 let max_inline = INLINE_LEN_MAX as usize;
850 let allocs = measure_allocs(|| {
851 for n in 0..=max_inline {
852 let s = SmallBytes::from_slice(&[0u8; INLINE_CAP][..n]);
853 std::hint::black_box(&s);
854 std::hint::black_box(s.as_slice());
855 std::hint::black_box(s.len());
856 let c = s.clone(); std::hint::black_box(&c);
858 drop(c);
859 drop(s);
860 }
861 });
862 assert_eq!(
863 allocs, 0,
864 "expected SSO inline path to be alloc-free, got {allocs} allocs"
865 );
866 }
867
868 #[test]
869 fn heap_payload_does_allocate() {
870 let max_inline = INLINE_LEN_MAX as usize;
874 let allocs = measure_allocs(|| {
875 let s = SmallBytes::from_slice(&[7u8; INLINE_CAP + 8][..max_inline + 1]);
876 std::hint::black_box(&s);
877 drop(s);
878 });
879 assert!(
880 allocs >= 1,
881 "expected the heap path to allocate at least once, got {allocs}"
882 );
883 }
884}