Skip to main content

azul_css/props/basic/
error.rs

1//! C-compatible (`#[repr(C)]`) error types for CSS parsing failures.
2//!
3//! Mirrors `core::num::ParseFloatError` and `core::num::ParseIntError` for FFI use,
4//! and provides generic invalid-value error wrappers.
5
6use crate::corety::AzString;
7
8/// Simple "invalid value" error, used for basic parsing failures
9#[derive(Debug, Copy, Clone, Eq, PartialEq)]
10pub struct InvalidValueErr<'a>(pub &'a str);
11
12/// Owned version of `InvalidValueErr` with `AzString`.
13#[derive(Debug, Clone, PartialEq, Eq)]
14#[repr(C)]
15pub struct InvalidValueErrOwned {
16    pub value: AzString,
17}
18
19/// C-compatible enum mirroring `core::num::ParseFloatError` internals.
20///
21/// `core::num::ParseFloatError` is a 1-byte enum with variants `Empty` and `Invalid`,
22/// but its `kind` field is private. We mirror the variants here for FFI compatibility.
23#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
24#[repr(C)]
25pub enum ParseFloatError {
26    /// Input string was empty.
27    Empty,
28    /// Input string was not a valid float literal.
29    Invalid,
30}
31
32impl ParseFloatError {
33    /// Convert from `core::num::ParseFloatError` by comparing against known error instances.
34    fn from_std(e: &core::num::ParseFloatError) -> Self {
35        // Compare against the known Empty error instance to avoid
36        // relying on Display message wording or allocating a format string.
37        let empty_err = "".parse::<f32>().unwrap_err();
38        if *e == empty_err {
39            Self::Empty
40        } else {
41            Self::Invalid
42        }
43    }
44
45    /// Reconstruct a `core::num::ParseFloatError` from our C-compatible variant.
46    #[must_use] pub fn to_std(&self) -> core::num::ParseFloatError {
47        match self {
48            Self::Empty => "".parse::<f32>().unwrap_err(),
49            Self::Invalid => "x".parse::<f32>().unwrap_err(),
50        }
51    }
52}
53
54impl core::fmt::Display for ParseFloatError {
55    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56        match self {
57            Self::Empty => write!(f, "cannot parse float from empty string"),
58            Self::Invalid => write!(f, "invalid float literal"),
59        }
60    }
61}
62
63impl From<core::num::ParseFloatError> for ParseFloatError {
64    fn from(e: core::num::ParseFloatError) -> Self {
65        Self::from_std(&e)
66    }
67}
68
69/// C-compatible enum mirroring `core::num::ParseIntError` internals.
70///
71/// `core::num::ParseIntError` is a 1-byte enum with variants matching `IntErrorKind`.
72/// We mirror them here for FFI compatibility.
73#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
74#[repr(C)]
75pub enum ParseIntError {
76    /// Input string was empty.
77    Empty,
78    /// Input contained an invalid digit.
79    InvalidDigit,
80    /// Input overflowed the target integer type (positive).
81    PosOverflow,
82    /// Input overflowed the target integer type (negative).
83    NegOverflow,
84    /// Input was zero but zero is not allowed (rarely used).
85    Zero,
86}
87
88impl ParseIntError {
89    /// Convert from `core::num::ParseIntError` using the stable `kind()` method.
90    const fn from_std(e: &core::num::ParseIntError) -> Self {
91        use core::num::IntErrorKind;
92        match e.kind() {
93            IntErrorKind::Empty => Self::Empty,
94            IntErrorKind::PosOverflow => Self::PosOverflow,
95            IntErrorKind::NegOverflow => Self::NegOverflow,
96            IntErrorKind::Zero => Self::Zero,
97            _ => Self::InvalidDigit, // future-proofing
98        }
99    }
100
101    /// Reconstruct a `core::num::ParseIntError` from our C-compatible variant.
102    #[must_use] pub fn to_std(&self) -> core::num::ParseIntError {
103        match self {
104            Self::Empty => "".parse::<i32>().unwrap_err(),
105            Self::InvalidDigit => "x".parse::<i32>().unwrap_err(),
106            Self::PosOverflow => "99999999999999999999".parse::<i32>().unwrap_err(),
107            Self::NegOverflow => "-99999999999999999999".parse::<i32>().unwrap_err(),
108            Self::Zero => {
109                // Zero variant cannot be reproduced on stable Rust; falls back to InvalidDigit.
110                // Note: round-tripping Zero through to_std() then from_std() yields InvalidDigit.
111                "x".parse::<i32>().unwrap_err()
112            }
113        }
114    }
115}
116
117impl core::fmt::Display for ParseIntError {
118    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119        match self {
120            Self::Empty => write!(f, "cannot parse integer from empty string"),
121            Self::InvalidDigit => write!(f, "invalid digit found in string"),
122            Self::PosOverflow => write!(f, "number too large to fit in target type"),
123            Self::NegOverflow => write!(f, "number too small to fit in target type"),
124            Self::Zero => write!(f, "number would be zero for non-zero type"),
125        }
126    }
127}
128
129impl From<core::num::ParseIntError> for ParseIntError {
130    fn from(e: core::num::ParseIntError) -> Self {
131        Self::from_std(&e)
132    }
133}
134
135/// Wrapper for a `ParseFloatError` paired with the input string that failed.
136/// Used by multiple Owned error enums that need to store both the error and input.
137#[derive(Debug, Clone, PartialEq, Eq)]
138#[repr(C)]
139pub struct ParseFloatErrorWithInput {
140    pub error: ParseFloatError,
141    pub input: AzString,
142}
143
144/// Wrapper for `WrongNumberOfComponents` errors in CSS filter/transform parsing.
145#[derive(Debug, Clone, PartialEq, Eq)]
146#[repr(C)]
147pub struct WrongComponentCountError {
148    pub expected: usize,
149    pub got: usize,
150    pub input: AzString,
151}
152
153impl InvalidValueErr<'_> {
154    #[must_use] pub fn to_contained(&self) -> InvalidValueErrOwned {
155        InvalidValueErrOwned { value: self.0.to_string().into() }
156    }
157}
158
159impl InvalidValueErrOwned {
160    #[must_use] pub fn to_shared(&self) -> InvalidValueErr<'_> {
161        InvalidValueErr(self.value.as_str())
162    }
163}
164
165#[cfg(test)]
166#[allow(clippy::too_many_lines)]
167mod autotest_generated {
168    use core::num::IntErrorKind;
169    use std::{
170        collections::hash_map::DefaultHasher,
171        hash::{Hash, Hasher},
172    };
173
174    use super::*;
175
176    // =====================================================================
177    // helpers
178    // =====================================================================
179
180    /// Parse `s` as `T`, expecting failure, and funnel the std error through
181    /// the private `from_std` constructor under test.
182    fn float_kind<T>(s: &str) -> ParseFloatError
183    where
184        T: core::str::FromStr<Err = core::num::ParseFloatError>,
185    {
186        match s.parse::<T>() {
187            Ok(_) => panic!("expected {s:?} to FAIL to parse as a float"),
188            Err(e) => ParseFloatError::from_std(&e),
189        }
190    }
191
192    fn int_kind<T>(s: &str) -> ParseIntError
193    where
194        T: core::str::FromStr<Err = core::num::ParseIntError>,
195    {
196        match s.parse::<T>() {
197            Ok(_) => panic!("expected {s:?} to FAIL to parse as an integer"),
198            Err(e) => ParseIntError::from_std(&e),
199        }
200    }
201
202    fn std_float_err(s: &str) -> core::num::ParseFloatError {
203        s.parse::<f32>().expect_err("input should not parse")
204    }
205
206    fn std_int_err(s: &str) -> core::num::ParseIntError {
207        s.parse::<i32>().expect_err("input should not parse")
208    }
209
210    fn hash_of<T: Hash>(v: &T) -> u64 {
211        let mut h = DefaultHasher::new();
212        v.hash(&mut h);
213        h.finish()
214    }
215
216    const ALL_FLOAT: [ParseFloatError; 2] = [ParseFloatError::Empty, ParseFloatError::Invalid];
217
218    const ALL_INT: [ParseIntError; 5] = [
219        ParseIntError::Empty,
220        ParseIntError::InvalidDigit,
221        ParseIntError::PosOverflow,
222        ParseIntError::NegOverflow,
223        ParseIntError::Zero,
224    ];
225
226    // =====================================================================
227    // ParseFloatError::from_std  (constructor, private)
228    // =====================================================================
229
230    #[test]
231    fn float_from_std_empty_string_maps_to_empty() {
232        assert_eq!(float_kind::<f32>(""), ParseFloatError::Empty);
233        // The comparison instance inside `from_std` is built from `f32`; an error
234        // produced by an `f64` parse must still classify as `Empty` (std compares
235        // the private `kind`, not the source type).
236        assert_eq!(float_kind::<f64>(""), ParseFloatError::Empty);
237    }
238
239    #[test]
240    fn float_from_std_blank_input_is_invalid_not_empty() {
241        // A string that *looks* empty but is not: `from_std` must NOT collapse
242        // these into `Empty`, because std trims nothing.
243        for s in [
244            " ",
245            "  ",
246            "\t",
247            "\n",
248            "\r\n",
249            "\u{a0}",    // NBSP
250            "\u{feff}",  // BOM
251            "\u{200b}",  // zero-width space
252            "\u{0}",     // NUL
253        ] {
254            assert_eq!(
255                float_kind::<f32>(s),
256                ParseFloatError::Invalid,
257                "blank-ish input {s:?} must be Invalid, not Empty"
258            );
259        }
260    }
261
262    #[test]
263    fn float_from_std_malformed_inputs_are_invalid() {
264        for s in [
265            "x", ".", "-", "+", "e", "e5", "5e", "1.2.3", "0x1f", "1,5", "--1", "++1", "1 ", " 1",
266            "1_0", "NaNx", "infinit", "1/2", "abc", "1e", "1e+", "-.",
267        ] {
268            assert_eq!(
269                float_kind::<f32>(s),
270                ParseFloatError::Invalid,
271                "malformed input {s:?} should be Invalid"
272            );
273        }
274    }
275
276    #[test]
277    fn float_from_std_non_ascii_digits_are_invalid() {
278        for s in [
279            "١٢٣",     // Arabic-Indic digits
280            "123",  // fullwidth digits
281            "½",       // vulgar fraction
282            "😀",
283            "٣.٥",
284            "1\u{301}", // combining acute after a valid digit
285            "Ⅻ",        // roman numeral
286        ] {
287            assert_eq!(
288                float_kind::<f32>(s),
289                ParseFloatError::Invalid,
290                "unicode input {s:?} should be Invalid"
291            );
292        }
293    }
294
295    #[test]
296    fn float_from_std_huge_malformed_input_does_not_panic_or_hang() {
297        let mut huge = "9".repeat(100_000);
298        huge.push('x');
299        assert_eq!(float_kind::<f32>(&huge), ParseFloatError::Invalid);
300
301        // 100k leading zeros followed by garbage: still just Invalid.
302        let mut zeros = "0".repeat(100_000);
303        zeros.push_str("..");
304        assert_eq!(float_kind::<f32>(&zeros), ParseFloatError::Invalid);
305    }
306
307    #[test]
308    fn float_from_impl_agrees_with_from_std() {
309        for s in ["", " ", "x", "1.2.3", "😀"] {
310            let a: ParseFloatError = std_float_err(s).into();
311            let b = ParseFloatError::from_std(&std_float_err(s));
312            assert_eq!(a, b, "From<> and from_std disagree for {s:?}");
313        }
314    }
315
316    // =====================================================================
317    // float numeric limits: magnitude overflow never reaches our error type
318    // =====================================================================
319
320    #[test]
321    fn float_magnitude_overflow_saturates_to_infinity_instead_of_erroring() {
322        // No `ParseFloatError` is produced for out-of-range magnitudes — std
323        // saturates. Anything relying on an "overflow" variant would be wrong.
324        assert!(
325            "1e400".parse::<f32>().expect("saturates, does not error").is_infinite(),
326            "huge positive exponent should saturate to +inf"
327        );
328        assert!("-1e400".parse::<f32>().expect("saturates").is_sign_negative());
329        assert_eq!("1e-400".parse::<f32>().expect("underflows to zero"), 0.0);
330
331        let huge = "9".repeat(100_000);
332        assert!(huge.parse::<f32>().expect("saturates").is_infinite());
333    }
334
335    #[test]
336    fn float_nan_and_inf_literals_parse_and_never_error() {
337        assert!("nan".parse::<f32>().expect("nan is valid").is_nan());
338        assert!("NaN".parse::<f32>().expect("NaN is valid").is_nan());
339        assert!("inf".parse::<f32>().expect("inf is valid").is_infinite());
340        assert!("infinity".parse::<f32>().expect("infinity is valid").is_infinite());
341        assert!("-inf".parse::<f32>().expect("-inf is valid").is_sign_negative());
342        assert!("-0".parse::<f32>().expect("-0 is valid").is_sign_negative());
343    }
344
345    // =====================================================================
346    // ParseFloatError::to_std  (getter) + round-trip
347    // =====================================================================
348
349    #[test]
350    fn float_to_std_returns_the_matching_std_error() {
351        assert_eq!(ParseFloatError::Empty.to_std(), std_float_err(""));
352        assert_eq!(ParseFloatError::Invalid.to_std(), std_float_err("x"));
353    }
354
355    #[test]
356    fn float_to_std_variants_stay_distinct() {
357        // If these ever collapsed, `from_std` would misclassify every error.
358        assert_ne!(ParseFloatError::Empty.to_std(), ParseFloatError::Invalid.to_std());
359    }
360
361    #[test]
362    fn float_to_std_is_deterministic() {
363        for v in ALL_FLOAT {
364            assert_eq!(v.to_std(), v.to_std(), "to_std() must be stable for {v:?}");
365        }
366    }
367
368    #[test]
369    fn float_round_trip_encode_decode_is_identity() {
370        for v in ALL_FLOAT {
371            assert_eq!(ParseFloatError::from_std(&v.to_std()), v, "round-trip lost {v:?}");
372            assert_eq!(ParseFloatError::from(v.to_std()), v);
373        }
374    }
375
376    // =====================================================================
377    // ParseIntError::from_std  (constructor, private)
378    // =====================================================================
379
380    #[test]
381    fn int_from_std_empty_string_maps_to_empty_for_every_width() {
382        assert_eq!(int_kind::<i8>(""), ParseIntError::Empty);
383        assert_eq!(int_kind::<u8>(""), ParseIntError::Empty);
384        assert_eq!(int_kind::<i32>(""), ParseIntError::Empty);
385        assert_eq!(int_kind::<u128>(""), ParseIntError::Empty);
386        assert_eq!(int_kind::<usize>(""), ParseIntError::Empty);
387        assert_eq!(int_kind::<isize>(""), ParseIntError::Empty);
388    }
389
390    #[test]
391    fn int_from_std_malformed_inputs_are_invalid_digit() {
392        for s in [
393            "x", " ", "  ", "\t", "+", "-", "+-1", "--1", "1 ", " 1", "1_000", "0x10", "1.0",
394            "1e3", "abc", "\u{0}", "1\u{0}", "٣", "123", "😀", "½", ",", "1,000",
395        ] {
396            assert_eq!(
397                int_kind::<i32>(s),
398                ParseIntError::InvalidDigit,
399                "malformed input {s:?} should be InvalidDigit"
400            );
401        }
402    }
403
404    #[test]
405    fn int_from_std_negative_into_unsigned_is_invalid_digit_not_neg_overflow() {
406        // std rejects the '-' sign as a digit for unsigned types rather than
407        // reporting NegOverflow — a classifier that assumed otherwise would be wrong.
408        assert_eq!(int_kind::<u32>("-1"), ParseIntError::InvalidDigit);
409        assert_eq!(int_kind::<u8>("-0"), ParseIntError::InvalidDigit);
410        assert_eq!(int_kind::<u128>("-99999999999999999999999999"), ParseIntError::InvalidDigit);
411    }
412
413    #[test]
414    fn int_from_std_positive_overflow_boundaries() {
415        // exactly MAX parses; MAX + 1 overflows.
416        assert_eq!(i32::MAX.to_string().parse::<i32>(), Ok(i32::MAX));
417        assert_eq!(int_kind::<i32>("2147483648"), ParseIntError::PosOverflow);
418        assert_eq!(u8::MAX.to_string().parse::<u8>(), Ok(u8::MAX));
419        assert_eq!(int_kind::<u8>("256"), ParseIntError::PosOverflow);
420        assert_eq!(i8::MAX.to_string().parse::<i8>(), Ok(i8::MAX));
421        assert_eq!(int_kind::<i8>("128"), ParseIntError::PosOverflow);
422        assert_eq!(
423            int_kind::<u128>("340282366920938463463374607431768211456"),
424            ParseIntError::PosOverflow
425        );
426    }
427
428    #[test]
429    fn int_from_std_negative_overflow_boundaries() {
430        assert_eq!(i32::MIN.to_string().parse::<i32>(), Ok(i32::MIN));
431        assert_eq!(int_kind::<i32>("-2147483649"), ParseIntError::NegOverflow);
432        assert_eq!(i8::MIN.to_string().parse::<i8>(), Ok(i8::MIN));
433        assert_eq!(int_kind::<i8>("-129"), ParseIntError::NegOverflow);
434        assert_eq!(int_kind::<i128>("-99999999999999999999999999999999999999999"), ParseIntError::NegOverflow);
435    }
436
437    #[test]
438    fn int_from_std_huge_digit_runs_overflow_without_panic() {
439        let huge = "9".repeat(10_000);
440        assert_eq!(int_kind::<i32>(&huge), ParseIntError::PosOverflow);
441        assert_eq!(int_kind::<u128>(&huge), ParseIntError::PosOverflow);
442
443        let huge_neg = format!("-{huge}");
444        assert_eq!(int_kind::<i64>(&huge_neg), ParseIntError::NegOverflow);
445    }
446
447    #[test]
448    fn int_leading_zeros_do_not_produce_a_false_overflow() {
449        // 10k leading zeros: the digit loop multiplies by 10 each step, so a naive
450        // overflow check would trip here. It must still parse cleanly.
451        let padded = format!("{}5", "0".repeat(10_000));
452        assert_eq!(padded.parse::<i32>(), Ok(5));
453        assert_eq!("0000000000000000000000000000005".parse::<i32>(), Ok(5));
454    }
455
456    #[test]
457    fn int_from_std_zero_variant_is_reachable_via_nonzero_types() {
458        // Contrary to the note on `to_std`, `IntErrorKind::Zero` IS constructible on
459        // stable via the NonZero* parsers — so `from_std` really can return `Zero`.
460        assert_eq!(int_kind::<core::num::NonZeroU8>("0"), ParseIntError::Zero);
461        assert_eq!(int_kind::<core::num::NonZeroI32>("0"), ParseIntError::Zero);
462        assert_eq!(int_kind::<core::num::NonZeroUsize>("0"), ParseIntError::Zero);
463        // ...while other failures on the same type keep their own classification.
464        assert_eq!(int_kind::<core::num::NonZeroU8>(""), ParseIntError::Empty);
465        assert_eq!(int_kind::<core::num::NonZeroU8>("x"), ParseIntError::InvalidDigit);
466        assert_eq!(int_kind::<core::num::NonZeroU8>("256"), ParseIntError::PosOverflow);
467    }
468
469    #[test]
470    fn int_from_impl_agrees_with_from_std() {
471        for s in ["", "x", "99999999999999999999", "-99999999999999999999", "😀"] {
472            let a: ParseIntError = std_int_err(s).into();
473            let b = ParseIntError::from_std(&std_int_err(s));
474            assert_eq!(a, b, "From<> and from_std disagree for {s:?}");
475        }
476    }
477
478    // =====================================================================
479    // ParseIntError::to_std  (getter) + round-trip
480    // =====================================================================
481
482    #[test]
483    fn int_to_std_maps_each_variant_onto_the_expected_std_kind() {
484        assert!(matches!(ParseIntError::Empty.to_std().kind(), IntErrorKind::Empty));
485        assert!(matches!(ParseIntError::InvalidDigit.to_std().kind(), IntErrorKind::InvalidDigit));
486        assert!(matches!(ParseIntError::PosOverflow.to_std().kind(), IntErrorKind::PosOverflow));
487        assert!(matches!(ParseIntError::NegOverflow.to_std().kind(), IntErrorKind::NegOverflow));
488        // Documented lossy case: `Zero` degrades to an InvalidDigit std error.
489        assert!(matches!(ParseIntError::Zero.to_std().kind(), IntErrorKind::InvalidDigit));
490    }
491
492    #[test]
493    fn int_to_std_is_deterministic() {
494        for v in ALL_INT {
495            assert_eq!(v.to_std(), v.to_std(), "to_std() must be stable for {v:?}");
496        }
497    }
498
499    #[test]
500    fn int_round_trip_encode_decode_is_identity_except_for_zero() {
501        for v in [
502            ParseIntError::Empty,
503            ParseIntError::InvalidDigit,
504            ParseIntError::PosOverflow,
505            ParseIntError::NegOverflow,
506        ] {
507            assert_eq!(ParseIntError::from_std(&v.to_std()), v, "round-trip lost {v:?}");
508            assert_eq!(ParseIntError::from(v.to_std()), v);
509        }
510
511        // `Zero` is the one variant that does NOT survive to_std() -> from_std(),
512        // exactly as the code comments document. (It is *not* an un-representable
513        // kind though — see `int_from_std_zero_variant_is_reachable_via_nonzero_types`.)
514        assert_eq!(
515            ParseIntError::from_std(&ParseIntError::Zero.to_std()),
516            ParseIntError::InvalidDigit,
517            "Zero round-trip is documented as lossy"
518        );
519    }
520
521    #[test]
522    fn int_to_std_variants_stay_distinct_where_they_must() {
523        let empty = ParseIntError::Empty.to_std();
524        let invalid = ParseIntError::InvalidDigit.to_std();
525        let pos = ParseIntError::PosOverflow.to_std();
526        let neg = ParseIntError::NegOverflow.to_std();
527        assert_ne!(empty, invalid);
528        assert_ne!(invalid, pos);
529        assert_ne!(pos, neg);
530        assert_ne!(empty, neg);
531        // Zero aliases InvalidDigit (documented).
532        assert_eq!(ParseIntError::Zero.to_std(), invalid);
533    }
534
535    // =====================================================================
536    // Display / Debug (serializers)
537    // =====================================================================
538
539    #[test]
540    fn display_output_is_non_empty_and_unique_per_variant() {
541        let float_msgs: Vec<String> = ALL_FLOAT.iter().map(ToString::to_string).collect();
542        for m in &float_msgs {
543            assert!(!m.is_empty(), "float Display must not be empty");
544        }
545        assert_ne!(float_msgs[0], float_msgs[1], "float variants must be distinguishable");
546
547        let int_msgs: Vec<String> = ALL_INT.iter().map(ToString::to_string).collect();
548        for m in &int_msgs {
549            assert!(!m.is_empty(), "int Display must not be empty");
550        }
551        for i in 0..int_msgs.len() {
552            for j in (i + 1)..int_msgs.len() {
553                assert_ne!(int_msgs[i], int_msgs[j], "int variants {i}/{j} share a message");
554            }
555        }
556    }
557
558    #[test]
559    fn display_mirrors_the_std_error_messages() {
560        // The whole point of these types is to be a faithful FFI mirror of the std
561        // errors; if std ever reworded a message, this catches the drift.
562        assert_eq!(ParseFloatError::Empty.to_string(), std_float_err("").to_string());
563        assert_eq!(ParseFloatError::Invalid.to_string(), std_float_err("x").to_string());
564
565        assert_eq!(ParseIntError::Empty.to_string(), std_int_err("").to_string());
566        assert_eq!(ParseIntError::InvalidDigit.to_string(), std_int_err("x").to_string());
567        assert_eq!(
568            ParseIntError::PosOverflow.to_string(),
569            std_int_err("99999999999999999999").to_string()
570        );
571        assert_eq!(
572            ParseIntError::NegOverflow.to_string(),
573            std_int_err("-99999999999999999999").to_string()
574        );
575        // `Zero` cannot go through `to_std()` (it aliases InvalidDigit), so compare
576        // against a genuine Zero-kind error obtained from a NonZero parse.
577        let std_zero = "0"
578            .parse::<core::num::NonZeroU8>()
579            .expect_err("parsing 0 as NonZeroU8 must fail");
580        assert!(matches!(std_zero.kind(), IntErrorKind::Zero));
581        assert_eq!(ParseIntError::Zero.to_string(), std_zero.to_string());
582    }
583
584    #[test]
585    fn display_with_formatter_flags_does_not_panic() {
586        for v in ALL_INT {
587            let msg = v.to_string();
588            let padded = format!("{v:>60}");
589            assert!(!padded.is_empty());
590            assert!(padded.contains(&msg), "padding must not corrupt the message");
591            // precision / fill / alternate flags: no panic, still produces output
592            assert!(!format!("{v:.3}").is_empty());
593            assert!(!format!("{v:*^10}").is_empty());
594            assert!(!format!("{v:#?}").is_empty());
595        }
596        for v in ALL_FLOAT {
597            assert!(!format!("{v:>60}").is_empty());
598            assert!(!format!("{v:.1}").is_empty());
599            assert!(!format!("{v:?}").is_empty());
600        }
601    }
602
603    #[test]
604    fn debug_output_names_the_variant() {
605        assert_eq!(format!("{:?}", ParseFloatError::Empty), "Empty");
606        assert_eq!(format!("{:?}", ParseIntError::PosOverflow), "PosOverflow");
607        assert_eq!(format!("{:?}", ParseIntError::Zero), "Zero");
608    }
609
610    // =====================================================================
611    // derived-trait invariants (Eq / Ord / Hash / Copy)
612    // =====================================================================
613
614    #[test]
615    fn error_enums_have_consistent_eq_hash_and_ord() {
616        for (i, a) in ALL_INT.iter().enumerate() {
617            assert_eq!(hash_of(a), hash_of(&ALL_INT[i]), "equal values must hash equal");
618            for (j, b) in ALL_INT.iter().enumerate() {
619                assert_eq!(a == b, i == j, "only identical variants may compare equal");
620                assert_eq!(a.cmp(b), i.cmp(&j), "Ord must follow declaration order");
621            }
622        }
623        assert!(ParseFloatError::Empty < ParseFloatError::Invalid);
624        assert_eq!(hash_of(&ParseFloatError::Empty), hash_of(&ParseFloatError::Empty));
625        assert_ne!(ParseFloatError::Empty, ParseFloatError::Invalid);
626    }
627
628    #[test]
629    fn error_enums_are_copy_and_survive_a_sort() {
630        let mut v = [
631            ParseIntError::Zero,
632            ParseIntError::Empty,
633            ParseIntError::NegOverflow,
634            ParseIntError::InvalidDigit,
635            ParseIntError::PosOverflow,
636        ];
637        v.sort_unstable();
638        assert_eq!(v, ALL_INT);
639
640        let a = ParseIntError::Zero;
641        let b = a; // Copy, not move
642        assert_eq!(a, b);
643    }
644
645    // =====================================================================
646    // InvalidValueErr::to_contained / InvalidValueErrOwned::to_shared
647    // =====================================================================
648
649    #[test]
650    fn invalid_value_err_round_trips_through_owned() {
651        for s in [
652            "",
653            "a",
654            "border-radius",
655            "   ",
656            "\n\t",
657            "börder-radiüs 😀",
658            "٣.٥",
659            "\u{feff}leading-bom",
660            "trailing-nul\u{0}",
661            "a\u{0}b",
662        ] {
663            let shared = InvalidValueErr(s);
664            let owned = shared.to_contained();
665            assert_eq!(owned.value.as_str(), s, "to_contained lost {s:?}");
666            assert_eq!(owned.to_shared(), shared, "round-trip changed {s:?}");
667            assert_eq!(owned.to_shared().0, s);
668        }
669    }
670
671    #[test]
672    fn invalid_value_err_empty_string_is_not_confused_with_default() {
673        let owned = InvalidValueErr("").to_contained();
674        assert_eq!(owned.value, AzString::default());
675        assert!(owned.value.as_str().is_empty());
676        assert_eq!(owned.to_shared(), InvalidValueErr(""));
677        assert_eq!(owned, InvalidValueErrOwned { value: AzString::default() });
678    }
679
680    #[test]
681    fn invalid_value_err_preserves_interior_nul_bytes() {
682        // If the AzString conversion ever went through a C-string, this would
683        // truncate at the NUL.
684        let s = "a\u{0}b";
685        let owned = InvalidValueErr(s).to_contained();
686        assert_eq!(owned.value.as_bytes(), b"a\0b");
687        assert_eq!(owned.value.as_str().len(), 3);
688        assert_eq!(owned.to_shared().0.len(), 3);
689    }
690
691    #[test]
692    fn to_contained_deep_copies_and_outlives_its_source() {
693        let owned = {
694            let src = String::from("temporary-buffer");
695            let copied = InvalidValueErr(src.as_str()).to_contained();
696            assert!(
697                !core::ptr::eq(copied.value.as_str().as_ptr(), src.as_str().as_ptr()),
698                "to_contained must copy, not alias the borrowed input"
699            );
700            drop(src);
701            copied
702        };
703        assert_eq!(owned.value.as_str(), "temporary-buffer");
704    }
705
706    #[test]
707    fn to_shared_borrows_the_owned_buffer_without_copying() {
708        let owned = InvalidValueErr("shared-buffer").to_contained();
709        let shared = owned.to_shared();
710        assert!(
711            core::ptr::eq(shared.0.as_ptr(), owned.value.as_str().as_ptr()),
712            "to_shared must borrow the existing buffer"
713        );
714        // calling it twice yields the same view
715        assert_eq!(owned.to_shared(), owned.to_shared());
716    }
717
718    #[test]
719    fn invalid_value_err_handles_a_huge_payload() {
720        let big = "ü".repeat(100_000); // 200_000 bytes, non-ASCII
721        let owned = InvalidValueErr(big.as_str()).to_contained();
722        assert_eq!(owned.value.as_str().len(), big.len());
723        assert_eq!(owned.value.as_bytes().len(), 200_000);
724        assert_eq!(owned.to_shared().0, big.as_str());
725        assert_eq!(owned.clone(), owned);
726    }
727
728    #[test]
729    fn invalid_value_err_owned_equality_is_by_content() {
730        let a = InvalidValueErr("x").to_contained();
731        let b = InvalidValueErrOwned { value: AzString::from("x") };
732        let c = InvalidValueErrOwned { value: AzString::from("y") };
733        assert_eq!(a, b);
734        assert_ne!(a, c);
735        assert_eq!(a.clone(), a);
736        assert_eq!(a.to_shared(), b.to_shared());
737    }
738
739    // =====================================================================
740    // ParseFloatErrorWithInput / WrongComponentCountError
741    // =====================================================================
742
743    #[test]
744    fn parse_float_error_with_input_keeps_error_and_input_together() {
745        let input = "1.2.3";
746        let err = ParseFloatErrorWithInput {
747            error: ParseFloatError::from(std_float_err(input)),
748            input: AzString::from(input),
749        };
750        assert_eq!(err.error, ParseFloatError::Invalid);
751        assert_eq!(err.input.as_str(), input);
752        assert_eq!(err.clone(), err);
753
754        let empty = ParseFloatErrorWithInput {
755            error: ParseFloatError::from(std_float_err("")),
756            input: AzString::default(),
757        };
758        assert_eq!(empty.error, ParseFloatError::Empty);
759        assert!(empty.input.as_str().is_empty());
760        assert_ne!(empty, err);
761        assert!(!format!("{err:?}").is_empty());
762    }
763
764    #[test]
765    fn wrong_component_count_error_survives_usize_extremes() {
766        let e = WrongComponentCountError {
767            expected: usize::MAX,
768            got: 0,
769            input: AzString::from("rgba(1)"),
770        };
771        assert_eq!(e.expected, usize::MAX);
772        assert_eq!(e.got, 0);
773        assert_eq!(e.input.as_str(), "rgba(1)");
774        assert_eq!(e.clone(), e);
775        assert!(!format!("{e:?}").is_empty());
776
777        let same_but_got_max = WrongComponentCountError {
778            expected: usize::MAX,
779            got: usize::MAX,
780            input: AzString::from("rgba(1)"),
781        };
782        assert_ne!(e, same_but_got_max, "`got` participates in equality");
783
784        // A 0-expected / 0-got degenerate error is still constructible and inert.
785        let zeroed = WrongComponentCountError {
786            expected: 0,
787            got: 0,
788            input: AzString::default(),
789        };
790        assert_eq!(zeroed.expected, zeroed.got);
791        assert!(zeroed.input.as_str().is_empty());
792    }
793}