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

            
6
use crate::corety::AzString;
7

            
8
/// Simple "invalid value" error, used for basic parsing failures
9
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
10
pub struct InvalidValueErr<'a>(pub &'a str);
11

            
12
/// Owned version of `InvalidValueErr` with `AzString`.
13
#[derive(Debug, Clone, PartialEq, Eq)]
14
#[repr(C)]
15
pub 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)]
25
pub enum ParseFloatError {
26
    /// Input string was empty.
27
    Empty,
28
    /// Input string was not a valid float literal.
29
    Invalid,
30
}
31

            
32
impl ParseFloatError {
33
    /// Convert from `core::num::ParseFloatError` by comparing against known error instances.
34
254
    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
254
        let empty_err = "".parse::<f32>().unwrap_err();
38
254
        if *e == empty_err {
39
24
            Self::Empty
40
        } else {
41
230
            Self::Invalid
42
        }
43
254
    }
44

            
45
    /// Reconstruct a `core::num::ParseFloatError` from our C-compatible variant.
46
    #[must_use]
47
65
    pub fn to_std(&self) -> core::num::ParseFloatError {
48
65
        match self {
49
20
            Self::Empty => "".parse::<f32>().unwrap_err(),
50
45
            Self::Invalid => "x".parse::<f32>().unwrap_err(),
51
        }
52
65
    }
53
}
54

            
55
impl core::fmt::Display for ParseFloatError {
56
21
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
57
21
        match self {
58
8
            Self::Empty => write!(f, "cannot parse float from empty string"),
59
13
            Self::Invalid => write!(f, "invalid float literal"),
60
        }
61
21
    }
62
}
63

            
64
impl From<core::num::ParseFloatError> for ParseFloatError {
65
205
    fn from(e: core::num::ParseFloatError) -> Self {
66
205
        Self::from_std(&e)
67
205
    }
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)]
76
pub 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

            
89
impl ParseIntError {
90
    /// Convert from `core::num::ParseIntError` using the stable `kind()` method.
91
121
    const fn from_std(e: &core::num::ParseIntError) -> Self {
92
        use core::num::IntErrorKind;
93
121
        match e.kind() {
94
12
            IntErrorKind::Empty => Self::Empty,
95
31
            IntErrorKind::PosOverflow => Self::PosOverflow,
96
9
            IntErrorKind::NegOverflow => Self::NegOverflow,
97
4
            IntErrorKind::Zero => Self::Zero,
98
65
            _ => Self::InvalidDigit, // future-proofing
99
        }
100
121
    }
101

            
102
    /// Reconstruct a `core::num::ParseIntError` from our C-compatible variant.
103
    #[must_use]
104
36
    pub fn to_std(&self) -> core::num::ParseIntError {
105
36
        match self {
106
7
            Self::Empty => "".parse::<i32>().unwrap_err(),
107
8
            Self::InvalidDigit => "x".parse::<i32>().unwrap_err(),
108
8
            Self::PosOverflow => "99999999999999999999".parse::<i32>().unwrap_err(),
109
7
            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
6
                "x".parse::<i32>().unwrap_err()
114
            }
115
        }
116
36
    }
117
}
118

            
119
impl core::fmt::Display for ParseIntError {
120
32
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
121
32
        match self {
122
6
            Self::Empty => write!(f, "cannot parse integer from empty string"),
123
6
            Self::InvalidDigit => write!(f, "invalid digit found in string"),
124
8
            Self::PosOverflow => write!(f, "number too large to fit in target type"),
125
6
            Self::NegOverflow => write!(f, "number too small to fit in target type"),
126
6
            Self::Zero => write!(f, "number would be zero for non-zero type"),
127
        }
128
32
    }
129
}
130

            
131
impl From<core::num::ParseIntError> for ParseIntError {
132
63
    fn from(e: core::num::ParseIntError) -> Self {
133
63
        Self::from_std(&e)
134
63
    }
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)]
141
pub 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)]
149
pub struct WrongComponentCountError {
150
    pub expected: usize,
151
    pub got: usize,
152
    pub input: AzString,
153
}
154

            
155
impl InvalidValueErr<'_> {
156
    #[must_use]
157
99
    pub fn to_contained(&self) -> InvalidValueErrOwned {
158
99
        InvalidValueErrOwned {
159
99
            value: self.0.to_string().into(),
160
99
        }
161
99
    }
162
}
163

            
164
impl InvalidValueErrOwned {
165
    #[must_use]
166
110
    pub fn to_shared(&self) -> InvalidValueErr<'_> {
167
110
        InvalidValueErr(self.value.as_str())
168
110
    }
169
}
170

            
171
#[cfg(test)]
172
#[allow(clippy::too_many_lines)]
173
mod 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
            "οΌ‘οΌ’οΌ“", // 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
            "οΌ‘οΌ’οΌ“",
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
}