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

            
8
use alloc::{
9
    string::{String, ToString},
10
    vec::Vec,
11
};
12

            
13
use 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)]
40
pub 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

            
47
impl EmptyStruct {
48
    /// Create a new `EmptyStruct` value (equivalent to `()`)
49
    #[must_use]
50
169
    pub const fn new() -> Self {
51
169
        Self { _reserved: 0 }
52
169
    }
53
}
54

            
55
impl From<()> for EmptyStruct {
56
1
    fn from((): ()) -> Self {
57
1
        Self::default()
58
1
    }
59
}
60

            
61
impl From<EmptyStruct> for () {
62
1
    fn from(_: EmptyStruct) -> Self {
63
        
64
1
    }
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)]
75
pub 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)]
96
pub struct LayoutDebugMessage {
97
    pub message_type: LayoutDebugMessageType,
98
    pub message: AzString,
99
    pub location: AzString,
100
}
101

            
102
impl LayoutDebugMessage {
103
    /// Create a new debug message with automatic caller location tracking
104
    #[track_caller]
105
12619190
    pub fn new(message_type: LayoutDebugMessageType, message: impl Into<String>) -> Self {
106
12619190
        let location = core::panic::Location::caller();
107
12619190
        Self {
108
12619190
            message_type,
109
12619190
            message: AzString::from_string(message.into()),
110
12619190
            location: AzString::from_string(format!(
111
12619190
                "{}:{}:{}",
112
12619190
                location.file(),
113
12619190
                location.line(),
114
12619190
                location.column()
115
12619190
            )),
116
12619190
        }
117
12619190
    }
118

            
119
    /// Helper for Info messages
120
    #[track_caller]
121
11044692
    pub fn info(message: impl Into<String>) -> Self {
122
11044692
        Self::new(LayoutDebugMessageType::Info, message)
123
11044692
    }
124

            
125
    /// Helper for Warning messages
126
    #[track_caller]
127
56
    pub fn warning(message: impl Into<String>) -> Self {
128
56
        Self::new(LayoutDebugMessageType::Warning, message)
129
56
    }
130

            
131
    /// Helper for Error messages
132
    #[track_caller]
133
6
    pub fn error(message: impl Into<String>) -> Self {
134
6
        Self::new(LayoutDebugMessageType::Error, message)
135
6
    }
136

            
137
    /// Helper for `BoxProps` debug messages
138
    #[track_caller]
139
381728
    pub fn box_props(message: impl Into<String>) -> Self {
140
381728
        Self::new(LayoutDebugMessageType::BoxProps, message)
141
381728
    }
142

            
143
    /// Helper for CSS Getter debug messages
144
    #[track_caller]
145
14
    pub fn css_getter(message: impl Into<String>) -> Self {
146
14
        Self::new(LayoutDebugMessageType::CssGetter, message)
147
14
    }
148

            
149
    /// Helper for BFC Layout debug messages
150
    #[track_caller]
151
4
    pub fn bfc_layout(message: impl Into<String>) -> Self {
152
4
        Self::new(LayoutDebugMessageType::BfcLayout, message)
153
4
    }
154

            
155
    /// Helper for IFC Layout debug messages
156
    #[track_caller]
157
1132136
    pub fn ifc_layout(message: impl Into<String>) -> Self {
158
1132136
        Self::new(LayoutDebugMessageType::IfcLayout, message)
159
1132136
    }
160

            
161
    /// Helper for Table Layout debug messages
162
    #[track_caller]
163
13162
    pub fn table_layout(message: impl Into<String>) -> Self {
164
13162
        Self::new(LayoutDebugMessageType::TableLayout, message)
165
13162
    }
166

            
167
    /// Helper for Display Type debug messages
168
    #[track_caller]
169
4
    pub fn display_type(message: impl Into<String>) -> Self {
170
4
        Self::new(LayoutDebugMessageType::DisplayType, message)
171
4
    }
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)]
179
pub struct AzString {
180
    pub vec: U8Vec,
181
}
182

            
183
impl_option!(
184
    AzString,
185
    OptionString,
186
    copy = false,
187
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
188
);
189

            
190
static DEFAULT_STR: &str = "";
191

            
192
impl Default for AzString {
193
141951
    fn default() -> Self {
194
141951
        DEFAULT_STR.into()
195
141951
    }
196
}
197

            
198
impl<'a> From<&'a str> for AzString {
199
7799952
    fn from(s: &'a str) -> Self {
200
7799952
        s.to_string().into()
201
7799952
    }
202
}
203

            
204
impl AsRef<str> for AzString {
205
295
    fn as_ref(&self) -> &str {
206
295
        self.as_str()
207
295
    }
208
}
209

            
210
impl core::fmt::Debug for AzString {
211
932618
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
212
932618
        self.as_str().fmt(f)
213
932618
    }
214
}
215

            
216
impl core::fmt::Display for AzString {
217
32534
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
218
32534
        self.as_str().fmt(f)
219
32534
    }
220
}
221

            
222
impl AzString {
223
    #[inline]
224
2520412
    #[must_use] pub const fn from_const_str(s: &'static str) -> Self {
225
2520412
        Self {
226
2520412
            vec: U8Vec::from_const_slice(s.as_bytes()),
227
2520412
        }
228
2520412
    }
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
10
    #[must_use] pub unsafe fn from_c_str(ptr: *const i8) -> Self { unsafe {
246
10
        if ptr.is_null() {
247
1
            return Self::default();
248
9
        }
249
9
        let c_str = core::ffi::CStr::from_ptr(ptr as *const core::ffi::c_char);
250
9
        let bytes = c_str.to_bytes();
251
9
        Self::copy_from_bytes(bytes.as_ptr(), 0, bytes.len())
252
10
    }}
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
21
    #[must_use] pub fn copy_from_bytes(ptr: *const u8, start: usize, len: usize) -> Self {
268
21
        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
21
        if core::str::from_utf8(raw.as_ref()).is_ok() {
276
19
            return Self { vec: raw };
277
2
        }
278
2
        let s = String::from_utf8_lossy(raw.as_ref()).into_owned();
279
2
        Self::from_string(s)
280
21
    }
281

            
282
    #[inline] // web-lift: inline through the sret-in-X0 chain (see copy_from_bytes)
283
46939525
    #[must_use] pub const fn from_string(s: String) -> Self {
284
46939525
        Self {
285
46939525
            vec: U8Vec::from_vec(s.into_bytes()),
286
46939525
        }
287
46939525
    }
288

            
289
    #[inline]
290
32138213
    #[must_use] pub fn as_str(&self) -> &str {
291
32138213
        unsafe { core::str::from_utf8_unchecked(self.vec.as_ref()) }
292
32138213
    }
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
3146780
    #[must_use] pub fn clone_self(&self) -> Self {
298
3146780
        Self {
299
3146780
            vec: self.vec.clone_self(),
300
3146780
        }
301
3146780
    }
302

            
303
    #[inline]
304
13036
    #[must_use] pub fn into_library_owned_string(self) -> String {
305
13036
        match self.vec.destructor {
306
            U8VecDestructor::NoDestructor | U8VecDestructor::External(_) | U8VecDestructor::AlreadyDestroyed => {
307
3
                self.as_str().to_string()
308
            }
309
            U8VecDestructor::DefaultRust => {
310
13033
                let m = core::mem::ManuallyDrop::new(self);
311
13033
                unsafe { String::from_raw_parts(m.vec.ptr.cast_mut(), m.vec.len, m.vec.cap) }
312
            }
313
        }
314
13036
    }
315

            
316
    #[inline]
317
54
    #[must_use] pub fn as_bytes(&self) -> &[u8] {
318
54
        self.vec.as_ref()
319
54
    }
320

            
321
    #[inline]
322
3
    #[must_use] pub fn into_bytes(self) -> U8Vec {
323
3
        let m = core::mem::ManuallyDrop::new(self);
324
3
        U8Vec {
325
3
            ptr: m.vec.ptr,
326
3
            len: m.vec.len,
327
3
            cap: m.vec.cap,
328
3
            destructor: m.vec.destructor,
329
3
        }
330
3
    }
331

            
332
    /// Returns the length of the string in bytes (not including null terminator)
333
    #[inline]
334
314
    #[must_use] pub const fn len(&self) -> usize {
335
314
        self.vec.len
336
314
    }
337

            
338
    /// Returns true if the string is empty
339
    #[inline]
340
98
    #[must_use] pub const fn is_empty(&self) -> bool {
341
98
        self.vec.len == 0
342
98
    }
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
8
    #[must_use] pub fn to_c_str(&self) -> U8Vec {
351
8
        let bytes = self.as_bytes();
352
8
        let mut result = Vec::with_capacity(bytes.len() + 1);
353
8
        result.extend_from_slice(bytes);
354
8
        result.push(0); // null terminator
355
8
        U8Vec::from_vec(result)
