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
46impl EmptyStruct {
47    /// Create a new `EmptyStruct` value (equivalent to `()`)
48    #[must_use]
49    pub const fn new() -> Self {
50        Self { _reserved: 0 }
51    }
52}
53
54impl From<()> for EmptyStruct {
55    fn from((): ()) -> Self {
56        Self::default()
57    }
58}
59
60impl From<EmptyStruct> for () {
61    fn from(_: EmptyStruct) -> Self {}
62}
63
64// ============================================================================
65// Debug message types
66// ============================================================================
67
68/// Debug message severity or category for layout diagnostics.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
70#[repr(C)]
71#[derive(Default)]
72pub enum LayoutDebugMessageType {
73    #[default]
74    Info,
75    Warning,
76    Error,
77    // Layout-specific categories for filtering
78    BoxProps,
79    CssGetter,
80    /// Block Formatting Context layout
81    BfcLayout,
82    /// Inline Formatting Context layout
83    IfcLayout,
84    TableLayout,
85    DisplayType,
86    PositionCalculation,
87}
88
89/// A debug message emitted during layout, with severity, text, and source location.
90#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd)]
91#[repr(C)]
92pub struct LayoutDebugMessage {
93    pub message_type: LayoutDebugMessageType,
94    pub message: AzString,
95    pub location: AzString,
96}
97
98impl LayoutDebugMessage {
99    /// Create a new debug message with automatic caller location tracking
100    #[track_caller]
101    pub fn new(message_type: LayoutDebugMessageType, message: impl Into<String>) -> Self {
102        let location = core::panic::Location::caller();
103        Self {
104            message_type,
105            message: AzString::from_string(message.into()),
106            location: AzString::from_string(format!(
107                "{}:{}:{}",
108                location.file(),
109                location.line(),
110                location.column()
111            )),
112        }
113    }
114
115    /// Helper for Info messages
116    #[track_caller]
117    pub fn info(message: impl Into<String>) -> Self {
118        Self::new(LayoutDebugMessageType::Info, message)
119    }
120
121    /// Helper for Warning messages
122    #[track_caller]
123    pub fn warning(message: impl Into<String>) -> Self {
124        Self::new(LayoutDebugMessageType::Warning, message)
125    }
126
127    /// Helper for Error messages
128    #[track_caller]
129    pub fn error(message: impl Into<String>) -> Self {
130        Self::new(LayoutDebugMessageType::Error, message)
131    }
132
133    /// Helper for `BoxProps` debug messages
134    #[track_caller]
135    pub fn box_props(message: impl Into<String>) -> Self {
136        Self::new(LayoutDebugMessageType::BoxProps, message)
137    }
138
139    /// Helper for CSS Getter debug messages
140    #[track_caller]
141    pub fn css_getter(message: impl Into<String>) -> Self {
142        Self::new(LayoutDebugMessageType::CssGetter, message)
143    }
144
145    /// Helper for BFC Layout debug messages
146    #[track_caller]
147    pub fn bfc_layout(message: impl Into<String>) -> Self {
148        Self::new(LayoutDebugMessageType::BfcLayout, message)
149    }
150
151    /// Helper for IFC Layout debug messages
152    #[track_caller]
153    pub fn ifc_layout(message: impl Into<String>) -> Self {
154        Self::new(LayoutDebugMessageType::IfcLayout, message)
155    }
156
157    /// Helper for Table Layout debug messages
158    #[track_caller]
159    pub fn table_layout(message: impl Into<String>) -> Self {
160        Self::new(LayoutDebugMessageType::TableLayout, message)
161    }
162
163    /// Helper for Display Type debug messages
164    #[track_caller]
165    pub fn display_type(message: impl Into<String>) -> Self {
166        Self::new(LayoutDebugMessageType::DisplayType, message)
167    }
168}
169
170/// FFI-safe string type backed by [`U8Vec`] with destructor-based memory management.
171///
172/// Contents are guaranteed to be valid UTF-8 by all safe constructors.
173/// Memory ownership is tracked via the inner `U8Vec`'s destructor field.
174#[repr(C)]
175pub struct AzString {
176    pub vec: U8Vec,
177}
178
179impl_option!(
180    AzString,
181    OptionString,
182    copy = false,
183    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
184);
185
186static DEFAULT_STR: &str = "";
187
188impl Default for AzString {
189    fn default() -> Self {
190        DEFAULT_STR.into()
191    }
192}
193
194impl<'a> From<&'a str> for AzString {
195    fn from(s: &'a str) -> Self {
196        s.to_string().into()
197    }
198}
199
200impl AsRef<str> for AzString {
201    fn as_ref(&self) -> &str {
202        self.as_str()
203    }
204}
205
206impl core::fmt::Debug for AzString {
207    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
208        self.as_str().fmt(f)
209    }
210}
211
212impl core::fmt::Display for AzString {
213    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
214        self.as_str().fmt(f)
215    }
216}
217
218impl AzString {
219    #[inline]
220    #[must_use]
221    pub const fn from_const_str(s: &'static str) -> Self {
222        Self {
223            vec: U8Vec::from_const_slice(s.as_bytes()),
224        }
225    }
226
227    /// Creates a new `AzString` from a null-terminated C string (const char*).
228    /// This copies the string data into a new allocation.
229    ///
230    /// # Safety
231    /// - `ptr` must be a valid pointer to a null-terminated UTF-8 string
232    /// - The string must remain valid for the duration of this call
233    ///
234    /// Note: `ptr` is `*const i8` rather than `*const core::ffi::c_char`
235    /// so the auto-generated FFI signature in `dll_api_internal.rs`
236    /// (which uses a literal `i8`) matches on every target —
237    /// `c_char` is `i8` on x86/ARM Apple/Windows/Linux but `u8` on
238    /// Android, which would otherwise produce a `*const u8 vs *const i8`
239    /// mismatch at codegen-call sites. We cast internally before
240    /// handing the pointer to `CStr::from_ptr`.
241    #[inline]
242    #[must_use]
243    pub unsafe fn from_c_str(ptr: *const i8) -> Self {
244        unsafe {
245            if ptr.is_null() {
246                return Self::default();
247            }
248            let c_str = core::ffi::CStr::from_ptr(ptr as *const core::ffi::c_char);
249            let bytes = c_str.to_bytes();
250            Self::copy_from_bytes(bytes.as_ptr(), 0, bytes.len())
251        }
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]
268    pub fn copy_from_bytes(ptr: *const u8, start: usize, len: usize) -> Self {
269        let raw = U8Vec::copy_from_bytes(ptr, start, len);
270        // web-lift FIX (2026-06-03): FAST PATH for already-valid UTF-8 (the common case, incl. all
271        // ASCII like "Hello") — wrap the U8Vec directly, avoiding the `String::from_utf8_lossy()
272        // .into_owned()` std sret-in-X0 call that the lift mis-threads (the returned String comes
273        // back with len=0 → empty AzString). `core::str::from_utf8` returns a `Result<&str,_>` (a
274        // slice, NOT a by-value struct) so it has no sret to mis-thread. Also a real perf win (no
275        // 2nd alloc+copy for valid input). Slow path (rare, invalid UTF-8) keeps the lossy replace.
276        if core::str::from_utf8(raw.as_ref()).is_ok() {
277            return Self { vec: raw };
278        }
279        let s = String::from_utf8_lossy(raw.as_ref()).into_owned();
280        Self::from_string(s)
281    }
282
283    #[inline] // web-lift: inline through the sret-in-X0 chain (see copy_from_bytes)
284    #[must_use]
285    pub const fn from_string(s: String) -> Self {
286        Self {
287            vec: U8Vec::from_vec(s.into_bytes()),
288        }
289    }
290
291    #[inline]
292    #[must_use]
293    pub fn as_str(&self) -> &str {
294        unsafe { core::str::from_utf8_unchecked(self.vec.as_ref()) }
295    }
296
297    /// NOTE: CLONES the memory if the memory is external or &'static
298    /// Moves the memory out if the memory is library-allocated
299    #[inline]
300    #[must_use]
301    pub fn clone_self(&self) -> Self {
302        Self {
303            vec: self.vec.clone_self(),
304        }
305    }
306
307    #[inline]
308    #[must_use]
309    pub fn into_library_owned_string(self) -> String {
310        match self.vec.destructor {
311            U8VecDestructor::NoDestructor
312            | U8VecDestructor::External(_)
313            | U8VecDestructor::AlreadyDestroyed => self.as_str().to_string(),
314            U8VecDestructor::DefaultRust => {
315                let m = core::mem::ManuallyDrop::new(self);
316                unsafe { String::from_raw_parts(m.vec.ptr.cast_mut(), m.vec.len, m.vec.cap) }
317            }
318        }
319    }
320
321    #[inline]
322    #[must_use]
323    pub fn as_bytes(&self) -> &[u8] {
324        self.vec.as_ref()
325    }
326
327    #[inline]
328    #[must_use]
329    pub fn into_bytes(self) -> U8Vec {
330        let m = core::mem::ManuallyDrop::new(self);
331        U8Vec {
332            ptr: m.vec.ptr,
333            len: m.vec.len,
334            cap: m.vec.cap,
335            destructor: m.vec.destructor,
336        }
337    }
338
339    /// Returns the length of the string in bytes (not including null terminator)
340    #[inline]
341    #[must_use]
342    pub const fn len(&self) -> usize {
343        self.vec.len
344    }
345
346    /// Returns true if the string is empty
347    #[inline]
348    #[must_use]
349    pub const fn is_empty(&self) -> bool {
350        self.vec.len == 0
351    }
352
353    /// Creates a null-terminated copy of the string for C FFI usage.
354    /// Returns a new `U8Vec` that contains the string data followed by a null byte.
355    /// The caller is responsible for freeing this memory.
356    ///
357    /// Use this when you need to pass a string to C code that expects `const char*`.
358    #[inline]
359    #[must_use]
360    pub fn to_c_str(&self) -> U8Vec {
361        let bytes = self.as_bytes();
362        let mut result = Vec::with_capacity(bytes.len() + 1);
363        result.extend_from_slice(bytes);
364        result.push(0); // null terminator
365        U8Vec::from_vec(result)
366    }
367
368    /// Shared implementation for UTF-16 decoding with a caller-supplied byte-order function.
369    ///
370    /// # Safety
371    /// - `ptr` must be valid for reading `len` bytes
372    /// - `len` must be even (UTF-16 uses 2 bytes per code unit)
373    unsafe fn from_utf16_with_byte_order(
374        ptr: *const u8,
375        len: usize,
376        from_bytes: fn([u8; 2]) -> u16,
377    ) -> Self {
378        unsafe {
379            if ptr.is_null() || len == 0 {
380                return Self::default();
381            }
382
383            // UTF-16 requires pairs of bytes
384            if !len.is_multiple_of(2) {
385                return Self::default();
386            }
387
388            let byte_slice = core::slice::from_raw_parts(ptr, len);
389            let code_units: Vec<u16> = byte_slice
390                .chunks_exact(2)
391                .map(|chunk| from_bytes([chunk[0], chunk[1]]))
392                .collect();
393
394            String::from_utf16(&code_units).map_or_else(|_| Self::default(), Self::from_string)
395        }
396    }
397
398    /// Creates a new `AzString` from UTF-16 encoded bytes (little-endian).
399    /// Returns an empty string if the input is invalid UTF-16 or has odd length.
400    ///
401    /// # Arguments
402    /// * `ptr` - Pointer to UTF-16 encoded bytes
403    /// * `len` - Length in bytes (not code units) - must be even
404    ///
405    /// # Safety
406    /// - `ptr` must be valid for reading `len` bytes
407    /// - `len` must be even (UTF-16 uses 2 bytes per code unit)
408    #[inline]
409    pub unsafe fn from_utf16_le(ptr: *const u8, len: usize) -> Self {
410        unsafe { Self::from_utf16_with_byte_order(ptr, len, u16::from_le_bytes) }
411    }
412
413    /// Creates a new `AzString` from UTF-16 encoded bytes (big-endian).
414    /// Returns an empty string if the input is invalid UTF-16 or has odd length.
415    ///
416    /// # Arguments
417    /// * `ptr` - Pointer to UTF-16 encoded bytes
418    /// * `len` - Length in bytes (not code units) - must be even
419    ///
420    /// # Safety
421    /// - `ptr` must be valid for reading `len` bytes
422    /// - `len` must be even (UTF-16 uses 2 bytes per code unit)
423    #[inline]
424    pub unsafe fn from_utf16_be(ptr: *const u8, len: usize) -> Self {
425        unsafe { Self::from_utf16_with_byte_order(ptr, len, u16::from_be_bytes) }
426    }
427
428    /// Creates a new `AzString` from UTF-8 bytes with lossy conversion.
429    /// Invalid UTF-8 sequences are replaced with the Unicode replacement character (U+FFFD).
430    ///
431    /// # Safety
432    /// - `ptr` must be valid for reading `len` bytes
433    #[inline]
434    #[must_use]
435    pub unsafe fn from_utf8_lossy(ptr: *const u8, len: usize) -> Self {
436        unsafe {
437            if ptr.is_null() || len == 0 {
438                return Self::default();
439            }
440
441            let byte_slice = core::slice::from_raw_parts(ptr, len);
442            let s = String::from_utf8_lossy(byte_slice).into_owned();
443            Self::from_string(s)
444        }
445    }
446
447    /// Creates a new `AzString` from UTF-8 bytes.
448    /// Returns an empty string if the input is not valid UTF-8.
449    ///
450    /// # Safety
451    /// - `ptr` must be valid for reading `len` bytes
452    #[inline]
453    #[must_use]
454    pub unsafe fn from_utf8(ptr: *const u8, len: usize) -> Self {
455        unsafe {
456            if ptr.is_null() || len == 0 {
457                return Self::default();
458            }
459
460            let byte_slice = core::slice::from_raw_parts(ptr, len);
461            core::str::from_utf8(byte_slice)
462                .map_or_else(|_| Self::default(), |s| Self::from_string(s.to_string()))
463        }
464    }
465}
466
467impl From<String> for AzString {
468    fn from(input: String) -> Self {
469        Self::from_string(input)
470    }
471}
472
473impl PartialOrd for AzString {
474    fn partial_cmp(&self, rhs: &Self) -> Option<core::cmp::Ordering> {
475        self.as_str().partial_cmp(rhs.as_str())
476    }
477}
478
479impl Ord for AzString {
480    fn cmp(&self, rhs: &Self) -> core::cmp::Ordering {
481        self.as_str().cmp(rhs.as_str())
482    }
483}
484
485impl Clone for AzString {
486    fn clone(&self) -> Self {
487        self.clone_self()
488    }
489}
490
491impl PartialEq for AzString {
492    fn eq(&self, rhs: &Self) -> bool {
493        self.as_str().eq(rhs.as_str())
494    }
495}
496
497impl Eq for AzString {}
498
499impl core::hash::Hash for AzString {
500    fn hash<H>(&self, state: &mut H)
501    where
502        H: core::hash::Hasher,
503    {
504        self.as_str().hash(state);
505    }
506}
507
508impl core::ops::Deref for AzString {
509    type Target = str;
510
511    fn deref(&self) -> &str {
512        self.as_str()
513    }
514}
515
516impl_option!(
517    u8,
518    OptionU8,
519    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
520);
521
522impl_vec!(
523    u8,
524    U8Vec,
525    U8VecDestructor,
526    U8VecDestructorType,
527    U8VecSlice,
528    OptionU8
529);
530impl_vec_mut!(u8, U8Vec);
531impl_vec_debug!(u8, U8Vec);
532impl_vec_partialord!(u8, U8Vec);
533impl_vec_ord!(u8, U8Vec);
534impl_vec_clone!(u8, U8Vec, U8VecDestructor);
535impl_vec_partialeq!(u8, U8Vec);
536impl_vec_eq!(u8, U8Vec);
537impl_vec_hash!(u8, U8Vec);
538
539impl U8Vec {
540    /// Copies bytes from a pointer into a new Vec.
541    /// This is useful for C FFI where you have a `uint8_t`* buffer.
542    ///
543    /// # Safety contract (caller must ensure)
544    /// - `ptr` must be valid for reading `start + len` bytes
545    /// - `start + len` must not overflow
546    #[inline] // web-lift: inline through the sret-in-X0 chain (see AzString::copy_from_bytes)
547    #[allow(clippy::not_unsafe_ptr_arg_deref)]
548    // 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.
549    #[must_use]
550    pub fn copy_from_bytes(ptr: *const u8, start: usize, len: usize) -> Self {
551        if ptr.is_null() || len == 0 {
552            return Self::new();
553        }
554        debug_assert!(
555            start.checked_add(len).is_some(),
556            "U8Vec::copy_from_bytes: start + len overflows"
557        );
558        let slice = unsafe { core::slice::from_raw_parts(ptr.add(start), len) };
559        Self::from_vec(slice.to_vec())
560    }
561}
562
563impl_option!(
564    U8Vec,
565    OptionU8Vec,
566    copy = false,
567    [Debug, Clone, PartialEq, Ord, PartialOrd, Eq, Hash]
568);
569
570impl_vec!(
571    u16,
572    U16Vec,
573    U16VecDestructor,
574    U16VecDestructorType,
575    U16VecSlice,
576    OptionU16
577);
578impl_vec_debug!(u16, U16Vec);
579impl_vec_partialord!(u16, U16Vec);
580impl_vec_ord!(u16, U16Vec);
581impl_vec_clone!(u16, U16Vec, U16VecDestructor);
582impl_vec_partialeq!(u16, U16Vec);
583impl_vec_eq!(u16, U16Vec);
584impl_vec_hash!(u16, U16Vec);
585
586impl_vec!(
587    f32,
588    F32Vec,
589    F32VecDestructor,
590    F32VecDestructorType,
591    F32VecSlice,
592    OptionF32
593);
594impl_vec_debug!(f32, F32Vec);
595impl_vec_partialord!(f32, F32Vec);
596impl_vec_clone!(f32, F32Vec, F32VecDestructor);
597impl_vec_partialeq!(f32, F32Vec);
598
599// Vec<char>
600impl_vec!(
601    u32,
602    U32Vec,
603    U32VecDestructor,
604    U32VecDestructorType,
605    U32VecSlice,
606    OptionU32
607);
608impl_vec_mut!(u32, U32Vec);
609impl_option!(
610    U32Vec,
611    OptionU32Vec,
612    copy = false,
613    [Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash]
614);
615impl_vec_debug!(u32, U32Vec);
616impl_vec_partialord!(u32, U32Vec);
617impl_vec_ord!(u32, U32Vec);
618impl_vec_clone!(u32, U32Vec, U32VecDestructor);
619impl_vec_partialeq!(u32, U32Vec);
620impl_vec_eq!(u32, U32Vec);
621impl_vec_hash!(u32, U32Vec);
622
623impl_vec!(
624    AzString,
625    StringVec,
626    StringVecDestructor,
627    StringVecDestructorType,
628    StringVecSlice,
629    OptionString
630);
631impl_vec_debug!(AzString, StringVec);
632impl_vec_partialord!(AzString, StringVec);
633impl_vec_ord!(AzString, StringVec);
634impl_vec_clone!(AzString, StringVec, StringVecDestructor);
635impl_vec_partialeq!(AzString, StringVec);
636impl_vec_eq!(AzString, StringVec);
637impl_vec_hash!(AzString, StringVec);
638
639impl From<Vec<String>> for StringVec {
640    fn from(v: Vec<String>) -> Self {
641        let new_v: Vec<AzString> = v.into_iter().map(Into::into).collect();
642        new_v.into()
643    }
644}
645
646impl_option!(
647    StringVec,
648    OptionStringVec,
649    copy = false,
650    [Debug, Clone, PartialOrd, PartialEq, Ord, Eq, Hash]
651);
652
653impl_option!(
654    u16,
655    OptionU16,
656    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
657);
658impl_option!(
659    u32,
660    OptionU32,
661    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
662);
663impl_option!(
664    u64,
665    OptionU64,
666    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
667);
668impl_option!(
669    usize,
670    OptionUsize,
671    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
672);
673impl_option!(
674    i16,
675    OptionI16,
676    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
677);
678impl_option!(
679    i32,
680    OptionI32,
681    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
682);
683impl_option!(
684    bool,
685    OptionBool,
686    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
687);
688impl_option!(f32, OptionF32, [Debug, Copy, Clone, PartialEq]);
689impl_option!(f64, OptionF64, [Debug, Copy, Clone, PartialEq, PartialOrd]);
690
691// Manual implementations for Hash and Ord on OptionF32 (since f32 doesn't implement these traits)
692impl core::hash::Hash for OptionF32 {
693    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
694        match self {
695            Self::None => 0u8.hash(state),
696            Self::Some(v) => {
697                1u8.hash(state);
698                v.to_bits().hash(state);
699            }
700        }
701    }
702}
703
704impl Eq for OptionF32 {}
705
706// Manual PartialOrd delegating to Ord keeps the two consistent (the derived
707// PartialOrd would diverge from the manual Ord — see derive_ord_xor_partial_ord).
708impl PartialOrd for OptionF32 {
709    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
710        Some(self.cmp(other))
711    }
712}
713
714impl Ord for OptionF32 {
715    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
716        match (self, other) {
717            (Self::None, Self::None) => core::cmp::Ordering::Equal,
718            (Self::None, Self::Some(_)) => core::cmp::Ordering::Less,
719            (Self::Some(_), Self::None) => core::cmp::Ordering::Greater,
720            (Self::Some(a), Self::Some(b)) => {
721                a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
722            }
723        }
724    }
725}
726
727// ============================================================================
728// StringArena — bump allocator for AzString bytes
729// ============================================================================
730//
731// Consolidates thousands of small AzString allocations (tag names,
732// attribute values, text content) into a handful of 64 KiB chunks.
733// Each arena-backed AzString uses `U8VecDestructor::External` and stashes
734// a cloned `Arc<StringArenaInner>` pointer in the `cap` field — dropping
735// the AzString decrements the refcount, and the backing bytes are freed
736// only when the last reference goes away. This works across FFI without
737// changing any public struct layout.
738
739use alloc::sync::Arc;
740use core::cell::UnsafeCell;
741
742/// Shared interior of a [`StringArena`]. Refcounted via `Arc<Self>`;
743/// never accessed through its `Arc` for mutation — only the owning
744/// `StringArena` (with `&mut self`) mutates the chunks.
745struct StringArenaInner {
746    /// Pre-allocated byte chunks. Pointers into a chunk stay valid
747    /// because we never push past `Vec::capacity()` — no reallocation.
748    chunks: UnsafeCell<Vec<Vec<u8>>>,
749    /// Remaining unused bytes in the last chunk; `0` when a fresh
750    /// chunk is needed.
751    current_remaining: UnsafeCell<usize>,
752}
753
754// Safety:
755// - Mutation through `UnsafeCell` only happens via `&mut StringArena`,
756//   which owns the sole external reference to `Arc<StringArenaInner>`
757//   held in a `StringArena`. Other `Arc` references live inside AzString
758//   destructors and never touch chunks — they only drop the Arc.
759// - `Arc<T>` itself needs `T: Send + Sync` to cross threads; since the
760//   destructor can run on any thread, we claim Send+Sync and rely on the
761//   single-writer invariant for mutation safety.
762unsafe impl Send for StringArenaInner {}
763unsafe impl Sync for StringArenaInner {}
764
765/// Bump allocator backing arena-owned `AzString` instances.
766///
767/// Every `AzString` returned by [`StringArena::intern`] holds a cloned
768/// `Arc` to this arena; the backing bytes stay alive until the last
769/// such `AzString` (and the arena handle itself) is dropped.
770///
771/// Intended use: create one arena per XML/HTML parse pass, intern all
772/// tag names / attribute values / text content through it, then drop the
773/// handle. The `AzStrings` embedded in the resulting `StyledDom` keep the
774/// arena alive for as long as they need the bytes.
775pub struct StringArena {
776    inner: Arc<StringArenaInner>,
777}
778
779impl core::fmt::Debug for StringArena {
780    // StringArenaInner holds UnsafeCell chunks (not Debug) — opaque by design.
781    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
782        f.debug_struct("StringArena").finish_non_exhaustive()
783    }
784}
785
786impl StringArena {
787    /// Size of a freshly allocated chunk. Large enough that a typical
788    /// DOM parse fits in 1-2 chunks, small enough to not over-allocate
789    /// for small documents.
790    pub const CHUNK_SIZE: usize = 64 * 1024;
791
792    #[must_use]
793    pub fn new() -> Self {
794        Self {
795            inner: Arc::new(StringArenaInner {
796                chunks: UnsafeCell::new(Vec::new()),
797                current_remaining: UnsafeCell::new(0),
798            }),
799        }
800    }
801
802    /// Returns `(chunk_count, total_bytes_used)` for metrics.
803    #[must_use]
804    pub fn metrics(&self) -> (usize, usize) {
805        // Safety: metrics is read-only; the caller holds &self so no
806        // concurrent mutation via &mut self is possible.
807        unsafe {
808            let chunks = &*self.inner.chunks.get();
809            let total: usize = chunks.iter().map(Vec::len).sum();
810            (chunks.len(), total)
811        }
812    }
813
814    /// Intern `s` into the arena and return an `AzString` whose backing
815    /// bytes live inside the arena. The returned `AzString` owns a cloned
816    /// `Arc` reference; dropping it decrements the refcount, and the
817    /// arena frees its chunks when the final reference is released.
818    ///
819    /// # Panics
820    ///
821    /// Panics if the arena's internal chunk list is unexpectedly empty when
822    /// appending a non-oversized string (an invariant violation that cannot
823    /// occur through the public API, since a chunk is allocated on demand).
824    pub fn intern(&mut self, s: &str) -> AzString {
825        let bytes = s.as_bytes();
826        let len = bytes.len();
827
828        let ptr: *const u8 = if len == 0 {
829            // Empty strings don't need arena storage; a non-null dangling
830            // pointer is fine because `len == 0` means nobody will deref.
831            core::ptr::NonNull::<u8>::dangling().as_ptr()
832        } else {
833            // Safety: `&mut self` ⇒ exclusive access to inner chunks.
834            unsafe {
835                let chunks: &mut Vec<Vec<u8>> = &mut *self.inner.chunks.get();
836                let remaining: &mut usize = &mut *self.inner.current_remaining.get();
837
838                // Oversized strings get their own dedicated chunk so we
839                // don't waste the tail of the current chunk.
840                if len > Self::CHUNK_SIZE / 2 {
841                    let mut v = Vec::with_capacity(len);
842                    v.extend_from_slice(bytes);
843                    let p = v.as_ptr();
844                    chunks.push(v);
845                    // This dedicated chunk is FULL (len == cap). `remaining` must not
846                    // keep describing the previous chunk, or the next small intern
847                    // below would see a stale positive `remaining`, skip allocating,
848                    // and `extend_from_slice` into THIS chunk — reallocating it and
849                    // dangling the `p` we just handed out.
850                    *remaining = 0;
851                    p
852                } else {
853                    if *remaining < len {
854                        chunks.push(Vec::with_capacity(Self::CHUNK_SIZE));
855                        *remaining = Self::CHUNK_SIZE;
856                    }
857                    // Safety: chunk was allocated with capacity ≥ len and
858                    // `remaining` tracks unused capacity — no realloc.
859                    let chunk = chunks.last_mut().unwrap();
860                    let offset = chunk.len();
861                    chunk.extend_from_slice(bytes);
862                    *remaining -= len;
863                    chunk.as_ptr().add(offset)
864                }
865            }
866        };
867
868        // Each AzString carries its own Arc reference count. Stash the
869        // raw Arc pointer in `cap` so the External destructor can decrement.
870        let arc_raw = Arc::into_raw(Arc::clone(&self.inner));
871
872        AzString {
873            vec: U8Vec {
874                ptr,
875                len,
876                // NOTE: `cap` stores an Arc pointer, not a capacity. This
877                // works because the `External` destructor path never calls
878                // `Vec::from_raw_parts(ptr, len, cap)` — only `DefaultRust`
879                // does that.
880                cap: arc_raw as usize,
881                destructor: U8VecDestructor::External(arena_string_destructor),
882            },
883        }
884    }
885}
886
887impl Default for StringArena {
888    fn default() -> Self {
889        Self::new()
890    }
891}
892
893/// Destructor installed on every arena-backed `AzString`. Reads the Arc
894/// pointer out of `cap` and drops one Arc reference; when the count
895/// reaches zero the `StringArenaInner` is freed.
896extern "C" fn arena_string_destructor(vec: *mut U8Vec) {
897    // Safety: called at most once per AzString drop. `cap` was set by
898    // `StringArena::intern` to `Arc::into_raw(Arc<StringArenaInner>)`.
899    unsafe {
900        let v = &mut *vec;
901        let arc_raw = v.cap as *const StringArenaInner;
902        if !arc_raw.is_null() {
903            drop(Arc::from_raw(arc_raw));
904            // Prevent a hypothetical double-drop from dereferencing
905            // freed memory.
906            v.cap = 0;
907        }
908    }
909}
910
911#[cfg(test)]
912mod string_arena_tests {
913    use super::*;
914
915    #[test]
916    fn intern_round_trip() {
917        let mut arena = StringArena::new();
918        let a = arena.intern("hello");
919        let b = arena.intern("world");
920        let c = arena.intern("");
921        assert_eq!(a.as_str(), "hello");
922        assert_eq!(b.as_str(), "world");
923        assert_eq!(c.as_str(), "");
924    }
925
926    #[test]
927    fn strings_outlive_arena_handle() {
928        let a = {
929            let mut arena = StringArena::new();
930            arena.intern("survives drop of arena handle")
931        };
932        assert_eq!(a.as_str(), "survives drop of arena handle");
933    }
934
935    #[test]
936    fn oversized_string_gets_dedicated_chunk() {
937        let mut arena = StringArena::new();
938        let big = "x".repeat(StringArena::CHUNK_SIZE);
939        let s = arena.intern(&big);
940        assert_eq!(s.len(), big.len());
941        assert_eq!(s.as_str(), big.as_str());
942    }
943
944    #[test]
945    fn many_small_strings_share_chunk() {
946        let mut arena = StringArena::new();
947        let mut strings = Vec::new();
948        for i in 0..100 {
949            strings.push(arena.intern(&format!("s{i}")));
950        }
951        let (chunks, _bytes) = arena.metrics();
952        assert!(
953            chunks <= 2,
954            "expected ≤2 chunks for 100 small strings, got {chunks}"
955        );
956        for (i, s) in strings.iter().enumerate() {
957            assert_eq!(s.as_str(), format!("s{i}"));
958        }
959    }
960
961    #[test]
962    fn clone_deep_copies_and_is_independent() {
963        // Cloning an External AzString deep-copies into DefaultRust, so
964        // the clone doesn't depend on the arena at all.
965        let clone = {
966            let mut arena = StringArena::new();
967
968            arena.intern("deep-copy test")
969        };
970        assert_eq!(clone.as_str(), "deep-copy test");
971    }
972}
973
974#[cfg(test)]
975#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
976mod autotest_generated {
977    use super::*;
978
979    // ------------------------------------------------------------------
980    // helpers
981    // ------------------------------------------------------------------
982
983    /// Minimal FNV-1a hasher so the Hash-consistency tests don't depend on
984    /// `std` being linked (the crate keeps a `#![no_std]` line commented out).
985    struct Fnv(u64);
986
987    impl core::hash::Hasher for Fnv {
988        fn finish(&self) -> u64 {
989            self.0
990        }
991        fn write(&mut self, bytes: &[u8]) {
992            for b in bytes {
993                self.0 ^= u64::from(*b);
994                self.0 = self.0.wrapping_mul(0x0100_0000_01b3);
995            }
996        }
997    }
998
999    fn hash_of<T: core::hash::Hash>(t: &T) -> u64 {
1000        use core::hash::{Hash, Hasher};
1001        let mut h = Fnv(0xcbf2_9ce4_8422_2325);
1002        Hash::hash(t, &mut h);
1003        h.finish()
1004    }
1005
1006    /// UTF-16 code units of `s`, serialized to bytes with the given byte order.
1007    fn utf16_bytes(s: &str, little_endian: bool) -> Vec<u8> {
1008        s.encode_utf16()
1009            .flat_map(|u| {
1010                let b = if little_endian {
1011                    u.to_le_bytes()
1012                } else {
1013                    u.to_be_bytes()
1014                };
1015                [b[0], b[1]]
1016            })
1017            .collect()
1018    }
1019
1020    // ==================================================================
1021    // EmptyStruct
1022    // ==================================================================
1023
1024    #[test]
1025    fn empty_struct_new_invariants() {
1026        let e = EmptyStruct::new();
1027        assert_eq!(e._reserved, 0, "_reserved must always be initialized to 0");
1028        assert_eq!(e, EmptyStruct::default(), "new() must equal default()");
1029    }
1030
1031    #[test]
1032    fn empty_struct_is_ffi_safe_non_zero_size() {
1033        // The whole point of the type: `()` is zero-sized and not FFI-safe.
1034        assert_eq!(size_of::<EmptyStruct>(), 1);
1035        assert_eq!(align_of::<EmptyStruct>(), 1);
1036    }
1037
1038    #[test]
1039    fn empty_struct_unit_conversions_round_trip() {
1040        let from_unit = EmptyStruct::from(());
1041        assert_eq!(from_unit, EmptyStruct::new());
1042        let back: () = EmptyStruct::new().into();
1043        assert_eq!(back, ());
1044    }
1045
1046    #[test]
1047    fn empty_struct_total_order_is_trivial() {
1048        // Every EmptyStruct is equal to every other one, so Ord/Hash must agree.
1049        let a = EmptyStruct::new();
1050        let b = EmptyStruct::default();
1051        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
1052        assert_eq!(hash_of(&a), hash_of(&b));
1053    }
1054
1055    // ==================================================================
1056    // LayoutDebugMessage
1057    // ==================================================================
1058
1059    #[test]
1060    fn debug_message_new_records_fields_and_caller_location() {
1061        let m = LayoutDebugMessage::new(LayoutDebugMessageType::Warning, "disk on fire");
1062        assert_eq!(m.message_type, LayoutDebugMessageType::Warning);
1063        assert_eq!(m.message.as_str(), "disk on fire");
1064        assert!(
1065            m.location.as_str().contains("corety.rs"),
1066            "#[track_caller] must record THIS file, got {:?}",
1067            m.location.as_str()
1068        );
1069
1070        // location is "file:line:column" — the last two segments must be numbers.
1071        let parts: Vec<&str> = m.location.as_str().rsplitn(3, ':').collect();
1072        assert_eq!(parts.len(), 3, "location must be file:line:column");
1073        assert!(parts[0].parse::<u32>().is_ok(), "column must parse as u32");
1074        assert!(parts[1].parse::<u32>().is_ok(), "line must parse as u32");
1075    }
1076
1077    #[test]
1078    fn debug_message_track_caller_propagates_through_helpers() {
1079        // If #[track_caller] were missing on the helpers, both locations would
1080        // collapse to the same line inside LayoutDebugMessage::new().
1081        let a = LayoutDebugMessage::info("a");
1082        let b = LayoutDebugMessage::info("b");
1083        assert_ne!(
1084            a.location.as_str(),
1085            b.location.as_str(),
1086            "two call sites on different lines must record different locations"
1087        );
1088        assert!(a.location.as_str().contains("corety.rs"));
1089    }
1090
1091    #[test]
1092    fn debug_message_helpers_set_the_right_type() {
1093        assert_eq!(
1094            LayoutDebugMessage::info("x").message_type,
1095            LayoutDebugMessageType::Info
1096        );
1097        assert_eq!(
1098            LayoutDebugMessage::warning("x").message_type,
1099            LayoutDebugMessageType::Warning
1100        );
1101        assert_eq!(
1102            LayoutDebugMessage::error("x").message_type,
1103            LayoutDebugMessageType::Error
1104        );
1105        assert_eq!(
1106            LayoutDebugMessage::box_props("x").message_type,
1107            LayoutDebugMessageType::BoxProps
1108        );
1109        assert_eq!(
1110            LayoutDebugMessage::css_getter("x").message_type,
1111            LayoutDebugMessageType::CssGetter
1112        );
1113        assert_eq!(
1114            LayoutDebugMessage::bfc_layout("x").message_type,
1115            LayoutDebugMessageType::BfcLayout
1116        );
1117        assert_eq!(
1118            LayoutDebugMessage::ifc_layout("x").message_type,
1119            LayoutDebugMessageType::IfcLayout
1120        );
1121        assert_eq!(
1122            LayoutDebugMessage::table_layout("x").message_type,
1123            LayoutDebugMessageType::TableLayout
1124        );
1125        assert_eq!(
1126            LayoutDebugMessage::display_type("x").message_type,
1127            LayoutDebugMessageType::DisplayType
1128        );
1129    }
1130
1131    #[test]
1132    fn debug_message_helpers_preserve_the_message_verbatim() {
1133        // Every helper must forward the payload untouched, including empty
1134        // and unicode payloads.
1135        for m in [
1136            LayoutDebugMessage::info(""),
1137            LayoutDebugMessage::warning(""),
1138            LayoutDebugMessage::error(""),
1139            LayoutDebugMessage::box_props(""),
1140            LayoutDebugMessage::css_getter(""),
1141            LayoutDebugMessage::bfc_layout(""),
1142            LayoutDebugMessage::ifc_layout(""),
1143            LayoutDebugMessage::table_layout(""),
1144            LayoutDebugMessage::display_type(""),
1145        ] {
1146            assert!(m.message.is_empty());
1147            assert!(!m.location.is_empty(), "location is always filled in");
1148        }
1149
1150        let weird = "ünïcødé \u{1F600}\n\t\"quoted\" \u{0}nul";
1151        assert_eq!(LayoutDebugMessage::error(weird).message.as_str(), weird);
1152    }
1153
1154    #[test]
1155    fn debug_message_handles_huge_message_without_panicking() {
1156        let huge = "m".repeat(1_000_000);
1157        let m = LayoutDebugMessage::new(LayoutDebugMessageType::PositionCalculation, huge.clone());
1158        assert_eq!(m.message.len(), 1_000_000);
1159        assert_eq!(m.message.as_str(), huge.as_str());
1160        assert_eq!(
1161            m.message_type,
1162            LayoutDebugMessageType::PositionCalculation,
1163            "the variant with no helper must still be constructible via new()"
1164        );
1165    }
1166
1167    #[test]
1168    fn debug_message_default_is_empty_info() {
1169        let m = LayoutDebugMessage::default();
1170        assert_eq!(m.message_type, LayoutDebugMessageType::Info);
1171        assert!(m.message.is_empty());
1172        assert!(m.location.is_empty(), "default() does not track a caller");
1173        assert_eq!(
1174            LayoutDebugMessageType::default(),
1175            LayoutDebugMessageType::Info
1176        );
1177    }
1178
1179    #[test]
1180    fn debug_message_accepts_string_and_str_via_into() {
1181        // `impl Into<String>` must work for both &str and String.
1182        let from_str = LayoutDebugMessage::info("borrowed");
1183        let from_string = LayoutDebugMessage::info(String::from("owned"));
1184        assert_eq!(from_str.message.as_str(), "borrowed");
1185        assert_eq!(from_string.message.as_str(), "owned");
1186    }
1187
1188    #[test]
1189    fn debug_message_clone_is_a_deep_equal_copy() {
1190        let m = LayoutDebugMessage::error("clone me \u{1F600}");
1191        let c = m.clone();
1192        assert_eq!(c, m);
1193        assert_ne!(
1194            c.message.as_bytes().as_ptr(),
1195            m.message.as_bytes().as_ptr(),
1196            "clone must deep-copy the library-owned message bytes"
1197        );
1198    }
1199
1200    // ==================================================================
1201    // AzString — constructors
1202    // ==================================================================
1203
1204    #[test]
1205    fn azstring_default_is_empty_and_readable() {
1206        let s = AzString::default();
1207        assert_eq!(s.as_str(), "");
1208        assert_eq!(s.len(), 0);
1209        assert!(s.is_empty());
1210        assert_eq!(s.as_bytes(), b"");
1211    }
1212
1213    #[test]
1214    fn azstring_from_const_str_borrows_the_static_and_never_frees_it() {
1215        // One binding, used for both the construction and the pointer check —
1216        // rustc is not obliged to dedupe two identical string literals.
1217        const TEXT: &str = "static text";
1218        let s = AzString::from_const_str(TEXT);
1219        assert_eq!(s.as_str(), TEXT);
1220        assert_eq!(s.len(), 11);
1221        assert!(
1222            matches!(s.vec.destructor, U8VecDestructor::NoDestructor),
1223            "a &'static str must not get a freeing destructor"
1224        );
1225        assert_eq!(
1226            s.vec.ptr,
1227            TEXT.as_bytes().as_ptr(),
1228            "from_const_str must alias the static, not copy it"
1229        );
1230    }
1231
1232    #[test]
1233    fn azstring_from_const_str_empty_and_unicode() {
1234        let empty = AzString::from_const_str("");
1235        assert!(empty.is_empty());
1236        assert_eq!(empty.as_str(), "");
1237        assert_eq!(empty.len(), 0);
1238
1239        let uni = AzString::from_const_str("héllo \u{1F600}");
1240        assert_eq!(uni.as_str(), "héllo \u{1F600}");
1241        // len() is BYTES, not chars: 5 ASCII-ish + 1 extra for é + space + 4 for the emoji
1242        assert_eq!(uni.len(), "héllo \u{1F600}".len());
1243        assert_ne!(
1244            uni.len(),
1245            uni.as_str().chars().count(),
1246            "len() must be a byte length, not a char count"
1247        );
1248    }
1249
1250    #[test]
1251    fn azstring_from_string_round_trips_edge_values() {
1252        for input in [
1253            String::new(),
1254            String::from(" "),
1255            String::from("\t\n\r"),
1256            String::from("0"),
1257            String::from("-0"),
1258            String::from("9223372036854775807"), // i64::MAX
1259            String::from("NaN"),
1260            String::from("inf"),
1261            String::from("  valid  "),
1262            String::from("valid;garbage"),
1263            String::from("\u{1F600}\u{0301}\u{0}"), // emoji + combining mark + NUL
1264            "{".repeat(10_000),                     // deeply "nested" junk: no parser, no overflow
1265        ] {
1266            let s = AzString::from_string(input.clone());
1267            assert_eq!(s.as_str(), input.as_str(), "from_string must be verbatim");
1268            assert_eq!(s.len(), input.len());
1269            assert_eq!(s.is_empty(), input.is_empty());
1270            // round-trip back out
1271            assert_eq!(s.into_library_owned_string(), input);
1272        }
1273    }
1274
1275    #[test]
1276    fn azstring_from_string_handles_a_megabyte() {
1277        let huge = "x".repeat(1_000_000);
1278        let s = AzString::from_string(huge.clone());
1279        assert_eq!(s.len(), 1_000_000);
1280        assert_eq!(s.as_str().len(), huge.len());
1281        assert!(s.as_str().bytes().all(|b| b == b'x'));
1282    }
1283
1284    #[test]
1285    fn azstring_from_string_preserves_the_original_capacity() {
1286        // into_library_owned_string rebuilds the String via from_raw_parts(ptr, len, cap).
1287        // If `cap` were not carried through faithfully, this would corrupt the heap.
1288        let mut owned = String::with_capacity(4096);
1289        owned.push_str("hi");
1290        let s = AzString::from_string(owned);
1291        assert!(matches!(s.vec.destructor, U8VecDestructor::DefaultRust));
1292        let back = s.into_library_owned_string();
1293        assert_eq!(back, "hi");
1294        assert!(
1295            back.capacity() >= 4096,
1296            "capacity must survive the AzString round-trip, got {}",
1297            back.capacity()
1298        );
1299    }
1300
1301    // ==================================================================
1302    // AzString::copy_from_bytes  (numeric / pointer edge cases)
1303    // ==================================================================
1304
1305    #[test]
1306    fn azstring_copy_from_bytes_zero_len_is_empty() {
1307        let buf = b"hello";
1308        let s = AzString::copy_from_bytes(buf.as_ptr(), 0, 0);
1309        assert!(s.is_empty());
1310        assert_eq!(s.as_str(), "");
1311    }
1312
1313    #[test]
1314    fn azstring_copy_from_bytes_null_ptr_is_empty() {
1315        let s = AzString::copy_from_bytes(core::ptr::null(), 0, 16);
1316        assert!(s.is_empty());
1317        assert_eq!(s.as_str(), "");
1318    }
1319
1320    #[test]
1321    fn azstring_copy_from_bytes_honours_the_start_offset() {
1322        let buf = b"0123456789";
1323        let s = AzString::copy_from_bytes(buf.as_ptr(), 3, 4);
1324        assert_eq!(s.as_str(), "3456");
1325        assert_eq!(s.len(), 4);
1326    }
1327
1328    #[test]
1329    fn azstring_copy_from_bytes_start_at_end_with_zero_len_is_empty() {
1330        // start == buf.len() is only legal because len == 0 short-circuits
1331        // before the pointer is ever offset.
1332        let buf = b"abc";
1333        let s = AzString::copy_from_bytes(buf.as_ptr(), buf.len(), 0);
1334        assert!(s.is_empty());
1335    }
1336
1337    #[test]
1338    fn azstring_copy_from_bytes_zero_len_wins_over_start_overflow() {
1339        // start + len overflows usize, but len == 0 must short-circuit BEFORE
1340        // the debug_assert / ptr.add() — no panic, no UB.
1341        let buf = b"abc";
1342        let s = AzString::copy_from_bytes(buf.as_ptr(), usize::MAX, 0);
1343        assert!(s.is_empty());
1344    }
1345
1346    #[test]
1347    fn azstring_copy_from_bytes_null_wins_over_max_len() {
1348        // The null check must precede everything, even for absurd start/len.
1349        let s = AzString::copy_from_bytes(core::ptr::null(), usize::MAX, usize::MAX);
1350        assert!(s.is_empty());
1351        assert_eq!(s.as_str(), "");
1352    }
1353
1354    #[test]
1355    fn azstring_copy_from_bytes_replaces_invalid_utf8_lossily() {
1356        // Slicing "héllo" mid-codepoint leaves a stray continuation byte (0xA9),
1357        // which must become U+FFFD so the as_str() UTF-8 invariant still holds.
1358        let buf = "héllo".as_bytes();
1359        assert_eq!(buf[1], 0xC3);
1360        assert_eq!(buf[2], 0xA9);
1361        let s = AzString::copy_from_bytes(buf.as_ptr(), 2, 2);
1362        assert_eq!(s.as_str(), "\u{FFFD}l");
1363        // The UTF-8 invariant as_str() relies on must actually hold:
1364        assert!(core::str::from_utf8(s.as_bytes()).is_ok());
1365    }
1366
1367    #[test]
1368    fn azstring_copy_from_bytes_keeps_valid_utf8_byte_for_byte() {
1369        let buf = "héllo \u{1F600}".as_bytes();
1370        let s = AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len());
1371        assert_eq!(s.as_str(), "héllo \u{1F600}");
1372        assert_eq!(s.as_bytes(), buf);
1373    }
1374
1375    #[test]
1376    fn azstring_copy_from_bytes_preserves_interior_nul() {
1377        let buf = b"a\0b";
1378        let s = AzString::copy_from_bytes(buf.as_ptr(), 0, 3);
1379        assert_eq!(s.len(), 3, "an interior NUL is data, not a terminator");
1380        assert_eq!(s.as_bytes(), b"a\0b");
1381    }
1382
1383    // ==================================================================
1384    // U8Vec::copy_from_bytes  (numeric / pointer edge cases)
1385    // ==================================================================
1386
1387    #[test]
1388    fn u8vec_copy_from_bytes_zero_len_is_empty() {
1389        let buf = b"hello";
1390        let v = U8Vec::copy_from_bytes(buf.as_ptr(), 0, 0);
1391        assert!(v.is_empty());
1392        assert_eq!(v.as_ref(), b"");
1393    }
1394
1395    #[test]
1396    fn u8vec_copy_from_bytes_null_ptr_is_empty() {
1397        let v = U8Vec::copy_from_bytes(core::ptr::null(), 0, 8);
1398        assert!(v.is_empty());
1399        assert_eq!(v.len(), 0);
1400    }
1401
1402    #[test]
1403    fn u8vec_copy_from_bytes_null_wins_over_max_start_and_len() {
1404        // Neither the debug_assert nor ptr.add() may be reached for a null ptr.
1405        let v = U8Vec::copy_from_bytes(core::ptr::null(), usize::MAX, usize::MAX);
1406        assert!(v.is_empty());
1407    }
1408
1409    #[test]
1410    fn u8vec_copy_from_bytes_zero_len_wins_over_start_overflow() {
1411        // start + len overflows, but len == 0 short-circuits first.
1412        let buf = b"abc";
1413        let v = U8Vec::copy_from_bytes(buf.as_ptr(), usize::MAX, 0);
1414        assert!(v.is_empty());
1415    }
1416
1417    #[test]
1418    fn u8vec_copy_from_bytes_copies_the_requested_window() {
1419        let buf: Vec<u8> = (0u8..=255).collect();
1420        let v = U8Vec::copy_from_bytes(buf.as_ptr(), 250, 6);
1421        assert_eq!(v.as_ref(), &[250, 251, 252, 253, 254, 255]);
1422        assert_eq!(v.len(), 6);
1423    }
1424
1425    #[test]
1426    fn u8vec_copy_from_bytes_owns_its_copy() {
1427        // The copy must survive the source buffer being dropped.
1428        let v = {
1429            let buf = vec![1u8, 2, 3, 4];
1430            U8Vec::copy_from_bytes(buf.as_ptr(), 1, 2)
1431        };
1432        assert_eq!(v.as_ref(), &[2, 3]);
1433        assert!(matches!(v.destructor, U8VecDestructor::DefaultRust));
1434    }
1435
1436    #[test]
1437    fn u8vec_copy_from_bytes_accepts_all_byte_values() {
1438        // Arbitrary (non-UTF-8) bytes must round-trip unchanged — U8Vec has no
1439        // encoding invariant, unlike AzString.
1440        let buf: Vec<u8> = (0u8..=255).collect();
1441        let v = U8Vec::copy_from_bytes(buf.as_ptr(), 0, buf.len());
1442        assert_eq!(v.as_ref(), buf.as_slice());
1443    }
1444
1445    // ==================================================================
1446    // AzString::from_c_str
1447    // ==================================================================
1448
1449    #[test]
1450    fn azstring_from_c_str_null_is_empty() {
1451        let s = unsafe { AzString::from_c_str(core::ptr::null()) };
1452        assert!(s.is_empty());
1453        assert_eq!(s.as_str(), "");
1454    }
1455
1456    #[test]
1457    fn azstring_from_c_str_reads_up_to_the_terminator() {
1458        let c = b"hello\0trailing garbage\0";
1459        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1460        assert_eq!(s.as_str(), "hello");
1461        assert_eq!(s.len(), 5, "the NUL terminator is not part of the string");
1462    }
1463
1464    #[test]
1465    fn azstring_from_c_str_empty_c_string_is_empty() {
1466        let c = b"\0";
1467        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1468        assert!(s.is_empty());
1469    }
1470
1471    #[test]
1472    fn azstring_from_c_str_replaces_non_utf8_bytes() {
1473        // A latin-1 "café" is not valid UTF-8; it must come back lossily
1474        // rather than violating the as_str() invariant.
1475        let c = b"caf\xE9\0";
1476        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1477        assert_eq!(s.as_str(), "caf\u{FFFD}");
1478        assert!(core::str::from_utf8(s.as_bytes()).is_ok());
1479    }
1480
1481    #[test]
1482    fn azstring_from_c_str_handles_a_long_c_string() {
1483        let mut c = "z".repeat(100_000).into_bytes();
1484        c.push(0);
1485        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1486        assert_eq!(s.len(), 100_000);
1487    }
1488
1489    // ==================================================================
1490    // AzString::to_c_str  (+ round trip through from_c_str)
1491    // ==================================================================
1492
1493    #[test]
1494    fn azstring_to_c_str_appends_exactly_one_nul() {
1495        let s = AzString::from_const_str("abc");
1496        let c = s.to_c_str();
1497        assert_eq!(c.as_ref(), b"abc\0");
1498        assert_eq!(c.len(), s.len() + 1);
1499    }
1500
1501    #[test]
1502    fn azstring_to_c_str_of_empty_is_just_the_terminator() {
1503        let c = AzString::default().to_c_str();
1504        assert_eq!(c.as_ref(), b"\0");
1505        assert_eq!(c.len(), 1);
1506    }
1507
1508    #[test]
1509    fn azstring_c_str_round_trip() {
1510        for original in ["", "abc", "héllo \u{1F600}", "  spaced  "] {
1511            let s = AzString::from_const_str(original);
1512            let c = s.to_c_str();
1513            let back = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1514            assert_eq!(back.as_str(), original, "C round-trip must be lossless");
1515            assert_eq!(back, s);
1516        }
1517    }
1518
1519    #[test]
1520    fn azstring_c_str_round_trip_truncates_at_an_interior_nul() {
1521        // Documented C-string reality: a string containing a NUL cannot survive
1522        // a *const char* round-trip. Assert the truncation is deterministic
1523        // rather than pretending it round-trips.
1524        let s = AzString::from_string(String::from("a\0b"));
1525        let c = s.to_c_str();
1526        assert_eq!(c.as_ref(), b"a\0b\0", "to_c_str keeps the interior NUL");
1527        let back = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
1528        assert_eq!(back.as_str(), "a", "from_c_str stops at the first NUL");
1529    }
1530
1531    #[test]
1532    fn azstring_to_c_str_is_an_independent_allocation() {
1533        let s = AzString::from_const_str("shared?");
1534        let c = s.to_c_str();
1535        assert!(matches!(c.destructor, U8VecDestructor::DefaultRust));
1536        assert_ne!(
1537            c.as_ptr(),
1538            s.as_bytes().as_ptr(),
1539            "to_c_str must copy, not alias the source"
1540        );
1541        assert_eq!(s.as_str(), "shared?", "source must be untouched");
1542    }
1543
1544    // ==================================================================
1545    // AzString::from_utf8 / from_utf8_lossy
1546    // ==================================================================
1547
1548    #[test]
1549    fn azstring_from_utf8_null_or_zero_len_is_empty() {
1550        let buf = b"abc";
1551        assert!(unsafe { AzString::from_utf8(core::ptr::null(), 3) }.is_empty());
1552        assert!(unsafe { AzString::from_utf8(buf.as_ptr(), 0) }.is_empty());
1553        assert!(unsafe { AzString::from_utf8_lossy(core::ptr::null(), 3) }.is_empty());
1554        assert!(unsafe { AzString::from_utf8_lossy(buf.as_ptr(), 0) }.is_empty());
1555    }
1556
1557    #[test]
1558    fn azstring_from_utf8_accepts_valid_multibyte() {
1559        let buf = "héllo \u{1F600}".as_bytes();
1560        let s = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
1561        assert_eq!(s.as_str(), "héllo \u{1F600}");
1562        assert_eq!(s.len(), buf.len());
1563    }
1564
1565    #[test]
1566    fn azstring_from_utf8_rejects_invalid_but_lossy_replaces_it() {
1567        // A truncated 2-byte sequence: strict → empty, lossy → U+FFFD.
1568        let buf = b"caf\xC3";
1569        let strict = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
1570        assert!(
1571            strict.is_empty(),
1572            "from_utf8 must return an EMPTY string for invalid UTF-8, got {:?}",
1573            strict.as_str()
1574        );
1575        let lossy = unsafe { AzString::from_utf8_lossy(buf.as_ptr(), buf.len()) };
1576        assert_eq!(lossy.as_str(), "caf\u{FFFD}");
1577    }
1578
1579    #[test]
1580    fn azstring_from_utf8_rejects_overlong_and_stray_continuations() {
1581        for bad in [
1582            &b"\xC0\xAF"[..],         // overlong encoding of '/'
1583            &b"\xED\xA0\x80"[..],     // UTF-16 surrogate half, illegal in UTF-8
1584            &b"\xF8\x88\x80\x80"[..], // 5-byte sequence, illegal since RFC 3629
1585            &b"\x80"[..],             // stray continuation byte
1586            &b"\xFF\xFE"[..],         // never-valid bytes
1587        ] {
1588            let strict = unsafe { AzString::from_utf8(bad.as_ptr(), bad.len()) };
1589            assert!(strict.is_empty(), "from_utf8 must reject {bad:?}");
1590
1591            let lossy = unsafe { AzString::from_utf8_lossy(bad.as_ptr(), bad.len()) };
1592            assert!(
1593                lossy.as_str().contains('\u{FFFD}'),
1594                "from_utf8_lossy must substitute U+FFFD for {bad:?}"
1595            );
1596            // Both paths must uphold the UTF-8 invariant that as_str() assumes.
1597            assert!(core::str::from_utf8(lossy.as_bytes()).is_ok());
1598        }
1599    }
1600
1601    #[test]
1602    fn azstring_from_utf8_keeps_interior_nul() {
1603        let buf = b"a\0b";
1604        let s = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
1605        assert_eq!(s.len(), 3);
1606        assert_eq!(s.as_bytes(), b"a\0b");
1607    }
1608
1609    #[test]
1610    fn azstring_from_utf8_handles_a_megabyte() {
1611        let buf = "y".repeat(1_000_000);
1612        let s = unsafe { AzString::from_utf8(buf.as_bytes().as_ptr(), buf.len()) };
1613        assert_eq!(s.len(), 1_000_000);
1614    }
1615
1616    // ==================================================================
1617    // AzString::from_utf16_le / from_utf16_be / from_utf16_with_byte_order
1618    // ==================================================================
1619
1620    #[test]
1621    fn azstring_from_utf16_le_decodes_bmp_and_surrogate_pairs() {
1622        let text = "héllo \u{1F600}"; // the emoji needs a surrogate pair
1623        let bytes = utf16_bytes(text, true);
1624        let s = unsafe { AzString::from_utf16_le(bytes.as_ptr(), bytes.len()) };
1625        assert_eq!(s.as_str(), text);
1626    }
1627
1628    #[test]
1629    fn azstring_from_utf16_be_decodes_bmp_and_surrogate_pairs() {
1630        let text = "héllo \u{1F600}";
1631        let bytes = utf16_bytes(text, false);
1632        let s = unsafe { AzString::from_utf16_be(bytes.as_ptr(), bytes.len()) };
1633        assert_eq!(s.as_str(), text);
1634    }
1635
1636    #[test]
1637    fn azstring_from_utf16_byte_order_actually_matters() {
1638        // Decoding LE bytes as BE must NOT silently yield the same text.
1639        let le = utf16_bytes("AB", true);
1640        assert_eq!(le.as_slice(), &[0x41, 0x00, 0x42, 0x00]);
1641        let as_be = unsafe { AzString::from_utf16_be(le.as_ptr(), le.len()) };
1642        assert_eq!(
1643            as_be.as_str(),
1644            "\u{4100}\u{4200}",
1645            "BE decode of LE bytes must byte-swap, not guess"
1646        );
1647        assert_ne!(as_be.as_str(), "AB");
1648    }
1649
1650    #[test]
1651    fn azstring_from_utf16_odd_length_is_empty() {
1652        let bytes = utf16_bytes("hello", true);
1653        let odd = bytes.len() - 1;
1654        assert_eq!(odd % 2, 1);
1655        // Still inside the buffer, so this is a safe call — it must be rejected
1656        // on the length check, not read a half code unit.
1657        assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), odd) }.is_empty());
1658        assert!(unsafe { AzString::from_utf16_be(bytes.as_ptr(), odd) }.is_empty());
1659        // The smallest odd length of all:
1660        assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), 1) }.is_empty());
1661    }
1662
1663    #[test]
1664    fn azstring_from_utf16_null_or_zero_len_is_empty() {
1665        let bytes = utf16_bytes("hi", true);
1666        assert!(unsafe { AzString::from_utf16_le(core::ptr::null(), 4) }.is_empty());
1667        assert!(unsafe { AzString::from_utf16_be(core::ptr::null(), 4) }.is_empty());
1668        assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), 0) }.is_empty());
1669        assert!(unsafe { AzString::from_utf16_be(bytes.as_ptr(), 0) }.is_empty());
1670    }
1671
1672    #[test]
1673    fn azstring_from_utf16_unpaired_surrogate_is_empty() {
1674        // A lone high surrogate is not valid UTF-16 → documented empty result.
1675        let lone_high: [u8; 2] = 0xD83C_u16.to_le_bytes();
1676        assert!(unsafe { AzString::from_utf16_le(lone_high.as_ptr(), 2) }.is_empty());
1677
1678        // A lone LOW surrogate, and a reversed (low-then-high) pair.
1679        let lone_low: [u8; 2] = 0xDF89_u16.to_le_bytes();
1680        assert!(unsafe { AzString::from_utf16_le(lone_low.as_ptr(), 2) }.is_empty());
1681
1682        let reversed: Vec<u8> = [0xDF89_u16, 0xD83C_u16]
1683            .iter()
1684            .flat_map(|u| u.to_le_bytes())
1685            .collect();
1686        assert!(unsafe { AzString::from_utf16_le(reversed.as_ptr(), reversed.len()) }.is_empty());
1687    }
1688
1689    #[test]
1690    fn azstring_from_utf16_decodes_noncharacters_and_nul() {
1691        // U+FFFE / U+0000 are valid code points (not surrogates) — they must
1692        // decode rather than being treated as an error or a terminator.
1693        let units: Vec<u8> = [0x0041_u16, 0x0000, 0xFFFE]
1694            .iter()
1695            .flat_map(|u| u.to_le_bytes())
1696            .collect();
1697        let s = unsafe { AzString::from_utf16_le(units.as_ptr(), units.len()) };
1698        assert_eq!(s.as_str(), "A\u{0}\u{FFFE}");
1699        assert_eq!(s.len(), 1 + 1 + 3);
1700    }
1701
1702    #[test]
1703    fn azstring_from_utf16_handles_100k_code_units() {
1704        let text = "ab".repeat(50_000);
1705        let bytes = utf16_bytes(&text, true);
1706        assert_eq!(bytes.len(), 200_000);
1707        let s = unsafe { AzString::from_utf16_le(bytes.as_ptr(), bytes.len()) };
1708        assert_eq!(s.len(), 100_000);
1709    }
1710
1711    #[test]
1712    fn azstring_from_utf16_with_byte_order_honours_the_supplied_fn() {
1713        // The private shared impl must use the caller's byte-order fn verbatim.
1714        fn swap_halves(b: [u8; 2]) -> u16 {
1715            u16::from_be_bytes(b)
1716        }
1717        let le = utf16_bytes("Az", true);
1718        let via_shared = unsafe {
1719            AzString::from_utf16_with_byte_order(le.as_ptr(), le.len(), u16::from_le_bytes)
1720        };
1721        assert_eq!(via_shared.as_str(), "Az");
1722
1723        let swapped =
1724            unsafe { AzString::from_utf16_with_byte_order(le.as_ptr(), le.len(), swap_halves) };
1725        assert_eq!(swapped.as_str(), "\u{4100}\u{7A00}");
1726
1727        // The odd-length / null guards live in the shared impl, so check them here too.
1728        assert!(unsafe {
1729            AzString::from_utf16_with_byte_order(le.as_ptr(), 3, u16::from_le_bytes)
1730        }
1731        .is_empty());
1732        assert!(unsafe {
1733            AzString::from_utf16_with_byte_order(core::ptr::null(), 2, u16::from_le_bytes)
1734        }
1735        .is_empty());
1736    }
1737
1738    // ==================================================================
1739    // AzString — getters / predicates / conversions
1740    // ==================================================================
1741
1742    #[test]
1743    fn azstring_as_str_and_as_bytes_agree_for_every_constructor() {
1744        let buf = "mixed \u{1F600}".as_bytes();
1745        let mut arena = StringArena::new();
1746        let strings = [
1747            AzString::default(),
1748            AzString::from_const_str("mixed \u{1F600}"),
1749            AzString::from_string(String::from("mixed \u{1F600}")),
1750            AzString::from("mixed \u{1F600}"),
1751            AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len()),
1752            unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) },
1753            arena.intern("mixed \u{1F600}"),
1754        ];
1755        for s in &strings {
1756            assert_eq!(
1757                s.as_bytes(),
1758                s.as_str().as_bytes(),
1759                "as_bytes() and as_str() must view the same memory"
1760            );
1761            assert_eq!(s.len(), s.as_bytes().len());
1762            assert_eq!(s.is_empty(), s.len() == 0);
1763            let via_as_ref: &str = s.as_ref();
1764            assert_eq!(via_as_ref, s.as_str(), "AsRef must match as_str");
1765            assert_eq!(&**s, s.as_str(), "Deref must match as_str");
1766        }
1767    }
1768
1769    #[test]
1770    fn azstring_is_empty_only_for_zero_bytes() {
1771        assert!(AzString::default().is_empty());
1772        assert!(AzString::from_const_str("").is_empty());
1773        assert!(AzString::from_string(String::new()).is_empty());
1774        // Whitespace and a NUL byte are content, not emptiness.
1775        assert!(!AzString::from_const_str(" ").is_empty());
1776        assert!(!AzString::from_const_str("\t\n").is_empty());
1777        assert!(!AzString::from_string(String::from("\0")).is_empty());
1778        assert_eq!(AzString::from_string(String::from("\0")).len(), 1);
1779    }
1780
1781    #[test]
1782    fn azstring_len_counts_bytes_not_chars() {
1783        assert_eq!(AzString::from_const_str("é").len(), 2);
1784        assert_eq!(AzString::from_const_str("\u{1F600}").len(), 4);
1785        assert_eq!(AzString::from_const_str("e\u{0301}").len(), 3); // combining accent
1786        assert_eq!(
1787            AzString::from_const_str("\u{1F600}")
1788                .as_str()
1789                .chars()
1790                .count(),
1791            1
1792        );
1793    }
1794
1795    #[test]
1796    fn azstring_into_bytes_moves_without_copying_or_double_freeing() {
1797        let s = AzString::from_string(String::from("payload"));
1798        let ptr = s.as_bytes().as_ptr();
1799        let (len, cap) = (s.vec.len, s.vec.cap);
1800        let v = s.into_bytes();
1801        assert_eq!(v.as_ref(), b"payload");
1802        assert_eq!(v.as_ptr(), ptr, "into_bytes must move, not copy");
1803        assert_eq!(v.len(), len);
1804        assert_eq!(v.capacity(), cap);
1805        assert!(matches!(v.destructor, U8VecDestructor::DefaultRust));
1806        // Dropping `v` here frees the buffer exactly once (the source was
1807        // ManuallyDrop'd) — a double free would abort the test process.
1808    }
1809
1810    #[test]
1811    fn azstring_into_bytes_preserves_a_non_owning_destructor() {
1812        let v = AzString::from_const_str("static").into_bytes();
1813        assert_eq!(v.as_ref(), b"static");
1814        assert!(
1815            matches!(v.destructor, U8VecDestructor::NoDestructor),
1816            "a &'static-backed AzString must not gain a freeing destructor"
1817        );
1818    }
1819
1820    #[test]
1821    fn azstring_into_bytes_of_empty_is_empty() {
1822        let v = AzString::default().into_bytes();
1823        assert!(v.is_empty());
1824        assert_eq!(v.as_ref(), b"");
1825    }
1826
1827    #[test]
1828    fn azstring_into_library_owned_string_works_for_all_destructors() {
1829        // DefaultRust: moves the allocation out.
1830        assert_eq!(
1831            AzString::from_string(String::from("owned \u{1F600}")).into_library_owned_string(),
1832            "owned \u{1F600}"
1833        );
1834        // NoDestructor: must COPY the static, never take ownership of it.
1835        assert_eq!(
1836            AzString::from_const_str("static").into_library_owned_string(),
1837            "static"
1838        );
1839        // External (arena-backed): must copy out of the arena.
1840        let owned = {
1841            let mut arena = StringArena::new();
1842            let s = arena.intern("interned");
1843            s.into_library_owned_string()
1844        };
1845        assert_eq!(
1846            owned, "interned",
1847            "must outlive the arena it was copied from"
1848        );
1849        // Empty / default.
1850        assert_eq!(AzString::default().into_library_owned_string(), "");
1851    }
1852
1853    #[test]
1854    fn azstring_into_library_owned_string_copies_static_memory() {
1855        let mut owned = AzString::from_const_str("static").into_library_owned_string();
1856        // If this had aliased the &'static str, mutating it would be UB /
1857        // a segfault writing to rodata.
1858        owned.push_str(" + mutable");
1859        assert_eq!(owned, "static + mutable");
1860    }
1861
1862    // ==================================================================
1863    // AzString::clone_self
1864    // ==================================================================
1865
1866    #[test]
1867    fn azstring_clone_self_deep_copies_library_owned_memory() {
1868        let s = AzString::from_string(String::from("deep"));
1869        let c = s.clone_self();
1870        assert_eq!(c, s);
1871        assert_ne!(
1872            c.as_bytes().as_ptr(),
1873            s.as_bytes().as_ptr(),
1874            "a DefaultRust clone must own a fresh allocation"
1875        );
1876        assert!(matches!(c.vec.destructor, U8VecDestructor::DefaultRust));
1877    }
1878
1879    #[test]
1880    fn azstring_clone_self_shares_static_memory() {
1881        let s = AzString::from_const_str("static");
1882        let c = s.clone_self();
1883        assert_eq!(c, s);
1884        assert_eq!(
1885            c.as_bytes().as_ptr(),
1886            s.as_bytes().as_ptr(),
1887            "cloning a &'static-backed string should alias, not allocate"
1888        );
1889        assert!(matches!(c.vec.destructor, U8VecDestructor::NoDestructor));
1890    }
1891
1892    #[test]
1893    fn azstring_clone_self_of_empty_and_unicode() {
1894        for s in [
1895            AzString::default(),
1896            AzString::from_const_str(""),
1897            AzString::from_string(String::from("\u{1F600}\u{0}\u{0301}")),
1898        ] {
1899            let c = s.clone_self();
1900            assert_eq!(c.as_str(), s.as_str());
1901            assert_eq!(c.len(), s.len());
1902        }
1903    }
1904
1905    #[test]
1906    fn azstring_clone_trait_matches_clone_self() {
1907        let s = AzString::from_string(String::from("via trait"));
1908        assert_eq!(s.clone(), s.clone_self());
1909    }
1910
1911    // ==================================================================
1912    // AzString — Debug / Display round trips (fmt)
1913    // ==================================================================
1914
1915    #[test]
1916    fn azstring_display_round_trips_through_from() {
1917        for original in [
1918            "",
1919            " ",
1920            "plain",
1921            "héllo \u{1F600}",
1922            "with \"quotes\" and \\ backslash",
1923            "line\nbreak\ttab",
1924            "e\u{0301} combining",
1925        ] {
1926            let s = AzString::from(original);
1927            let rendered = format!("{s}");
1928            assert_eq!(rendered, original, "Display must emit the string verbatim");
1929            let reparsed = AzString::from(rendered.as_str());
1930            assert_eq!(reparsed, s, "parse(serialize(x)) == x");
1931            // serialize(parse(serialize(x))) == serialize(x)
1932            assert_eq!(format!("{reparsed}"), rendered);
1933        }
1934    }
1935
1936    #[test]
1937    fn azstring_debug_matches_str_debug_and_escapes() {
1938        let s = AzString::from("a\"b\\c\nd");
1939        let expected = format!("{:?}", "a\"b\\c\nd");
1940        assert_eq!(
1941            format!("{s:?}"),
1942            expected,
1943            "Debug must delegate to str::fmt"
1944        );
1945        assert!(
1946            format!("{s:?}").starts_with('"'),
1947            "Debug output must be quoted"
1948        );
1949        assert!(
1950            !format!("{s:?}").contains('\n'),
1951            "Debug must escape newlines"
1952        );
1953    }
1954
1955    #[test]
1956    fn azstring_debug_and_display_of_empty_do_not_panic() {
1957        assert_eq!(format!("{:?}", AzString::default()), "\"\"");
1958        assert_eq!(format!("{}", AzString::default()), "");
1959        assert_eq!(format!("{:?}", AzString::from_const_str("")), "\"\"");
1960    }
1961
1962    #[test]
1963    fn azstring_display_of_a_megabyte_is_lossless() {
1964        let huge = "q".repeat(1_000_000);
1965        let s = AzString::from_string(huge.clone());
1966        assert_eq!(format!("{s}").len(), huge.len());
1967    }
1968
1969    #[test]
1970    fn azstring_debug_is_stable_across_constructors() {
1971        // Same text, different memory ownership → identical rendering.
1972        let buf = "same".as_bytes();
1973        let a = AzString::from_const_str("same");
1974        let b = AzString::from_string(String::from("same"));
1975        let c = AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len());
1976        assert_eq!(format!("{a:?}"), format!("{b:?}"));
1977        assert_eq!(format!("{b:?}"), format!("{c:?}"));
1978        assert_eq!(format!("{a}"), format!("{c}"));
1979    }
1980
1981    // ==================================================================
1982    // AzString — Eq / Ord / Hash invariants
1983    // ==================================================================
1984
1985    #[test]
1986    fn azstring_eq_and_hash_ignore_memory_ownership() {
1987        let buf = "key".as_bytes();
1988        let mut arena = StringArena::new();
1989        let variants = [
1990            AzString::from_const_str("key"),
1991            AzString::from_string(String::from("key")),
1992            AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len()),
1993            arena.intern("key"),
1994        ];
1995        for v in &variants {
1996            assert_eq!(
1997                *v, variants[0],
1998                "equality must compare CONTENT, not pointers"
1999            );
2000            assert_eq!(
2001                hash_of(v),
2002                hash_of(&variants[0]),
2003                "Hash must agree with Eq across destructor kinds"
2004            );
2005            assert_eq!(
2006                hash_of(v),
2007                hash_of(&"key"),
2008                "AzString must hash like the &str it wraps"
2009            );
2010        }
2011    }
2012
2013    #[test]
2014    fn azstring_ord_matches_str_ord() {
2015        let mut v = [
2016            AzString::from("b"),
2017            AzString::from(""),
2018            AzString::from("\u{1F600}"),
2019            AzString::from("a"),
2020            AzString::from("ab"),
2021        ];
2022        v.sort();
2023        let sorted: Vec<&str> = v.iter().map(AzString::as_str).collect();
2024        assert_eq!(sorted, ["", "a", "ab", "b", "\u{1F600}"]);
2025        assert_eq!(
2026            AzString::from("a").partial_cmp(&AzString::from("b")),
2027            Some(core::cmp::Ordering::Less)
2028        );
2029        assert_eq!(
2030            AzString::from("x").cmp(&AzString::from("x")),
2031            core::cmp::Ordering::Equal
2032        );
2033    }
2034
2035    // ==================================================================
2036    // StringArena
2037    // ==================================================================
2038
2039    #[test]
2040    fn arena_new_starts_empty() {
2041        let arena = StringArena::new();
2042        assert_eq!(arena.metrics(), (0, 0), "a fresh arena allocates nothing");
2043        assert_eq!(StringArena::default().metrics(), (0, 0));
2044    }
2045
2046    #[test]
2047    fn arena_metrics_track_chunks_and_bytes() {
2048        let mut arena = StringArena::new();
2049        let _a = arena.intern("abc");
2050        let (chunks, bytes) = arena.metrics();
2051        assert_eq!(chunks, 1);
2052        assert_eq!(bytes, 3);
2053        let _b = arena.intern("de");
2054        let (chunks, bytes) = arena.metrics();
2055        assert_eq!(chunks, 1, "a second small string reuses the open chunk");
2056        assert_eq!(bytes, 5);
2057    }
2058
2059    #[test]
2060    fn arena_empty_string_allocates_nothing_and_is_readable() {
2061        let mut arena = StringArena::new();
2062        let e = arena.intern("");
2063        assert!(e.is_empty());
2064        assert_eq!(e.as_str(), "");
2065        assert_eq!(arena.metrics(), (0, 0), "empty strings need no storage");
2066        assert!(
2067            !e.vec.ptr.is_null(),
2068            "the dangling ptr must still be non-null"
2069        );
2070    }
2071
2072    #[test]
2073    fn arena_string_is_external_and_stashes_an_arc_in_cap() {
2074        let mut arena = StringArena::new();
2075        let s = arena.intern("hi");
2076        assert!(matches!(s.vec.destructor, U8VecDestructor::External(_)));
2077        assert_ne!(s.vec.cap, 0, "cap holds the Arc pointer, not a capacity");
2078        assert_eq!(s.as_str(), "hi");
2079    }
2080
2081    #[test]
2082    fn arena_intern_refcounts_each_string() {
2083        let mut arena = StringArena::new();
2084        assert_eq!(Arc::strong_count(&arena.inner), 1);
2085        let a = arena.intern("one");
2086        let b = arena.intern("two");
2087        assert_eq!(
2088            Arc::strong_count(&arena.inner),
2089            3,
2090            "each interned string must hold its own Arc reference"
2091        );
2092        drop(a);
2093        assert_eq!(Arc::strong_count(&arena.inner), 2);
2094        drop(b);
2095        assert_eq!(Arc::strong_count(&arena.inner), 1);
2096    }
2097
2098    #[test]
2099    fn arena_clone_deep_copies_and_does_not_bump_the_refcount() {
2100        let mut arena = StringArena::new();
2101        let s = arena.intern("interned");
2102        let c = s.clone_self();
2103        assert_eq!(
2104            Arc::strong_count(&arena.inner),
2105            2,
2106            "cloning an External string deep-copies; it must NOT retain the arena"
2107        );
2108        assert!(matches!(c.vec.destructor, U8VecDestructor::DefaultRust));
2109        assert_eq!(c.as_str(), "interned");
2110        assert_ne!(c.vec.ptr, s.vec.ptr);
2111    }
2112
2113    #[test]
2114    fn arena_clone_outlives_the_arena_and_the_original() {
2115        let clone = {
2116            let mut arena = StringArena::new();
2117            let s = arena.intern("deep-copied out of the arena");
2118            let c = s.clone_self();
2119            drop(s);
2120            drop(arena);
2121            c
2122        };
2123        assert_eq!(clone.as_str(), "deep-copied out of the arena");
2124    }
2125
2126    #[test]
2127    fn arena_exact_half_chunk_boundary_fills_one_chunk_exactly() {
2128        // len == CHUNK_SIZE / 2 is NOT "oversized" (the check is `>`), so two of
2129        // them must fit in a single chunk with zero reallocation, and the third
2130        // must open a new one.
2131        let mut arena = StringArena::new();
2132        let half = "h".repeat(StringArena::CHUNK_SIZE / 2);
2133        let a = arena.intern(&half);
2134        let b = arena.intern(&half);
2135        assert_eq!(arena.metrics().0, 1, "two half-chunks must share one chunk");
2136        let c = arena.intern(&half);
2137        assert_eq!(arena.metrics().0, 2, "the third must open a new chunk");
2138
2139        // If the exact-fit append had reallocated, `a`/`b` would now dangle.
2140        assert_eq!(a.as_str(), half);
2141        assert_eq!(b.as_str(), half);
2142        assert_eq!(c.as_str(), half);
2143    }
2144
2145    #[test]
2146    fn arena_oversized_string_is_readable_and_gets_its_own_chunk() {
2147        let mut arena = StringArena::new();
2148        let big = "b".repeat(StringArena::CHUNK_SIZE + 1);
2149        let s = arena.intern(&big);
2150        assert_eq!(s.len(), big.len());
2151        assert_eq!(s.as_str(), big.as_str());
2152        assert_eq!(arena.metrics(), (1, big.len()));
2153    }
2154
2155    #[test]
2156    fn arena_many_interleaved_sizes_all_read_back_correctly() {
2157        let mut arena = StringArena::new();
2158        let mut kept = Vec::new();
2159        for i in 0..200 {
2160            let s = format!("s{i}-{}", "p".repeat(i % 17));
2161            kept.push((arena.intern(&s), s));
2162        }
2163        for (interned, expected) in &kept {
2164            assert_eq!(interned.as_str(), expected.as_str());
2165        }
2166    }
2167
2168    #[test]
2169    fn arena_interns_unicode_and_nul_bytes_verbatim() {
2170        let mut arena = StringArena::new();
2171        let weird = "héllo \u{1F600}\u{0}\u{0301}";
2172        let s = arena.intern(weird);
2173        assert_eq!(s.as_str(), weird);
2174        assert_eq!(s.len(), weird.len());
2175    }
2176
2177    #[test]
2178    fn arena_strings_outlive_the_handle_even_when_interleaved() {
2179        let (a, b) = {
2180            let mut arena = StringArena::new();
2181            let a = arena.intern("first");
2182            let big = "z".repeat(StringArena::CHUNK_SIZE * 2);
2183            let _dropped = arena.intern(&big);
2184            let b = arena.intern("second");
2185            (a, b)
2186        };
2187        assert_eq!(a.as_str(), "first");
2188        assert_eq!(b.as_str(), "second");
2189    }
2190
2191    /// RED — genuine bug in `StringArena::intern` (use-after-free).
2192    ///
2193    /// The oversized branch pushes a *dedicated, completely full* chunk
2194    /// (`len == cap`) but never touches `current_remaining`. If a small string
2195    /// was interned first, `remaining` is still > 0, so the next small intern
2196    /// skips the "push a fresh chunk" branch and appends into
2197    /// `chunks.last_mut()` — which is now that full dedicated chunk. The
2198    /// `extend_from_slice` therefore grows a `len == cap` Vec, reallocating it
2199    /// and leaving the `AzString` handed out for the oversized string pointing
2200    /// at freed memory.
2201    ///
2202    /// This test only inspects chunk *lengths* — it never dereferences the
2203    /// dangling pointer, so the test itself stays UB-free.
2204    #[test]
2205    fn arena_small_after_oversized_must_not_grow_the_full_dedicated_chunk() {
2206        let mut arena = StringArena::new();
2207
2208        // 1. open a shared chunk, leaving current_remaining > 0
2209        let _small = arena.intern("a");
2210
2211        // 2. oversized → dedicated chunk with len == cap == big_len
2212        let big = "x".repeat(StringArena::CHUNK_SIZE);
2213        let big_len = big.len();
2214        let interned_big = arena.intern(&big);
2215        assert_eq!(
2216            interned_big.as_str(),
2217            big.as_str(),
2218            "valid before the next intern"
2219        );
2220
2221        // 3. another small string: must NOT be appended into the full chunk
2222        let _small2 = arena.intern("y");
2223
2224        // Safety: read-only look at the chunk lengths; no chunk data is read
2225        // and the (possibly dangling) `interned_big.vec.ptr` is never deref'd.
2226        let grew = unsafe {
2227            let chunks = &*arena.inner.chunks.get();
2228            chunks.iter().any(|c| c.len() > big_len)
2229        };
2230        assert!(
2231            !grew,
2232            "intern() appended a small string into the FULL dedicated chunk of an oversized \
2233             string (len == cap), which reallocates that Vec and leaves every AzString pointing \
2234             into it dangling — a use-after-free. Root cause: the oversized branch pushes a chunk \
2235             without resetting `current_remaining`, so the next small string takes the \
2236             `chunks.last_mut()` fast path onto the wrong chunk."
2237        );
2238    }
2239
2240    // ==================================================================
2241    // arena_string_destructor
2242    // ==================================================================
2243
2244    #[test]
2245    fn arena_destructor_drops_one_arc_ref_and_is_idempotent() {
2246        let inner = Arc::new(StringArenaInner {
2247            chunks: UnsafeCell::new(Vec::new()),
2248            current_remaining: UnsafeCell::new(0),
2249        });
2250        let raw = Arc::into_raw(Arc::clone(&inner));
2251        assert_eq!(Arc::strong_count(&inner), 2);
2252
2253        let mut v = U8Vec {
2254            ptr: core::ptr::NonNull::<u8>::dangling().as_ptr().cast_const(),
2255            len: 0,
2256            cap: raw as usize,
2257            destructor: U8VecDestructor::External(arena_string_destructor),
2258        };
2259
2260        arena_string_destructor(&mut v);
2261        assert_eq!(
2262            Arc::strong_count(&inner),
2263            1,
2264            "the destructor must release exactly one Arc reference"
2265        );
2266        assert_eq!(
2267            v.cap, 0,
2268            "cap must be zeroed to guard against a double drop"
2269        );
2270
2271        // A second call must be a no-op rather than a double free.
2272        arena_string_destructor(&mut v);
2273        assert_eq!(Arc::strong_count(&inner), 1);
2274        assert_eq!(v.cap, 0);
2275
2276        // `v` still carries the External destructor; dropping it runs the
2277        // destructor a third time — also a no-op, since cap == 0.
2278        drop(v);
2279        assert_eq!(Arc::strong_count(&inner), 1);
2280    }
2281
2282    #[test]
2283    fn arena_destructor_tolerates_a_null_arc_pointer() {
2284        // cap == 0 (e.g. a zeroed FFI husk) must not be turned into Arc::from_raw(null).
2285        let mut v = U8Vec {
2286            ptr: core::ptr::null(),
2287            len: 0,
2288            cap: 0,
2289            destructor: U8VecDestructor::NoDestructor,
2290        };
2291        arena_string_destructor(&mut v);
2292        assert_eq!(v.cap, 0);
2293    }
2294
2295    #[test]
2296    fn arena_last_reference_frees_the_chunks() {
2297        // The arena's bytes must survive until the LAST AzString goes away,
2298        // and dropping in either order must not double-free.
2299        let inner_ptr;
2300        let s = {
2301            let mut arena = StringArena::new();
2302            let s = arena.intern("outlives the handle");
2303            inner_ptr = Arc::as_ptr(&arena.inner);
2304            assert_eq!(Arc::strong_count(&arena.inner), 2);
2305            s
2306        };
2307        // The arena handle is gone but the string still owns a reference.
2308        assert_eq!(s.as_str(), "outlives the handle");
2309        assert_eq!(s.vec.cap as *const StringArenaInner, inner_ptr);
2310        drop(s); // final reference → chunks freed here, exactly once
2311    }
2312}