1use alloc::{
9 string::{String, ToString},
10 vec::Vec,
11};
12
13use crate::props::basic::ColorU;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
36#[repr(C)]
37#[derive(Default)]
38#[allow(clippy::pub_underscore_fields)]
40pub struct EmptyStruct {
41 pub _reserved: u8,
44}
45
46
47impl EmptyStruct {
48 #[must_use]
50 pub const fn new() -> Self {
51 Self { _reserved: 0 }
52 }
53}
54
55impl From<()> for EmptyStruct {
56 fn from((): ()) -> Self {
57 Self::default()
58 }
59}
60
61impl From<EmptyStruct> for () {
62 fn from(_: EmptyStruct) -> Self {
63
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
73#[repr(C)]
74#[derive(Default)]
75pub enum LayoutDebugMessageType {
76 #[default]
77 Info,
78 Warning,
79 Error,
80 BoxProps,
82 CssGetter,
83 BfcLayout,
85 IfcLayout,
87 TableLayout,
88 DisplayType,
89 PositionCalculation,
90}
91
92
93#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd)]
95#[repr(C)]
96pub struct LayoutDebugMessage {
97 pub message_type: LayoutDebugMessageType,
98 pub message: AzString,
99 pub location: AzString,
100}
101
102impl LayoutDebugMessage {
103 #[track_caller]
105 pub fn new(message_type: LayoutDebugMessageType, message: impl Into<String>) -> Self {
106 let location = core::panic::Location::caller();
107 Self {
108 message_type,
109 message: AzString::from_string(message.into()),
110 location: AzString::from_string(format!(
111 "{}:{}:{}",
112 location.file(),
113 location.line(),
114 location.column()
115 )),
116 }
117 }
118
119 #[track_caller]
121 pub fn info(message: impl Into<String>) -> Self {
122 Self::new(LayoutDebugMessageType::Info, message)
123 }
124
125 #[track_caller]
127 pub fn warning(message: impl Into<String>) -> Self {
128 Self::new(LayoutDebugMessageType::Warning, message)
129 }
130
131 #[track_caller]
133 pub fn error(message: impl Into<String>) -> Self {
134 Self::new(LayoutDebugMessageType::Error, message)
135 }
136
137 #[track_caller]
139 pub fn box_props(message: impl Into<String>) -> Self {
140 Self::new(LayoutDebugMessageType::BoxProps, message)
141 }
142
143 #[track_caller]
145 pub fn css_getter(message: impl Into<String>) -> Self {
146 Self::new(LayoutDebugMessageType::CssGetter, message)
147 }
148
149 #[track_caller]
151 pub fn bfc_layout(message: impl Into<String>) -> Self {
152 Self::new(LayoutDebugMessageType::BfcLayout, message)
153 }
154
155 #[track_caller]
157 pub fn ifc_layout(message: impl Into<String>) -> Self {
158 Self::new(LayoutDebugMessageType::IfcLayout, message)
159 }
160
161 #[track_caller]
163 pub fn table_layout(message: impl Into<String>) -> Self {
164 Self::new(LayoutDebugMessageType::TableLayout, message)
165 }
166
167 #[track_caller]
169 pub fn display_type(message: impl Into<String>) -> Self {
170 Self::new(LayoutDebugMessageType::DisplayType, message)
171 }
172}
173
174#[repr(C)]
179pub struct AzString {
180 pub vec: U8Vec,
181}
182
183impl_option!(
184 AzString,
185 OptionString,
186 copy = false,
187 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
188);
189
190static DEFAULT_STR: &str = "";
191
192impl Default for AzString {
193 fn default() -> Self {
194 DEFAULT_STR.into()
195 }
196}
197
198impl<'a> From<&'a str> for AzString {
199 fn from(s: &'a str) -> Self {
200 s.to_string().into()
201 }
202}
203
204impl AsRef<str> for AzString {
205 fn as_ref(&self) -> &str {
206 self.as_str()
207 }
208}
209
210impl core::fmt::Debug for AzString {
211 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
212 self.as_str().fmt(f)
213 }
214}
215
216impl core::fmt::Display for AzString {
217 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
218 self.as_str().fmt(f)
219 }
220}
221
222impl AzString {
223 #[inline]
224 #[must_use] pub const fn from_const_str(s: &'static str) -> Self {
225 Self {
226 vec: U8Vec::from_const_slice(s.as_bytes()),
227 }
228 }
229
230 #[inline]
245 #[must_use] pub unsafe fn from_c_str(ptr: *const i8) -> Self { unsafe {
246 if ptr.is_null() {
247 return Self::default();
248 }
249 let c_str = core::ffi::CStr::from_ptr(ptr as *const core::ffi::c_char);
250 let bytes = c_str.to_bytes();
251 Self::copy_from_bytes(bytes.as_ptr(), 0, bytes.len())
252 }}
253
254 #[inline]
267 #[must_use] pub fn copy_from_bytes(ptr: *const u8, start: usize, len: usize) -> Self {
268 let raw = U8Vec::copy_from_bytes(ptr, start, len);
269 if core::str::from_utf8(raw.as_ref()).is_ok() {
276 return Self { vec: raw };
277 }
278 let s = String::from_utf8_lossy(raw.as_ref()).into_owned();
279 Self::from_string(s)
280 }
281
282 #[inline] #[must_use] pub const fn from_string(s: String) -> Self {
284 Self {
285 vec: U8Vec::from_vec(s.into_bytes()),
286 }
287 }
288
289 #[inline]
290 #[must_use] pub fn as_str(&self) -> &str {
291 unsafe { core::str::from_utf8_unchecked(self.vec.as_ref()) }
292 }
293
294 #[inline]
297 #[must_use] pub fn clone_self(&self) -> Self {
298 Self {
299 vec: self.vec.clone_self(),
300 }
301 }
302
303 #[inline]
304 #[must_use] pub fn into_library_owned_string(self) -> String {
305 match self.vec.destructor {
306 U8VecDestructor::NoDestructor | U8VecDestructor::External(_) | U8VecDestructor::AlreadyDestroyed => {
307 self.as_str().to_string()
308 }
309 U8VecDestructor::DefaultRust => {
310 let m = core::mem::ManuallyDrop::new(self);
311 unsafe { String::from_raw_parts(m.vec.ptr.cast_mut(), m.vec.len, m.vec.cap) }
312 }
313 }
314 }
315
316 #[inline]
317 #[must_use] pub fn as_bytes(&self) -> &[u8] {
318 self.vec.as_ref()
319 }
320
321 #[inline]
322 #[must_use] pub fn into_bytes(self) -> U8Vec {
323 let m = core::mem::ManuallyDrop::new(self);
324 U8Vec {
325 ptr: m.vec.ptr,
326 len: m.vec.len,
327 cap: m.vec.cap,
328 destructor: m.vec.destructor,
329 }
330 }
331
332 #[inline]
334 #[must_use] pub const fn len(&self) -> usize {
335 self.vec.len
336 }
337
338 #[inline]
340 #[must_use] pub const fn is_empty(&self) -> bool {
341 self.vec.len == 0
342 }
343
344 #[inline]
350 #[must_use] pub fn to_c_str(&self) -> U8Vec {
351 let bytes = self.as_bytes();
352 let mut result = Vec::with_capacity(bytes.len() + 1);
353 result.extend_from_slice(bytes);
354 result.push(0); U8Vec::from_vec(result)
356 }
357
358 unsafe fn from_utf16_with_byte_order(
364 ptr: *const u8,
365 len: usize,
366 from_bytes: fn([u8; 2]) -> u16,
367 ) -> Self { unsafe {
368 if ptr.is_null() || len == 0 {
369 return Self::default();
370 }
371
372 if !len.is_multiple_of(2) {
374 return Self::default();
375 }
376
377 let byte_slice = core::slice::from_raw_parts(ptr, len);
378 let code_units: Vec<u16> = byte_slice
379 .chunks_exact(2)
380 .map(|chunk| from_bytes([chunk[0], chunk[1]]))
381 .collect();
382
383 String::from_utf16(&code_units).map_or_else(|_| Self::default(), Self::from_string)
384 }}
385
386 #[inline]
397 pub unsafe fn from_utf16_le(ptr: *const u8, len: usize) -> Self { unsafe {
398 Self::from_utf16_with_byte_order(ptr, len, u16::from_le_bytes)
399 }}
400
401 #[inline]
412 pub unsafe fn from_utf16_be(ptr: *const u8, len: usize) -> Self { unsafe {
413 Self::from_utf16_with_byte_order(ptr, len, u16::from_be_bytes)
414 }}
415
416 #[inline]
422 #[must_use] pub unsafe fn from_utf8_lossy(ptr: *const u8, len: usize) -> Self { unsafe {
423 if ptr.is_null() || len == 0 {
424 return Self::default();
425 }
426
427 let byte_slice = core::slice::from_raw_parts(ptr, len);
428 let s = String::from_utf8_lossy(byte_slice).into_owned();
429 Self::from_string(s)
430 }}
431
432 #[inline]
438 #[must_use] pub unsafe fn from_utf8(ptr: *const u8, len: usize) -> Self { unsafe {
439 if ptr.is_null() || len == 0 {
440 return Self::default();
441 }
442
443 let byte_slice = core::slice::from_raw_parts(ptr, len);
444 core::str::from_utf8(byte_slice)
445 .map_or_else(|_| Self::default(), |s| Self::from_string(s.to_string()))
446 }}
447}
448
449impl From<String> for AzString {
450 fn from(input: String) -> Self {
451 Self::from_string(input)
452 }
453}
454
455impl PartialOrd for AzString {
456 fn partial_cmp(&self, rhs: &Self) -> Option<core::cmp::Ordering> {
457 self.as_str().partial_cmp(rhs.as_str())
458 }
459}
460
461impl Ord for AzString {
462 fn cmp(&self, rhs: &Self) -> core::cmp::Ordering {
463 self.as_str().cmp(rhs.as_str())
464 }
465}
466
467impl Clone for AzString {
468 fn clone(&self) -> Self {
469 self.clone_self()
470 }
471}
472
473impl PartialEq for AzString {
474 fn eq(&self, rhs: &Self) -> bool {
475 self.as_str().eq(rhs.as_str())
476 }
477}
478
479impl Eq for AzString {}
480
481impl core::hash::Hash for AzString {
482 fn hash<H>(&self, state: &mut H)
483 where
484 H: core::hash::Hasher,
485 {
486 self.as_str().hash(state);
487 }
488}
489
490impl core::ops::Deref for AzString {
491 type Target = str;
492
493 fn deref(&self) -> &str {
494 self.as_str()
495 }
496}
497
498impl_option!(
499 u8,
500 OptionU8,
501 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
502);
503
504impl_vec!(u8, U8Vec, U8VecDestructor, U8VecDestructorType, U8VecSlice, OptionU8);
505impl_vec_mut!(u8, U8Vec);
506impl_vec_debug!(u8, U8Vec);
507impl_vec_partialord!(u8, U8Vec);
508impl_vec_ord!(u8, U8Vec);
509impl_vec_clone!(u8, U8Vec, U8VecDestructor);
510impl_vec_partialeq!(u8, U8Vec);
511impl_vec_eq!(u8, U8Vec);
512impl_vec_hash!(u8, U8Vec);
513
514impl U8Vec {
515 #[inline] #[allow(clippy::not_unsafe_ptr_arg_deref)] #[must_use] pub fn copy_from_bytes(ptr: *const u8, start: usize, len: usize) -> Self {
524 if ptr.is_null() || len == 0 {
525 return Self::new();
526 }
527 debug_assert!(
528 start.checked_add(len).is_some(),
529 "U8Vec::copy_from_bytes: start + len overflows"
530 );
531 let slice = unsafe { core::slice::from_raw_parts(ptr.add(start), len) };
532 Self::from_vec(slice.to_vec())
533 }
534}
535
536impl_option!(
537 U8Vec,
538 OptionU8Vec,
539 copy = false,
540 [Debug, Clone, PartialEq, Ord, PartialOrd, Eq, Hash]
541);
542
543impl_vec!(u16, U16Vec, U16VecDestructor, U16VecDestructorType, U16VecSlice, OptionU16);
544impl_vec_debug!(u16, U16Vec);
545impl_vec_partialord!(u16, U16Vec);
546impl_vec_ord!(u16, U16Vec);
547impl_vec_clone!(u16, U16Vec, U16VecDestructor);
548impl_vec_partialeq!(u16, U16Vec);
549impl_vec_eq!(u16, U16Vec);
550impl_vec_hash!(u16, U16Vec);
551
552impl_vec!(f32, F32Vec, F32VecDestructor, F32VecDestructorType, F32VecSlice, OptionF32);
553impl_vec_debug!(f32, F32Vec);
554impl_vec_partialord!(f32, F32Vec);
555impl_vec_clone!(f32, F32Vec, F32VecDestructor);
556impl_vec_partialeq!(f32, F32Vec);
557
558impl_vec!(u32, U32Vec, U32VecDestructor, U32VecDestructorType, U32VecSlice, OptionU32);
560impl_vec_mut!(u32, U32Vec);
561impl_vec_debug!(u32, U32Vec);
562impl_vec_partialord!(u32, U32Vec);
563impl_vec_ord!(u32, U32Vec);
564impl_vec_clone!(u32, U32Vec, U32VecDestructor);
565impl_vec_partialeq!(u32, U32Vec);
566impl_vec_eq!(u32, U32Vec);
567impl_vec_hash!(u32, U32Vec);
568
569impl_vec!(AzString, StringVec, StringVecDestructor, StringVecDestructorType, StringVecSlice, OptionString);
570impl_vec_debug!(AzString, StringVec);
571impl_vec_partialord!(AzString, StringVec);
572impl_vec_ord!(AzString, StringVec);
573impl_vec_clone!(AzString, StringVec, StringVecDestructor);
574impl_vec_partialeq!(AzString, StringVec);
575impl_vec_eq!(AzString, StringVec);
576impl_vec_hash!(AzString, StringVec);
577
578impl From<Vec<String>> for StringVec {
579 fn from(v: Vec<String>) -> Self {
580 let new_v: Vec<AzString> = v.into_iter().map(Into::into).collect();
581 new_v.into()
582 }
583}
584
585impl_option!(
586 StringVec,
587 OptionStringVec,
588 copy = false,
589 [Debug, Clone, PartialOrd, PartialEq, Ord, Eq, Hash]
590);
591
592impl_option!(
593 u16,
594 OptionU16,
595 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
596);
597impl_option!(
598 u32,
599 OptionU32,
600 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
601);
602impl_option!(
603 u64,
604 OptionU64,
605 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
606);
607impl_option!(
608 usize,
609 OptionUsize,
610 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
611);
612impl_option!(
613 i16,
614 OptionI16,
615 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
616);
617impl_option!(
618 i32,
619 OptionI32,
620 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
621);
622impl_option!(bool, OptionBool, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
623impl_option!(f32, OptionF32, [Debug, Copy, Clone, PartialEq]);
624impl_option!(f64, OptionF64, [Debug, Copy, Clone, PartialEq, PartialOrd]);
625
626impl core::hash::Hash for OptionF32 {
628 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
629 match self {
630 Self::None => 0u8.hash(state),
631 Self::Some(v) => {
632 1u8.hash(state);
633 v.to_bits().hash(state);
634 }
635 }
636 }
637}
638
639impl Eq for OptionF32 {}
640
641impl PartialOrd for OptionF32 {
644 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
645 Some(self.cmp(other))
646 }
647}
648
649impl Ord for OptionF32 {
650 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
651 match (self, other) {
652 (Self::None, Self::None) => core::cmp::Ordering::Equal,
653 (Self::None, Self::Some(_)) => core::cmp::Ordering::Less,
654 (Self::Some(_), Self::None) => core::cmp::Ordering::Greater,
655 (Self::Some(a), Self::Some(b)) => {
656 a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
657 }
658 }
659 }
660}
661
662use alloc::sync::Arc;
675use core::cell::UnsafeCell;
676
677struct StringArenaInner {
681 chunks: UnsafeCell<Vec<Vec<u8>>>,
684 current_remaining: UnsafeCell<usize>,
687}
688
689unsafe impl Send for StringArenaInner {}
698unsafe impl Sync for StringArenaInner {}
699
700pub struct StringArena {
711 inner: Arc<StringArenaInner>,
712}
713
714impl core::fmt::Debug for StringArena {
715 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
717 f.debug_struct("StringArena").finish_non_exhaustive()
718 }
719}
720
721impl StringArena {
722 pub const CHUNK_SIZE: usize = 64 * 1024;
726
727 #[must_use] pub fn new() -> Self {
728 Self {
729 inner: Arc::new(StringArenaInner {
730 chunks: UnsafeCell::new(Vec::new()),
731 current_remaining: UnsafeCell::new(0),
732 }),
733 }
734 }
735
736 #[must_use] pub fn metrics(&self) -> (usize, usize) {
738 unsafe {
741 let chunks = &*self.inner.chunks.get();
742 let total: usize = chunks.iter().map(Vec::len).sum();
743 (chunks.len(), total)
744 }
745 }
746
747 pub fn intern(&mut self, s: &str) -> AzString {
758 let bytes = s.as_bytes();
759 let len = bytes.len();
760
761 let ptr: *const u8 = if len == 0 {
762 core::ptr::NonNull::<u8>::dangling().as_ptr()
765 } else {
766 unsafe {
768 let chunks: &mut Vec<Vec<u8>> = &mut *self.inner.chunks.get();
769 let remaining: &mut usize = &mut *self.inner.current_remaining.get();
770
771 if len > Self::CHUNK_SIZE / 2 {
774 let mut v = Vec::with_capacity(len);
775 v.extend_from_slice(bytes);
776 let p = v.as_ptr();
777 chunks.push(v);
778 p
779 } else {
780 if *remaining < len {
781 chunks.push(Vec::with_capacity(Self::CHUNK_SIZE));
782 *remaining = Self::CHUNK_SIZE;
783 }
784 let chunk = chunks.last_mut().unwrap();
787 let offset = chunk.len();
788 chunk.extend_from_slice(bytes);
789 *remaining -= len;
790 chunk.as_ptr().add(offset)
791 }
792 }
793 };
794
795 let arc_raw = Arc::into_raw(Arc::clone(&self.inner));
798
799 AzString {
800 vec: U8Vec {
801 ptr,
802 len,
803 cap: arc_raw as usize,
808 destructor: U8VecDestructor::External(arena_string_destructor),
809 },
810 }
811 }
812}
813
814impl Default for StringArena {
815 fn default() -> Self {
816 Self::new()
817 }
818}
819
820extern "C" fn arena_string_destructor(vec: *mut U8Vec) {
824 unsafe {
827 let v = &mut *vec;
828 let arc_raw = v.cap as *const StringArenaInner;
829 if !arc_raw.is_null() {
830 drop(Arc::from_raw(arc_raw));
831 v.cap = 0;
834 }
835 }
836}
837
838#[cfg(test)]
839mod string_arena_tests {
840 use super::*;
841
842 #[test]
843 fn intern_round_trip() {
844 let mut arena = StringArena::new();
845 let a = arena.intern("hello");
846 let b = arena.intern("world");
847 let c = arena.intern("");
848 assert_eq!(a.as_str(), "hello");
849 assert_eq!(b.as_str(), "world");
850 assert_eq!(c.as_str(), "");
851 }
852
853 #[test]
854 fn strings_outlive_arena_handle() {
855 let a = {
856 let mut arena = StringArena::new();
857 arena.intern("survives drop of arena handle")
858 };
859 assert_eq!(a.as_str(), "survives drop of arena handle");
860 }
861
862 #[test]
863 fn oversized_string_gets_dedicated_chunk() {
864 let mut arena = StringArena::new();
865 let big = "x".repeat(StringArena::CHUNK_SIZE);
866 let s = arena.intern(&big);
867 assert_eq!(s.len(), big.len());
868 assert_eq!(s.as_str(), big.as_str());
869 }
870
871 #[test]
872 fn many_small_strings_share_chunk() {
873 let mut arena = StringArena::new();
874 let mut strings = Vec::new();
875 for i in 0..100 {
876 strings.push(arena.intern(&format!("s{i}")));
877 }
878 let (chunks, _bytes) = arena.metrics();
879 assert!(chunks <= 2, "expected ≤2 chunks for 100 small strings, got {chunks}");
880 for (i, s) in strings.iter().enumerate() {
881 assert_eq!(s.as_str(), format!("s{i}"));
882 }
883 }
884
885 #[test]
886 fn clone_deep_copies_and_is_independent() {
887 let clone = {
890 let mut arena = StringArena::new();
891
892 arena.intern("deep-copy test")
893 };
894 assert_eq!(clone.as_str(), "deep-copy test");
895 }
896}
897
898#[cfg(test)]
899#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
900mod autotest_generated {
901 use super::*;
902
903 struct Fnv(u64);
910
911 impl core::hash::Hasher for Fnv {
912 fn finish(&self) -> u64 {
913 self.0
914 }
915 fn write(&mut self, bytes: &[u8]) {
916 for b in bytes {
917 self.0 ^= u64::from(*b);
918 self.0 = self.0.wrapping_mul(0x0100_0000_01b3);
919 }
920 }
921 }
922
923 fn hash_of<T: core::hash::Hash>(t: &T) -> u64 {
924 use core::hash::{Hash, Hasher};
925 let mut h = Fnv(0xcbf2_9ce4_8422_2325);
926 Hash::hash(t, &mut h);
927 h.finish()
928 }
929
930 fn utf16_bytes(s: &str, little_endian: bool) -> Vec<u8> {
932 s.encode_utf16()
933 .flat_map(|u| {
934 let b = if little_endian {
935 u.to_le_bytes()
936 } else {
937 u.to_be_bytes()
938 };
939 [b[0], b[1]]
940 })
941 .collect()
942 }
943
944 #[test]
949 fn empty_struct_new_invariants() {
950 let e = EmptyStruct::new();
951 assert_eq!(e._reserved, 0, "_reserved must always be initialized to 0");
952 assert_eq!(e, EmptyStruct::default(), "new() must equal default()");
953 }
954
955 #[test]
956 fn empty_struct_is_ffi_safe_non_zero_size() {
957 assert_eq!(core::mem::size_of::<EmptyStruct>(), 1);
959 assert_eq!(core::mem::align_of::<EmptyStruct>(), 1);
960 }
961
962 #[test]
963 fn empty_struct_unit_conversions_round_trip() {
964 let from_unit = EmptyStruct::from(());
965 assert_eq!(from_unit, EmptyStruct::new());
966 let back: () = EmptyStruct::new().into();
967 assert_eq!(back, ());
968 }
969
970 #[test]
971 fn empty_struct_total_order_is_trivial() {
972 let a = EmptyStruct::new();
974 let b = EmptyStruct::default();
975 assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
976 assert_eq!(hash_of(&a), hash_of(&b));
977 }
978
979 #[test]
984 fn debug_message_new_records_fields_and_caller_location() {
985 let m = LayoutDebugMessage::new(LayoutDebugMessageType::Warning, "disk on fire");
986 assert_eq!(m.message_type, LayoutDebugMessageType::Warning);
987 assert_eq!(m.message.as_str(), "disk on fire");
988 assert!(
989 m.location.as_str().contains("corety.rs"),
990 "#[track_caller] must record THIS file, got {:?}",
991 m.location.as_str()
992 );
993
994 let parts: Vec<&str> = m.location.as_str().rsplitn(3, ':').collect();
996 assert_eq!(parts.len(), 3, "location must be file:line:column");
997 assert!(parts[0].parse::<u32>().is_ok(), "column must parse as u32");
998 assert!(parts[1].parse::<u32>().is_ok(), "line must parse as u32");
999 }
1000
1001 #[test]
1002 fn debug_message_track_caller_propagates_through_helpers() {
1003 let a = LayoutDebugMessage::info("a");
1006 let b = LayoutDebugMessage::info("b");
1007 assert_ne!(
1008 a.location.as_str(),
1009 b.location.as_str(),
1010 "two call sites on different lines must record different locations"
1011 );
1012 assert!(a.location.as_str().contains("corety.rs"));
1013 }
1014
1015 #[test]
1016 fn debug_message_helpers_set_the_right_type() {
1017 assert_eq!(
1018 LayoutDebugMessage::info("x").message_type,
1019 LayoutDebugMessageType::Info
1020 );
1021 assert_eq!(
1022 LayoutDebugMessage::warning("x").message_type,
1023 LayoutDebugMessageType::Warning
1024 );
1025 assert_eq!(
1026 LayoutDebugMessage::error("x").message_type,
1027 LayoutDebugMessageType::Error
1028 );
1029 assert_eq!(
1030 LayoutDebugMessage::box_props("x").message_type,
1031 LayoutDebugMessageType::BoxProps
1032 );
1033 assert_eq!(
1034 LayoutDebugMessage::css_getter("x").message_type,
1035 LayoutDebugMessageType::CssGetter
1036 );
1037 assert_eq!(
1038 LayoutDebugMessage::bfc_layout("x").message_type,
1039 LayoutDebugMessageType::BfcLayout
1040 );
1041 assert_eq!(
1042 LayoutDebugMessage::ifc_layout("x").message_type,
1043 LayoutDebugMessageType::IfcLayout
1044 );
1045 assert_eq!(
1046 LayoutDebugMessage::table_layout("x").message_type,
1047 LayoutDebugMessageType::TableLayout
1048 );
1049 assert_eq!(
1050 LayoutDebugMessage::display_type("x").message_type,
1051 LayoutDebugMessageType::DisplayType
1052 );
1053 }
1054
1055 #[test]
1056 fn debug_message_helpers_preserve_the_message_verbatim() {
1057 for m in [
1060 LayoutDebugMessage::info(""),
1061 LayoutDebugMessage::warning(""),
1062 LayoutDebugMessage::error(""),
1063 LayoutDebugMessage::box_props(""),
1064 LayoutDebugMessage::css_getter(""),
1065 LayoutDebugMessage::bfc_layout(""),
1066 LayoutDebugMessage::ifc_layout(""),
1067 LayoutDebugMessage::table_layout(""),
1068 LayoutDebugMessage::display_type(""),
1069 ] {
1070 assert!(m.message.is_empty());
1071 assert!(!m.location.is_empty(), "location is always filled in");
1072 }
1073
1074 let weird = "ünïcødé \u{1F600}\n\t\"quoted\" \u{0}nul";
1075 assert_eq!(LayoutDebugMessage::error(weird).message.as_str(), weird);
1076 }
1077
1078 #[test]
1079 fn debug_message_handles_huge_message_without_panicking() {
1080 let huge = "m".repeat(1_000_000);
1081 let m = LayoutDebugMessage::new(LayoutDebugMessageType::PositionCalculation, huge.clone());
1082 assert_eq!(m.message.len(), 1_000_000);
1083 assert_eq!(m.message.as_str(), huge.as_str());
1084 assert_eq!(
1085 m.message_type,
1086 LayoutDebugMessageType::PositionCalculation,
1087 "the variant with no helper must still be constructible via new()"
1088 );
1089 }
1090
1091 #[test]
1092 fn debug_message_default_is_empty_info() {
1093 let m = LayoutDebugMessage::default();
1094 assert_eq!(m.message_type, LayoutDebugMessageType::Info);
1095 assert!(m.message.is_empty());
1096 assert!(m.location.is_empty(), "default() does not track a caller");
1097 assert_eq!(LayoutDebugMessageType::default(), LayoutDebugMessageType::Info);
1098 }
1099
1100 #[test]
1101 fn debug_message_accepts_string_and_str_via_into() {
1102 let from_str = LayoutDebugMessage::info("borrowed");
1104 let from_string = LayoutDebugMessage::info(String::from("owned"));
1105 assert_eq!(from_str.message.as_str(), "borrowed");
1106 assert_eq!(from_string.message.as_str(), "owned");
1107 }
1108
1109 #[test]
1110 fn debug_message_clone_is_a_deep_equal_copy() {
1111 let m = LayoutDebugMessage::error("clone me \u{1F600}");
1112 let c = m.clone();
1113 assert_eq!(c, m);
1114 assert_ne!(
1115 c.message.as_bytes().as_ptr(),
1116 m.message.as_bytes().as_ptr(),
1117 "clone must deep-copy the library-owned message bytes"
1118 );
1119 }
1120
1121 #[test]
1126 fn azstring_default_is_empty_and_readable() {
1127 let s = AzString::default();
1128 assert_eq!(s.as_str(), "");
1129 assert_eq!(s.len(), 0);
1130 assert!(s.is_empty());
1131 assert_eq!(s.as_bytes(), b"");
1132 }
1133
1134 #[test]
1135 fn azstring_from_const_str_borrows_the_static_and_never_frees_it() {
1136 const TEXT: &str = "static text";
1139 let s = AzString::from_const_str(TEXT);
1140 assert_eq!(s.as_str(), TEXT);
1141 assert_eq!(s.len(), 11);
1142 assert!(
1143 matches!(s.vec.destructor, U8VecDestructor::NoDestructor),
1144 "a &'static str must not get a freeing destructor"
1145 );
1146 assert_eq!(
1147 s.vec.ptr,
1148 TEXT.as_bytes().as_ptr(),
1149 "from_const_str must alias the static, not copy it"
1150 );
1151 }
1152
1153 #[test]
1154 fn azstring_from_const_str_empty_and_unicode() {
1155 let empty = AzString::from_const_str("");
1156 assert!(empty.is_empty());
1157 assert_eq!(empty.as_str(), "");
1158 assert_eq!(empty.len(), 0);
1159
1160 let uni = AzString::from_const_str("héllo \u{1F600}");
1161 assert_eq!(uni.as_str(), "héllo \u{1F600}");
1162 assert_eq!(uni.len(), "héllo \u{1F600}".len());
1164 assert_ne!(
1165 uni.len(),
1166 uni.as_str().chars().count(),
1167 "len() must be a byte length, not a char count"
1168 );
1169 }
1170
1171 #[test]
1172 fn azstring_from_string_round_trips_edge_values() {
1173 for input in [
1174 String::new(),
1175 String::from(" "),
1176 String::from("\t\n\r"),
1177 String::from("0"),
1178 String::from("-0"),
1179 String::from("9223372036854775807"), String::from("NaN"),
1181 String::from("inf"),
1182 String::from(" valid "),
1183 String::from("valid;garbage"),
1184 String::from("\u{1F600}\u{0301}\u{0}"), "{".repeat(10_000), ] {
1187 let s = AzString::from_string(input.clone());
1188 assert_eq!(s.as_str(), input.as_str(), "from_string must be verbatim");
1189 assert_eq!(s.len(), input.len());
1190 assert_eq!(s.is_empty(), input.is_empty());
1191 assert_eq!(s.into_library_owned_string(), input);
1193 }
1194 }
1195
1196 #[test]
1197 fn azstring_from_string_handles_a_megabyte() {
1198 let huge = "x".repeat(1_000_000);
1199 let s = AzString::from_string(huge.clone());
1200 assert_eq!(s.len(), 1_000_000);
1201 assert_eq!(s.as_str().len(), huge.len());
1202 assert!(s.as_str().bytes().all(|b| b == b'x'));
1203 }
1204
1205 #[test]
1206 fn azstring_from_string_preserves_the_original_capacity() {
1207 let mut owned = String::with_capacity(4096);
1210 owned.push_str("hi");
1211 let s = AzString::from_string(owned);
1212 assert!(matches!(s.vec.destructor, U8VecDestructor::DefaultRust));
1213 let back = s.into_library_owned_string();
1214 assert_eq!(back, "hi");
1215 assert!(
1216 back.capacity() >= 4096,
1217 "capacity must survive the AzString round-trip, got {}",
1218 back.capacity()
1219 );
1220 }
1221
1222 #[test]
1227 fn azstring_copy_from_bytes_zero_len_is_empty() {
1228 let buf = b"hello";
1229 let s = AzString::copy_from_bytes(buf.as_ptr(), 0, 0);
1230 assert!(s.is_empty());
1231 assert_eq!(s.as_str(), "");
1232 }
1233
1234 #[test]
1235 fn azstring_copy_from_bytes_null_ptr_is_empty() {
1236 let s = AzString::copy_from_bytes(core::ptr::null(), 0, 16);
1237 assert!(s.is_empty());
1238 assert_eq!(s.as_str(), "");
1239 }
1240
1241 #[test]
1242 fn azstring_copy_from_bytes_honours_the_start_offset() {
1243 let buf = b"0123456789";
1244 let s = AzString::copy_from_bytes(buf.as_ptr(), 3, 4);
1245 assert_eq!(s.as_str(), "3456");
1246 assert_eq!(s.len(), 4);
1247 }
1248
1249 #[test]
1250 fn azstring_copy_from_bytes_start_at_end_with_zero_len_is_empty() {
1251 let buf = b"abc";
1254 let s = AzString::copy_from_bytes(buf.as_ptr(), buf.len(), 0);
1255 assert!(s.is_empty());
1256 }
1257
1258 #[test]
1259 fn azstring_copy_from_bytes_zero_len_wins_over_start_overflow() {
1260 let buf = b"abc";
1263 let s = AzString::copy_from_bytes(buf.as_ptr(), usize::MAX, 0);
1264 assert!(s.is_empty());
1265 }
1266
1267 #[test]
1268 fn azstring_copy_from_bytes_null_wins_over_max_len() {
1269 let s = AzString::copy_from_bytes(core::ptr::null(), usize::MAX, usize::MAX);
1271 assert!(s.is_empty());
1272 assert_eq!(s.as_str(), "");
1273 }
1274
1275 #[test]
1276 fn azstring_copy_from_bytes_replaces_invalid_utf8_lossily() {
1277 let buf = "héllo".as_bytes();
1280 assert_eq!(buf[1], 0xC3);
1281 assert_eq!(buf[2], 0xA9);
1282 let s = AzString::copy_from_bytes(buf.as_ptr(), 2, 2);
1283 assert_eq!(s.as_str(), "\u{FFFD}l");
1284 assert!(core::str::from_utf8(s.as_bytes()).is_ok());
1286 }
1287
1288 #[test]
1289 fn azstring_copy_from_bytes_keeps_valid_utf8_byte_for_byte() {
1290 let buf = "héllo \u{1F600}".as_bytes();
1291 let s = AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len());
1292 assert_eq!(s.as_str(), "héllo \u{1F600}");
1293 assert_eq!(s.as_bytes(), buf);
1294 }
1295
1296 #[test]
1297 fn azstring_copy_from_bytes_preserves_interior_nul() {
1298 let buf = b"a\0b";
1299 let s = AzString::copy_from_bytes(buf.as_ptr(), 0, 3);
1300 assert_eq!(s.len(), 3, "an interior NUL is data, not a terminator");
1301 assert_eq!(s.as_bytes(), b"a\0b");
1302 }
1303
1304 #[test]
1309 fn u8vec_copy_from_bytes_zero_len_is_empty() {
1310 let buf = b"hello";
1311 let v = U8Vec::copy_from_bytes(buf.as_ptr(), 0, 0);
1312 assert!(v.is_empty());
1313 assert_eq!(v.as_ref(), b"");
1314 }
1315
1316 #[test]
1317 fn u8vec_copy_from_bytes_null_ptr_is_empty() {
1318 let v = U8Vec::copy_from_bytes(core::ptr::null(), 0, 8);
1319 assert!(v.is_empty());
1320 assert_eq!(v.len(), 0);
1321 }
1322
1323 #[test]
1324 fn u8vec_copy_from_bytes_null_wins_over_max_start_and_len() {
1325 let v = U8Vec::copy_from_bytes(core::ptr::null(), usize::MAX, usize::MAX);
1327 assert!(v.is_empty());
1328 }
1329
1330 #[test]
1331 fn u8vec_copy_from_bytes_zero_len_wins_over_start_overflow() {
1332 let buf = b"abc";
1334 let v = U8Vec::copy_from_bytes(buf.as_ptr(), usize::MAX, 0);
1335 assert!(v.is_empty());
1336 }
1337
1338 #[test]
1339 fn u8vec_copy_from_bytes_copies_the_requested_window() {
1340 let buf: Vec<u8> = (0u8..=255).collect();
1341 let v = U8Vec::copy_from_bytes(buf.as_ptr(), 250, 6);
1342 assert_eq!(v.as_ref(), &[250, 251, 252, 253, 254, 255]);
1343 assert_eq!(v.len(), 6);
1344 }
1345
1346 #[test]
1347 fn u8vec_copy_from_bytes_owns_its_copy() {
1348 let v = {
1350 let buf = vec![1u8, 2, 3, 4];
1351 U8Vec::copy_from_bytes(buf.as_ptr(), 1, 2)
1352 };
1353 assert_eq!(v.as_ref(), &[2, 3]);
1354 assert!(matches!(v.destructor, U8VecDestructor::DefaultRust));
1355 }
1356
1357 #[test]
1358 fn u8vec_copy_from_bytes_accepts_all_byte_values() {
1359 let buf: Vec<u8> = (0u8..=255).collect();
1362 let v = U8Vec::copy_from_bytes(buf.as_ptr(), 0, buf.len());
1363 assert_eq!(v.as_ref(), buf.as_slice());
1364 }
1365
1366 #[test]
1371 fn azstring_from_c_str_null_is_empty() {
1372 let s = unsafe { AzString::from_c_str(core::ptr::null()) };
1373 assert!(s.is_empty());
1374 assert_eq!(s.as_str(), "");
1375 }
1376
1377 #[test]
1378 fn azstring_from_c_str_reads_up_to_the_terminator() {
1379 let c = b"hello\0trailing garbage\0";
1380 let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1381 assert_eq!(s.as_str(), "hello");
1382 assert_eq!(s.len(), 5, "the NUL terminator is not part of the string");
1383 }
1384
1385 #[test]
1386 fn azstring_from_c_str_empty_c_string_is_empty() {
1387 let c = b"\0";
1388 let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1389 assert!(s.is_empty());
1390 }
1391
1392 #[test]
1393 fn azstring_from_c_str_replaces_non_utf8_bytes() {
1394 let c = b"caf\xE9\0";
1397 let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1398 assert_eq!(s.as_str(), "caf\u{FFFD}");
1399 assert!(core::str::from_utf8(s.as_bytes()).is_ok());
1400 }
1401
1402 #[test]
1403 fn azstring_from_c_str_handles_a_long_c_string() {
1404 let mut c = "z".repeat(100_000).into_bytes();
1405 c.push(0);
1406 let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1407 assert_eq!(s.len(), 100_000);
1408 }
1409
1410 #[test]
1415 fn azstring_to_c_str_appends_exactly_one_nul() {
1416 let s = AzString::from_const_str("abc");
1417 let c = s.to_c_str();
1418 assert_eq!(c.as_ref(), b"abc\0");
1419 assert_eq!(c.len(), s.len() + 1);
1420 }
1421
1422 #[test]
1423 fn azstring_to_c_str_of_empty_is_just_the_terminator() {
1424 let c = AzString::default().to_c_str();
1425 assert_eq!(c.as_ref(), b"\0");
1426 assert_eq!(c.len(), 1);
1427 }
1428
1429 #[test]
1430 fn azstring_c_str_round_trip() {
1431 for original in ["", "abc", "héllo \u{1F600}", " spaced "] {
1432 let s = AzString::from_const_str(original);
1433 let c = s.to_c_str();
1434 let back = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1435 assert_eq!(back.as_str(), original, "C round-trip must be lossless");
1436 assert_eq!(back, s);
1437 }
1438 }
1439
1440 #[test]
1441 fn azstring_c_str_round_trip_truncates_at_an_interior_nul() {
1442 let s = AzString::from_string(String::from("a\0b"));
1446 let c = s.to_c_str();
1447 assert_eq!(c.as_ref(), b"a\0b\0", "to_c_str keeps the interior NUL");
1448 let back = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1449 assert_eq!(back.as_str(), "a", "from_c_str stops at the first NUL");
1450 }
1451
1452 #[test]
1453 fn azstring_to_c_str_is_an_independent_allocation() {
1454 let s = AzString::from_const_str("shared?");
1455 let c = s.to_c_str();
1456 assert!(matches!(c.destructor, U8VecDestructor::DefaultRust));
1457 assert_ne!(
1458 c.as_ptr(),
1459 s.as_bytes().as_ptr(),
1460 "to_c_str must copy, not alias the source"
1461 );
1462 assert_eq!(s.as_str(), "shared?", "source must be untouched");
1463 }
1464
1465 #[test]
1470 fn azstring_from_utf8_null_or_zero_len_is_empty() {
1471 let buf = b"abc";
1472 assert!(unsafe { AzString::from_utf8(core::ptr::null(), 3) }.is_empty());
1473 assert!(unsafe { AzString::from_utf8(buf.as_ptr(), 0) }.is_empty());
1474 assert!(unsafe { AzString::from_utf8_lossy(core::ptr::null(), 3) }.is_empty());
1475 assert!(unsafe { AzString::from_utf8_lossy(buf.as_ptr(), 0) }.is_empty());
1476 }
1477
1478 #[test]
1479 fn azstring_from_utf8_accepts_valid_multibyte() {
1480 let buf = "héllo \u{1F600}".as_bytes();
1481 let s = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
1482 assert_eq!(s.as_str(), "héllo \u{1F600}");
1483 assert_eq!(s.len(), buf.len());
1484 }
1485
1486 #[test]
1487 fn azstring_from_utf8_rejects_invalid_but_lossy_replaces_it() {
1488 let buf = b"caf\xC3";
1490 let strict = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
1491 assert!(
1492 strict.is_empty(),
1493 "from_utf8 must return an EMPTY string for invalid UTF-8, got {:?}",
1494 strict.as_str()
1495 );
1496 let lossy = unsafe { AzString::from_utf8_lossy(buf.as_ptr(), buf.len()) };
1497 assert_eq!(lossy.as_str(), "caf\u{FFFD}");
1498 }
1499
1500 #[test]
1501 fn azstring_from_utf8_rejects_overlong_and_stray_continuations() {
1502 for bad in [
1503 &b"\xC0\xAF"[..], &b"\xED\xA0\x80"[..], &b"\xF8\x88\x80\x80"[..], &b"\x80"[..], &b"\xFF\xFE"[..], ] {
1509 let strict = unsafe { AzString::from_utf8(bad.as_ptr(), bad.len()) };
1510 assert!(strict.is_empty(), "from_utf8 must reject {bad:?}");
1511
1512 let lossy = unsafe { AzString::from_utf8_lossy(bad.as_ptr(), bad.len()) };
1513 assert!(
1514 lossy.as_str().contains('\u{FFFD}'),
1515 "from_utf8_lossy must substitute U+FFFD for {bad:?}"
1516 );
1517 assert!(core::str::from_utf8(lossy.as_bytes()).is_ok());
1519 }
1520 }
1521
1522 #[test]
1523 fn azstring_from_utf8_keeps_interior_nul() {
1524 let buf = b"a\0b";
1525 let s = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
1526 assert_eq!(s.len(), 3);
1527 assert_eq!(s.as_bytes(), b"a\0b");
1528 }
1529
1530 #[test]
1531 fn azstring_from_utf8_handles_a_megabyte() {
1532 let buf = "y".repeat(1_000_000);
1533 let s = unsafe { AzString::from_utf8(buf.as_bytes().as_ptr(), buf.len()) };
1534 assert_eq!(s.len(), 1_000_000);
1535 }
1536
1537 #[test]
1542 fn azstring_from_utf16_le_decodes_bmp_and_surrogate_pairs() {
1543 let text = "héllo \u{1F600}"; let bytes = utf16_bytes(text, true);
1545 let s = unsafe { AzString::from_utf16_le(bytes.as_ptr(), bytes.len()) };
1546 assert_eq!(s.as_str(), text);
1547 }
1548
1549 #[test]
1550 fn azstring_from_utf16_be_decodes_bmp_and_surrogate_pairs() {
1551 let text = "héllo \u{1F600}";
1552 let bytes = utf16_bytes(text, false);
1553 let s = unsafe { AzString::from_utf16_be(bytes.as_ptr(), bytes.len()) };
1554 assert_eq!(s.as_str(), text);
1555 }
1556
1557 #[test]
1558 fn azstring_from_utf16_byte_order_actually_matters() {
1559 let le = utf16_bytes("AB", true);
1561 assert_eq!(le.as_slice(), &[0x41, 0x00, 0x42, 0x00]);
1562 let as_be = unsafe { AzString::from_utf16_be(le.as_ptr(), le.len()) };
1563 assert_eq!(
1564 as_be.as_str(),
1565 "\u{4100}\u{4200}",
1566 "BE decode of LE bytes must byte-swap, not guess"
1567 );
1568 assert_ne!(as_be.as_str(), "AB");
1569 }
1570
1571 #[test]
1572 fn azstring_from_utf16_odd_length_is_empty() {
1573 let bytes = utf16_bytes("hello", true);
1574 let odd = bytes.len() - 1;
1575 assert_eq!(odd % 2, 1);
1576 assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), odd) }.is_empty());
1579 assert!(unsafe { AzString::from_utf16_be(bytes.as_ptr(), odd) }.is_empty());
1580 assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), 1) }.is_empty());
1582 }
1583
1584 #[test]
1585 fn azstring_from_utf16_null_or_zero_len_is_empty() {
1586 let bytes = utf16_bytes("hi", true);
1587 assert!(unsafe { AzString::from_utf16_le(core::ptr::null(), 4) }.is_empty());
1588 assert!(unsafe { AzString::from_utf16_be(core::ptr::null(), 4) }.is_empty());
1589 assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), 0) }.is_empty());
1590 assert!(unsafe { AzString::from_utf16_be(bytes.as_ptr(), 0) }.is_empty());
1591 }
1592
1593 #[test]
1594 fn azstring_from_utf16_unpaired_surrogate_is_empty() {
1595 let lone_high: [u8; 2] = 0xD83C_u16.to_le_bytes();
1597 assert!(unsafe { AzString::from_utf16_le(lone_high.as_ptr(), 2) }.is_empty());
1598
1599 let lone_low: [u8; 2] = 0xDF89_u16.to_le_bytes();
1601 assert!(unsafe { AzString::from_utf16_le(lone_low.as_ptr(), 2) }.is_empty());
1602
1603 let reversed: Vec<u8> = [0xDF89_u16, 0xD83C_u16]
1604 .iter()
1605 .flat_map(|u| u.to_le_bytes())
1606 .collect();
1607 assert!(unsafe { AzString::from_utf16_le(reversed.as_ptr(), reversed.len()) }.is_empty());
1608 }
1609
1610 #[test]
1611 fn azstring_from_utf16_decodes_noncharacters_and_nul() {
1612 let units: Vec<u8> = [0x0041_u16, 0x0000, 0xFFFE]
1615 .iter()
1616 .flat_map(|u| u.to_le_bytes())
1617 .collect();
1618 let s = unsafe { AzString::from_utf16_le(units.as_ptr(), units.len()) };
1619 assert_eq!(s.as_str(), "A\u{0}\u{FFFE}");
1620 assert_eq!(s.len(), 1 + 1 + 3);
1621 }
1622
1623 #[test]
1624 fn azstring_from_utf16_handles_100k_code_units() {
1625 let text = "ab".repeat(50_000);
1626 let bytes = utf16_bytes(&text, true);
1627 assert_eq!(bytes.len(), 200_000);
1628 let s = unsafe { AzString::from_utf16_le(bytes.as_ptr(), bytes.len()) };
1629 assert_eq!(s.len(), 100_000);
1630 }
1631
1632 #[test]
1633 fn azstring_from_utf16_with_byte_order_honours_the_supplied_fn() {
1634 fn swap_halves(b: [u8; 2]) -> u16 {
1636 u16::from_be_bytes(b)
1637 }
1638 let le = utf16_bytes("Az", true);
1639 let via_shared = unsafe {
1640 AzString::from_utf16_with_byte_order(le.as_ptr(), le.len(), u16::from_le_bytes)
1641 };
1642 assert_eq!(via_shared.as_str(), "Az");
1643
1644 let swapped = unsafe {
1645 AzString::from_utf16_with_byte_order(le.as_ptr(), le.len(), swap_halves)
1646 };
1647 assert_eq!(swapped.as_str(), "\u{4100}\u{7A00}");
1648
1649 assert!(unsafe {
1651 AzString::from_utf16_with_byte_order(le.as_ptr(), 3, u16::from_le_bytes)
1652 }
1653 .is_empty());
1654 assert!(unsafe {
1655 AzString::from_utf16_with_byte_order(core::ptr::null(), 2, u16::from_le_bytes)
1656 }
1657 .is_empty());
1658 }
1659
1660 #[test]
1665 fn azstring_as_str_and_as_bytes_agree_for_every_constructor() {
1666 let buf = "mixed \u{1F600}".as_bytes();
1667 let mut arena = StringArena::new();
1668 let strings = [
1669 AzString::default(),
1670 AzString::from_const_str("mixed \u{1F600}"),
1671 AzString::from_string(String::from("mixed \u{1F600}")),
1672 AzString::from("mixed \u{1F600}"),
1673 AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len()),
1674 unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) },
1675 arena.intern("mixed \u{1F600}"),
1676 ];
1677 for s in &strings {
1678 assert_eq!(
1679 s.as_bytes(),
1680 s.as_str().as_bytes(),
1681 "as_bytes() and as_str() must view the same memory"
1682 );
1683 assert_eq!(s.len(), s.as_bytes().len());
1684 assert_eq!(s.is_empty(), s.len() == 0);
1685 let via_as_ref: &str = s.as_ref();
1686 assert_eq!(via_as_ref, s.as_str(), "AsRef must match as_str");
1687 assert_eq!(&**s, s.as_str(), "Deref must match as_str");
1688 }
1689 }
1690
1691 #[test]
1692 fn azstring_is_empty_only_for_zero_bytes() {
1693 assert!(AzString::default().is_empty());
1694 assert!(AzString::from_const_str("").is_empty());
1695 assert!(AzString::from_string(String::new()).is_empty());
1696 assert!(!AzString::from_const_str(" ").is_empty());
1698 assert!(!AzString::from_const_str("\t\n").is_empty());
1699 assert!(!AzString::from_string(String::from("\0")).is_empty());
1700 assert_eq!(AzString::from_string(String::from("\0")).len(), 1);
1701 }
1702
1703 #[test]
1704 fn azstring_len_counts_bytes_not_chars() {
1705 assert_eq!(AzString::from_const_str("é").len(), 2);
1706 assert_eq!(AzString::from_const_str("\u{1F600}").len(), 4);
1707 assert_eq!(AzString::from_const_str("e\u{0301}").len(), 3); assert_eq!(AzString::from_const_str("\u{1F600}").as_str().chars().count(), 1);
1709 }
1710
1711 #[test]
1712 fn azstring_into_bytes_moves_without_copying_or_double_freeing() {
1713 let s = AzString::from_string(String::from("payload"));
1714 let ptr = s.as_bytes().as_ptr();
1715 let (len, cap) = (s.vec.len, s.vec.cap);
1716 let v = s.into_bytes();
1717 assert_eq!(v.as_ref(), b"payload");
1718 assert_eq!(v.as_ptr(), ptr, "into_bytes must move, not copy");
1719 assert_eq!(v.len(), len);
1720 assert_eq!(v.capacity(), cap);
1721 assert!(matches!(v.destructor, U8VecDestructor::DefaultRust));
1722 }
1725
1726 #[test]
1727 fn azstring_into_bytes_preserves_a_non_owning_destructor() {
1728 let v = AzString::from_const_str("static").into_bytes();
1729 assert_eq!(v.as_ref(), b"static");
1730 assert!(
1731 matches!(v.destructor, U8VecDestructor::NoDestructor),
1732 "a &'static-backed AzString must not gain a freeing destructor"
1733 );
1734 }
1735
1736 #[test]
1737 fn azstring_into_bytes_of_empty_is_empty() {
1738 let v = AzString::default().into_bytes();
1739 assert!(v.is_empty());
1740 assert_eq!(v.as_ref(), b"");
1741 }
1742
1743 #[test]
1744 fn azstring_into_library_owned_string_works_for_all_destructors() {
1745 assert_eq!(
1747 AzString::from_string(String::from("owned \u{1F600}")).into_library_owned_string(),
1748 "owned \u{1F600}"
1749 );
1750 assert_eq!(
1752 AzString::from_const_str("static").into_library_owned_string(),
1753 "static"
1754 );
1755 let owned = {
1757 let mut arena = StringArena::new();
1758 let s = arena.intern("interned");
1759 s.into_library_owned_string()
1760 };
1761 assert_eq!(owned, "interned", "must outlive the arena it was copied from");
1762 assert_eq!(AzString::default().into_library_owned_string(), "");
1764 }
1765
1766 #[test]
1767 fn azstring_into_library_owned_string_copies_static_memory() {
1768 let mut owned = AzString::from_const_str("static").into_library_owned_string();
1769 owned.push_str(" + mutable");
1772 assert_eq!(owned, "static + mutable");
1773 }
1774
1775 #[test]
1780 fn azstring_clone_self_deep_copies_library_owned_memory() {
1781 let s = AzString::from_string(String::from("deep"));
1782 let c = s.clone_self();
1783 assert_eq!(c, s);
1784 assert_ne!(
1785 c.as_bytes().as_ptr(),
1786 s.as_bytes().as_ptr(),
1787 "a DefaultRust clone must own a fresh allocation"
1788 );
1789 assert!(matches!(c.vec.destructor, U8VecDestructor::DefaultRust));
1790 }
1791
1792 #[test]
1793 fn azstring_clone_self_shares_static_memory() {
1794 let s = AzString::from_const_str("static");
1795 let c = s.clone_self();
1796 assert_eq!(c, s);
1797 assert_eq!(
1798 c.as_bytes().as_ptr(),
1799 s.as_bytes().as_ptr(),
1800 "cloning a &'static-backed string should alias, not allocate"
1801 );
1802 assert!(matches!(c.vec.destructor, U8VecDestructor::NoDestructor));
1803 }
1804
1805 #[test]
1806 fn azstring_clone_self_of_empty_and_unicode() {
1807 for s in [
1808 AzString::default(),
1809 AzString::from_const_str(""),
1810 AzString::from_string(String::from("\u{1F600}\u{0}\u{0301}")),
1811 ] {
1812 let c = s.clone_self();
1813 assert_eq!(c.as_str(), s.as_str());
1814 assert_eq!(c.len(), s.len());
1815 }
1816 }
1817
1818 #[test]
1819 fn azstring_clone_trait_matches_clone_self() {
1820 let s = AzString::from_string(String::from("via trait"));
1821 assert_eq!(s.clone(), s.clone_self());
1822 }
1823
1824 #[test]
1829 fn azstring_display_round_trips_through_from() {
1830 for original in [
1831 "",
1832 " ",
1833 "plain",
1834 "héllo \u{1F600}",
1835 "with \"quotes\" and \\ backslash",
1836 "line\nbreak\ttab",
1837 "e\u{0301} combining",
1838 ] {
1839 let s = AzString::from(original);
1840 let rendered = format!("{s}");
1841 assert_eq!(rendered, original, "Display must emit the string verbatim");
1842 let reparsed = AzString::from(rendered.as_str());
1843 assert_eq!(reparsed, s, "parse(serialize(x)) == x");
1844 assert_eq!(format!("{reparsed}"), rendered);
1846 }
1847 }
1848
1849 #[test]
1850 fn azstring_debug_matches_str_debug_and_escapes() {
1851 let s = AzString::from("a\"b\\c\nd");
1852 let expected = format!("{:?}", "a\"b\\c\nd");
1853 assert_eq!(format!("{s:?}"), expected, "Debug must delegate to str::fmt");
1854 assert!(format!("{s:?}").starts_with('"'), "Debug output must be quoted");
1855 assert!(!format!("{s:?}").contains('\n'), "Debug must escape newlines");
1856 }
1857
1858 #[test]
1859 fn azstring_debug_and_display_of_empty_do_not_panic() {
1860 assert_eq!(format!("{:?}", AzString::default()), "\"\"");
1861 assert_eq!(format!("{}", AzString::default()), "");
1862 assert_eq!(format!("{:?}", AzString::from_const_str("")), "\"\"");
1863 }
1864
1865 #[test]
1866 fn azstring_display_of_a_megabyte_is_lossless() {
1867 let huge = "q".repeat(1_000_000);
1868 let s = AzString::from_string(huge.clone());
1869 assert_eq!(format!("{s}").len(), huge.len());
1870 }
1871
1872 #[test]
1873 fn azstring_debug_is_stable_across_constructors() {
1874 let buf = "same".as_bytes();
1876 let a = AzString::from_const_str("same");
1877 let b = AzString::from_string(String::from("same"));
1878 let c = AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len());
1879 assert_eq!(format!("{a:?}"), format!("{b:?}"));
1880 assert_eq!(format!("{b:?}"), format!("{c:?}"));
1881 assert_eq!(format!("{a}"), format!("{c}"));
1882 }
1883
1884 #[test]
1889 fn azstring_eq_and_hash_ignore_memory_ownership() {
1890 let buf = "key".as_bytes();
1891 let mut arena = StringArena::new();
1892 let variants = [
1893 AzString::from_const_str("key"),
1894 AzString::from_string(String::from("key")),
1895 AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len()),
1896 arena.intern("key"),
1897 ];
1898 for v in &variants {
1899 assert_eq!(*v, variants[0], "equality must compare CONTENT, not pointers");
1900 assert_eq!(
1901 hash_of(v),
1902 hash_of(&variants[0]),
1903 "Hash must agree with Eq across destructor kinds"
1904 );
1905 assert_eq!(
1906 hash_of(v),
1907 hash_of(&"key"),
1908 "AzString must hash like the &str it wraps"
1909 );
1910 }
1911 }
1912
1913 #[test]
1914 fn azstring_ord_matches_str_ord() {
1915 let mut v = [
1916 AzString::from("b"),
1917 AzString::from(""),
1918 AzString::from("\u{1F600}"),
1919 AzString::from("a"),
1920 AzString::from("ab"),
1921 ];
1922 v.sort();
1923 let sorted: Vec<&str> = v.iter().map(AzString::as_str).collect();
1924 assert_eq!(sorted, ["", "a", "ab", "b", "\u{1F600}"]);
1925 assert_eq!(
1926 AzString::from("a").partial_cmp(&AzString::from("b")),
1927 Some(core::cmp::Ordering::Less)
1928 );
1929 assert_eq!(
1930 AzString::from("x").cmp(&AzString::from("x")),
1931 core::cmp::Ordering::Equal
1932 );
1933 }
1934
1935 #[test]
1940 fn arena_new_starts_empty() {
1941 let arena = StringArena::new();
1942 assert_eq!(arena.metrics(), (0, 0), "a fresh arena allocates nothing");
1943 assert_eq!(StringArena::default().metrics(), (0, 0));
1944 }
1945
1946 #[test]
1947 fn arena_metrics_track_chunks_and_bytes() {
1948 let mut arena = StringArena::new();
1949 let _a = arena.intern("abc");
1950 let (chunks, bytes) = arena.metrics();
1951 assert_eq!(chunks, 1);
1952 assert_eq!(bytes, 3);
1953 let _b = arena.intern("de");
1954 let (chunks, bytes) = arena.metrics();
1955 assert_eq!(chunks, 1, "a second small string reuses the open chunk");
1956 assert_eq!(bytes, 5);
1957 }
1958
1959 #[test]
1960 fn arena_empty_string_allocates_nothing_and_is_readable() {
1961 let mut arena = StringArena::new();
1962 let e = arena.intern("");
1963 assert!(e.is_empty());
1964 assert_eq!(e.as_str(), "");
1965 assert_eq!(arena.metrics(), (0, 0), "empty strings need no storage");
1966 assert!(!e.vec.ptr.is_null(), "the dangling ptr must still be non-null");
1967 }
1968
1969 #[test]
1970 fn arena_string_is_external_and_stashes_an_arc_in_cap() {
1971 let mut arena = StringArena::new();
1972 let s = arena.intern("hi");
1973 assert!(matches!(s.vec.destructor, U8VecDestructor::External(_)));
1974 assert_ne!(s.vec.cap, 0, "cap holds the Arc pointer, not a capacity");
1975 assert_eq!(s.as_str(), "hi");
1976 }
1977
1978 #[test]
1979 fn arena_intern_refcounts_each_string() {
1980 let mut arena = StringArena::new();
1981 assert_eq!(Arc::strong_count(&arena.inner), 1);
1982 let a = arena.intern("one");
1983 let b = arena.intern("two");
1984 assert_eq!(
1985 Arc::strong_count(&arena.inner),
1986 3,
1987 "each interned string must hold its own Arc reference"
1988 );
1989 drop(a);
1990 assert_eq!(Arc::strong_count(&arena.inner), 2);
1991 drop(b);
1992 assert_eq!(Arc::strong_count(&arena.inner), 1);
1993 }
1994
1995 #[test]
1996 fn arena_clone_deep_copies_and_does_not_bump_the_refcount() {
1997 let mut arena = StringArena::new();
1998 let s = arena.intern("interned");
1999 let c = s.clone_self();
2000 assert_eq!(
2001 Arc::strong_count(&arena.inner),
2002 2,
2003 "cloning an External string deep-copies; it must NOT retain the arena"
2004 );
2005 assert!(matches!(c.vec.destructor, U8VecDestructor::DefaultRust));
2006 assert_eq!(c.as_str(), "interned");
2007 assert_ne!(c.vec.ptr, s.vec.ptr);
2008 }
2009
2010 #[test]
2011 fn arena_clone_outlives_the_arena_and_the_original() {
2012 let clone = {
2013 let mut arena = StringArena::new();
2014 let s = arena.intern("deep-copied out of the arena");
2015 let c = s.clone_self();
2016 drop(s);
2017 drop(arena);
2018 c
2019 };
2020 assert_eq!(clone.as_str(), "deep-copied out of the arena");
2021 }
2022
2023 #[test]
2024 fn arena_exact_half_chunk_boundary_fills_one_chunk_exactly() {
2025 let mut arena = StringArena::new();
2029 let half = "h".repeat(StringArena::CHUNK_SIZE / 2);
2030 let a = arena.intern(&half);
2031 let b = arena.intern(&half);
2032 assert_eq!(arena.metrics().0, 1, "two half-chunks must share one chunk");
2033 let c = arena.intern(&half);
2034 assert_eq!(arena.metrics().0, 2, "the third must open a new chunk");
2035
2036 assert_eq!(a.as_str(), half);
2038 assert_eq!(b.as_str(), half);
2039 assert_eq!(c.as_str(), half);
2040 }
2041
2042 #[test]
2043 fn arena_oversized_string_is_readable_and_gets_its_own_chunk() {
2044 let mut arena = StringArena::new();
2045 let big = "b".repeat(StringArena::CHUNK_SIZE + 1);
2046 let s = arena.intern(&big);
2047 assert_eq!(s.len(), big.len());
2048 assert_eq!(s.as_str(), big.as_str());
2049 assert_eq!(arena.metrics(), (1, big.len()));
2050 }
2051
2052 #[test]
2053 fn arena_many_interleaved_sizes_all_read_back_correctly() {
2054 let mut arena = StringArena::new();
2055 let mut kept = Vec::new();
2056 for i in 0..200 {
2057 let s = format!("s{i}-{}", "p".repeat(i % 17));
2058 kept.push((arena.intern(&s), s));
2059 }
2060 for (interned, expected) in &kept {
2061 assert_eq!(interned.as_str(), expected.as_str());
2062 }
2063 }
2064
2065 #[test]
2066 fn arena_interns_unicode_and_nul_bytes_verbatim() {
2067 let mut arena = StringArena::new();
2068 let weird = "héllo \u{1F600}\u{0}\u{0301}";
2069 let s = arena.intern(weird);
2070 assert_eq!(s.as_str(), weird);
2071 assert_eq!(s.len(), weird.len());
2072 }
2073
2074 #[test]
2075 fn arena_strings_outlive_the_handle_even_when_interleaved() {
2076 let (a, b) = {
2077 let mut arena = StringArena::new();
2078 let a = arena.intern("first");
2079 let big = "z".repeat(StringArena::CHUNK_SIZE * 2);
2080 let _dropped = arena.intern(&big);
2081 let b = arena.intern("second");
2082 (a, b)
2083 };
2084 assert_eq!(a.as_str(), "first");
2085 assert_eq!(b.as_str(), "second");
2086 }
2087
2088 #[test]
2102 fn arena_small_after_oversized_must_not_grow_the_full_dedicated_chunk() {
2103 let mut arena = StringArena::new();
2104
2105 let _small = arena.intern("a");
2107
2108 let big = "x".repeat(StringArena::CHUNK_SIZE);
2110 let big_len = big.len();
2111 let interned_big = arena.intern(&big);
2112 assert_eq!(interned_big.as_str(), big.as_str(), "valid before the next intern");
2113
2114 let _small2 = arena.intern("y");
2116
2117 let grew = unsafe {
2120 let chunks = &*arena.inner.chunks.get();
2121 chunks.iter().any(|c| c.len() > big_len)
2122 };
2123 assert!(
2124 !grew,
2125 "intern() appended a small string into the FULL dedicated chunk of an oversized \
2126 string (len == cap), which reallocates that Vec and leaves every AzString pointing \
2127 into it dangling — a use-after-free. Root cause: the oversized branch pushes a chunk \
2128 without resetting `current_remaining`, so the next small string takes the \
2129 `chunks.last_mut()` fast path onto the wrong chunk."
2130 );
2131 }
2132
2133 #[test]
2138 fn arena_destructor_drops_one_arc_ref_and_is_idempotent() {
2139 let inner = Arc::new(StringArenaInner {
2140 chunks: UnsafeCell::new(Vec::new()),
2141 current_remaining: UnsafeCell::new(0),
2142 });
2143 let raw = Arc::into_raw(Arc::clone(&inner));
2144 assert_eq!(Arc::strong_count(&inner), 2);
2145
2146 let mut v = U8Vec {
2147 ptr: core::ptr::NonNull::<u8>::dangling().as_ptr().cast_const(),
2148 len: 0,
2149 cap: raw as usize,
2150 destructor: U8VecDestructor::External(arena_string_destructor),
2151 };
2152
2153 arena_string_destructor(&mut v);
2154 assert_eq!(
2155 Arc::strong_count(&inner),
2156 1,
2157 "the destructor must release exactly one Arc reference"
2158 );
2159 assert_eq!(v.cap, 0, "cap must be zeroed to guard against a double drop");
2160
2161 arena_string_destructor(&mut v);
2163 assert_eq!(Arc::strong_count(&inner), 1);
2164 assert_eq!(v.cap, 0);
2165
2166 drop(v);
2169 assert_eq!(Arc::strong_count(&inner), 1);
2170 }
2171
2172 #[test]
2173 fn arena_destructor_tolerates_a_null_arc_pointer() {
2174 let mut v = U8Vec {
2176 ptr: core::ptr::null(),
2177 len: 0,
2178 cap: 0,
2179 destructor: U8VecDestructor::NoDestructor,
2180 };
2181 arena_string_destructor(&mut v);
2182 assert_eq!(v.cap, 0);
2183 }
2184
2185 #[test]
2186 fn arena_last_reference_frees_the_chunks() {
2187 let inner_ptr;
2190 let s = {
2191 let mut arena = StringArena::new();
2192 let s = arena.intern("outlives the handle");
2193 inner_ptr = Arc::as_ptr(&arena.inner);
2194 assert_eq!(Arc::strong_count(&arena.inner), 2);
2195 s
2196 };
2197 assert_eq!(s.as_str(), "outlives the handle");
2199 assert_eq!(s.vec.cap as *const StringArenaInner, inner_ptr);
2200 drop(s); }
2202}