356
8
    }
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
19
    unsafe fn from_utf16_with_byte_order(
364
19
        ptr: *const u8,
365
19
        len: usize,
366
19
        from_bytes: fn([u8; 2]) -> u16,
367
19
    ) -> Self { unsafe {
368
19
        if ptr.is_null() || len == 0 {
369
5
            return Self::default();
370
14
        }
371

            
372
        // UTF-16 requires pairs of bytes
373
14
        if !len.is_multiple_of(2) {
374
4
            return Self::default();
375
10
        }
376

            
377
10
        let byte_slice = core::slice::from_raw_parts(ptr, len);
378
10
        let code_units: Vec<u16> = byte_slice
379
10
            .chunks_exact(2)
380
100029
            .map(|chunk| from_bytes([chunk[0], chunk[1]]))
381
10
            .collect();
382

            
383
10
        String::from_utf16(&code_units).map_or_else(|_| Self::default(), Self::from_string)
384
19
    }}
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
10
    pub unsafe fn from_utf16_le(ptr: *const u8, len: usize) -> Self { unsafe {
398
10
        Self::from_utf16_with_byte_order(ptr, len, u16::from_le_bytes)
399
10
    }}
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
5
    pub unsafe fn from_utf16_be(ptr: *const u8, len: usize) -> Self { unsafe {
413
5
        Self::from_utf16_with_byte_order(ptr, len, u16::from_be_bytes)
414
5
    }}
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
8
    #[must_use] pub unsafe fn from_utf8_lossy(ptr: *const u8, len: usize) -> Self { unsafe {
423
8
        if ptr.is_null() || len == 0 {
424
2
            return Self::default();
425
6
        }
426
        
427
6
        let byte_slice = core::slice::from_raw_parts(ptr, len);
428
6
        let s = String::from_utf8_lossy(byte_slice).into_owned();
429
6
        Self::from_string(s)
430
8
    }}
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
12
    #[must_use] pub unsafe fn from_utf8(ptr: *const u8, len: usize) -> Self { unsafe {
439
12
        if ptr.is_null() || len == 0 {
440
2
            return Self::default();
441
10
        }
442
        
443
10
        let byte_slice = core::slice::from_raw_parts(ptr, len);
444
10
        core::str::from_utf8(byte_slice)
445
10
            .map_or_else(|_| Self::default(), |s| Self::from_string(s.to_string()))
446
12
    }}
447
}
448

            
449
impl From<String> for AzString {
450
11205949
    fn from(input: String) -> Self {
451
11205949
        Self::from_string(input)
452
11205949
    }
453
}
454

            
455
impl PartialOrd for AzString {
456
96
    fn partial_cmp(&self, rhs: &Self) -> Option<core::cmp::Ordering> {
457
96
        self.as_str().partial_cmp(rhs.as_str())
458
96
    }
459
}
460

            
461
impl Ord for AzString {
462
671
    fn cmp(&self, rhs: &Self) -> core::cmp::Ordering {
463
671
        self.as_str().cmp(rhs.as_str())
464
671
    }
465
}
466

            
467
impl Clone for AzString {
468
2929176
    fn clone(&self) -> Self {
469
2929176
        self.clone_self()
470
2929176
    }
471
}
472

            
473
impl PartialEq for AzString {
474
10852486
    fn eq(&self, rhs: &Self) -> bool {
475
10852486
        self.as_str().eq(rhs.as_str())
476
10852486
    }
477
}
478

            
479
impl Eq for AzString {}
480

            
481
impl core::hash::Hash for AzString {
482
578497
    fn hash<H>(&self, state: &mut H)
483
578497
    where
484
578497
        H: core::hash::Hasher,
485
    {
486
578497
        self.as_str().hash(state);
487
578497
    }
488
}
489

            
490
impl core::ops::Deref for AzString {
491
    type Target = str;
492

            
493
140900
    fn deref(&self) -> &str {
494
140900
        self.as_str()
495
140900
    }
496
}
497

            
498
impl_option!(
499
    u8,
500
    OptionU8,
501
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
502
);
503

            
504
impl_vec!(u8, U8Vec, U8VecDestructor, U8VecDestructorType, U8VecSlice, OptionU8);
505
impl_vec_mut!(u8, U8Vec);
506
impl_vec_debug!(u8, U8Vec);
507
impl_vec_partialord!(u8, U8Vec);
508
impl_vec_ord!(u8, U8Vec);
509
impl_vec_clone!(u8, U8Vec, U8VecDestructor);
510
impl_vec_partialeq!(u8, U8Vec);
511
impl_vec_eq!(u8, U8Vec);
512
impl_vec_hash!(u8, U8Vec);
513

            
514
impl 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
28
    #[must_use] pub fn copy_from_bytes(ptr: *const u8, start: usize, len: usize) -> Self {
524
28
        if ptr.is_null() || len == 0 {
525
11
            return Self::new();
526
17
        }
527
17
        debug_assert!(
528
            start.checked_add(len).is_some(),
529
            "U8Vec::copy_from_bytes: start + len overflows"
530
        );
531
17
        let slice = unsafe { core::slice::from_raw_parts(ptr.add(start), len) };
532
17
        Self::from_vec(slice.to_vec())
533
28
    }
534
}
535

            
536
impl_option!(
537
    U8Vec,
538
    OptionU8Vec,
539
    copy = false,
540
    [Debug, Clone, PartialEq, Ord, PartialOrd, Eq, Hash]
541
);
542

            
543
impl_vec!(u16, U16Vec, U16VecDestructor, U16VecDestructorType, U16VecSlice, OptionU16);
544
impl_vec_debug!(u16, U16Vec);
545
impl_vec_partialord!(u16, U16Vec);
546
impl_vec_ord!(u16, U16Vec);
547
impl_vec_clone!(u16, U16Vec, U16VecDestructor);
548
impl_vec_partialeq!(u16, U16Vec);
549
impl_vec_eq!(u16, U16Vec);
550
impl_vec_hash!(u16, U16Vec);
551

            
552
impl_vec!(f32, F32Vec, F32VecDestructor, F32VecDestructorType, F32VecSlice, OptionF32);
553
impl_vec_debug!(f32, F32Vec);
554
impl_vec_partialord!(f32, F32Vec);
555
impl_vec_clone!(f32, F32Vec, F32VecDestructor);
556
impl_vec_partialeq!(f32, F32Vec);
557

            
558
// Vec<char>
559
impl_vec!(u32, U32Vec, U32VecDestructor, U32VecDestructorType, U32VecSlice, OptionU32);
560
impl_vec_mut!(u32, U32Vec);
561
impl_vec_debug!(u32, U32Vec);
562
impl_vec_partialord!(u32, U32Vec);
563
impl_vec_ord!(u32, U32Vec);
564
impl_vec_clone!(u32, U32Vec, U32VecDestructor);
565
impl_vec_partialeq!(u32, U32Vec);
566
impl_vec_eq!(u32, U32Vec);
567
impl_vec_hash!(u32, U32Vec);
568

            
569
impl_vec!(AzString, StringVec, StringVecDestructor, StringVecDestructorType, StringVecSlice, OptionString);
570
impl_vec_debug!(AzString, StringVec);
571
impl_vec_partialord!(AzString, StringVec);
572
impl_vec_ord!(AzString, StringVec);
573
impl_vec_clone!(AzString, StringVec, StringVecDestructor);
574
impl_vec_partialeq!(AzString, StringVec);
575
impl_vec_eq!(AzString, StringVec);
576
impl_vec_hash!(AzString, StringVec);
577

            
578
impl 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

            
585
impl_option!(
586
    StringVec,
587
    OptionStringVec,
588
    copy = false,
589
    [Debug, Clone, PartialOrd, PartialEq, Ord, Eq, Hash]
590
);
591

            
592
impl_option!(
593
    u16,
594
    OptionU16,
595
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
596
);
597
impl_option!(
598
    u32,
599
    OptionU32,
600
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
601
);
602
impl_option!(
603
    u64,
604
    OptionU64,
605
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
606
);
607
impl_option!(
608
    usize,
609
    OptionUsize,
610
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
611
);
612
impl_option!(
613
    i16,
614
    OptionI16,
615
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
616
);
617
impl_option!(
618
    i32,
619
    OptionI32,
620
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
621
);
622
impl_option!(bool, OptionBool, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
623
impl_option!(f32, OptionF32, [Debug, Copy, Clone, PartialEq]);
624
impl_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)
627
impl 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

            
639
impl 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).
643
impl PartialOrd for OptionF32 {
644
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
645
        Some(self.cmp(other))
646
    }
647
}
648

            
649
impl Ord for OptionF32 {
650
29
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
651
29
        match (self, other) {
652
17
            (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
12
            (Self::Some(a), Self::Some(b)) => {
656
12
                a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
657
            }
658
        }
659
29
    }
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

            
674
use alloc::sync::Arc;
675
use 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.
680
struct 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.
697
unsafe impl Send for StringArenaInner {}
698
unsafe 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.
710
pub struct StringArena {
711
    inner: Arc<StringArenaInner>,
712
}
713

            
714
impl 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

            
721
impl 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
3995
    #[must_use] pub fn new() -> Self {
728
3995
        Self {
729
3995
            inner: Arc::new(StringArenaInner {
730
3995
                chunks: UnsafeCell::new(Vec::new()),
731
3995
                current_remaining: UnsafeCell::new(0),
732
3995
            }),
733
3995
        }
734
3995
    }
735

            
736
    /// Returns `(chunk_count, total_bytes_used)` for metrics.
737
9
    #[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
9
            let chunks = &*self.inner.chunks.get();
742
9
            let total: usize = chunks.iter().map(Vec::len).sum();
743
9
            (chunks.len(), total)
744
        }
