Skip to main content

azul_css/
corety.rs

1//! Core FFI-safe types used across crate boundaries.
2//!
3//! This module defines the fundamental types for FFI interop: [`AzString`] (an FFI-safe
4//! string backed by [`U8Vec`] with destructor-based memory management), [`EmptyStruct`] (a
5//! non-zero-size unit type), and various `Vec`/`Option` wrappers generated by the
6//! `impl_vec!` and `impl_option!` macros.
7
8use alloc::{
9    string::{String, ToString},
10    vec::Vec,
11};
12
13use crate::props::basic::ColorU;
14
15// ============================================================================
16// EmptyStruct type - FFI-safe replacement for ()
17// ============================================================================
18
19/// FFI-safe void type to replace `()` in Result types.
20/// 
21/// Since `()` (unit type) has zero size, it's not FFI-safe.
22/// This type provides a minimal 1-byte representation that can be
23/// safely passed across the C ABI boundary.
24/// 
25/// # Usage
26/// Instead of `Result<(), Error>`, use `Result<EmptyStruct, Error>`.
27/// 
28/// # Example
29/// ```ignore
30/// fn do_something() -> Result<EmptyStruct, MyError> {
31///     // ... do work ...
32///     Ok(EmptyStruct::default())
33/// }
34/// ```
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
36#[repr(C)]
37#[derive(Default)]
38// `_reserved` is a deliberate padding-field name (C-ABI / api.json); cannot rename.
39#[allow(clippy::pub_underscore_fields)]
40pub struct EmptyStruct {
41    /// Reserved byte to ensure the struct has non-zero size.
42    /// Always initialized to 0.
43    pub _reserved: u8,
44}
45
46
47impl EmptyStruct {
48    /// Create a new `EmptyStruct` value (equivalent to `()`)
49    #[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// ============================================================================
68// Debug message types
69// ============================================================================
70
71/// Debug message severity or category for layout diagnostics.
72#[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    // Layout-specific categories for filtering
81    BoxProps,
82    CssGetter,
83    /// Block Formatting Context layout
84    BfcLayout,
85    /// Inline Formatting Context layout
86    IfcLayout,
87    TableLayout,
88    DisplayType,
89    PositionCalculation,
90}
91
92
93/// A debug message emitted during layout, with severity, text, and source location.
94#[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    /// Create a new debug message with automatic caller location tracking
104    #[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    /// Helper for Info messages
120    #[track_caller]
121    pub fn info(message: impl Into<String>) -> Self {
122        Self::new(LayoutDebugMessageType::Info, message)
123    }
124
125    /// Helper for Warning messages
126    #[track_caller]
127    pub fn warning(message: impl Into<String>) -> Self {
128        Self::new(LayoutDebugMessageType::Warning, message)
129    }
130
131    /// Helper for Error messages
132    #[track_caller]
133    pub fn error(message: impl Into<String>) -> Self {
134        Self::new(LayoutDebugMessageType::Error, message)
135    }
136
137    /// Helper for `BoxProps` debug messages
138    #[track_caller]
139    pub fn box_props(message: impl Into<String>) -> Self {
140        Self::new(LayoutDebugMessageType::BoxProps, message)
141    }
142
143    /// Helper for CSS Getter debug messages
144    #[track_caller]
145    pub fn css_getter(message: impl Into<String>) -> Self {
146        Self::new(LayoutDebugMessageType::CssGetter, message)
147    }
148
149    /// Helper for BFC Layout debug messages
150    #[track_caller]
151    pub fn bfc_layout(message: impl Into<String>) -> Self {
152        Self::new(LayoutDebugMessageType::BfcLayout, message)
153    }
154
155    /// Helper for IFC Layout debug messages
156    #[track_caller]
157    pub fn ifc_layout(message: impl Into<String>) -> Self {
158        Self::new(LayoutDebugMessageType::IfcLayout, message)
159    }
160
161    /// Helper for Table Layout debug messages
162    #[track_caller]
163    pub fn table_layout(message: impl Into<String>) -> Self {
164        Self::new(LayoutDebugMessageType::TableLayout, message)
165    }
166
167    /// Helper for Display Type debug messages
168    #[track_caller]
169    pub fn display_type(message: impl Into<String>) -> Self {
170        Self::new(LayoutDebugMessageType::DisplayType, message)
171    }
172}
173
174/// FFI-safe string type backed by [`U8Vec`] with destructor-based memory management.
175///
176/// Contents are guaranteed to be valid UTF-8 by all safe constructors.
177/// Memory ownership is tracked via the inner `U8Vec`'s destructor field.
178#[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    /// Creates a new `AzString` from a null-terminated C string (const char*).
231    /// This copies the string data into a new allocation.
232    ///
233    /// # Safety
234    /// - `ptr` must be a valid pointer to a null-terminated UTF-8 string
235    /// - The string must remain valid for the duration of this call
236    ///
237    /// Note: `ptr` is `*const i8` rather than `*const core::ffi::c_char`
238    /// so the auto-generated FFI signature in `dll_api_internal.rs`
239    /// (which uses a literal `i8`) matches on every target —
240    /// `c_char` is `i8` on x86/ARM Apple/Windows/Linux but `u8` on
241    /// Android, which would otherwise produce a `*const u8 vs *const i8`
242    /// mismatch at codegen-call sites. We cast internally before
243    /// handing the pointer to `CStr::from_ptr`.
244    #[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    /// Copies bytes from a pointer into a new `AzString`.
255    /// This is useful for C FFI where you have a char* buffer.
256    ///
257    /// Invalid UTF-8 sequences are replaced with U+FFFD to maintain
258    /// the UTF-8 invariant required by [`as_str()`](Self::as_str).
259    ///
260    /// `#[inline]` (2026-06-03 web-lift FIX): forces inlining into the
261    /// `extern "C" AzString_copyFromBytes` wrapper so there is NO separate
262    /// C-ABI(X8-sret) → Rust-ABI(X0-sret) boundary call. The lift mis-threads
263    /// %state across that sret-in-X0 shift (X1/ptr 0x13f80→garbage, X3/len 5→0,
264    /// empty `AzString`); inlining lets the wrapper do the alloc/memcpy directly
265    /// with the standard X8-sret ABI the cascade proves works.
266    #[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        // web-lift FIX (2026-06-03): FAST PATH for already-valid UTF-8 (the common case, incl. all
270        // ASCII like "Hello") — wrap the U8Vec directly, avoiding the `String::from_utf8_lossy()
271        // .into_owned()` std sret-in-X0 call that the lift mis-threads (the returned String comes
272        // back with len=0 → empty AzString). `core::str::from_utf8` returns a `Result<&str,_>` (a
273        // slice, NOT a by-value struct) so it has no sret to mis-thread. Also a real perf win (no
274        // 2nd alloc+copy for valid input). Slow path (rare, invalid UTF-8) keeps the lossy replace.
275        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] // web-lift: inline through the sret-in-X0 chain (see copy_from_bytes)
283    #[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    /// NOTE: CLONES the memory if the memory is external or &'static
295    /// Moves the memory out if the memory is library-allocated
296    #[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    /// Returns the length of the string in bytes (not including null terminator)
333    #[inline]
334    #[must_use] pub const fn len(&self) -> usize {
335        self.vec.len
336    }
337
338    /// Returns true if the string is empty
339    #[inline]
340    #[must_use] pub const fn is_empty(&self) -> bool {
341        self.vec.len == 0
342    }
343
344    /// Creates a null-terminated copy of the string for C FFI usage.
345    /// Returns a new `U8Vec` that contains the string data followed by a null byte.
346    /// The caller is responsible for freeing this memory.
347    ///
348    /// Use this when you need to pass a string to C code that expects `const char*`.
349    #[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); // null terminator
355        U8Vec::from_vec(result)
356    }
357
358    /// Shared implementation for UTF-16 decoding with a caller-supplied byte-order function.
359    ///
360    /// # Safety
361    /// - `ptr` must be valid for reading `len` bytes
362    /// - `len` must be even (UTF-16 uses 2 bytes per code unit)
363    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        // UTF-16 requires pairs of bytes
373        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    /// Creates a new `AzString` from UTF-16 encoded bytes (little-endian).
387    /// Returns an empty string if the input is invalid UTF-16 or has odd length.
388    ///
389    /// # Arguments
390    /// * `ptr` - Pointer to UTF-16 encoded bytes
391    /// * `len` - Length in bytes (not code units) - must be even
392    ///
393    /// # Safety
394    /// - `ptr` must be valid for reading `len` bytes
395    /// - `len` must be even (UTF-16 uses 2 bytes per code unit)
396    #[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    /// Creates a new `AzString` from UTF-16 encoded bytes (big-endian).
402    /// Returns an empty string if the input is invalid UTF-16 or has odd length.
403    ///
404    /// # Arguments
405    /// * `ptr` - Pointer to UTF-16 encoded bytes
406    /// * `len` - Length in bytes (not code units) - must be even
407    ///
408    /// # Safety
409    /// - `ptr` must be valid for reading `len` bytes
410    /// - `len` must be even (UTF-16 uses 2 bytes per code unit)
411    #[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    /// Creates a new `AzString` from UTF-8 bytes with lossy conversion.
417    /// Invalid UTF-8 sequences are replaced with the Unicode replacement character (U+FFFD).
418    ///
419    /// # Safety
420    /// - `ptr` must be valid for reading `len` bytes
421    #[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    /// Creates a new `AzString` from UTF-8 bytes.
433    /// Returns an empty string if the input is not valid UTF-8.
434    ///
435    /// # Safety
436    /// - `ptr` must be valid for reading `len` bytes
437    #[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    /// Copies bytes from a pointer into a new Vec.
516    /// This is useful for C FFI where you have a `uint8_t`* buffer.
517    ///
518    /// # Safety contract (caller must ensure)
519    /// - `ptr` must be valid for reading `start + len` bytes
520    /// - `start + len` must not overflow
521    #[inline] // web-lift: inline through the sret-in-X0 chain (see AzString::copy_from_bytes)
522    #[allow(clippy::not_unsafe_ptr_arg_deref)] // SAFETY/FFI: `*const T` is the C-ABI signature; the fn null-checks then derefs under the documented caller contract (C guarantees a valid ptr/len). Marking it `unsafe fn` would force unsafe blocks into the generated dll bindings.
523    #[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
558// Vec<char>
559impl_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
626// Manual implementations for Hash and Ord on OptionF32 (since f32 doesn't implement these traits)
627impl 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
641// Manual PartialOrd delegating to Ord keeps the two consistent (the derived
642// PartialOrd would diverge from the manual Ord — see derive_ord_xor_partial_ord).
643impl 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
662// ============================================================================
663// StringArena — bump allocator for AzString bytes
664// ============================================================================
665//
666// Consolidates thousands of small AzString allocations (tag names,
667// attribute values, text content) into a handful of 64 KiB chunks.
668// Each arena-backed AzString uses `U8VecDestructor::External` and stashes
669// a cloned `Arc<StringArenaInner>` pointer in the `cap` field — dropping
670// the AzString decrements the refcount, and the backing bytes are freed
671// only when the last reference goes away. This works across FFI without
672// changing any public struct layout.
673
674use alloc::sync::Arc;
675use core::cell::UnsafeCell;
676
677/// Shared interior of a [`StringArena`]. Refcounted via `Arc<Self>`;
678/// never accessed through its `Arc` for mutation — only the owning
679/// `StringArena` (with `&mut self`) mutates the chunks.
680struct StringArenaInner {
681    /// Pre-allocated byte chunks. Pointers into a chunk stay valid
682    /// because we never push past `Vec::capacity()` — no reallocation.
683    chunks: UnsafeCell<Vec<Vec<u8>>>,
684    /// Remaining unused bytes in the last chunk; `0` when a fresh
685    /// chunk is needed.
686    current_remaining: UnsafeCell<usize>,
687}
688
689// Safety:
690// - Mutation through `UnsafeCell` only happens via `&mut StringArena`,
691//   which owns the sole external reference to `Arc<StringArenaInner>`
692//   held in a `StringArena`. Other `Arc` references live inside AzString
693//   destructors and never touch chunks — they only drop the Arc.
694// - `Arc<T>` itself needs `T: Send + Sync` to cross threads; since the
695//   destructor can run on any thread, we claim Send+Sync and rely on the
696//   single-writer invariant for mutation safety.
697unsafe impl Send for StringArenaInner {}
698unsafe impl Sync for StringArenaInner {}
699
700/// Bump allocator backing arena-owned `AzString` instances.
701///
702/// Every `AzString` returned by [`StringArena::intern`] holds a cloned
703/// `Arc` to this arena; the backing bytes stay alive until the last
704/// such `AzString` (and the arena handle itself) is dropped.
705///
706/// Intended use: create one arena per XML/HTML parse pass, intern all
707/// tag names / attribute values / text content through it, then drop the
708/// handle. The `AzStrings` embedded in the resulting `StyledDom` keep the
709/// arena alive for as long as they need the bytes.
710pub struct StringArena {
711    inner: Arc<StringArenaInner>,
712}
713
714impl core::fmt::Debug for StringArena {
715    // StringArenaInner holds UnsafeCell chunks (not Debug) — opaque by design.
716    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    /// Size of a freshly allocated chunk. Large enough that a typical
723    /// DOM parse fits in 1-2 chunks, small enough to not over-allocate
724    /// for small documents.
725    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    /// Returns `(chunk_count, total_bytes_used)` for metrics.
737    #[must_use] pub fn metrics(&self) -> (usize, usize) {
738        // Safety: metrics is read-only; the caller holds &self so no
739        // concurrent mutation via &mut self is possible.
740        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    /// Intern `s` into the arena and return an `AzString` whose backing
748    /// bytes live inside the arena. The returned `AzString` owns a cloned
749    /// `Arc` reference; dropping it decrements the refcount, and the
750    /// arena frees its chunks when the final reference is released.
751    ///
752    /// # Panics
753    ///
754    /// Panics if the arena's internal chunk list is unexpectedly empty when
755    /// appending a non-oversized string (an invariant violation that cannot
756    /// occur through the public API, since a chunk is allocated on demand).
757    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            // Empty strings don't need arena storage; a non-null dangling
763            // pointer is fine because `len == 0` means nobody will deref.
764            core::ptr::NonNull::<u8>::dangling().as_ptr()
765        } else {
766            // Safety: `&mut self` ⇒ exclusive access to inner chunks.
767            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                // Oversized strings get their own dedicated chunk so we
772                // don't waste the tail of the current chunk.
773                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                    // This dedicated chunk is FULL (len == cap). `remaining` must not
779                    // keep describing the previous chunk, or the next small intern
780                    // below would see a stale positive `remaining`, skip allocating,
781                    // and `extend_from_slice` into THIS chunk — reallocating it and
782                    // dangling the `p` we just handed out.
783                    *remaining = 0;
784                    p
785                } else {
786                    if *remaining < len {
787                        chunks.push(Vec::with_capacity(Self::CHUNK_SIZE));
788                        *remaining = Self::CHUNK_SIZE;
789                    }
790                    // Safety: chunk was allocated with capacity ≥ len and
791                    // `remaining` tracks unused capacity — no realloc.
792                    let chunk = chunks.last_mut().unwrap();
793                    let offset = chunk.len();
794                    chunk.extend_from_slice(bytes);
795                    *remaining -= len;
796                    chunk.as_ptr().add(offset)
797                }
798            }
799        };
800
801        // Each AzString carries its own Arc reference count. Stash the
802        // raw Arc pointer in `cap` so the External destructor can decrement.
803        let arc_raw = Arc::into_raw(Arc::clone(&self.inner));
804
805        AzString {
806            vec: U8Vec {
807                ptr,
808                len,
809                // NOTE: `cap` stores an Arc pointer, not a capacity. This
810                // works because the `External` destructor path never calls
811                // `Vec::from_raw_parts(ptr, len, cap)` — only `DefaultRust`
812                // does that.
813                cap: arc_raw as usize,
814                destructor: U8VecDestructor::External(arena_string_destructor),
815            },
816        }
817    }
818}
819
820impl Default for StringArena {
821    fn default() -> Self {
822        Self::new()
823    }
824}
825
826/// Destructor installed on every arena-backed `AzString`. Reads the Arc
827/// pointer out of `cap` and drops one Arc reference; when the count
828/// reaches zero the `StringArenaInner` is freed.
829extern "C" fn arena_string_destructor(vec: *mut U8Vec) {
830    // Safety: called at most once per AzString drop. `cap` was set by
831    // `StringArena::intern` to `Arc::into_raw(Arc<StringArenaInner>)`.
832    unsafe {
833        let v = &mut *vec;
834        let arc_raw = v.cap as *const StringArenaInner;
835        if !arc_raw.is_null() {
836            drop(Arc::from_raw(arc_raw));
837            // Prevent a hypothetical double-drop from dereferencing
838            // freed memory.
839            v.cap = 0;
840        }
841    }
842}
843
844#[cfg(test)]
845mod string_arena_tests {
846    use super::*;
847
848    #[test]
849    fn intern_round_trip() {
850        let mut arena = StringArena::new();
851        let a = arena.intern("hello");
852        let b = arena.intern("world");
853        let c = arena.intern("");
854        assert_eq!(a.as_str(), "hello");
855        assert_eq!(b.as_str(), "world");
856        assert_eq!(c.as_str(), "");
857    }
858
859    #[test]
860    fn strings_outlive_arena_handle() {
861        let a = {
862            let mut arena = StringArena::new();
863            arena.intern("survives drop of arena handle")
864        };
865        assert_eq!(a.as_str(), "survives drop of arena handle");
866    }
867
868    #[test]
869    fn oversized_string_gets_dedicated_chunk() {
870        let mut arena = StringArena::new();
871        let big = "x".repeat(StringArena::CHUNK_SIZE);
872        let s = arena.intern(&big);
873        assert_eq!(s.len(), big.len());
874        assert_eq!(s.as_str(), big.as_str());
875    }
876
877    #[test]
878    fn many_small_strings_share_chunk() {
879        let mut arena = StringArena::new();
880        let mut strings = Vec::new();
881        for i in 0..100 {
882            strings.push(arena.intern(&format!("s{i}")));
883        }
884        let (chunks, _bytes) = arena.metrics();
885        assert!(chunks <= 2, "expected ≤2 chunks for 100 small strings, got {chunks}");
886        for (i, s) in strings.iter().enumerate() {
887            assert_eq!(s.as_str(), format!("s{i}"));
888        }
889    }
890
891    #[test]
892    fn clone_deep_copies_and_is_independent() {
893        // Cloning an External AzString deep-copies into DefaultRust, so
894        // the clone doesn't depend on the arena at all.
895        let clone = {
896            let mut arena = StringArena::new();
897
898            arena.intern("deep-copy test")
899        };
900        assert_eq!(clone.as_str(), "deep-copy test");
901    }
902}
903
904#[cfg(test)]
905#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
906mod autotest_generated {
907    use super::*;
908
909    // ------------------------------------------------------------------
910    // helpers
911    // ------------------------------------------------------------------
912
913    /// Minimal FNV-1a hasher so the Hash-consistency tests don't depend on
914    /// `std` being linked (the crate keeps a `#![no_std]` line commented out).
915    struct Fnv(u64);
916
917    impl core::hash::Hasher for Fnv {
918        fn finish(&self) -> u64 {
919            self.0
920        }
921        fn write(&mut self, bytes: &[u8]) {
922            for b in bytes {
923                self.0 ^= u64::from(*b);
924                self.0 = self.0.wrapping_mul(0x0100_0000_01b3);
925            }
926        }
927    }
928
929    fn hash_of<T: core::hash::Hash>(t: &T) -> u64 {
930        use core::hash::{Hash, Hasher};
931        let mut h = Fnv(0xcbf2_9ce4_8422_2325);
932        Hash::hash(t, &mut h);
933        h.finish()
934    }
935
936    /// UTF-16 code units of `s`, serialized to bytes with the given byte order.
937    fn utf16_bytes(s: &str, little_endian: bool) -> Vec<u8> {
938        s.encode_utf16()
939            .flat_map(|u| {
940                let b = if little_endian {
941                    u.to_le_bytes()
942                } else {
943                    u.to_be_bytes()
944                };
945                [b[0], b[1]]
946            })
947            .collect()
948    }
949
950    // ==================================================================
951    // EmptyStruct
952    // ==================================================================
953
954    #[test]
955    fn empty_struct_new_invariants() {
956        let e = EmptyStruct::new();
957        assert_eq!(e._reserved, 0, "_reserved must always be initialized to 0");
958        assert_eq!(e, EmptyStruct::default(), "new() must equal default()");
959    }
960
961    #[test]
962    fn empty_struct_is_ffi_safe_non_zero_size() {
963        // The whole point of the type: `()` is zero-sized and not FFI-safe.
964        assert_eq!(size_of::<EmptyStruct>(), 1);
965        assert_eq!(align_of::<EmptyStruct>(), 1);
966    }
967
968    #[test]
969    fn empty_struct_unit_conversions_round_trip() {
970        let from_unit = EmptyStruct::from(());
971        assert_eq!(from_unit, EmptyStruct::new());
972        let back: () = EmptyStruct::new().into();
973        assert_eq!(back, ());
974    }
975
976    #[test]
977    fn empty_struct_total_order_is_trivial() {
978        // Every EmptyStruct is equal to every other one, so Ord/Hash must agree.
979        let a = EmptyStruct::new();
980        let b = EmptyStruct::default();
981        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
982        assert_eq!(hash_of(&a), hash_of(&b));
983    }
984
985    // ==================================================================
986    // LayoutDebugMessage
987    // ==================================================================
988
989    #[test]
990    fn debug_message_new_records_fields_and_caller_location() {
991        let m = LayoutDebugMessage::new(LayoutDebugMessageType::Warning, "disk on fire");
992        assert_eq!(m.message_type, LayoutDebugMessageType::Warning);
993        assert_eq!(m.message.as_str(), "disk on fire");
994        assert!(
995            m.location.as_str().contains("corety.rs"),
996            "#[track_caller] must record THIS file, got {:?}",
997            m.location.as_str()
998        );
999
1000        // location is "file:line:column" — the last two segments must be numbers.
1001        let parts: Vec<&str> = m.location.as_str().rsplitn(3, ':').collect();
1002        assert_eq!(parts.len(), 3, "location must be file:line:column");
1003        assert!(parts[0].parse::<u32>().is_ok(), "column must parse as u32");
1004        assert!(parts[1].parse::<u32>().is_ok(), "line must parse as u32");
1005    }
1006
1007    #[test]
1008    fn debug_message_track_caller_propagates_through_helpers() {
1009        // If #[track_caller] were missing on the helpers, both locations would
1010        // collapse to the same line inside LayoutDebugMessage::new().
1011        let a = LayoutDebugMessage::info("a");
1012        let b = LayoutDebugMessage::info("b");
1013        assert_ne!(
1014            a.location.as_str(),
1015            b.location.as_str(),
1016            "two call sites on different lines must record different locations"
1017        );
1018        assert!(a.location.as_str().contains("corety.rs"));
1019    }
1020
1021    #[test]
1022    fn debug_message_helpers_set_the_right_type() {
1023        assert_eq!(
1024            LayoutDebugMessage::info("x").message_type,
1025            LayoutDebugMessageType::Info
1026        );
1027        assert_eq!(
1028            LayoutDebugMessage::warning("x").message_type,
1029            LayoutDebugMessageType::Warning
1030        );
1031        assert_eq!(
1032            LayoutDebugMessage::error("x").message_type,
1033            LayoutDebugMessageType::Error
1034        );
1035        assert_eq!(
1036            LayoutDebugMessage::box_props("x").message_type,
1037            LayoutDebugMessageType::BoxProps
1038        );
1039        assert_eq!(
1040            LayoutDebugMessage::css_getter("x").message_type,
1041            LayoutDebugMessageType::CssGetter
1042        );
1043        assert_eq!(
1044            LayoutDebugMessage::bfc_layout("x").message_type,
1045            LayoutDebugMessageType::BfcLayout
1046        );
1047        assert_eq!(
1048            LayoutDebugMessage::ifc_layout("x").message_type,
1049            LayoutDebugMessageType::IfcLayout
1050        );
1051        assert_eq!(
1052            LayoutDebugMessage::table_layout("x").message_type,
1053            LayoutDebugMessageType::TableLayout
1054        );
1055        assert_eq!(
1056            LayoutDebugMessage::display_type("x").message_type,
1057            LayoutDebugMessageType::DisplayType
1058        );
1059    }
1060
1061    #[test]
1062    fn debug_message_helpers_preserve_the_message_verbatim() {
1063        // Every helper must forward the payload untouched, including empty
1064        // and unicode payloads.
1065        for m in [
1066            LayoutDebugMessage::info(""),
1067            LayoutDebugMessage::warning(""),
1068            LayoutDebugMessage::error(""),
1069            LayoutDebugMessage::box_props(""),
1070            LayoutDebugMessage::css_getter(""),
1071            LayoutDebugMessage::bfc_layout(""),
1072            LayoutDebugMessage::ifc_layout(""),
1073            LayoutDebugMessage::table_layout(""),
1074            LayoutDebugMessage::display_type(""),
1075        ] {
1076            assert!(m.message.is_empty());
1077            assert!(!m.location.is_empty(), "location is always filled in");
1078        }
1079
1080        let weird = "ünïcødé \u{1F600}\n\t\"quoted\" \u{0}nul";
1081        assert_eq!(LayoutDebugMessage::error(weird).message.as_str(), weird);
1082    }
1083
1084    #[test]
1085    fn debug_message_handles_huge_message_without_panicking() {
1086        let huge = "m".repeat(1_000_000);
1087        let m = LayoutDebugMessage::new(LayoutDebugMessageType::PositionCalculation, huge.clone());
1088        assert_eq!(m.message.len(), 1_000_000);
1089        assert_eq!(m.message.as_str(), huge.as_str());
1090        assert_eq!(
1091            m.message_type,
1092            LayoutDebugMessageType::PositionCalculation,
1093            "the variant with no helper must still be constructible via new()"
1094        );
1095    }
1096
1097    #[test]
1098    fn debug_message_default_is_empty_info() {
1099        let m = LayoutDebugMessage::default();
1100        assert_eq!(m.message_type, LayoutDebugMessageType::Info);
1101        assert!(m.message.is_empty());
1102        assert!(m.location.is_empty(), "default() does not track a caller");
1103        assert_eq!(LayoutDebugMessageType::default(), LayoutDebugMessageType::Info);
1104    }
1105
1106    #[test]
1107    fn debug_message_accepts_string_and_str_via_into() {
1108        // `impl Into<String>` must work for both &str and String.
1109        let from_str = LayoutDebugMessage::info("borrowed");
1110        let from_string = LayoutDebugMessage::info(String::from("owned"));
1111        assert_eq!(from_str.message.as_str(), "borrowed");
1112        assert_eq!(from_string.message.as_str(), "owned");
1113    }
1114
1115    #[test]
1116    fn debug_message_clone_is_a_deep_equal_copy() {
1117        let m = LayoutDebugMessage::error("clone me \u{1F600}");
1118        let c = m.clone();
1119        assert_eq!(c, m);
1120        assert_ne!(
1121            c.message.as_bytes().as_ptr(),
1122            m.message.as_bytes().as_ptr(),
1123            "clone must deep-copy the library-owned message bytes"
1124        );
1125    }
1126
1127    // ==================================================================
1128    // AzString — constructors
1129    // ==================================================================
1130
1131    #[test]
1132    fn azstring_default_is_empty_and_readable() {
1133        let s = AzString::default();
1134        assert_eq!(s.as_str(), "");
1135        assert_eq!(s.len(), 0);
1136        assert!(s.is_empty());
1137        assert_eq!(s.as_bytes(), b"");
1138    }
1139
1140    #[test]
1141    fn azstring_from_const_str_borrows_the_static_and_never_frees_it() {
1142        // One binding, used for both the construction and the pointer check —
1143        // rustc is not obliged to dedupe two identical string literals.
1144        const TEXT: &str = "static text";
1145        let s = AzString::from_const_str(TEXT);
1146        assert_eq!(s.as_str(), TEXT);
1147        assert_eq!(s.len(), 11);
1148        assert!(
1149            matches!(s.vec.destructor, U8VecDestructor::NoDestructor),
1150            "a &'static str must not get a freeing destructor"
1151        );
1152        assert_eq!(
1153            s.vec.ptr,
1154            TEXT.as_bytes().as_ptr(),
1155            "from_const_str must alias the static, not copy it"
1156        );
1157    }
1158
1159    #[test]
1160    fn azstring_from_const_str_empty_and_unicode() {
1161        let empty = AzString::from_const_str("");
1162        assert!(empty.is_empty());
1163        assert_eq!(empty.as_str(), "");
1164        assert_eq!(empty.len(), 0);
1165
1166        let uni = AzString::from_const_str("héllo \u{1F600}");
1167        assert_eq!(uni.as_str(), "héllo \u{1F600}");
1168        // len() is BYTES, not chars: 5 ASCII-ish + 1 extra for é + space + 4 for the emoji
1169        assert_eq!(uni.len(), "héllo \u{1F600}".len());
1170        assert_ne!(
1171            uni.len(),
1172            uni.as_str().chars().count(),
1173            "len() must be a byte length, not a char count"
1174        );
1175    }
1176
1177    #[test]
1178    fn azstring_from_string_round_trips_edge_values() {
1179        for input in [
1180            String::new(),
1181            String::from(" "),
1182            String::from("\t\n\r"),
1183            String::from("0"),
1184            String::from("-0"),
1185            String::from("9223372036854775807"), // i64::MAX
1186            String::from("NaN"),
1187            String::from("inf"),
1188            String::from("  valid  "),
1189            String::from("valid;garbage"),
1190            String::from("\u{1F600}\u{0301}\u{0}"), // emoji + combining mark + NUL
1191            "{".repeat(10_000),                     // deeply "nested" junk: no parser, no overflow
1192        ] {
1193            let s = AzString::from_string(input.clone());
1194            assert_eq!(s.as_str(), input.as_str(), "from_string must be verbatim");
1195            assert_eq!(s.len(), input.len());
1196            assert_eq!(s.is_empty(), input.is_empty());
1197            // round-trip back out
1198            assert_eq!(s.into_library_owned_string(), input);
1199        }
1200    }
1201
1202    #[test]
1203    fn azstring_from_string_handles_a_megabyte() {
1204        let huge = "x".repeat(1_000_000);
1205        let s = AzString::from_string(huge.clone());
1206        assert_eq!(s.len(), 1_000_000);
1207        assert_eq!(s.as_str().len(), huge.len());
1208        assert!(s.as_str().bytes().all(|b| b == b'x'));
1209    }
1210
1211    #[test]
1212    fn azstring_from_string_preserves_the_original_capacity() {
1213        // into_library_owned_string rebuilds the String via from_raw_parts(ptr, len, cap).
1214        // If `cap` were not carried through faithfully, this would corrupt the heap.
1215        let mut owned = String::with_capacity(4096);
1216        owned.push_str("hi");
1217        let s = AzString::from_string(owned);
1218        assert!(matches!(s.vec.destructor, U8VecDestructor::DefaultRust));
1219        let back = s.into_library_owned_string();
1220        assert_eq!(back, "hi");
1221        assert!(
1222            back.capacity() >= 4096,
1223            "capacity must survive the AzString round-trip, got {}",
1224            back.capacity()
1225        );
1226    }
1227
1228    // ==================================================================
1229    // AzString::copy_from_bytes  (numeric / pointer edge cases)
1230    // ==================================================================
1231
1232    #[test]
1233    fn azstring_copy_from_bytes_zero_len_is_empty() {
1234        let buf = b"hello";
1235        let s = AzString::copy_from_bytes(buf.as_ptr(), 0, 0);
1236        assert!(s.is_empty());
1237        assert_eq!(s.as_str(), "");
1238    }
1239
1240    #[test]
1241    fn azstring_copy_from_bytes_null_ptr_is_empty() {
1242        let s = AzString::copy_from_bytes(core::ptr::null(), 0, 16);
1243        assert!(s.is_empty());
1244        assert_eq!(s.as_str(), "");
1245    }
1246
1247    #[test]
1248    fn azstring_copy_from_bytes_honours_the_start_offset() {
1249        let buf = b"0123456789";
1250        let s = AzString::copy_from_bytes(buf.as_ptr(), 3, 4);
1251        assert_eq!(s.as_str(), "3456");
1252        assert_eq!(s.len(), 4);
1253    }
1254
1255    #[test]
1256    fn azstring_copy_from_bytes_start_at_end_with_zero_len_is_empty() {
1257        // start == buf.len() is only legal because len == 0 short-circuits
1258        // before the pointer is ever offset.
1259        let buf = b"abc";
1260        let s = AzString::copy_from_bytes(buf.as_ptr(), buf.len(), 0);
1261        assert!(s.is_empty());
1262    }
1263
1264    #[test]
1265    fn azstring_copy_from_bytes_zero_len_wins_over_start_overflow() {
1266        // start + len overflows usize, but len == 0 must short-circuit BEFORE
1267        // the debug_assert / ptr.add() — no panic, no UB.
1268        let buf = b"abc";
1269        let s = AzString::copy_from_bytes(buf.as_ptr(), usize::MAX, 0);
1270        assert!(s.is_empty());
1271    }
1272
1273    #[test]
1274    fn azstring_copy_from_bytes_null_wins_over_max_len() {
1275        // The null check must precede everything, even for absurd start/len.
1276        let s = AzString::copy_from_bytes(core::ptr::null(), usize::MAX, usize::MAX);
1277        assert!(s.is_empty());
1278        assert_eq!(s.as_str(), "");
1279    }
1280
1281    #[test]
1282    fn azstring_copy_from_bytes_replaces_invalid_utf8_lossily() {
1283        // Slicing "héllo" mid-codepoint leaves a stray continuation byte (0xA9),
1284        // which must become U+FFFD so the as_str() UTF-8 invariant still holds.
1285        let buf = "héllo".as_bytes();
1286        assert_eq!(buf[1], 0xC3);
1287        assert_eq!(buf[2], 0xA9);
1288        let s = AzString::copy_from_bytes(buf.as_ptr(), 2, 2);
1289        assert_eq!(s.as_str(), "\u{FFFD}l");
1290        // The UTF-8 invariant as_str() relies on must actually hold:
1291        assert!(core::str::from_utf8(s.as_bytes()).is_ok());
1292    }
1293
1294    #[test]
1295    fn azstring_copy_from_bytes_keeps_valid_utf8_byte_for_byte() {
1296        let buf = "héllo \u{1F600}".as_bytes();
1297        let s = AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len());
1298        assert_eq!(s.as_str(), "héllo \u{1F600}");
1299        assert_eq!(s.as_bytes(), buf);
1300    }
1301
1302    #[test]
1303    fn azstring_copy_from_bytes_preserves_interior_nul() {
1304        let buf = b"a\0b";
1305        let s = AzString::copy_from_bytes(buf.as_ptr(), 0, 3);
1306        assert_eq!(s.len(), 3, "an interior NUL is data, not a terminator");
1307        assert_eq!(s.as_bytes(), b"a\0b");
1308    }
1309
1310    // ==================================================================
1311    // U8Vec::copy_from_bytes  (numeric / pointer edge cases)
1312    // ==================================================================
1313
1314    #[test]
1315    fn u8vec_copy_from_bytes_zero_len_is_empty() {
1316        let buf = b"hello";
1317        let v = U8Vec::copy_from_bytes(buf.as_ptr(), 0, 0);
1318        assert!(v.is_empty());
1319        assert_eq!(v.as_ref(), b"");
1320    }
1321
1322    #[test]
1323    fn u8vec_copy_from_bytes_null_ptr_is_empty() {
1324        let v = U8Vec::copy_from_bytes(core::ptr::null(), 0, 8);
1325        assert!(v.is_empty());
1326        assert_eq!(v.len(), 0);
1327    }
1328
1329    #[test]
1330    fn u8vec_copy_from_bytes_null_wins_over_max_start_and_len() {
1331        // Neither the debug_assert nor ptr.add() may be reached for a null ptr.
1332        let v = U8Vec::copy_from_bytes(core::ptr::null(), usize::MAX, usize::MAX);
1333        assert!(v.is_empty());
1334    }
1335
1336    #[test]
1337    fn u8vec_copy_from_bytes_zero_len_wins_over_start_overflow() {
1338        // start + len overflows, but len == 0 short-circuits first.
1339        let buf = b"abc";
1340        let v = U8Vec::copy_from_bytes(buf.as_ptr(), usize::MAX, 0);
1341        assert!(v.is_empty());
1342    }
1343
1344    #[test]
1345    fn u8vec_copy_from_bytes_copies_the_requested_window() {
1346        let buf: Vec<u8> = (0u8..=255).collect();
1347        let v = U8Vec::copy_from_bytes(buf.as_ptr(), 250, 6);
1348        assert_eq!(v.as_ref(), &[250, 251, 252, 253, 254, 255]);
1349        assert_eq!(v.len(), 6);
1350    }
1351
1352    #[test]
1353    fn u8vec_copy_from_bytes_owns_its_copy() {
1354        // The copy must survive the source buffer being dropped.
1355        let v = {
1356            let buf = vec![1u8, 2, 3, 4];
1357            U8Vec::copy_from_bytes(buf.as_ptr(), 1, 2)
1358        };
1359        assert_eq!(v.as_ref(), &[2, 3]);
1360        assert!(matches!(v.destructor, U8VecDestructor::DefaultRust));
1361    }
1362
1363    #[test]
1364    fn u8vec_copy_from_bytes_accepts_all_byte_values() {
1365        // Arbitrary (non-UTF-8) bytes must round-trip unchanged — U8Vec has no
1366        // encoding invariant, unlike AzString.
1367        let buf: Vec<u8> = (0u8..=255).collect();
1368        let v = U8Vec::copy_from_bytes(buf.as_ptr(), 0, buf.len());
1369        assert_eq!(v.as_ref(), buf.as_slice());
1370    }
1371
1372    // ==================================================================
1373    // AzString::from_c_str
1374    // ==================================================================
1375
1376    #[test]
1377    fn azstring_from_c_str_null_is_empty() {
1378        let s = unsafe { AzString::from_c_str(core::ptr::null()) };
1379        assert!(s.is_empty());
1380        assert_eq!(s.as_str(), "");
1381    }
1382
1383    #[test]
1384    fn azstring_from_c_str_reads_up_to_the_terminator() {
1385        let c = b"hello\0trailing garbage\0";
1386        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1387        assert_eq!(s.as_str(), "hello");
1388        assert_eq!(s.len(), 5, "the NUL terminator is not part of the string");
1389    }
1390
1391    #[test]
1392    fn azstring_from_c_str_empty_c_string_is_empty() {
1393        let c = b"\0";
1394        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1395        assert!(s.is_empty());
1396    }
1397
1398    #[test]
1399    fn azstring_from_c_str_replaces_non_utf8_bytes() {
1400        // A latin-1 "café" is not valid UTF-8; it must come back lossily
1401        // rather than violating the as_str() invariant.
1402        let c = b"caf\xE9\0";
1403        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1404        assert_eq!(s.as_str(), "caf\u{FFFD}");
1405        assert!(core::str::from_utf8(s.as_bytes()).is_ok());
1406    }
1407
1408    #[test]
1409    fn azstring_from_c_str_handles_a_long_c_string() {
1410        let mut c = "z".repeat(100_000).into_bytes();
1411        c.push(0);
1412        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1413        assert_eq!(s.len(), 100_000);
1414    }
1415
1416    // ==================================================================
1417    // AzString::to_c_str  (+ round trip through from_c_str)
1418    // ==================================================================
1419
1420    #[test]
1421    fn azstring_to_c_str_appends_exactly_one_nul() {
1422        let s = AzString::from_const_str("abc");
1423        let c = s.to_c_str();
1424        assert_eq!(c.as_ref(), b"abc\0");
1425        assert_eq!(c.len(), s.len() + 1);
1426    }
1427
1428    #[test]
1429    fn azstring_to_c_str_of_empty_is_just_the_terminator() {
1430        let c = AzString::default().to_c_str();
1431        assert_eq!(c.as_ref(), b"\0");
1432        assert_eq!(c.len(), 1);
1433    }
1434
1435    #[test]
1436    fn azstring_c_str_round_trip() {
1437        for original in ["", "abc", "héllo \u{1F600}", "  spaced  "] {
1438            let s = AzString::from_const_str(original);
1439            let c = s.to_c_str();
1440            let back = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1441            assert_eq!(back.as_str(), original, "C round-trip must be lossless");
1442            assert_eq!(back, s);
1443        }
1444    }
1445
1446    #[test]
1447    fn azstring_c_str_round_trip_truncates_at_an_interior_nul() {
1448        // Documented C-string reality: a string containing a NUL cannot survive
1449        // a *const char* round-trip. Assert the truncation is deterministic
1450        // rather than pretending it round-trips.
1451        let s = AzString::from_string(String::from("a\0b"));
1452        let c = s.to_c_str();
1453        assert_eq!(c.as_ref(), b"a\0b\0", "to_c_str keeps the interior NUL");
1454        let back = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1455        assert_eq!(back.as_str(), "a", "from_c_str stops at the first NUL");
1456    }
1457
1458    #[test]
1459    fn azstring_to_c_str_is_an_independent_allocation() {
1460        let s = AzString::from_const_str("shared?");
1461        let c = s.to_c_str();
1462        assert!(matches!(c.destructor, U8VecDestructor::DefaultRust));
1463        assert_ne!(
1464            c.as_ptr(),
1465            s.as_bytes().as_ptr(),
1466            "to_c_str must copy, not alias the source"
1467        );
1468        assert_eq!(s.as_str(), "shared?", "source must be untouched");
1469    }
1470
1471    // ==================================================================
1472    // AzString::from_utf8 / from_utf8_lossy
1473    // ==================================================================
1474
1475    #[test]
1476    fn azstring_from_utf8_null_or_zero_len_is_empty() {
1477        let buf = b"abc";
1478        assert!(unsafe { AzString::from_utf8(core::ptr::null(), 3) }.is_empty());
1479        assert!(unsafe { AzString::from_utf8(buf.as_ptr(), 0) }.is_empty());
1480        assert!(unsafe { AzString::from_utf8_lossy(core::ptr::null(), 3) }.is_empty());
1481        assert!(unsafe { AzString::from_utf8_lossy(buf.as_ptr(), 0) }.is_empty());
1482    }
1483
1484    #[test]
1485    fn azstring_from_utf8_accepts_valid_multibyte() {
1486        let buf = "héllo \u{1F600}".as_bytes();
1487        let s = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
1488        assert_eq!(s.as_str(), "héllo \u{1F600}");
1489        assert_eq!(s.len(), buf.len());
1490    }
1491
1492    #[test]
1493    fn azstring_from_utf8_rejects_invalid_but_lossy_replaces_it() {
1494        // A truncated 2-byte sequence: strict → empty, lossy → U+FFFD.
1495        let buf = b"caf\xC3";
1496        let strict = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
1497        assert!(
1498            strict.is_empty(),
1499            "from_utf8 must return an EMPTY string for invalid UTF-8, got {:?}",
1500            strict.as_str()
1501        );
1502        let lossy = unsafe { AzString::from_utf8_lossy(buf.as_ptr(), buf.len()) };
1503        assert_eq!(lossy.as_str(), "caf\u{FFFD}");
1504    }
1505
1506    #[test]
1507    fn azstring_from_utf8_rejects_overlong_and_stray_continuations() {
1508        for bad in [
1509            &b"\xC0\xAF"[..],         // overlong encoding of '/'
1510            &b"\xED\xA0\x80"[..],     // UTF-16 surrogate half, illegal in UTF-8
1511            &b"\xF8\x88\x80\x80"[..], // 5-byte sequence, illegal since RFC 3629
1512            &b"\x80"[..],             // stray continuation byte
1513            &b"\xFF\xFE"[..],         // never-valid bytes
1514        ] {
1515            let strict = unsafe { AzString::from_utf8(bad.as_ptr(), bad.len()) };
1516            assert!(strict.is_empty(), "from_utf8 must reject {bad:?}");
1517
1518            let lossy = unsafe { AzString::from_utf8_lossy(bad.as_ptr(), bad.len()) };
1519            assert!(
1520                lossy.as_str().contains('\u{FFFD}'),
1521                "from_utf8_lossy must substitute U+FFFD for {bad:?}"
1522            );
1523            // Both paths must uphold the UTF-8 invariant that as_str() assumes.
1524            assert!(core::str::from_utf8(lossy.as_bytes()).is_ok());
1525        }
1526    }
1527
1528    #[test]
1529    fn azstring_from_utf8_keeps_interior_nul() {
1530        let buf = b"a\0b";
1531        let s = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
1532        assert_eq!(s.len(), 3);
1533        assert_eq!(s.as_bytes(), b"a\0b");
1534    }
1535
1536    #[test]
1537    fn azstring_from_utf8_handles_a_megabyte() {
1538        let buf = "y".repeat(1_000_000);
1539        let s = unsafe { AzString::from_utf8(buf.as_bytes().as_ptr(), buf.len()) };
1540        assert_eq!(s.len(), 1_000_000);
1541    }
1542
1543    // ==================================================================
1544    // AzString::from_utf16_le / from_utf16_be / from_utf16_with_byte_order
1545    // ==================================================================
1546
1547    #[test]
1548    fn azstring_from_utf16_le_decodes_bmp_and_surrogate_pairs() {
1549        let text = "héllo \u{1F600}"; // the emoji needs a surrogate pair
1550        let bytes = utf16_bytes(text, true);
1551        let s = unsafe { AzString::from_utf16_le(bytes.as_ptr(), bytes.len()) };
1552        assert_eq!(s.as_str(), text);
1553    }
1554
1555    #[test]
1556    fn azstring_from_utf16_be_decodes_bmp_and_surrogate_pairs() {
1557        let text = "héllo \u{1F600}";
1558        let bytes = utf16_bytes(text, false);
1559        let s = unsafe { AzString::from_utf16_be(bytes.as_ptr(), bytes.len()) };
1560        assert_eq!(s.as_str(), text);
1561    }
1562
1563    #[test]
1564    fn azstring_from_utf16_byte_order_actually_matters() {
1565        // Decoding LE bytes as BE must NOT silently yield the same text.
1566        let le = utf16_bytes("AB", true);
1567        assert_eq!(le.as_slice(), &[0x41, 0x00, 0x42, 0x00]);
1568        let as_be = unsafe { AzString::from_utf16_be(le.as_ptr(), le.len()) };
1569        assert_eq!(
1570            as_be.as_str(),
1571            "\u{4100}\u{4200}",
1572            "BE decode of LE bytes must byte-swap, not guess"
1573        );
1574        assert_ne!(as_be.as_str(), "AB");
1575    }
1576
1577    #[test]
1578    fn azstring_from_utf16_odd_length_is_empty() {
1579        let bytes = utf16_bytes("hello", true);
1580        let odd = bytes.len() - 1;
1581        assert_eq!(odd % 2, 1);
1582        // Still inside the buffer, so this is a safe call — it must be rejected
1583        // on the length check, not read a half code unit.
1584        assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), odd) }.is_empty());
1585        assert!(unsafe { AzString::from_utf16_be(bytes.as_ptr(), odd) }.is_empty());
1586        // The smallest odd length of all:
1587        assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), 1) }.is_empty());
1588    }
1589
1590    #[test]
1591    fn azstring_from_utf16_null_or_zero_len_is_empty() {
1592        let bytes = utf16_bytes("hi", true);
1593        assert!(unsafe { AzString::from_utf16_le(core::ptr::null(), 4) }.is_empty());
1594        assert!(unsafe { AzString::from_utf16_be(core::ptr::null(), 4) }.is_empty());
1595        assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), 0) }.is_empty());
1596        assert!(unsafe { AzString::from_utf16_be(bytes.as_ptr(), 0) }.is_empty());
1597    }
1598
1599    #[test]
1600    fn azstring_from_utf16_unpaired_surrogate_is_empty() {
1601        // A lone high surrogate is not valid UTF-16 → documented empty result.
1602        let lone_high: [u8; 2] = 0xD83C_u16.to_le_bytes();
1603        assert!(unsafe { AzString::from_utf16_le(lone_high.as_ptr(), 2) }.is_empty());
1604
1605        // A lone LOW surrogate, and a reversed (low-then-high) pair.
1606        let lone_low: [u8; 2] = 0xDF89_u16.to_le_bytes();
1607        assert!(unsafe { AzString::from_utf16_le(lone_low.as_ptr(), 2) }.is_empty());
1608
1609        let reversed: Vec<u8> = [0xDF89_u16, 0xD83C_u16]
1610            .iter()
1611            .flat_map(|u| u.to_le_bytes())
1612            .collect();
1613        assert!(unsafe { AzString::from_utf16_le(reversed.as_ptr(), reversed.len()) }.is_empty());
1614    }
1615
1616    #[test]
1617    fn azstring_from_utf16_decodes_noncharacters_and_nul() {
1618        // U+FFFE / U+0000 are valid code points (not surrogates) — they must
1619        // decode rather than being treated as an error or a terminator.
1620        let units: Vec<u8> = [0x0041_u16, 0x0000, 0xFFFE]
1621            .iter()
1622            .flat_map(|u| u.to_le_bytes())
1623            .collect();
1624        let s = unsafe { AzString::from_utf16_le(units.as_ptr(), units.len()) };
1625        assert_eq!(s.as_str(), "A\u{0}\u{FFFE}");
1626        assert_eq!(s.len(), 1 + 1 + 3);
1627    }
1628
1629    #[test]
1630    fn azstring_from_utf16_handles_100k_code_units() {
1631        let text = "ab".repeat(50_000);
1632        let bytes = utf16_bytes(&text, true);
1633        assert_eq!(bytes.len(), 200_000);
1634        let s = unsafe { AzString::from_utf16_le(bytes.as_ptr(), bytes.len()) };
1635        assert_eq!(s.len(), 100_000);
1636    }
1637
1638    #[test]
1639    fn azstring_from_utf16_with_byte_order_honours_the_supplied_fn() {
1640        // The private shared impl must use the caller's byte-order fn verbatim.
1641        fn swap_halves(b: [u8; 2]) -> u16 {
1642            u16::from_be_bytes(b)
1643        }
1644        let le = utf16_bytes("Az", true);
1645        let via_shared = unsafe {
1646            AzString::from_utf16_with_byte_order(le.as_ptr(), le.len(), u16::from_le_bytes)
1647        };
1648        assert_eq!(via_shared.as_str(), "Az");
1649
1650        let swapped = unsafe {
1651            AzString::from_utf16_with_byte_order(le.as_ptr(), le.len(), swap_halves)
1652        };
1653        assert_eq!(swapped.as_str(), "\u{4100}\u{7A00}");
1654
1655        // The odd-length / null guards live in the shared impl, so check them here too.
1656        assert!(unsafe {
1657            AzString::from_utf16_with_byte_order(le.as_ptr(), 3, u16::from_le_bytes)
1658        }
1659        .is_empty());
1660        assert!(unsafe {
1661            AzString::from_utf16_with_byte_order(core::ptr::null(), 2, u16::from_le_bytes)
1662        }
1663        .is_empty());
1664    }
1665
1666    // ==================================================================
1667    // AzString — getters / predicates / conversions
1668    // ==================================================================
1669
1670    #[test]
1671    fn azstring_as_str_and_as_bytes_agree_for_every_constructor() {
1672        let buf = "mixed \u{1F600}".as_bytes();
1673        let mut arena = StringArena::new();
1674        let strings = [
1675            AzString::default(),
1676            AzString::from_const_str("mixed \u{1F600}"),
1677            AzString::from_string(String::from("mixed \u{1F600}")),
1678            AzString::from("mixed \u{1F600}"),
1679            AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len()),
1680            unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) },
1681            arena.intern("mixed \u{1F600}"),
1682        ];
1683        for s in &strings {
1684            assert_eq!(
1685                s.as_bytes(),
1686                s.as_str().as_bytes(),
1687                "as_bytes() and as_str() must view the same memory"
1688            );
1689            assert_eq!(s.len(), s.as_bytes().len());
1690            assert_eq!(s.is_empty(), s.len() == 0);
1691            let via_as_ref: &str = s.as_ref();
1692            assert_eq!(via_as_ref, s.as_str(), "AsRef must match as_str");
1693            assert_eq!(&**s, s.as_str(), "Deref must match as_str");
1694        }
1695    }
1696
1697    #[test]
1698    fn azstring_is_empty_only_for_zero_bytes() {
1699        assert!(AzString::default().is_empty());
1700        assert!(AzString::from_const_str("").is_empty());
1701        assert!(AzString::from_string(String::new()).is_empty());
1702        // Whitespace and a NUL byte are content, not emptiness.
1703        assert!(!AzString::from_const_str(" ").is_empty());
1704        assert!(!AzString::from_const_str("\t\n").is_empty());
1705        assert!(!AzString::from_string(String::from("\0")).is_empty());
1706        assert_eq!(AzString::from_string(String::from("\0")).len(), 1);
1707    }
1708
1709    #[test]
1710    fn azstring_len_counts_bytes_not_chars() {
1711        assert_eq!(AzString::from_const_str("é").len(), 2);
1712        assert_eq!(AzString::from_const_str("\u{1F600}").len(), 4);
1713        assert_eq!(AzString::from_const_str("e\u{0301}").len(), 3); // combining accent
1714        assert_eq!(AzString::from_const_str("\u{1F600}").as_str().chars().count(), 1);
1715    }
1716
1717    #[test]
1718    fn azstring_into_bytes_moves_without_copying_or_double_freeing() {
1719        let s = AzString::from_string(String::from("payload"));
1720        let ptr = s.as_bytes().as_ptr();
1721        let (len, cap) = (s.vec.len, s.vec.cap);
1722        let v = s.into_bytes();
1723        assert_eq!(v.as_ref(), b"payload");
1724        assert_eq!(v.as_ptr(), ptr, "into_bytes must move, not copy");
1725        assert_eq!(v.len(), len);
1726        assert_eq!(v.capacity(), cap);
1727        assert!(matches!(v.destructor, U8VecDestructor::DefaultRust));
1728        // Dropping `v` here frees the buffer exactly once (the source was
1729        // ManuallyDrop'd) — a double free would abort the test process.
1730    }
1731
1732    #[test]
1733    fn azstring_into_bytes_preserves_a_non_owning_destructor() {
1734        let v = AzString::from_const_str("static").into_bytes();
1735        assert_eq!(v.as_ref(), b"static");
1736        assert!(
1737            matches!(v.destructor, U8VecDestructor::NoDestructor),
1738            "a &'static-backed AzString must not gain a freeing destructor"
1739        );
1740    }
1741
1742    #[test]
1743    fn azstring_into_bytes_of_empty_is_empty() {
1744        let v = AzString::default().into_bytes();
1745        assert!(v.is_empty());
1746        assert_eq!(v.as_ref(), b"");
1747    }
1748
1749    #[test]
1750    fn azstring_into_library_owned_string_works_for_all_destructors() {
1751        // DefaultRust: moves the allocation out.
1752        assert_eq!(
1753            AzString::from_string(String::from("owned \u{1F600}")).into_library_owned_string(),
1754            "owned \u{1F600}"
1755        );
1756        // NoDestructor: must COPY the static, never take ownership of it.
1757        assert_eq!(
1758            AzString::from_const_str("static").into_library_owned_string(),
1759            "static"
1760        );
1761        // External (arena-backed): must copy out of the arena.
1762        let owned = {
1763            let mut arena = StringArena::new();
1764            let s = arena.intern("interned");
1765            s.into_library_owned_string()
1766        };
1767        assert_eq!(owned, "interned", "must outlive the arena it was copied from");
1768        // Empty / default.
1769        assert_eq!(AzString::default().into_library_owned_string(), "");
1770    }
1771
1772    #[test]
1773    fn azstring_into_library_owned_string_copies_static_memory() {
1774        let mut owned = AzString::from_const_str("static").into_library_owned_string();
1775        // If this had aliased the &'static str, mutating it would be UB /
1776        // a segfault writing to rodata.
1777        owned.push_str(" + mutable");
1778        assert_eq!(owned, "static + mutable");
1779    }
1780
1781    // ==================================================================
1782    // AzString::clone_self
1783    // ==================================================================
1784
1785    #[test]
1786    fn azstring_clone_self_deep_copies_library_owned_memory() {
1787        let s = AzString::from_string(String::from("deep"));
1788        let c = s.clone_self();
1789        assert_eq!(c, s);
1790        assert_ne!(
1791            c.as_bytes().as_ptr(),
1792            s.as_bytes().as_ptr(),
1793            "a DefaultRust clone must own a fresh allocation"
1794        );
1795        assert!(matches!(c.vec.destructor, U8VecDestructor::DefaultRust));
1796    }
1797
1798    #[test]
1799    fn azstring_clone_self_shares_static_memory() {
1800        let s = AzString::from_const_str("static");
1801        let c = s.clone_self();
1802        assert_eq!(c, s);
1803        assert_eq!(
1804            c.as_bytes().as_ptr(),
1805            s.as_bytes().as_ptr(),
1806            "cloning a &'static-backed string should alias, not allocate"
1807        );
1808        assert!(matches!(c.vec.destructor, U8VecDestructor::NoDestructor));
1809    }
1810
1811    #[test]
1812    fn azstring_clone_self_of_empty_and_unicode() {
1813        for s in [
1814            AzString::default(),
1815            AzString::from_const_str(""),
1816            AzString::from_string(String::from("\u{1F600}\u{0}\u{0301}")),
1817        ] {
1818            let c = s.clone_self();
1819            assert_eq!(c.as_str(), s.as_str());
1820            assert_eq!(c.len(), s.len());
1821        }
1822    }
1823
1824    #[test]
1825    fn azstring_clone_trait_matches_clone_self() {
1826        let s = AzString::from_string(String::from("via trait"));
1827        assert_eq!(s.clone(), s.clone_self());
1828    }
1829
1830    // ==================================================================
1831    // AzString — Debug / Display round trips (fmt)
1832    // ==================================================================
1833
1834    #[test]
1835    fn azstring_display_round_trips_through_from() {
1836        for original in [
1837            "",
1838            " ",
1839            "plain",
1840            "héllo \u{1F600}",
1841            "with \"quotes\" and \\ backslash",
1842            "line\nbreak\ttab",
1843            "e\u{0301} combining",
1844        ] {
1845            let s = AzString::from(original);
1846            let rendered = format!("{s}");
1847            assert_eq!(rendered, original, "Display must emit the string verbatim");
1848            let reparsed = AzString::from(rendered.as_str());
1849            assert_eq!(reparsed, s, "parse(serialize(x)) == x");
1850            // serialize(parse(serialize(x))) == serialize(x)
1851            assert_eq!(format!("{reparsed}"), rendered);
1852        }
1853    }
1854
1855    #[test]
1856    fn azstring_debug_matches_str_debug_and_escapes() {
1857        let s = AzString::from("a\"b\\c\nd");
1858        let expected = format!("{:?}", "a\"b\\c\nd");
1859        assert_eq!(format!("{s:?}"), expected, "Debug must delegate to str::fmt");
1860        assert!(format!("{s:?}").starts_with('"'), "Debug output must be quoted");
1861        assert!(!format!("{s:?}").contains('\n'), "Debug must escape newlines");
1862    }
1863
1864    #[test]
1865    fn azstring_debug_and_display_of_empty_do_not_panic() {
1866        assert_eq!(format!("{:?}", AzString::default()), "\"\"");
1867        assert_eq!(format!("{}", AzString::default()), "");
1868        assert_eq!(format!("{:?}", AzString::from_const_str("")), "\"\"");
1869    }
1870
1871    #[test]
1872    fn azstring_display_of_a_megabyte_is_lossless() {
1873        let huge = "q".repeat(1_000_000);
1874        let s = AzString::from_string(huge.clone());
1875        assert_eq!(format!("{s}").len(), huge.len());
1876    }
1877
1878    #[test]
1879    fn azstring_debug_is_stable_across_constructors() {
1880        // Same text, different memory ownership → identical rendering.
1881        let buf = "same".as_bytes();
1882        let a = AzString::from_const_str("same");
1883        let b = AzString::from_string(String::from("same"));
1884        let c = AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len());
1885        assert_eq!(format!("{a:?}"), format!("{b:?}"));
1886        assert_eq!(format!("{b:?}"), format!("{c:?}"));
1887        assert_eq!(format!("{a}"), format!("{c}"));
1888    }
1889
1890    // ==================================================================
1891    // AzString — Eq / Ord / Hash invariants
1892    // ==================================================================
1893
1894    #[test]
1895    fn azstring_eq_and_hash_ignore_memory_ownership() {
1896        let buf = "key".as_bytes();
1897        let mut arena = StringArena::new();
1898        let variants = [
1899            AzString::from_const_str("key"),
1900            AzString::from_string(String::from("key")),
1901            AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len()),
1902            arena.intern("key"),
1903        ];
1904        for v in &variants {
1905            assert_eq!(*v, variants[0], "equality must compare CONTENT, not pointers");
1906            assert_eq!(
1907                hash_of(v),
1908                hash_of(&variants[0]),
1909                "Hash must agree with Eq across destructor kinds"
1910            );
1911            assert_eq!(
1912                hash_of(v),
1913                hash_of(&"key"),
1914                "AzString must hash like the &str it wraps"
1915            );
1916        }
1917    }
1918
1919    #[test]
1920    fn azstring_ord_matches_str_ord() {
1921        let mut v = [
1922            AzString::from("b"),
1923            AzString::from(""),
1924            AzString::from("\u{1F600}"),
1925            AzString::from("a"),
1926            AzString::from("ab"),
1927        ];
1928        v.sort();
1929        let sorted: Vec<&str> = v.iter().map(AzString::as_str).collect();
1930        assert_eq!(sorted, ["", "a", "ab", "b", "\u{1F600}"]);
1931        assert_eq!(
1932            AzString::from("a").partial_cmp(&AzString::from("b")),
1933            Some(core::cmp::Ordering::Less)
1934        );
1935        assert_eq!(
1936            AzString::from("x").cmp(&AzString::from("x")),
1937            core::cmp::Ordering::Equal
1938        );
1939    }
1940
1941    // ==================================================================
1942    // StringArena
1943    // ==================================================================
1944
1945    #[test]
1946    fn arena_new_starts_empty() {
1947        let arena = StringArena::new();
1948        assert_eq!(arena.metrics(), (0, 0), "a fresh arena allocates nothing");
1949        assert_eq!(StringArena::default().metrics(), (0, 0));
1950    }
1951
1952    #[test]
1953    fn arena_metrics_track_chunks_and_bytes() {
1954        let mut arena = StringArena::new();
1955        let _a = arena.intern("abc");
1956        let (chunks, bytes) = arena.metrics();
1957        assert_eq!(chunks, 1);
1958        assert_eq!(bytes, 3);
1959        let _b = arena.intern("de");
1960        let (chunks, bytes) = arena.metrics();
1961        assert_eq!(chunks, 1, "a second small string reuses the open chunk");
1962        assert_eq!(bytes, 5);
1963    }
1964
1965    #[test]
1966    fn arena_empty_string_allocates_nothing_and_is_readable() {
1967        let mut arena = StringArena::new();
1968        let e = arena.intern("");
1969        assert!(e.is_empty());
1970        assert_eq!(e.as_str(), "");
1971        assert_eq!(arena.metrics(), (0, 0), "empty strings need no storage");
1972        assert!(!e.vec.ptr.is_null(), "the dangling ptr must still be non-null");
1973    }
1974
1975    #[test]
1976    fn arena_string_is_external_and_stashes_an_arc_in_cap() {
1977        let mut arena = StringArena::new();
1978        let s = arena.intern("hi");
1979        assert!(matches!(s.vec.destructor, U8VecDestructor::External(_)));
1980        assert_ne!(s.vec.cap, 0, "cap holds the Arc pointer, not a capacity");
1981        assert_eq!(s.as_str(), "hi");
1982    }
1983
1984    #[test]
1985    fn arena_intern_refcounts_each_string() {
1986        let mut arena = StringArena::new();
1987        assert_eq!(Arc::strong_count(&arena.inner), 1);
1988        let a = arena.intern("one");
1989        let b = arena.intern("two");
1990        assert_eq!(
1991            Arc::strong_count(&arena.inner),
1992            3,
1993            "each interned string must hold its own Arc reference"
1994        );
1995        drop(a);
1996        assert_eq!(Arc::strong_count(&arena.inner), 2);
1997        drop(b);
1998        assert_eq!(Arc::strong_count(&arena.inner), 1);
1999    }
2000
2001    #[test]
2002    fn arena_clone_deep_copies_and_does_not_bump_the_refcount() {
2003        let mut arena = StringArena::new();
2004        let s = arena.intern("interned");
2005        let c = s.clone_self();
2006        assert_eq!(
2007            Arc::strong_count(&arena.inner),
2008            2,
2009            "cloning an External string deep-copies; it must NOT retain the arena"
2010        );
2011        assert!(matches!(c.vec.destructor, U8VecDestructor::DefaultRust));
2012        assert_eq!(c.as_str(), "interned");
2013        assert_ne!(c.vec.ptr, s.vec.ptr);
2014    }
2015
2016    #[test]
2017    fn arena_clone_outlives_the_arena_and_the_original() {
2018        let clone = {
2019            let mut arena = StringArena::new();
2020            let s = arena.intern("deep-copied out of the arena");
2021            let c = s.clone_self();
2022            drop(s);
2023            drop(arena);
2024            c
2025        };
2026        assert_eq!(clone.as_str(), "deep-copied out of the arena");
2027    }
2028
2029    #[test]
2030    fn arena_exact_half_chunk_boundary_fills_one_chunk_exactly() {
2031        // len == CHUNK_SIZE / 2 is NOT "oversized" (the check is `>`), so two of
2032        // them must fit in a single chunk with zero reallocation, and the third
2033        // must open a new one.
2034        let mut arena = StringArena::new();
2035        let half = "h".repeat(StringArena::CHUNK_SIZE / 2);
2036        let a = arena.intern(&half);
2037        let b = arena.intern(&half);
2038        assert_eq!(arena.metrics().0, 1, "two half-chunks must share one chunk");
2039        let c = arena.intern(&half);
2040        assert_eq!(arena.metrics().0, 2, "the third must open a new chunk");
2041
2042        // If the exact-fit append had reallocated, `a`/`b` would now dangle.
2043        assert_eq!(a.as_str(), half);
2044        assert_eq!(b.as_str(), half);
2045        assert_eq!(c.as_str(), half);
2046    }
2047
2048    #[test]
2049    fn arena_oversized_string_is_readable_and_gets_its_own_chunk() {
2050        let mut arena = StringArena::new();
2051        let big = "b".repeat(StringArena::CHUNK_SIZE + 1);
2052        let s = arena.intern(&big);
2053        assert_eq!(s.len(), big.len());
2054        assert_eq!(s.as_str(), big.as_str());
2055        assert_eq!(arena.metrics(), (1, big.len()));
2056    }
2057
2058    #[test]
2059    fn arena_many_interleaved_sizes_all_read_back_correctly() {
2060        let mut arena = StringArena::new();
2061        let mut kept = Vec::new();
2062        for i in 0..200 {
2063            let s = format!("s{i}-{}", "p".repeat(i % 17));
2064            kept.push((arena.intern(&s), s));
2065        }
2066        for (interned, expected) in &kept {
2067            assert_eq!(interned.as_str(), expected.as_str());
2068        }
2069    }
2070
2071    #[test]
2072    fn arena_interns_unicode_and_nul_bytes_verbatim() {
2073        let mut arena = StringArena::new();
2074        let weird = "héllo \u{1F600}\u{0}\u{0301}";
2075        let s = arena.intern(weird);
2076        assert_eq!(s.as_str(), weird);
2077        assert_eq!(s.len(), weird.len());
2078    }
2079
2080    #[test]
2081    fn arena_strings_outlive_the_handle_even_when_interleaved() {
2082        let (a, b) = {
2083            let mut arena = StringArena::new();
2084            let a = arena.intern("first");
2085            let big = "z".repeat(StringArena::CHUNK_SIZE * 2);
2086            let _dropped = arena.intern(&big);
2087            let b = arena.intern("second");
2088            (a, b)
2089        };
2090        assert_eq!(a.as_str(), "first");
2091        assert_eq!(b.as_str(), "second");
2092    }
2093
2094    /// RED — genuine bug in `StringArena::intern` (use-after-free).
2095    ///
2096    /// The oversized branch pushes a *dedicated, completely full* chunk
2097    /// (`len == cap`) but never touches `current_remaining`. If a small string
2098    /// was interned first, `remaining` is still > 0, so the next small intern
2099    /// skips the "push a fresh chunk" branch and appends into
2100    /// `chunks.last_mut()` — which is now that full dedicated chunk. The
2101    /// `extend_from_slice` therefore grows a `len == cap` Vec, reallocating it
2102    /// and leaving the `AzString` handed out for the oversized string pointing
2103    /// at freed memory.
2104    ///
2105    /// This test only inspects chunk *lengths* — it never dereferences the
2106    /// dangling pointer, so the test itself stays UB-free.
2107    #[test]
2108    fn arena_small_after_oversized_must_not_grow_the_full_dedicated_chunk() {
2109        let mut arena = StringArena::new();
2110
2111        // 1. open a shared chunk, leaving current_remaining > 0
2112        let _small = arena.intern("a");
2113
2114        // 2. oversized → dedicated chunk with len == cap == big_len
2115        let big = "x".repeat(StringArena::CHUNK_SIZE);
2116        let big_len = big.len();
2117        let interned_big = arena.intern(&big);
2118        assert_eq!(interned_big.as_str(), big.as_str(), "valid before the next intern");
2119
2120        // 3. another small string: must NOT be appended into the full chunk
2121        let _small2 = arena.intern("y");
2122
2123        // Safety: read-only look at the chunk lengths; no chunk data is read
2124        // and the (possibly dangling) `interned_big.vec.ptr` is never deref'd.
2125        let grew = unsafe {
2126            let chunks = &*arena.inner.chunks.get();
2127            chunks.iter().any(|c| c.len() > big_len)
2128        };
2129        assert!(
2130            !grew,
2131            "intern() appended a small string into the FULL dedicated chunk of an oversized \
2132             string (len == cap), which reallocates that Vec and leaves every AzString pointing \
2133             into it dangling — a use-after-free. Root cause: the oversized branch pushes a chunk \
2134             without resetting `current_remaining`, so the next small string takes the \
2135             `chunks.last_mut()` fast path onto the wrong chunk."
2136        );
2137    }
2138
2139    // ==================================================================
2140    // arena_string_destructor
2141    // ==================================================================
2142
2143    #[test]
2144    fn arena_destructor_drops_one_arc_ref_and_is_idempotent() {
2145        let inner = Arc::new(StringArenaInner {
2146            chunks: UnsafeCell::new(Vec::new()),
2147            current_remaining: UnsafeCell::new(0),
2148        });
2149        let raw = Arc::into_raw(Arc::clone(&inner));
2150        assert_eq!(Arc::strong_count(&inner), 2);
2151
2152        let mut v = U8Vec {
2153            ptr: core::ptr::NonNull::<u8>::dangling().as_ptr().cast_const(),
2154            len: 0,
2155            cap: raw as usize,
2156            destructor: U8VecDestructor::External(arena_string_destructor),
2157        };
2158
2159        arena_string_destructor(&mut v);
2160        assert_eq!(
2161            Arc::strong_count(&inner),
2162            1,
2163            "the destructor must release exactly one Arc reference"
2164        );
2165        assert_eq!(v.cap, 0, "cap must be zeroed to guard against a double drop");
2166
2167        // A second call must be a no-op rather than a double free.
2168        arena_string_destructor(&mut v);
2169        assert_eq!(Arc::strong_count(&inner), 1);
2170        assert_eq!(v.cap, 0);
2171
2172        // `v` still carries the External destructor; dropping it runs the
2173        // destructor a third time — also a no-op, since cap == 0.
2174        drop(v);
2175        assert_eq!(Arc::strong_count(&inner), 1);
2176    }
2177
2178    #[test]
2179    fn arena_destructor_tolerates_a_null_arc_pointer() {
2180        // cap == 0 (e.g. a zeroed FFI husk) must not be turned into Arc::from_raw(null).
2181        let mut v = U8Vec {
2182            ptr: core::ptr::null(),
2183            len: 0,
2184            cap: 0,
2185            destructor: U8VecDestructor::NoDestructor,
2186        };
2187        arena_string_destructor(&mut v);
2188        assert_eq!(v.cap, 0);
2189    }
2190
2191    #[test]
2192    fn arena_last_reference_frees_the_chunks() {
2193        // The arena's bytes must survive until the LAST AzString goes away,
2194        // and dropping in either order must not double-free.
2195        let inner_ptr;
2196        let s = {
2197            let mut arena = StringArena::new();
2198            let s = arena.intern("outlives the handle");
2199            inner_ptr = Arc::as_ptr(&arena.inner);
2200            assert_eq!(Arc::strong_count(&arena.inner), 2);
2201            s
2202        };
2203        // The arena handle is gone but the string still owns a reference.
2204        assert_eq!(s.as_str(), "outlives the handle");
2205        assert_eq!(s.vec.cap as *const StringArenaInner, inner_ptr);
2206        drop(s); // final reference → chunks freed here, exactly once
2207    }
2208}