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
65
    #[must_use] pub fn to_std(&self) -> core::num::ParseFloatError {
47
65
        match self {
48
20
            Self::Empty => "".parse::<f32>().unwrap_err(),
49
45
            Self::Invalid => "x".parse::<f32>().unwrap_err(),
50
        }
51
65
    }
52
}
53

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

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

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

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

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

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

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

            
159
impl InvalidValueErrOwned {
160
110
    #[must_use] pub fn to_shared(&self) -> InvalidValueErr<'_> {
161
110
        InvalidValueErr(self.value.as_str())
162
110
    }
163
}
164

            
165
#[cfg(test)]
166
#[allow(clippy::too_many_lines)]
167
mod 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
            "οΌ‘οΌ’οΌ“",  // 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}", "Ω£", "οΌ‘οΌ’οΌ“", "πŸ˜€", "Β½", ",", "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
}