745
9
    }
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
24761
    pub fn intern(&mut self, s: &str) -> AzString {
758
24761
        let bytes = s.as_bytes();
759
24761
        let len = bytes.len();
760

            
761
24761
        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
2
            core::ptr::NonNull::<u8>::dangling().as_ptr()
765
        } else {
766
            // Safety: `&mut self` ⇒ exclusive access to inner chunks.
767
            unsafe {
768
24759
                let chunks: &mut Vec<Vec<u8>> = &mut *self.inner.chunks.get();
769
24759
                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
24759
                if len > Self::CHUNK_SIZE / 2 {
774
16
                    let mut v = Vec::with_capacity(len);
775
16
                    v.extend_from_slice(bytes);
776
16
                    let p = v.as_ptr();
777
16
                    chunks.push(v);
778
                    // This dedicated chunk is FULL (len == cap). `remaining` must not
779
                    // keep describing the previous chunk, or the next small intern
780
                    // below would see a stale positive `remaining`, skip allocating,
781
                    // and `extend_from_slice` into THIS chunk — reallocating it and
782
                    // dangling the `p` we just handed out.
783
16
                    *remaining = 0;
784
16
                    p
785
                } else {
786
24743
                    if *remaining < len {
787
2421
                        chunks.push(Vec::with_capacity(Self::CHUNK_SIZE));
788
2421
                        *remaining = Self::CHUNK_SIZE;
789
22322
                    }
790
                    // Safety: chunk was allocated with capacity ≥ len and
791
                    // `remaining` tracks unused capacity — no realloc.
792
24743
                    let chunk = chunks.last_mut().unwrap();
793
24743
                    let offset = chunk.len();
794
24743
                    chunk.extend_from_slice(bytes);
795
24743
                    *remaining -= len;
796
24743
                    chunk.as_ptr().add(offset)
797
                }
798
            }
799
        };
800

            
801
        // Each AzString carries its own Arc reference count. Stash the
802
        // raw Arc pointer in `cap` so the External destructor can decrement.
803
24761
        let arc_raw = Arc::into_raw(Arc::clone(&self.inner));
804

            
805
24761
        AzString {
806
24761
            vec: U8Vec {
807
24761
                ptr,
808
24761
                len,
809
24761
                // NOTE: `cap` stores an Arc pointer, not a capacity. This
810
24761
                // works because the `External` destructor path never calls
811
24761
                // `Vec::from_raw_parts(ptr, len, cap)` — only `DefaultRust`
812
24761
                // does that.
813
24761
                cap: arc_raw as usize,
814
24761
                destructor: U8VecDestructor::External(arena_string_destructor),
815
24761
            },
816
24761
        }
817
24761
    }
