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