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                    p
779                } else {
780                    if *remaining < len {
781                        chunks.push(Vec::with_capacity(Self::CHUNK_SIZE));
782                        *remaining = Self::CHUNK_SIZE;
783                    }
784                    // Safety: chunk was allocated with capacity ≥ len and
785                    // `remaining` tracks unused capacity — no realloc.
786                    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        // Each AzString carries its own Arc reference count. Stash the
796        // raw Arc pointer in `cap` so the External destructor can decrement.
797        let arc_raw = Arc::into_raw(Arc::clone(&self.inner));
798
799        AzString {
800            vec: U8Vec {
801                ptr,
802                len,
803                // NOTE: `cap` stores an Arc pointer, not a capacity. This
804                // works because the `External` destructor path never calls
805                // `Vec::from_raw_parts(ptr, len, cap)` — only `DefaultRust`
806                // does that.
807                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
820/// Destructor installed on every arena-backed `AzString`. Reads the Arc
821/// pointer out of `cap` and drops one Arc reference; when the count
822/// reaches zero the `StringArenaInner` is freed.
823extern "C" fn arena_string_destructor(vec: *mut U8Vec) {
824    // Safety: called at most once per AzString drop. `cap` was set by
825    // `StringArena::intern` to `Arc::into_raw(Arc<StringArenaInner>)`.
826    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            // Prevent a hypothetical double-drop from dereferencing
832            // freed memory.
833            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        // Cloning an External AzString deep-copies into DefaultRust, so
888        // the clone doesn't depend on the arena at all.
889        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    // ------------------------------------------------------------------
904    // helpers
905    // ------------------------------------------------------------------
906
907    /// Minimal FNV-1a hasher so the Hash-consistency tests don't depend on
908    /// `std` being linked (the crate keeps a `#![no_std]` line commented out).
909    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    /// UTF-16 code units of `s`, serialized to bytes with the given byte order.
931    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    // ==================================================================
945    // EmptyStruct
946    // ==================================================================
947
948    #[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        // The whole point of the type: `()` is zero-sized and not FFI-safe.
958        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        // Every EmptyStruct is equal to every other one, so Ord/Hash must agree.
973        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    // ==================================================================
980    // LayoutDebugMessage
981    // ==================================================================
982
983    #[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        // location is "file:line:column" — the last two segments must be numbers.
995        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        // If #[track_caller] were missing on the helpers, both locations would
1004        // collapse to the same line inside LayoutDebugMessage::new().
1005        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        // Every helper must forward the payload untouched, including empty
1058        // and unicode payloads.
1059        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        // `impl Into<String>` must work for both &str and String.
1103        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    // ==================================================================
1122    // AzString — constructors
1123    // ==================================================================
1124
1125    #[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        // One binding, used for both the construction and the pointer check —
1137        // rustc is not obliged to dedupe two identical string literals.
1138        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        // len() is BYTES, not chars: 5 ASCII-ish + 1 extra for é + space + 4 for the emoji
1163        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"), // i64::MAX
1180            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}"), // emoji + combining mark + NUL
1185            "{".repeat(10_000),                     // deeply "nested" junk: no parser, no overflow
1186        ] {
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            // round-trip back out
1192            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        // into_library_owned_string rebuilds the String via from_raw_parts(ptr, len, cap).
1208        // If `cap` were not carried through faithfully, this would corrupt the heap.
1209        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    // ==================================================================
1223    // AzString::copy_from_bytes  (numeric / pointer edge cases)
1224    // ==================================================================
1225
1226    #[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        // start == buf.len() is only legal because len == 0 short-circuits
1252        // before the pointer is ever offset.
1253        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        // start + len overflows usize, but len == 0 must short-circuit BEFORE
1261        // the debug_assert / ptr.add() — no panic, no UB.
1262        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        // The null check must precede everything, even for absurd start/len.
1270        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        // Slicing "héllo" mid-codepoint leaves a stray continuation byte (0xA9),
1278        // which must become U+FFFD so the as_str() UTF-8 invariant still holds.
1279        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        // The UTF-8 invariant as_str() relies on must actually hold:
1285        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    // ==================================================================
1305    // U8Vec::copy_from_bytes  (numeric / pointer edge cases)
1306    // ==================================================================
1307
1308    #[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        // Neither the debug_assert nor ptr.add() may be reached for a null ptr.
1326        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        // start + len overflows, but len == 0 short-circuits first.
1333        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        // The copy must survive the source buffer being dropped.
1349        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        // Arbitrary (non-UTF-8) bytes must round-trip unchanged — U8Vec has no
1360        // encoding invariant, unlike AzString.
1361        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    // ==================================================================
1367    // AzString::from_c_str
1368    // ==================================================================
1369
1370    #[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        // A latin-1 "café" is not valid UTF-8; it must come back lossily
1395        // rather than violating the as_str() invariant.
1396        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    // ==================================================================
1411    // AzString::to_c_str  (+ round trip through from_c_str)
1412    // ==================================================================
1413
1414    #[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        // Documented C-string reality: a string containing a NUL cannot survive
1443        // a *const char* round-trip. Assert the truncation is deterministic
1444        // rather than pretending it round-trips.
1445        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    // ==================================================================
1466    // AzString::from_utf8 / from_utf8_lossy
1467    // ==================================================================
1468
1469    #[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        // A truncated 2-byte sequence: strict → empty, lossy → U+FFFD.
1489        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"[..],         // overlong encoding of '/'
1504            &b"\xED\xA0\x80"[..],     // UTF-16 surrogate half, illegal in UTF-8
1505            &b"\xF8\x88\x80\x80"[..], // 5-byte sequence, illegal since RFC 3629
1506            &b"\x80"[..],             // stray continuation byte
1507            &b"\xFF\xFE"[..],         // never-valid bytes
1508        ] {
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            // Both paths must uphold the UTF-8 invariant that as_str() assumes.
1518            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    // ==================================================================
1538    // AzString::from_utf16_le / from_utf16_be / from_utf16_with_byte_order
1539    // ==================================================================
1540
1541    #[test]
1542    fn azstring_from_utf16_le_decodes_bmp_and_surrogate_pairs() {
1543        let text = "héllo \u{1F600}"; // the emoji needs a surrogate pair
1544        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        // Decoding LE bytes as BE must NOT silently yield the same text.
1560        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        // Still inside the buffer, so this is a safe call — it must be rejected
1577        // on the length check, not read a half code unit.
1578        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        // The smallest odd length of all:
1581        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        // A lone high surrogate is not valid UTF-16 → documented empty result.
1596        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        // A lone LOW surrogate, and a reversed (low-then-high) pair.
1600        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        // U+FFFE / U+0000 are valid code points (not surrogates) — they must
1613        // decode rather than being treated as an error or a terminator.
1614        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        // The private shared impl must use the caller's byte-order fn verbatim.
1635        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        // The odd-length / null guards live in the shared impl, so check them here too.
1650        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    // ==================================================================
1661    // AzString — getters / predicates / conversions
1662    // ==================================================================
1663
1664    #[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        // Whitespace and a NUL byte are content, not emptiness.
1697        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); // combining accent
1708        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        // Dropping `v` here frees the buffer exactly once (the source was
1723        // ManuallyDrop'd) — a double free would abort the test process.
1724    }
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        // DefaultRust: moves the allocation out.
1746        assert_eq!(
1747            AzString::from_string(String::from("owned \u{1F600}")).into_library_owned_string(),
1748            "owned \u{1F600}"
1749        );
1750        // NoDestructor: must COPY the static, never take ownership of it.
1751        assert_eq!(
1752            AzString::from_const_str("static").into_library_owned_string(),
1753            "static"
1754        );
1755        // External (arena-backed): must copy out of the arena.
1756        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        // Empty / default.
1763        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        // If this had aliased the &'static str, mutating it would be UB /
1770        // a segfault writing to rodata.
1771        owned.push_str(" + mutable");
1772        assert_eq!(owned, "static + mutable");
1773    }
1774
1775    // ==================================================================
1776    // AzString::clone_self
1777    // ==================================================================
1778
1779    #[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    // ==================================================================
1825    // AzString — Debug / Display round trips (fmt)
1826    // ==================================================================
1827
1828    #[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            // serialize(parse(serialize(x))) == serialize(x)
1845            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        // Same text, different memory ownership → identical rendering.
1875        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    // ==================================================================
1885    // AzString — Eq / Ord / Hash invariants
1886    // ==================================================================
1887
1888    #[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    // ==================================================================
1936    // StringArena
1937    // ==================================================================
1938
1939    #[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        // len == CHUNK_SIZE / 2 is NOT "oversized" (the check is `>`), so two of
2026        // them must fit in a single chunk with zero reallocation, and the third
2027        // must open a new one.
2028        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        // If the exact-fit append had reallocated, `a`/`b` would now dangle.
2037        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    /// RED — genuine bug in `StringArena::intern` (use-after-free).
2089    ///
2090    /// The oversized branch pushes a *dedicated, completely full* chunk
2091    /// (`len == cap`) but never touches `current_remaining`. If a small string
2092    /// was interned first, `remaining` is still > 0, so the next small intern
2093    /// skips the "push a fresh chunk" branch and appends into
2094    /// `chunks.last_mut()` — which is now that full dedicated chunk. The
2095    /// `extend_from_slice` therefore grows a `len == cap` Vec, reallocating it
2096    /// and leaving the `AzString` handed out for the oversized string pointing
2097    /// at freed memory.
2098    ///
2099    /// This test only inspects chunk *lengths* — it never dereferences the
2100    /// dangling pointer, so the test itself stays UB-free.
2101    #[test]
2102    fn arena_small_after_oversized_must_not_grow_the_full_dedicated_chunk() {
2103        let mut arena = StringArena::new();
2104
2105        // 1. open a shared chunk, leaving current_remaining > 0
2106        let _small = arena.intern("a");
2107
2108        // 2. oversized → dedicated chunk with len == cap == big_len
2109        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        // 3. another small string: must NOT be appended into the full chunk
2115        let _small2 = arena.intern("y");
2116
2117        // Safety: read-only look at the chunk lengths; no chunk data is read
2118        // and the (possibly dangling) `interned_big.vec.ptr` is never deref'd.
2119        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    // ==================================================================
2134    // arena_string_destructor
2135    // ==================================================================
2136
2137    #[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        // A second call must be a no-op rather than a double free.
2162        arena_string_destructor(&mut v);
2163        assert_eq!(Arc::strong_count(&inner), 1);
2164        assert_eq!(v.cap, 0);
2165
2166        // `v` still carries the External destructor; dropping it runs the
2167        // destructor a third time — also a no-op, since cap == 0.
2168        drop(v);
2169        assert_eq!(Arc::strong_count(&inner), 1);
2170    }
2171
2172    #[test]
2173    fn arena_destructor_tolerates_a_null_arc_pointer() {
2174        // cap == 0 (e.g. a zeroed FFI husk) must not be turned into Arc::from_raw(null).
2175        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        // The arena's bytes must survive until the LAST AzString goes away,
2188        // and dropping in either order must not double-free.
2189        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        // The arena handle is gone but the string still owns a reference.
2198        assert_eq!(s.as_str(), "outlives the handle");
2199        assert_eq!(s.vec.cap as *const StringArenaInner, inner_ptr);
2200        drop(s); // final reference → chunks freed here, exactly once
2201    }
2202}