818
}
819

            
820
impl Default for StringArena {
821
1
    fn default() -> Self {
822
1
        Self::new()
823
1
    }
824
}
825

            
826
/// Destructor installed on every arena-backed `AzString`. Reads the Arc
827
/// pointer out of `cap` and drops one Arc reference; when the count
828
/// reaches zero the `StringArenaInner` is freed.
829
24765
extern "C" fn arena_string_destructor(vec: *mut U8Vec) {
830
    // Safety: called at most once per AzString drop. `cap` was set by
831
    // `StringArena::intern` to `Arc::into_raw(Arc<StringArenaInner>)`.
832
    unsafe {
833
24765
        let v = &mut *vec;
834
24765
        let arc_raw = v.cap as *const StringArenaInner;
835
24765
        if !arc_raw.is_null() {
836
24762
            drop(Arc::from_raw(arc_raw));
837
24762
            // Prevent a hypothetical double-drop from dereferencing
838
24762
            // freed memory.
839
24762
            v.cap = 0;
840
24762
        }
841
    }
842
24765
}
843

            
844
#[cfg(test)]
845
mod string_arena_tests {
846
    use super::*;
847

            
848
    #[test]
849
1
    fn intern_round_trip() {
850
1
        let mut arena = StringArena::new();
851
1
        let a = arena.intern("hello");
852
1
        let b = arena.intern("world");
853
1
        let c = arena.intern("");
854
1
        assert_eq!(a.as_str(), "hello");
855
1
        assert_eq!(b.as_str(), "world");
856
1
        assert_eq!(c.as_str(), "");
857
1
    }
858

            
859
    #[test]
860
1
    fn strings_outlive_arena_handle() {
861
1
        let a = {
862
1
            let mut arena = StringArena::new();
863
1
            arena.intern("survives drop of arena handle")
864
        };
865
1
        assert_eq!(a.as_str(), "survives drop of arena handle");
866
1
    }
867

            
868
    #[test]
869
1
    fn oversized_string_gets_dedicated_chunk() {
870
1
        let mut arena = StringArena::new();
871
1
        let big = "x".repeat(StringArena::CHUNK_SIZE);
872
1
        let s = arena.intern(&big);
873
1
        assert_eq!(s.len(), big.len());
874
1
        assert_eq!(s.as_str(), big.as_str());
875
1
    }
876

            
877
    #[test]
878
1
    fn many_small_strings_share_chunk() {
879
1
        let mut arena = StringArena::new();
880
1
        let mut strings = Vec::new();
881
101
        for i in 0..100 {
882
100
            strings.push(arena.intern(&format!("s{i}")));
883
100
        }
884
1
        let (chunks, _bytes) = arena.metrics();
885
1
        assert!(chunks <= 2, "expected ≤2 chunks for 100 small strings, got {chunks}");
886
100
        for (i, s) in strings.iter().enumerate() {
887
100
            assert_eq!(s.as_str(), format!("s{i}"));
888
        }
889
1
    }
890

            
891
    #[test]
892
1
    fn clone_deep_copies_and_is_independent() {
893
        // Cloning an External AzString deep-copies into DefaultRust, so
894
        // the clone doesn't depend on the arena at all.
895
1
        let clone = {
896
1
            let mut arena = StringArena::new();
897

            
898
1
            arena.intern("deep-copy test")
899
        };
900
1
        assert_eq!(clone.as_str(), "deep-copy test");
901
1
    }
902
}
903

            
904
#[cfg(test)]
905
#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
906
mod autotest_generated {
907
    use super::*;
908

            
909
    // ------------------------------------------------------------------
910
    // helpers
911
    // ------------------------------------------------------------------
912

            
913
    /// Minimal FNV-1a hasher so the Hash-consistency tests don't depend on
914
    /// `std` being linked (the crate keeps a `#![no_std]` line commented out).
915
    struct Fnv(u64);
916

            
917
    impl core::hash::Hasher for Fnv {
918
        fn finish(&self) -> u64 {
919
            self.0
920
        }
921
        fn write(&mut self, bytes: &[u8]) {
922
            for b in bytes {
923
                self.0 ^= u64::from(*b);
924
                self.0 = self.0.wrapping_mul(0x0100_0000_01b3);
925
            }
926
        }
927
    }
928

            
929
    fn hash_of<T: core::hash::Hash>(t: &T) -> u64 {
930
        use core::hash::{Hash, Hasher};
931
        let mut h = Fnv(0xcbf2_9ce4_8422_2325);
932
        Hash::hash(t, &mut h);
933
        h.finish()
934
    }
935

            
936
    /// UTF-16 code units of `s`, serialized to bytes with the given byte order.
937
    fn utf16_bytes(s: &str, little_endian: bool) -> Vec<u8> {
938
        s.encode_utf16()
939
            .flat_map(|u| {
940
                let b = if little_endian {
941
                    u.to_le_bytes()
942
                } else {
943
                    u.to_be_bytes()
944
                };
945
                [b[0], b[1]]
946
            })
947
            .collect()
948
    }
949

            
950
    // ==================================================================
951
    // EmptyStruct
952
    // ==================================================================
953

            
954
    #[test]
955
    fn empty_struct_new_invariants() {
956
        let e = EmptyStruct::new();
957
        assert_eq!(e._reserved, 0, "_reserved must always be initialized to 0");
958
        assert_eq!(e, EmptyStruct::default(), "new() must equal default()");
959
    }
960

            
961
    #[test]
962
    fn empty_struct_is_ffi_safe_non_zero_size() {
963
        // The whole point of the type: `()` is zero-sized and not FFI-safe.
964
        assert_eq!(size_of::<EmptyStruct>(), 1);
965
        assert_eq!(align_of::<EmptyStruct>(), 1);
966
    }
967

            
968
    #[test]
969
    fn empty_struct_unit_conversions_round_trip() {
970
        let from_unit = EmptyStruct::from(());
971
        assert_eq!(from_unit, EmptyStruct::new());
972
        let back: () = EmptyStruct::new().into();
973
        assert_eq!(back, ());
974
    }
975

            
976
    #[test]
977
    fn empty_struct_total_order_is_trivial() {
978
        // Every EmptyStruct is equal to every other one, so Ord/Hash must agree.
979
        let a = EmptyStruct::new();
980
        let b = EmptyStruct::default();
981
        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
982
        assert_eq!(hash_of(&a), hash_of(&b));
983
    }
984

            
985
    // ==================================================================
986
    // LayoutDebugMessage
987
    // ==================================================================
988

            
989
    #[test]
990
    fn debug_message_new_records_fields_and_caller_location() {
991
        let m = LayoutDebugMessage::new(LayoutDebugMessageType::Warning, "disk on fire");
992
        assert_eq!(m.message_type, LayoutDebugMessageType::Warning);
993
        assert_eq!(m.message.as_str(), "disk on fire");
994
        assert!(
995
            m.location.as_str().contains("corety.rs"),
996
            "#[track_caller] must record THIS file, got {:?}",
997
            m.location.as_str()
998
        );
999

            
        // location is "file:line:column" — the last two segments must be numbers.
        let parts: Vec<&str> = m.location.as_str().rsplitn(3, ':').collect();
        assert_eq!(parts.len(), 3, "location must be file:line:column");
        assert!(parts[0].parse::<u32>().is_ok(), "column must parse as u32");
        assert!(parts[1].parse::<u32>().is_ok(), "line must parse as u32");
    }
    #[test]
    fn debug_message_track_caller_propagates_through_helpers() {
        // If #[track_caller] were missing on the helpers, both locations would
        // collapse to the same line inside LayoutDebugMessage::new().
        let a = LayoutDebugMessage::info("a");
        let b = LayoutDebugMessage::info("b");
        assert_ne!(
            a.location.as_str(),
            b.location.as_str(),
            "two call sites on different lines must record different locations"
        );
        assert!(a.location.as_str().contains("corety.rs"));
    }
    #[test]
    fn debug_message_helpers_set_the_right_type() {
        assert_eq!(
            LayoutDebugMessage::info("x").message_type,
            LayoutDebugMessageType::Info
        );
        assert_eq!(
            LayoutDebugMessage::warning("x").message_type,
            LayoutDebugMessageType::Warning
        );
        assert_eq!(
            LayoutDebugMessage::error("x").message_type,
            LayoutDebugMessageType::Error
        );
        assert_eq!(
            LayoutDebugMessage::box_props("x").message_type,
            LayoutDebugMessageType::BoxProps
        );
        assert_eq!(
            LayoutDebugMessage::css_getter("x").message_type,
            LayoutDebugMessageType::CssGetter
        );
        assert_eq!(
            LayoutDebugMessage::bfc_layout("x").message_type,
            LayoutDebugMessageType::BfcLayout
        );
        assert_eq!(
            LayoutDebugMessage::ifc_layout("x").message_type,
            LayoutDebugMessageType::IfcLayout
        );
        assert_eq!(
            LayoutDebugMessage::table_layout("x").message_type,
            LayoutDebugMessageType::TableLayout
        );
        assert_eq!(
            LayoutDebugMessage::display_type("x").message_type,
            LayoutDebugMessageType::DisplayType
        );
    }
    #[test]
    fn debug_message_helpers_preserve_the_message_verbatim() {
        // Every helper must forward the payload untouched, including empty
        // and unicode payloads.
        for m in [
            LayoutDebugMessage::info(""),
            LayoutDebugMessage::warning(""),
            LayoutDebugMessage::error(""),
            LayoutDebugMessage::box_props(""),
            LayoutDebugMessage::css_getter(""),
            LayoutDebugMessage::bfc_layout(""),
            LayoutDebugMessage::ifc_layout(""),
            LayoutDebugMessage::table_layout(""),
            LayoutDebugMessage::display_type(""),
        ] {
            assert!(m.message.is_empty());
            assert!(!m.location.is_empty(), "location is always filled in");
        }
        let weird = "ünïcødé \u{1F600}\n\t\"quoted\" \u{0}nul";
        assert_eq!(LayoutDebugMessage::error(weird).message.as_str(), weird);
    }
    #[test]
    fn debug_message_handles_huge_message_without_panicking() {
        let huge = "m".repeat(1_000_000);
        let m = LayoutDebugMessage::new(LayoutDebugMessageType::PositionCalculation, huge.clone());
        assert_eq!(m.message.len(), 1_000_000);
        assert_eq!(m.message.as_str(), huge.as_str());
        assert_eq!(
            m.message_type,
            LayoutDebugMessageType::PositionCalculation,
            "the variant with no helper must still be constructible via new()"
        );
    }
    #[test]
    fn debug_message_default_is_empty_info() {
        let m = LayoutDebugMessage::default();
        assert_eq!(m.message_type, LayoutDebugMessageType::Info);
        assert!(m.message.is_empty());
        assert!(m.location.is_empty(), "default() does not track a caller");
        assert_eq!(LayoutDebugMessageType::default(), LayoutDebugMessageType::Info);
    }
    #[test]
    fn debug_message_accepts_string_and_str_via_into() {
        // `impl Into<String>` must work for both &str and String.
        let from_str = LayoutDebugMessage::info("borrowed");
        let from_string = LayoutDebugMessage::info(String::from("owned"));
        assert_eq!(from_str.message.as_str(), "borrowed");
        assert_eq!(from_string.message.as_str(), "owned");
    }
    #[test]
    fn debug_message_clone_is_a_deep_equal_copy() {
        let m = LayoutDebugMessage::error("clone me \u{1F600}");
        let c = m.clone();
        assert_eq!(c, m);
        assert_ne!(
            c.message.as_bytes().as_ptr(),
            m.message.as_bytes().as_ptr(),
            "clone must deep-copy the library-owned message bytes"
        );
    }
    // ==================================================================
    // AzString — constructors
    // ==================================================================
    #[test]
    fn azstring_default_is_empty_and_readable() {
        let s = AzString::default();
        assert_eq!(s.as_str(), "");
        assert_eq!(s.len(), 0);
        assert!(s.is_empty());
        assert_eq!(s.as_bytes(), b"");
    }
    #[test]
    fn azstring_from_const_str_borrows_the_static_and_never_frees_it() {
        // One binding, used for both the construction and the pointer check —
        // rustc is not obliged to dedupe two identical string literals.
        const TEXT: &str = "static text";
        let s = AzString::from_const_str(TEXT);
        assert_eq!(s.as_str(), TEXT);
        assert_eq!(s.len(), 11);
        assert!(
            matches!(s.vec.destructor, U8VecDestructor::NoDestructor),
            "a &'static str must not get a freeing destructor"
        );
        assert_eq!(
            s.vec.ptr,
            TEXT.as_bytes().as_ptr(),
            "from_const_str must alias the static, not copy it"
        );
    }
    #[test]
    fn azstring_from_const_str_empty_and_unicode() {
        let empty = AzString::from_const_str("");
        assert!(empty.is_empty());
        assert_eq!(empty.as_str(), "");
        assert_eq!(empty.len(), 0);
        let uni = AzString::from_const_str("héllo \u{1F600}");
        assert_eq!(uni.as_str(), "héllo \u{1F600}");
        // len() is BYTES, not chars: 5 ASCII-ish + 1 extra for é + space + 4 for the emoji
        assert_eq!(uni.len(), "héllo \u{1F600}".len());
        assert_ne!(
            uni.len(),
            uni.as_str().chars().count(),
            "len() must be a byte length, not a char count"
        );
    }
    #[test]
    fn azstring_from_string_round_trips_edge_values() {
        for input in [
            String::new(),
            String::from(" "),
            String::from("\t\n\r"),
            String::from("0"),
            String::from("-0"),
            String::from("9223372036854775807"), // i64::MAX
            String::from("NaN"),
            String::from("inf"),
            String::from("  valid  "),
            String::from("valid;garbage"),
            String::from("\u{1F600}\u{0301}\u{0}"), // emoji + combining mark + NUL
            "{".repeat(10_000),                     // deeply "nested" junk: no parser, no overflow
        ] {
            let s = AzString::from_string(input.clone());
            assert_eq!(s.as_str(), input.as_str(), "from_string must be verbatim");
            assert_eq!(s.len(), input.len());
            assert_eq!(s.is_empty(), input.is_empty());
            // round-trip back out
            assert_eq!(s.into_library_owned_string(), input);
        }
    }
    #[test]
    fn azstring_from_string_handles_a_megabyte() {
        let huge = "x".repeat(1_000_000);
        let s = AzString::from_string(huge.clone());
        assert_eq!(s.len(), 1_000_000);
        assert_eq!(s.as_str().len(), huge.len());
        assert!(s.as_str().bytes().all(|b| b == b'x'));
    }
    #[test]
    fn azstring_from_string_preserves_the_original_capacity() {
        // into_library_owned_string rebuilds the String via from_raw_parts(ptr, len, cap).
        // If `cap` were not carried through faithfully, this would corrupt the heap.
        let mut owned = String::with_capacity(4096);
        owned.push_str("hi");
        let s = AzString::from_string(owned);
        assert!(matches!(s.vec.destructor, U8VecDestructor::DefaultRust));
        let back = s.into_library_owned_string();
        assert_eq!(back, "hi");
        assert!(
            back.capacity() >= 4096,
            "capacity must survive the AzString round-trip, got {}",
            back.capacity()
        );
    }
    // ==================================================================
    // AzString::copy_from_bytes  (numeric / pointer edge cases)
    // ==================================================================
    #[test]
    fn azstring_copy_from_bytes_zero_len_is_empty() {
        let buf = b"hello";
        let s = AzString::copy_from_bytes(buf.as_ptr(), 0, 0);
        assert!(s.is_empty());
        assert_eq!(s.as_str(), "");
    }
    #[test]
    fn azstring_copy_from_bytes_null_ptr_is_empty() {
        let s = AzString::copy_from_bytes(core::ptr::null(), 0, 16);
        assert!(s.is_empty());
        assert_eq!(s.as_str(), "");
    }
    #[test]
    fn azstring_copy_from_bytes_honours_the_start_offset() {
        let buf = b"0123456789";
        let s = AzString::copy_from_bytes(buf.as_ptr(), 3, 4);
        assert_eq!(s.as_str(), "3456");
        assert_eq!(s.len(), 4);
    }
    #[test]
    fn azstring_copy_from_bytes_start_at_end_with_zero_len_is_empty() {
        // start == buf.len() is only legal because len == 0 short-circuits
        // before the pointer is ever offset.
        let buf = b"abc";
        let s = AzString::copy_from_bytes(buf.as_ptr(), buf.len(), 0);
        assert!(s.is_empty());
    }
    #[test]
    fn azstring_copy_from_bytes_zero_len_wins_over_start_overflow() {
        // start + len overflows usize, but len == 0 must short-circuit BEFORE
        // the debug_assert / ptr.add() — no panic, no UB.
        let buf = b"abc";
        let s = AzString::copy_from_bytes(buf.as_ptr(), usize::MAX, 0);
        assert!(s.is_empty());
    }
    #[test]
    fn azstring_copy_from_bytes_null_wins_over_max_len() {
        // The null check must precede everything, even for absurd start/len.
        let s = AzString::copy_from_bytes(core::ptr::null(), usize::MAX, usize::MAX);
        assert!(s.is_empty());
        assert_eq!(s.as_str(), "");
    }
    #[test]
    fn azstring_copy_from_bytes_replaces_invalid_utf8_lossily() {
        // Slicing "héllo" mid-codepoint leaves a stray continuation byte (0xA9),
        // which must become U+FFFD so the as_str() UTF-8 invariant still holds.
        let buf = "héllo".as_bytes();
        assert_eq!(buf[1], 0xC3);
        assert_eq!(buf[2], 0xA9);
        let s = AzString::copy_from_bytes(buf.as_ptr(), 2, 2);
        assert_eq!(s.as_str(), "\u{FFFD}l");
        // The UTF-8 invariant as_str() relies on must actually hold:
        assert!(core::str::from_utf8(s.as_bytes()).is_ok());
    }
    #[test]
    fn azstring_copy_from_bytes_keeps_valid_utf8_byte_for_byte() {
        let buf = "héllo \u{1F600}".as_bytes();
        let s = AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len());
        assert_eq!(s.as_str(), "héllo \u{1F600}");
        assert_eq!(s.as_bytes(), buf);
    }
    #[test]
    fn azstring_copy_from_bytes_preserves_interior_nul() {
        let buf = b"a\0b";
        let s = AzString::copy_from_bytes(buf.as_ptr(), 0, 3);
        assert_eq!(s.len(), 3, "an interior NUL is data, not a terminator");
        assert_eq!(s.as_bytes(), b"a\0b");
    }
    // ==================================================================
    // U8Vec::copy_from_bytes  (numeric / pointer edge cases)
    // ==================================================================
    #[test]
    fn u8vec_copy_from_bytes_zero_len_is_empty() {
        let buf = b"hello";
        let v = U8Vec::copy_from_bytes(buf.as_ptr(), 0, 0);
        assert!(v.is_empty());
        assert_eq!(v.as_ref(), b"");
    }
    #[test]
    fn u8vec_copy_from_bytes_null_ptr_is_empty() {
        let v = U8Vec::copy_from_bytes(core::ptr::null(), 0, 8);
        assert!(v.is_empty());
        assert_eq!(v.len(), 0);
    }
    #[test]
    fn u8vec_copy_from_bytes_null_wins_over_max_start_and_len() {
        // Neither the debug_assert nor ptr.add() may be reached for a null ptr.
        let v = U8Vec::copy_from_bytes(core::ptr::null(), usize::MAX, usize::MAX);
        assert!(v.is_empty());
    }
    #[test]
    fn u8vec_copy_from_bytes_zero_len_wins_over_start_overflow() {
        // start + len overflows, but len == 0 short-circuits first.
        let buf = b"abc";
        let v = U8Vec::copy_from_bytes(buf.as_ptr(), usize::MAX, 0);
        assert!(v.is_empty());
    }
    #[test]
    fn u8vec_copy_from_bytes_copies_the_requested_window() {
        let buf: Vec<u8> = (0u8..=255).collect();
        let v = U8Vec::copy_from_bytes(buf.as_ptr(), 250, 6);
        assert_eq!(v.as_ref(), &[250, 251, 252, 253, 254, 255]);
        assert_eq!(v.len(), 6);
    }
    #[test]
    fn u8vec_copy_from_bytes_owns_its_copy() {
        // The copy must survive the source buffer being dropped.
        let v = {
            let buf = vec![1u8, 2, 3, 4];
            U8Vec::copy_from_bytes(buf.as_ptr(), 1, 2)
        };
        assert_eq!(v.as_ref(), &[2, 3]);
        assert!(matches!(v.destructor, U8VecDestructor::DefaultRust));
    }
    #[test]
    fn u8vec_copy_from_bytes_accepts_all_byte_values() {
        // Arbitrary (non-UTF-8) bytes must round-trip unchanged — U8Vec has no
        // encoding invariant, unlike AzString.
        let buf: Vec<u8> = (0u8..=255).collect();
        let v = U8Vec::copy_from_bytes(buf.as_ptr(), 0, buf.len());
        assert_eq!(v.as_ref(), buf.as_slice());
    }
    // ==================================================================
    // AzString::from_c_str
    // ==================================================================
    #[test]
    fn azstring_from_c_str_null_is_empty() {
        let s = unsafe { AzString::from_c_str(core::ptr::null()) };
        assert!(s.is_empty());
        assert_eq!(s.as_str(), "");
    }
    #[test]
    fn azstring_from_c_str_reads_up_to_the_terminator() {
        let c = b"hello\0trailing garbage\0";
        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
        assert_eq!(s.as_str(), "hello");
        assert_eq!(s.len(), 5, "the NUL terminator is not part of the string");
    }
    #[test]
    fn azstring_from_c_str_empty_c_string_is_empty() {
        let c = b"\0";
        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
        assert!(s.is_empty());
    }
    #[test]
    fn azstring_from_c_str_replaces_non_utf8_bytes() {
        // A latin-1 "café" is not valid UTF-8; it must come back lossily
        // rather than violating the as_str() invariant.
        let c = b"caf\xE9\0";
        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
        assert_eq!(s.as_str(), "caf\u{FFFD}");
        assert!(core::str::from_utf8(s.as_bytes()).is_ok());
    }
    #[test]
    fn azstring_from_c_str_handles_a_long_c_string() {
        let mut c = "z".repeat(100_000).into_bytes();
        c.push(0);
        let s = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
        assert_eq!(s.len(), 100_000);
    }
    // ==================================================================
    // AzString::to_c_str  (+ round trip through from_c_str)
    // ==================================================================
    #[test]
    fn azstring_to_c_str_appends_exactly_one_nul() {
        let s = AzString::from_const_str("abc");
        let c = s.to_c_str();
        assert_eq!(c.as_ref(), b"abc\0");
        assert_eq!(c.len(), s.len() + 1);
    }
    #[test]
    fn azstring_to_c_str_of_empty_is_just_the_terminator() {
        let c = AzString::default().to_c_str();
        assert_eq!(c.as_ref(), b"\0");
        assert_eq!(c.len(), 1);
    }
    #[test]
    fn azstring_c_str_round_trip() {
        for original in ["", "abc", "héllo \u{1F600}", "  spaced  "] {
            let s = AzString::from_const_str(original);
            let c = s.to_c_str();
            let back = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
            assert_eq!(back.as_str(), original, "C round-trip must be lossless");
            assert_eq!(back, s);
        }
    }
    #[test]
    fn azstring_c_str_round_trip_truncates_at_an_interior_nul() {
        // Documented C-string reality: a string containing a NUL cannot survive
        // a *const char* round-trip. Assert the truncation is deterministic
        // rather than pretending it round-trips.
        let s = AzString::from_string(String::from("a\0b"));
        let c = s.to_c_str();
        assert_eq!(c.as_ref(), b"a\0b\0", "to_c_str keeps the interior NUL");
        let back = unsafe { AzString::from_c_str(c.as_ptr().cast::<i8>()) };
        assert_eq!(back.as_str(), "a", "from_c_str stops at the first NUL");
    }
    #[test]
    fn azstring_to_c_str_is_an_independent_allocation() {
        let s = AzString::from_const_str("shared?");
        let c = s.to_c_str();
        assert!(matches!(c.destructor, U8VecDestructor::DefaultRust));
        assert_ne!(
            c.as_ptr(),
            s.as_bytes().as_ptr(),
            "to_c_str must copy, not alias the source"
        );
        assert_eq!(s.as_str(), "shared?", "source must be untouched");
    }
    // ==================================================================
    // AzString::from_utf8 / from_utf8_lossy
    // ==================================================================
    #[test]
    fn azstring_from_utf8_null_or_zero_len_is_empty() {
        let buf = b"abc";
        assert!(unsafe { AzString::from_utf8(core::ptr::null(), 3) }.is_empty());
        assert!(unsafe { AzString::from_utf8(buf.as_ptr(), 0) }.is_empty());
        assert!(unsafe { AzString::from_utf8_lossy(core::ptr::null(), 3) }.is_empty());
        assert!(unsafe { AzString::from_utf8_lossy(buf.as_ptr(), 0) }.is_empty());
    }
    #[test]
    fn azstring_from_utf8_accepts_valid_multibyte() {
        let buf = "héllo \u{1F600}".as_bytes();
        let s = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
        assert_eq!(s.as_str(), "héllo \u{1F600}");
        assert_eq!(s.len(), buf.len());
    }
    #[test]
    fn azstring_from_utf8_rejects_invalid_but_lossy_replaces_it() {
        // A truncated 2-byte sequence: strict → empty, lossy → U+FFFD.
        let buf = b"caf\xC3";
        let strict = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
        assert!(
            strict.is_empty(),
            "from_utf8 must return an EMPTY string for invalid UTF-8, got {:?}",
            strict.as_str()
        );
        let lossy = unsafe { AzString::from_utf8_lossy(buf.as_ptr(), buf.len()) };
        assert_eq!(lossy.as_str(), "caf\u{FFFD}");
    }
    #[test]
    fn azstring_from_utf8_rejects_overlong_and_stray_continuations() {
        for bad in [
            &b"\xC0\xAF"[..],         // overlong encoding of '/'
            &b"\xED\xA0\x80"[..],     // UTF-16 surrogate half, illegal in UTF-8
            &b"\xF8\x88\x80\x80"[..], // 5-byte sequence, illegal since RFC 3629
            &b"\x80"[..],             // stray continuation byte
            &b"\xFF\xFE"[..],         // never-valid bytes
        ] {
            let strict = unsafe { AzString::from_utf8(bad.as_ptr(), bad.len()) };
            assert!(strict.is_empty(), "from_utf8 must reject {bad:?}");
            let lossy = unsafe { AzString::from_utf8_lossy(bad.as_ptr(), bad.len()) };
            assert!(
                lossy.as_str().contains('\u{FFFD}'),
                "from_utf8_lossy must substitute U+FFFD for {bad:?}"
            );
            // Both paths must uphold the UTF-8 invariant that as_str() assumes.
            assert!(core::str::from_utf8(lossy.as_bytes()).is_ok());
        }
    }
    #[test]
    fn azstring_from_utf8_keeps_interior_nul() {
        let buf = b"a\0b";
        let s = unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) };
        assert_eq!(s.len(), 3);
        assert_eq!(s.as_bytes(), b"a\0b");
    }
    #[test]
    fn azstring_from_utf8_handles_a_megabyte() {
        let buf = "y".repeat(1_000_000);
        let s = unsafe { AzString::from_utf8(buf.as_bytes().as_ptr(), buf.len()) };
        assert_eq!(s.len(), 1_000_000);
    }
    // ==================================================================
    // AzString::from_utf16_le / from_utf16_be / from_utf16_with_byte_order
    // ==================================================================
    #[test]
    fn azstring_from_utf16_le_decodes_bmp_and_surrogate_pairs() {
        let text = "héllo \u{1F600}"; // the emoji needs a surrogate pair
        let bytes = utf16_bytes(text, true);
        let s = unsafe { AzString::from_utf16_le(bytes.as_ptr(), bytes.len()) };
        assert_eq!(s.as_str(), text);
    }
    #[test]
    fn azstring_from_utf16_be_decodes_bmp_and_surrogate_pairs() {
        let text = "héllo \u{1F600}";
        let bytes = utf16_bytes(text, false);
        let s = unsafe { AzString::from_utf16_be(bytes.as_ptr(), bytes.len()) };
        assert_eq!(s.as_str(), text);
    }
    #[test]
    fn azstring_from_utf16_byte_order_actually_matters() {
        // Decoding LE bytes as BE must NOT silently yield the same text.
        let le = utf16_bytes("AB", true);
        assert_eq!(le.as_slice(), &[0x41, 0x00, 0x42, 0x00]);
        let as_be = unsafe { AzString::from_utf16_be(le.as_ptr(), le.len()) };
        assert_eq!(
            as_be.as_str(),
            "\u{4100}\u{4200}",
            "BE decode of LE bytes must byte-swap, not guess"
        );
        assert_ne!(as_be.as_str(), "AB");
    }
    #[test]
    fn azstring_from_utf16_odd_length_is_empty() {
        let bytes = utf16_bytes("hello", true);
        let odd = bytes.len() - 1;
        assert_eq!(odd % 2, 1);
        // Still inside the buffer, so this is a safe call — it must be rejected
        // on the length check, not read a half code unit.
        assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), odd) }.is_empty());
        assert!(unsafe { AzString::from_utf16_be(bytes.as_ptr(), odd) }.is_empty());
        // The smallest odd length of all:
        assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), 1) }.is_empty());
    }
    #[test]
    fn azstring_from_utf16_null_or_zero_len_is_empty() {
        let bytes = utf16_bytes("hi", true);
        assert!(unsafe { AzString::from_utf16_le(core::ptr::null(), 4) }.is_empty());
        assert!(unsafe { AzString::from_utf16_be(core::ptr::null(), 4) }.is_empty());
        assert!(unsafe { AzString::from_utf16_le(bytes.as_ptr(), 0) }.is_empty());
        assert!(unsafe { AzString::from_utf16_be(bytes.as_ptr(), 0) }.is_empty());
    }
    #[test]
    fn azstring_from_utf16_unpaired_surrogate_is_empty() {
        // A lone high surrogate is not valid UTF-16 → documented empty result.
        let lone_high: [u8; 2] = 0xD83C_u16.to_le_bytes();
        assert!(unsafe { AzString::from_utf16_le(lone_high.as_ptr(), 2) }.is_empty());
        // A lone LOW surrogate, and a reversed (low-then-high) pair.
        let lone_low: [u8; 2] = 0xDF89_u16.to_le_bytes();
        assert!(unsafe { AzString::from_utf16_le(lone_low.as_ptr(), 2) }.is_empty());
        let reversed: Vec<u8> = [0xDF89_u16, 0xD83C_u16]
            .iter()
            .flat_map(|u| u.to_le_bytes())
            .collect();
        assert!(unsafe { AzString::from_utf16_le(reversed.as_ptr(), reversed.len()) }.is_empty());
    }
    #[test]
    fn azstring_from_utf16_decodes_noncharacters_and_nul() {
        // U+FFFE / U+0000 are valid code points (not surrogates) — they must
        // decode rather than being treated as an error or a terminator.
        let units: Vec<u8> = [0x0041_u16, 0x0000, 0xFFFE]
            .iter()
            .flat_map(|u| u.to_le_bytes())
            .collect();
        let s = unsafe { AzString::from_utf16_le(units.as_ptr(), units.len()) };
        assert_eq!(s.as_str(), "A\u{0}\u{FFFE}");
        assert_eq!(s.len(), 1 + 1 + 3);
    }
    #[test]
    fn azstring_from_utf16_handles_100k_code_units() {
        let text = "ab".repeat(50_000);
        let bytes = utf16_bytes(&text, true);
        assert_eq!(bytes.len(), 200_000);
        let s = unsafe { AzString::from_utf16_le(bytes.as_ptr(), bytes.len()) };
        assert_eq!(s.len(), 100_000);
    }
    #[test]
    fn azstring_from_utf16_with_byte_order_honours_the_supplied_fn() {
        // The private shared impl must use the caller's byte-order fn verbatim.
        fn swap_halves(b: [u8; 2]) -> u16 {
            u16::from_be_bytes(b)
        }
        let le = utf16_bytes("Az", true);
        let via_shared = unsafe {
            AzString::from_utf16_with_byte_order(le.as_ptr(), le.len(), u16::from_le_bytes)
        };
        assert_eq!(via_shared.as_str(), "Az");
        let swapped = unsafe {
            AzString::from_utf16_with_byte_order(le.as_ptr(), le.len(), swap_halves)
        };
        assert_eq!(swapped.as_str(), "\u{4100}\u{7A00}");
        // The odd-length / null guards live in the shared impl, so check them here too.
        assert!(unsafe {
            AzString::from_utf16_with_byte_order(le.as_ptr(), 3, u16::from_le_bytes)
        }
        .is_empty());
        assert!(unsafe {
            AzString::from_utf16_with_byte_order(core::ptr::null(), 2, u16::from_le_bytes)
        }
        .is_empty());
    }
    // ==================================================================
    // AzString — getters / predicates / conversions
    // ==================================================================
    #[test]
    fn azstring_as_str_and_as_bytes_agree_for_every_constructor() {
        let buf = "mixed \u{1F600}".as_bytes();
        let mut arena = StringArena::new();
        let strings = [
            AzString::default(),
            AzString::from_const_str("mixed \u{1F600}"),
            AzString::from_string(String::from("mixed \u{1F600}")),
            AzString::from("mixed \u{1F600}"),
            AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len()),
            unsafe { AzString::from_utf8(buf.as_ptr(), buf.len()) },
            arena.intern("mixed \u{1F600}"),
        ];
        for s in &strings {
            assert_eq!(
                s.as_bytes(),
                s.as_str().as_bytes(),
                "as_bytes() and as_str() must view the same memory"
            );
            assert_eq!(s.len(), s.as_bytes().len());
            assert_eq!(s.is_empty(), s.len() == 0);
            let via_as_ref: &str = s.as_ref();
            assert_eq!(via_as_ref, s.as_str(), "AsRef must match as_str");
            assert_eq!(&**s, s.as_str(), "Deref must match as_str");
        }
    }
    #[test]
    fn azstring_is_empty_only_for_zero_bytes() {
        assert!(AzString::default().is_empty());
        assert!(AzString::from_const_str("").is_empty());
        assert!(AzString::from_string(String::new()).is_empty());
        // Whitespace and a NUL byte are content, not emptiness.
        assert!(!AzString::from_const_str(" ").is_empty());
        assert!(!AzString::from_const_str("\t\n").is_empty());
        assert!(!AzString::from_string(String::from("\0")).is_empty());
        assert_eq!(AzString::from_string(String::from("\0")).len(), 1);
    }
    #[test]
    fn azstring_len_counts_bytes_not_chars() {
        assert_eq!(AzString::from_const_str("é").len(), 2);
        assert_eq!(AzString::from_const_str("\u{1F600}").len(), 4);
        assert_eq!(AzString::from_const_str("e\u{0301}").len(), 3); // combining accent
        assert_eq!(AzString::from_const_str("\u{1F600}").as_str().chars().count(), 1);
    }
    #[test]
    fn azstring_into_bytes_moves_without_copying_or_double_freeing() {
        let s = AzString::from_string(String::from("payload"));
        let ptr = s.as_bytes().as_ptr();
        let (len, cap) = (s.vec.len, s.vec.cap);
        let v = s.into_bytes();
        assert_eq!(v.as_ref(), b"payload");
        assert_eq!(v.as_ptr(), ptr, "into_bytes must move, not copy");
        assert_eq!(v.len(), len);
        assert_eq!(v.capacity(), cap);
        assert!(matches!(v.destructor, U8VecDestructor::DefaultRust));
        // Dropping `v` here frees the buffer exactly once (the source was
        // ManuallyDrop'd) — a double free would abort the test process.
    }
    #[test]
    fn azstring_into_bytes_preserves_a_non_owning_destructor() {
        let v = AzString::from_const_str("static").into_bytes();
        assert_eq!(v.as_ref(), b"static");
        assert!(
            matches!(v.destructor, U8VecDestructor::NoDestructor),
            "a &'static-backed AzString must not gain a freeing destructor"
        );
    }
    #[test]
    fn azstring_into_bytes_of_empty_is_empty() {
        let v = AzString::default().into_bytes();
        assert!(v.is_empty());
        assert_eq!(v.as_ref(), b"");
    }
    #[test]
    fn azstring_into_library_owned_string_works_for_all_destructors() {
        // DefaultRust: moves the allocation out.
        assert_eq!(
            AzString::from_string(String::from("owned \u{1F600}")).into_library_owned_string(),
            "owned \u{1F600}"
        );
        // NoDestructor: must COPY the static, never take ownership of it.
        assert_eq!(
            AzString::from_const_str("static").into_library_owned_string(),
            "static"
        );
        // External (arena-backed): must copy out of the arena.
        let owned = {
            let mut arena = StringArena::new();
            let s = arena.intern("interned");
            s.into_library_owned_string()
        };
        assert_eq!(owned, "interned", "must outlive the arena it was copied from");
        // Empty / default.
        assert_eq!(AzString::default().into_library_owned_string(), "");
    }
    #[test]
    fn azstring_into_library_owned_string_copies_static_memory() {
        let mut owned = AzString::from_const_str("static").into_library_owned_string();
        // If this had aliased the &'static str, mutating it would be UB /
        // a segfault writing to rodata.
        owned.push_str(" + mutable");
        assert_eq!(owned, "static + mutable");
    }
    // ==================================================================
    // AzString::clone_self
    // ==================================================================
    #[test]
    fn azstring_clone_self_deep_copies_library_owned_memory() {
        let s = AzString::from_string(String::from("deep"));
        let c = s.clone_self();
        assert_eq!(c, s);
        assert_ne!(
            c.as_bytes().as_ptr(),
            s.as_bytes().as_ptr(),
            "a DefaultRust clone must own a fresh allocation"
        );
        assert!(matches!(c.vec.destructor, U8VecDestructor::DefaultRust));
    }
    #[test]
    fn azstring_clone_self_shares_static_memory() {
        let s = AzString::from_const_str("static");
        let c = s.clone_self();
        assert_eq!(c, s);
        assert_eq!(
            c.as_bytes().as_ptr(),
            s.as_bytes().as_ptr(),
            "cloning a &'static-backed string should alias, not allocate"
        );
        assert!(matches!(c.vec.destructor, U8VecDestructor::NoDestructor));
    }
    #[test]
    fn azstring_clone_self_of_empty_and_unicode() {
        for s in [
            AzString::default(),
            AzString::from_const_str(""),
            AzString::from_string(String::from("\u{1F600}\u{0}\u{0301}")),
        ] {
            let c = s.clone_self();
            assert_eq!(c.as_str(), s.as_str());
            assert_eq!(c.len(), s.len());
        }
    }
    #[test]
    fn azstring_clone_trait_matches_clone_self() {
        let s = AzString::from_string(String::from("via trait"));
        assert_eq!(s.clone(), s.clone_self());
    }
    // ==================================================================
    // AzString — Debug / Display round trips (fmt)
    // ==================================================================
    #[test]
    fn azstring_display_round_trips_through_from() {
        for original in [
            "",
            " ",
            "plain",
            "héllo \u{1F600}",
            "with \"quotes\" and \\ backslash",
            "line\nbreak\ttab",
            "e\u{0301} combining",
        ] {
            let s = AzString::from(original);
            let rendered = format!("{s}");
            assert_eq!(rendered, original, "Display must emit the string verbatim");
            let reparsed = AzString::from(rendered.as_str());
            assert_eq!(reparsed, s, "parse(serialize(x)) == x");
            // serialize(parse(serialize(x))) == serialize(x)
            assert_eq!(format!("{reparsed}"), rendered);
        }
    }
    #[test]
    fn azstring_debug_matches_str_debug_and_escapes() {
        let s = AzString::from("a\"b\\c\nd");
        let expected = format!("{:?}", "a\"b\\c\nd");
        assert_eq!(format!("{s:?}"), expected, "Debug must delegate to str::fmt");
        assert!(format!("{s:?}").starts_with('"'), "Debug output must be quoted");
        assert!(!format!("{s:?}").contains('\n'), "Debug must escape newlines");
    }
    #[test]
    fn azstring_debug_and_display_of_empty_do_not_panic() {
        assert_eq!(format!("{:?}", AzString::default()), "\"\"");
        assert_eq!(format!("{}", AzString::default()), "");
        assert_eq!(format!("{:?}", AzString::from_const_str("")), "\"\"");
    }
    #[test]
    fn azstring_display_of_a_megabyte_is_lossless() {
        let huge = "q".repeat(1_000_000);
        let s = AzString::from_string(huge.clone());
        assert_eq!(format!("{s}").len(), huge.len());
    }
    #[test]
    fn azstring_debug_is_stable_across_constructors() {
        // Same text, different memory ownership → identical rendering.
        let buf = "same".as_bytes();
        let a = AzString::from_const_str("same");
        let b = AzString::from_string(String::from("same"));
        let c = AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len());
        assert_eq!(format!("{a:?}"), format!("{b:?}"));
        assert_eq!(format!("{b:?}"), format!("{c:?}"));
        assert_eq!(format!("{a}"), format!("{c}"));
    }
    // ==================================================================
    // AzString — Eq / Ord / Hash invariants
    // ==================================================================
    #[test]
    fn azstring_eq_and_hash_ignore_memory_ownership() {
        let buf = "key".as_bytes();
        let mut arena = StringArena::new();
        let variants = [
            AzString::from_const_str("key"),
            AzString::from_string(String::from("key")),
            AzString::copy_from_bytes(buf.as_ptr(), 0, buf.len()),
            arena.intern("key"),
        ];
        for v in &variants {
            assert_eq!(*v, variants[0], "equality must compare CONTENT, not pointers");
            assert_eq!(
                hash_of(v),
                hash_of(&variants[0]),
                "Hash must agree with Eq across destructor kinds"
            );
            assert_eq!(
                hash_of(v),
                hash_of(&"key"),
                "AzString must hash like the &str it wraps"
            );
        }
    }
    #[test]
    fn azstring_ord_matches_str_ord() {
        let mut v = [
            AzString::from("b"),
            AzString::from(""),
            AzString::from("\u{1F600}"),
            AzString::from("a"),
            AzString::from("ab"),
        ];
        v.sort();
        let sorted: Vec<&str> = v.iter().map(AzString::as_str).collect();
        assert_eq!(sorted, ["", "a", "ab", "b", "\u{1F600}"]);
        assert_eq!(
            AzString::from("a").partial_cmp(&AzString::from("b")),
            Some(core::cmp::Ordering::Less)
        );
        assert_eq!(
            AzString::from("x").cmp(&AzString::from("x")),
            core::cmp::Ordering::Equal
        );
    }
    // ==================================================================
    // StringArena
    // ==================================================================
    #[test]
    fn arena_new_starts_empty() {
        let arena = StringArena::new();
        assert_eq!(arena.metrics(), (0, 0), "a fresh arena allocates nothing");
        assert_eq!(StringArena::default().metrics(), (0, 0));
    }
    #[test]
    fn arena_metrics_track_chunks_and_bytes() {
        let mut arena = StringArena::new();
        let _a = arena.intern("abc");
        let (chunks, bytes) = arena.metrics();
        assert_eq!(chunks, 1);
        assert_eq!(bytes, 3);
        let _b = arena.intern("de");
        let (chunks, bytes) = arena.metrics();
        assert_eq!(chunks, 1, "a second small string reuses the open chunk");
        assert_eq!(bytes, 5);
    }
    #[test]
    fn arena_empty_string_allocates_nothing_and_is_readable() {
        let mut arena = StringArena::new();
        let e = arena.intern("");
        assert!(e.is_empty());
        assert_eq!(e.as_str(), "");
        assert_eq!(arena.metrics(), (0, 0), "empty strings need no storage");
        assert!(!e.vec.ptr.is_null(), "the dangling ptr must still be non-null");
    }
    #[test]
    fn arena_string_is_external_and_stashes_an_arc_in_cap() {
        let mut arena = StringArena::new();
        let s = arena.intern("hi");
        assert!(matches!(s.vec.destructor, U8VecDestructor::External(_)));
        assert_ne!(s.vec.cap, 0, "cap holds the Arc pointer, not a capacity");
        assert_eq!(s.as_str(), "hi");
    }
    #[test]
    fn arena_intern_refcounts_each_string() {
        let mut arena = StringArena::new();
        assert_eq!(Arc::strong_count(&arena.inner), 1);
        let a = arena.intern("one");
        let b = arena.intern("two");
        assert_eq!(
            Arc::strong_count(&arena.inner),
            3,
            "each interned string must hold its own Arc reference"
        );
        drop(a);
        assert_eq!(Arc::strong_count(&arena.inner), 2);
        drop(b);
        assert_eq!(Arc::strong_count(&arena.inner), 1);
    }
    #[test]
    fn arena_clone_deep_copies_and_does_not_bump_the_refcount() {
        let mut arena = StringArena::new();
        let s = arena.intern("interned");
        let c = s.clone_self();
        assert_eq!(
            Arc::strong_count(&arena.inner),
            2,
            "cloning an External string deep-copies; it must NOT retain the arena"
        );
        assert!(matches!(c.vec.destructor, U8VecDestructor::DefaultRust));
        assert_eq!(c.as_str(), "interned");
        assert_ne!(c.vec.ptr, s.vec.ptr);
    }
    #[test]
    fn arena_clone_outlives_the_arena_and_the_original() {
        let clone = {
            let mut arena = StringArena::new();
            let s = arena.intern("deep-copied out of the arena");
            let c = s.clone_self();
            drop(s);
            drop(arena);
            c
        };
        assert_eq!(clone.as_str(), "deep-copied out of the arena");
    }
    #[test]
    fn arena_exact_half_chunk_boundary_fills_one_chunk_exactly() {
        // len == CHUNK_SIZE / 2 is NOT "oversized" (the check is `>`), so two of
        // them must fit in a single chunk with zero reallocation, and the third
        // must open a new one.
        let mut arena = StringArena::new();
        let half = "h".repeat(StringArena::CHUNK_SIZE / 2);
        let a = arena.intern(&half);
        let b = arena.intern(&half);
        assert_eq!(arena.metrics().0, 1, "two half-chunks must share one chunk");
        let c = arena.intern(&half);
        assert_eq!(arena.metrics().0, 2, "the third must open a new chunk");
        // If the exact-fit append had reallocated, `a`/`b` would now dangle.
        assert_eq!(a.as_str(), half);
        assert_eq!(b.as_str(), half);
        assert_eq!(c.as_str(), half);
    }
    #[test]
    fn arena_oversized_string_is_readable_and_gets_its_own_chunk() {
        let mut arena = StringArena::new();
        let big = "b".repeat(StringArena::CHUNK_SIZE + 1);
        let s = arena.intern(&big);
        assert_eq!(s.len(), big.len());
        assert_eq!(s.as_str(), big.as_str());
        assert_eq!(arena.metrics(), (1, big.len()));
    }
    #[test]
    fn arena_many_interleaved_sizes_all_read_back_correctly() {
        let mut arena = StringArena::new();
        let mut kept = Vec::new();
        for i in 0..200 {
            let s = format!("s{i}-{}", "p".repeat(i % 17));
            kept.push((arena.intern(&s), s));
        }
        for (interned, expected) in &kept {
            assert_eq!(interned.as_str(), expected.as_str());
        }
    }
    #[test]
    fn arena_interns_unicode_and_nul_bytes_verbatim() {
        let mut arena = StringArena::new();
        let weird = "héllo \u{1F600}\u{0}\u{0301}";
        let s = arena.intern(weird);
        assert_eq!(s.as_str(), weird);
        assert_eq!(s.len(), weird.len());
    }
    #[test]
    fn arena_strings_outlive_the_handle_even_when_interleaved() {
        let (a, b) = {
            let mut arena = StringArena::new();
            let a = arena.intern("first");
            let big = "z".repeat(StringArena::CHUNK_SIZE * 2);
            let _dropped = arena.intern(&big);
            let b = arena.intern("second");
            (a, b)
        };
        assert_eq!(a.as_str(), "first");
        assert_eq!(b.as_str(), "second");
    }
    /// RED — genuine bug in `StringArena::intern` (use-after-free).
    ///
    /// The oversized branch pushes a *dedicated, completely full* chunk
    /// (`len == cap`) but never touches `current_remaining`. If a small string
    /// was interned first, `remaining` is still > 0, so the next small intern
    /// skips the "push a fresh chunk" branch and appends into
    /// `chunks.last_mut()` — which is now that full dedicated chunk. The
    /// `extend_from_slice` therefore grows a `len == cap` Vec, reallocating it
    /// and leaving the `AzString` handed out for the oversized string pointing
    /// at freed memory.
    ///
    /// This test only inspects chunk *lengths* — it never dereferences the
    /// dangling pointer, so the test itself stays UB-free.
    #[test]
    fn arena_small_after_oversized_must_not_grow_the_full_dedicated_chunk() {
        let mut arena = StringArena::new();
        // 1. open a shared chunk, leaving current_remaining > 0
        let _small = arena.intern("a");
        // 2. oversized → dedicated chunk with len == cap == big_len
        let big = "x".repeat(StringArena::CHUNK_SIZE);
        let big_len = big.len();
        let interned_big = arena.intern(&big);
        assert_eq!(interned_big.as_str(), big.as_str(), "valid before the next intern");
        // 3. another small string: must NOT be appended into the full chunk
        let _small2 = arena.intern("y");
        // Safety: read-only look at the chunk lengths; no chunk data is read
        // and the (possibly dangling) `interned_big.vec.ptr` is never deref'd.
        let grew = unsafe {
            let chunks = &*arena.inner.chunks.get();
            chunks.iter().any(|c| c.len() > big_len)
        };
        assert!(
            !grew,
            "intern() appended a small string into the FULL dedicated chunk of an oversized \
             string (len == cap), which reallocates that Vec and leaves every AzString pointing \
             into it dangling — a use-after-free. Root cause: the oversized branch pushes a chunk \
             without resetting `current_remaining`, so the next small string takes the \
             `chunks.last_mut()` fast path onto the wrong chunk."
        );
    }
    // ==================================================================
    // arena_string_destructor
    // ==================================================================
    #[test]
    fn arena_destructor_drops_one_arc_ref_and_is_idempotent() {
        let inner = Arc::new(StringArenaInner {
            chunks: UnsafeCell::new(Vec::new()),
            current_remaining: UnsafeCell::new(0),
        });
        let raw = Arc::into_raw(Arc::clone(&inner));
        assert_eq!(Arc::strong_count(&inner), 2);
        let mut v = U8Vec {
            ptr: core::ptr::NonNull::<u8>::dangling().as_ptr().cast_const(),
            len: 0,
            cap: raw as usize,
            destructor: U8VecDestructor::External(arena_string_destructor),
        };
        arena_string_destructor(&mut v);
        assert_eq!(
            Arc::strong_count(&inner),
            1,
            "the destructor must release exactly one Arc reference"
        );
        assert_eq!(v.cap, 0, "cap must be zeroed to guard against a double drop");
        // A second call must be a no-op rather than a double free.
        arena_string_destructor(&mut v);
        assert_eq!(Arc::strong_count(&inner), 1);
        assert_eq!(v.cap, 0);
        // `v` still carries the External destructor; dropping it runs the
        // destructor a third time — also a no-op, since cap == 0.
        drop(v);
        assert_eq!(Arc::strong_count(&inner), 1);
    }
    #[test]
    fn arena_destructor_tolerates_a_null_arc_pointer() {
        // cap == 0 (e.g. a zeroed FFI husk) must not be turned into Arc::from_raw(null).
        let mut v = U8Vec {
            ptr: core::ptr::null(),
            len: 0,
            cap: 0,
            destructor: U8VecDestructor::NoDestructor,
        };
        arena_string_destructor(&mut v);
        assert_eq!(v.cap, 0);
    }
    #[test]
    fn arena_last_reference_frees_the_chunks() {
        // The arena's bytes must survive until the LAST AzString goes away,
        // and dropping in either order must not double-free.
        let inner_ptr;
        let s = {
            let mut arena = StringArena::new();
            let s = arena.intern("outlives the handle");
            inner_ptr = Arc::as_ptr(&arena.inner);
            assert_eq!(Arc::strong_count(&arena.inner), 2);
            s
        };
        // The arena handle is gone but the string still owns a reference.
        assert_eq!(s.as_str(), "outlives the handle");
        assert_eq!(s.vec.cap as *const StringArenaInner, inner_ptr);
        drop(s); // final reference → chunks freed here, exactly once
    }
}