1
//! CSS properties for border radius (`border-top-left-radius`,
2
//! `border-top-right-radius`, `border-bottom-left-radius`,
3
//! `border-bottom-right-radius`) and the `border-radius` shorthand parser.
4

            
5
use alloc::string::{String, ToString};
6
use crate::corety::AzString;
7

            
8
use crate::{
9
    props::{
10
        basic::pixel::{
11
            parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue,
12
        },
13
        macros::PixelValueTaker,
14
    },
15
};
16

            
17
// --- Property Struct Definitions ---
18

            
19
macro_rules! define_border_radius_property {
20
    ($struct_name:ident) => {
21
        #[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22
        #[repr(C)]
23
        pub struct $struct_name {
24
            pub inner: PixelValue,
25
        }
26

            
27
        impl ::core::fmt::Debug for $struct_name {
28
2
            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
29
2
                write!(f, "{}", self.inner)
30
2
            }
31
        }
32

            
33
        impl PixelValueTaker for $struct_name {
34
1
            fn from_pixel_value(inner: PixelValue) -> Self {
35
1
                Self { inner }
36
1
            }
37
        }
38

            
39
        impl_pixel_value!($struct_name);
40
    };
41
}
42

            
43
/// CSS `border-top-left-radius` property value.
44
define_border_radius_property!(StyleBorderTopLeftRadius);
45
/// CSS `border-top-right-radius` property value.
46
define_border_radius_property!(StyleBorderTopRightRadius);
47
/// CSS `border-bottom-left-radius` property value.
48
define_border_radius_property!(StyleBorderBottomLeftRadius);
49
/// CSS `border-bottom-right-radius` property value.
50
define_border_radius_property!(StyleBorderBottomRightRadius);
51

            
52
// --- Parser-only Struct ---
53

            
54
/// A temporary struct used only during the parsing of the `border-radius` shorthand property.
55
#[cfg(feature = "parser")]
56
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57
pub struct StyleBorderRadius {
58
    pub top_left: PixelValue,
59
    pub top_right: PixelValue,
60
    pub bottom_left: PixelValue,
61
    pub bottom_right: PixelValue,
62
}
63

            
64
// --- Error Types ---
65

            
66
/// Error for the shorthand `border-radius` property.
67
#[derive(Clone, PartialEq, Eq)]
68
pub enum CssBorderRadiusParseError<'a> {
69
    /// Too many values were provided (max is 4).
70
    TooManyValues(&'a str),
71
    /// An underlying pixel value could not be parsed.
72
    PixelValue(CssPixelValueParseError<'a>),
73
}
74

            
75
impl_debug_as_display!(CssBorderRadiusParseError<'a>);
76
impl_display! { CssBorderRadiusParseError<'a>, {
77
    TooManyValues(val) => format!("Too many values for border-radius: \"{}\"", val),
78
    PixelValue(e) => format!("{}", e),
79
}}
80
impl_from!(
81
    CssPixelValueParseError<'a>,
82
    CssBorderRadiusParseError::PixelValue
83
);
84

            
85
/// Owned version of `CssBorderRadiusParseError`.
86
#[derive(Debug, Clone, PartialEq, Eq)]
87
#[repr(C, u8)]
88
pub enum CssBorderRadiusParseErrorOwned {
89
    TooManyValues(AzString),
90
    PixelValue(CssPixelValueParseErrorOwned),
91
}
92

            
93
/// Newtype wrapper around `CssBorderRadiusParseErrorOwned` for the `border-radius` shorthand.
94
#[derive(Debug, Clone, PartialEq, Eq)]
95
#[repr(C)]
96
pub struct CssStyleBorderRadiusParseErrorOwned {
97
    pub inner: CssBorderRadiusParseErrorOwned,
98
}
99

            
100
impl From<CssBorderRadiusParseErrorOwned> for CssStyleBorderRadiusParseErrorOwned {
101
1
    fn from(v: CssBorderRadiusParseErrorOwned) -> Self {
102
1
        Self { inner: v }
103
1
    }
104
}
105

            
106
impl CssBorderRadiusParseError<'_> {
107
17
    #[must_use] pub fn to_contained(&self) -> CssBorderRadiusParseErrorOwned {
108
17
        match self {
109
10
            CssBorderRadiusParseError::TooManyValues(s) => {
110
10
                CssBorderRadiusParseErrorOwned::TooManyValues((*s).to_string().into())
111
            }
112
7
            CssBorderRadiusParseError::PixelValue(e) => {
113
7
                CssBorderRadiusParseErrorOwned::PixelValue(e.to_contained())
114
            }
115
        }
116
17
    }
117
}
118

            
119
impl CssBorderRadiusParseErrorOwned {
120
21
    #[must_use] pub fn to_shared(&self) -> CssBorderRadiusParseError<'_> {
121
21
        match self {
122
10
            Self::TooManyValues(s) => {
123
10
                CssBorderRadiusParseError::TooManyValues(s)
124
            }
125
11
            Self::PixelValue(e) => {
126
11
                CssBorderRadiusParseError::PixelValue(e.to_shared())
127
            }
128
        }
129
21
    }
130
}
131

            
132
/// Macro to generate error types for individual radius properties.
133
macro_rules! define_border_radius_parse_error {
134
    ($error_name:ident, $error_name_owned:ident) => {
135
        #[derive(Clone, PartialEq, Eq)]
136
        pub enum $error_name<'a> {
137
            PixelValue(CssPixelValueParseError<'a>),
138
        }
139

            
140
        impl_debug_as_display!($error_name<'a>);
141
        impl_display! { $error_name<'a>, {
142
            PixelValue(e) => format!("{}", e),
143
        }}
144

            
145
        impl_from!(CssPixelValueParseError<'a>, $error_name::PixelValue);
146

            
147
        #[derive(Debug, Clone, PartialEq, Eq)]
148
        #[repr(C, u8)]
149
        pub enum $error_name_owned {
150
            PixelValue(CssPixelValueParseErrorOwned),
151
        }
152

            
153
        impl $error_name<'_> {
154
5
            #[must_use] pub fn to_contained(&self) -> $error_name_owned {
155
5
                match self {
156
5
                    $error_name::PixelValue(e) => $error_name_owned::PixelValue(e.to_contained()),
157
                }
158
5
            }
159
        }
160

            
161
        impl $error_name_owned {
162
4
            #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
163
4
                match self {
164
4
                    $error_name_owned::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
165
                }
166
4
            }
167
        }
168
    };
169
}
170

            
171
define_border_radius_parse_error!(
172
    StyleBorderTopLeftRadiusParseError,
173
    StyleBorderTopLeftRadiusParseErrorOwned
174
);
175
define_border_radius_parse_error!(
176
    StyleBorderTopRightRadiusParseError,
177
    StyleBorderTopRightRadiusParseErrorOwned
178
);
179
define_border_radius_parse_error!(
180
    StyleBorderBottomLeftRadiusParseError,
181
    StyleBorderBottomLeftRadiusParseErrorOwned
182
);
183
define_border_radius_parse_error!(
184
    StyleBorderBottomRightRadiusParseError,
185
    StyleBorderBottomRightRadiusParseErrorOwned
186
);
187

            
188
// --- Parsing Functions ---
189

            
190
/// Parse the CSS `border-radius` shorthand into individual corner values.
191
#[cfg(feature = "parser")]
192
/// # Errors
193
///
194
/// Returns an error if `input` is not a valid CSS `border-radius` value.
195
352
pub fn parse_style_border_radius(
196
352
    input: &str,
197
352
) -> Result<StyleBorderRadius, CssBorderRadiusParseError<'_>> {
198
352
    let components: Vec<_> = input.split_whitespace().collect();
199
352
    let mut values = Vec::with_capacity(components.len());
200
100704
    for comp in &components {
201
100387
        values.push(parse_pixel_value(comp)?);
202
    }
203

            
204
317
    match values.len() {
205
289
        1 => Ok(StyleBorderRadius {
206
289
            top_left: values[0],
207
289
            top_right: values[0],
208
289
            bottom_right: values[0],
209
289
            bottom_left: values[0],
210
289
        }),
211
5
        2 => Ok(StyleBorderRadius {
212
5
            top_left: values[0],
213
5
            top_right: values[1],
214
5
            bottom_right: values[0],
215
5
            bottom_left: values[1],
216
5
        }),
217
2
        3 => Ok(StyleBorderRadius {
218
2
            top_left: values[0],
219
2
            top_right: values[1],
220
2
            bottom_right: values[2],
221
2
            bottom_left: values[1],
222
2
        }),
223
5
        4 => Ok(StyleBorderRadius {
224
5
            top_left: values[0],
225
5
            top_right: values[1],
226
5
            bottom_right: values[2],
227
5
            bottom_left: values[3],
228
5
        }),
229
16
        _ => Err(CssBorderRadiusParseError::TooManyValues(input)),
230
    }
231
352
}
232

            
233
/// Parse the CSS `border-top-left-radius` longhand property.
234
#[cfg(feature = "parser")]
235
/// # Errors
236
///
237
/// Returns an error if `input` is not a valid CSS `border-top-left-radius` value.
238
125
pub fn parse_style_border_top_left_radius(
239
125
    input: &str,
240
125
) -> Result<StyleBorderTopLeftRadius, StyleBorderTopLeftRadiusParseError<'_>> {
241
125
    let pixel_value = parse_pixel_value(input)?;
242
99
    Ok(StyleBorderTopLeftRadius { inner: pixel_value })
243
125
}
244

            
245
/// Parse the CSS `border-top-right-radius` longhand property.
246
#[cfg(feature = "parser")]
247
/// # Errors
248
///
249
/// Returns an error if `input` is not a valid CSS `border-top-right-radius` value.
250
123
pub fn parse_style_border_top_right_radius(
251
123
    input: &str,
252
123
) -> Result<StyleBorderTopRightRadius, StyleBorderTopRightRadiusParseError<'_>> {
253
123
    let pixel_value = parse_pixel_value(input)?;
254
97
    Ok(StyleBorderTopRightRadius { inner: pixel_value })
255
123
}
256

            
257
/// Parse the CSS `border-bottom-left-radius` longhand property.
258
#[cfg(feature = "parser")]
259
/// # Errors
260
///
261
/// Returns an error if `input` is not a valid CSS `border-bottom-left-radius` value.
262
123
pub fn parse_style_border_bottom_left_radius(
263
123
    input: &str,
264
123
) -> Result<StyleBorderBottomLeftRadius, StyleBorderBottomLeftRadiusParseError<'_>> {
265
123
    let pixel_value = parse_pixel_value(input)?;
266
97
    Ok(StyleBorderBottomLeftRadius { inner: pixel_value })
267
123
}
268

            
269
/// Parse the CSS `border-bottom-right-radius` longhand property.
270
#[cfg(feature = "parser")]
271
/// # Errors
272
///
273
/// Returns an error if `input` is not a valid CSS `border-bottom-right-radius` value.
274
123
pub fn parse_style_border_bottom_right_radius(
275
123
    input: &str,
276
123
) -> Result<StyleBorderBottomRightRadius, StyleBorderBottomRightRadiusParseError<'_>> {
277
123
    let pixel_value = parse_pixel_value(input)?;
278
97
    Ok(StyleBorderBottomRightRadius { inner: pixel_value })
279
123
}
280

            
281
#[cfg(all(test, feature = "parser"))]
282
mod tests {
283
    use super::*;
284

            
285
    #[test]
286
1
    fn test_parse_border_radius_shorthand() {
287
        // One value
288
1
        let result = parse_style_border_radius("10px").unwrap();
289
1
        assert_eq!(result.top_left, PixelValue::px(10.0));
290
1
        assert_eq!(result.top_right, PixelValue::px(10.0));
291
1
        assert_eq!(result.bottom_right, PixelValue::px(10.0));
292
1
        assert_eq!(result.bottom_left, PixelValue::px(10.0));
293

            
294
        // Two values
295
1
        let result = parse_style_border_radius("10px 5%").unwrap();
296
1
        assert_eq!(result.top_left, PixelValue::px(10.0));
297
1
        assert_eq!(result.top_right, PixelValue::percent(5.0));
298
1
        assert_eq!(result.bottom_right, PixelValue::px(10.0));
299
1
        assert_eq!(result.bottom_left, PixelValue::percent(5.0));
300

            
301
        // Three values
302
1
        let result = parse_style_border_radius("2px 4px 8px").unwrap();
303
1
        assert_eq!(result.top_left, PixelValue::px(2.0));
304
1
        assert_eq!(result.top_right, PixelValue::px(4.0));
305
1
        assert_eq!(result.bottom_right, PixelValue::px(8.0));
306
1
        assert_eq!(result.bottom_left, PixelValue::px(4.0));
307

            
308
        // Four values
309
1
        let result = parse_style_border_radius("1px 0 3px 4px").unwrap();
310
1
        assert_eq!(result.top_left, PixelValue::px(1.0));
311
1
        assert_eq!(result.top_right, PixelValue::px(0.0));
312
1
        assert_eq!(result.bottom_right, PixelValue::px(3.0));
313
1
        assert_eq!(result.bottom_left, PixelValue::px(4.0));
314

            
315
        // Weird whitespace
316
1
        let result = parse_style_border_radius("  1em   2em  ").unwrap();
317
1
        assert_eq!(result.top_left, PixelValue::em(1.0));
318
1
        assert_eq!(result.top_right, PixelValue::em(2.0));
319
1
    }
320

            
321
    #[test]
322
1
    fn test_parse_border_radius_shorthand_errors() {
323
1
        assert!(parse_style_border_radius("").is_err());
324
1
        assert!(parse_style_border_radius("1px 2px 3px 4px 5px").is_err());
325
1
        assert!(parse_style_border_radius("1px bad 3px").is_err());
326
1
    }
327

            
328
    #[test]
329
1
    fn test_parse_longhand_radius() {
330
1
        let result = parse_style_border_top_left_radius("25%").unwrap();
331
1
        assert_eq!(result.inner, PixelValue::percent(25.0));
332
1
    }
333
}
334

            
335
#[cfg(all(test, feature = "parser"))]
336
#[allow(
337
    clippy::float_cmp,
338
    clippy::unreadable_literal,
339
    clippy::too_many_lines,
340
    clippy::cast_precision_loss
341
)]
342
mod autotest_generated {
343
    use super::*;
344
    use crate::{css::PrintAsCssValue, props::basic::length::SizeMetric};
345

            
346
    /// Every metric that `parse_pixel_value` has a suffix for. `Vmin` is
347
    /// deliberately absent — see `vmin_radius_should_parse_but_is_shadowed_by_in`.
348
    const ROUNDTRIPPABLE_METRICS: [SizeMetric; 11] = [
349
        SizeMetric::Px,
350
        SizeMetric::Pt,
351
        SizeMetric::Em,
352
        SizeMetric::Rem,
353
        SizeMetric::In,
354
        SizeMetric::Cm,
355
        SizeMetric::Mm,
356
        SizeMetric::Percent,
357
        SizeMetric::Vw,
358
        SizeMetric::Vh,
359
        SizeMetric::Vmax,
360
    ];
361

            
362
    /// Values that survive the 1/1000 fixed-point quantization of `FloatValue`
363
    /// exactly, so a failed round-trip means a real parser/printer bug and not
364
    /// a rounding artifact.
365
    const EXACT_VALUES: [f32; 6] = [0.0, 1.0, 12.5, -3.25, 0.125, 1000.5];
366

            
367
    /// Inputs that are not valid CSS lengths in any position.
368
    const GARBAGE: [&str; 14] = [
369
        "bad",
370
        "px",
371
        "%",
372
        "em",
373
        "-",
374
        "+",
375
        ".",
376
        "e",
377
        "1..2px",
378
        "1,2px",
379
        "10px;",
380
        "10px!important",
381
        "\0",
382
        "\u{7f}\u{1}",
383
    ];
384

            
385
    fn all_longhands(input: &str) -> [Result<PixelValue, ()>; 4] {
386
        [
387
            parse_style_border_top_left_radius(input)
388
                .map(|v| v.inner)
389
                .map_err(|_| ()),
390
            parse_style_border_top_right_radius(input)
391
                .map(|v| v.inner)
392
                .map_err(|_| ()),
393
            parse_style_border_bottom_left_radius(input)
394
                .map(|v| v.inner)
395
                .map_err(|_| ()),
396
            parse_style_border_bottom_right_radius(input)
397
                .map(|v| v.inner)
398
                .map_err(|_| ()),
399
        ]
400
    }
401

            
402
    // ================================================= shorthand: malformed ===
403

            
404
    #[test]
405
    fn shorthand_rejects_empty_and_whitespace_only() {
406
        // `split_whitespace` yields zero components, which falls into the `_`
407
        // arm — so a *missing* value is reported as TooManyValues. That variant
408
        // is a misnomer for this input (see report), but it is still an Err.
409
        for input in ["", " ", "   ", "\t\n", "\r\n\t ", "\u{a0}"] {
410
            let err = parse_style_border_radius(input)
411
                .expect_err("whitespace-only border-radius must not parse");
412
            assert!(
413
                matches!(err, CssBorderRadiusParseError::TooManyValues(_)),
414
                "unexpected error for {input:?}: {err}"
415
            );
416
        }
417
    }
418

            
419
    #[test]
420
    fn shorthand_rejects_garbage_without_panicking() {
421
        for input in GARBAGE {
422
            assert!(
423
                parse_style_border_radius(input).is_err(),
424
                "garbage {input:?} was accepted"
425
            );
426
        }
427
    }
428

            
429
    #[test]
430
    fn shorthand_rejects_more_than_four_values() {
431
        let input = "1px 2px 3px 4px 5px";
432
        let err = parse_style_border_radius(input).unwrap_err();
433
        assert_eq!(err, CssBorderRadiusParseError::TooManyValues(input));
434
        // The error carries the *whole* input back, not just the extra value.
435
        assert!(format!("{err}").contains(input));
436
    }
437

            
438
    #[test]
439
    fn shorthand_reports_the_bad_component_before_counting_values() {
440
        // Values are parsed eagerly, so a malformed component wins over the
441
        // arity check even when the arity is also wrong.
442
        let err = parse_style_border_radius("1px 2px 3px 4px 5px bad").unwrap_err();
443
        assert_eq!(
444
            err,
445
            CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::InvalidPixelValue("bad"))
446
        );
447
    }
448

            
449
    #[test]
450
    fn shorthand_rejects_elliptical_slash_syntax() {
451
        // `border-radius: 10px / 20px` is valid CSS but unsupported here; the
452
        // important part is that it is *rejected* rather than silently
453
        // mis-parsed into the wrong corners.
454
        assert!(parse_style_border_radius("10px / 20px").is_err());
455
        assert!(parse_style_border_radius("10px/20px").is_err());
456
        assert!(parse_style_border_radius("1px 2px / 3px 4px").is_err());
457
    }
458

            
459
    #[test]
460
    fn shorthand_rejects_split_number_and_unit() {
461
        // "10 px" is two components: "10" (a bare number => px) and "px"
462
        // (a unit with no value) — the latter fails.
463
        let err = parse_style_border_radius("10 px").unwrap_err();
464
        assert_eq!(
465
            err,
466
            CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::NoValueGiven(
467
                "px",
468
                SizeMetric::Px
469
            ))
470
        );
471
    }
472

            
473
    // ================================================== shorthand: numerics ===
474

            
475
    #[test]
476
    fn shorthand_boundary_numbers_never_produce_nan_or_infinity() {
477
        // Whatever these do, the stored FloatValue must stay finite: it is an
478
        // isize under the hood and a NaN/inf leak would poison layout.
479
        let inputs = [
480
            "0",
481
            "-0",
482
            "+0",
483
            "0px",
484
            "-0px",
485
            "0.0001px",
486
            "1e-30px",
487
            "1e30px",
488
            "-1e30px",
489
            "3.4028235e38px",
490
            "-3.4028235e38px",
491
            "9223372036854775807px",
492
            "-9223372036854775808px",
493
            "340282350000000000000000000000000000000%",
494
        ];
495
        for input in inputs {
496
            let Ok(radius) = parse_style_border_radius(input) else {
497
                continue; // rejection is an equally safe outcome
498
            };
499
            for corner in [
500
                radius.top_left,
501
                radius.top_right,
502
                radius.bottom_left,
503
                radius.bottom_right,
504
            ] {
505
                let n = corner.number.get();
506
                assert!(n.is_finite(), "{input:?} produced non-finite {n}");
507
            }
508
        }
509
    }
510

            
511
    #[test]
512
    fn shorthand_nan_input_is_flattened_to_zero() {
513
        // f32::from_str accepts "NaN"; FloatValue::new then casts NaN*1000 to
514
        // isize, and `as` maps NaN to 0. So `border-radius: NaNpx` is silently
515
        // accepted as 0px rather than rejected — assert it at least cannot
516
        // smuggle a NaN into the layout engine.
517
        for input in ["NaN", "nan", "NaNpx", "-nan%"] {
518
            let radius = parse_style_border_radius(input)
519
                .unwrap_or_else(|e| panic!("{input:?} unexpectedly rejected: {e}"));
520
            let n = radius.top_left.number.get();
521
            assert!(!n.is_nan(), "{input:?} leaked a NaN");
522
            assert_eq!(n, 0.0, "{input:?} should quantize to 0");
523
        }
524
    }
525

            
526
    #[test]
527
    fn shorthand_infinite_input_saturates_instead_of_overflowing() {
528
        let pos = parse_style_border_radius("infpx").unwrap().top_left;
529
        let neg = parse_style_border_radius("-infpx").unwrap().top_left;
530
        assert!(pos.number.get().is_finite());
531
        assert!(neg.number.get().is_finite());
532
        assert!(pos.number.get() > 0.0);
533
        assert!(neg.number.get() < 0.0);
534
        // Saturation, not wraparound: +inf must not come back out negative.
535
        assert_eq!(pos.number.number(), isize::MAX);
536
        assert_eq!(neg.number.number(), isize::MIN);
537
    }
538

            
539
    #[test]
540
    fn shorthand_sub_quantum_values_truncate_to_zero() {
541
        // FloatValue keeps 1/1000 of a unit; anything smaller becomes 0.
542
        // A 0.0001px radius is therefore indistinguishable from no radius.
543
        let radius = parse_style_border_radius("0.0001px").unwrap();
544
        assert_eq!(radius.top_left, PixelValue::px(0.0));
545
        assert_eq!(radius.top_left.number.number(), 0);
546
    }
547

            
548
    #[test]
549
    fn shorthand_negative_zero_equals_positive_zero() {
550
        let neg = parse_style_border_radius("-0px").unwrap();
551
        let pos = parse_style_border_radius("0px").unwrap();
552
        assert_eq!(neg, pos);
553
        assert_eq!(neg.top_left.number.number(), 0);
554
    }
555

            
556
    // ==================================== shorthand: long / unicode / nested ===
557

            
558
    #[test]
559
    fn shorthand_extremely_long_input_terminates() {
560
        // 100k components: must reject on arity, not hang or blow the stack.
561
        let many = "1px ".repeat(100_000);
562
        assert!(matches!(
563
            parse_style_border_radius(&many),
564
            Err(CssBorderRadiusParseError::TooManyValues(_))
565
        ));
566

            
567
        // A single 100k-digit number: f32 parsing overflows to inf, which the
568
        // fixed-point cast saturates.
569
        let huge = format!("{}px", "1".repeat(100_000));
570
        let radius = parse_style_border_radius(&huge).unwrap();
571
        assert!(radius.top_left.number.get().is_finite());
572

            
573
        // 100k chars of pure junk in one token.
574
        let junk = "z".repeat(100_000);
575
        assert!(parse_style_border_radius(&junk).is_err());
576
    }
577

            
578
    #[test]
579
    fn shorthand_deeply_nested_brackets_do_not_stack_overflow() {
580
        let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
581
        assert!(parse_style_border_radius(&nested).is_err());
582

            
583
        let calls = format!("{}1px{}", "calc(".repeat(10_000), ")".repeat(10_000));
584
        assert!(parse_style_border_radius(&calls).is_err());
585
    }
586

            
587
    #[test]
588
    fn shorthand_unicode_input_is_rejected_without_panicking() {
589
        // Multibyte input must not be byte-sliced anywhere in the parser.
590
        for input in [
591
            "\u{1F600}",
592
            "10px\u{1F600}",
593
            "\u{1F600}px",
594
            "\u{661}\u{660}px", // arabic-indic digits
595
            "10\u{301}px",      // combining acute
596
            "10px\u{200b}",     // zero-width space (not a separator)
597
            "10px",           // fullwidth digits
598
            "10\u{202e}px",     // right-to-left override
599
        ] {
600
            assert!(
601
                parse_style_border_radius(input).is_err(),
602
                "unicode {input:?} was accepted"
603
            );
604
        }
605
    }
606

            
607
    #[test]
608
    fn shorthand_treats_non_breaking_space_as_a_value_separator() {
609
        // `str::split_whitespace` uses the Unicode White_Space property, so
610
        // U+00A0 separates values here even though CSS tokenization does not
611
        // treat it as whitespace (browsers reject this). Pinning current
612
        // behaviour — see report.
613
        let radius = parse_style_border_radius("1px\u{a0}2px").unwrap();
614
        assert_eq!(radius.top_left, PixelValue::px(1.0));
615
        assert_eq!(radius.top_right, PixelValue::px(2.0));
616
    }
617

            
618
    // ==================================== shorthand: corner-expansion invariants
619

            
620
    #[test]
621
    fn shorthand_one_value_fills_every_corner() {
622
        let radius = parse_style_border_radius("7px").unwrap();
623
        assert_eq!(radius.top_left, PixelValue::px(7.0));
624
        assert_eq!(radius.top_right, PixelValue::px(7.0));
625
        assert_eq!(radius.bottom_left, PixelValue::px(7.0));
626
        assert_eq!(radius.bottom_right, PixelValue::px(7.0));
627
    }
628

            
629
    #[test]
630
    fn shorthand_two_and_three_values_expand_along_the_diagonals() {
631
        // CSS Backgrounds 3 §5.1: 2 values => TL/BR = 1st, TR/BL = 2nd.
632
        let two = parse_style_border_radius("1px 2px").unwrap();
633
        assert_eq!(two.top_left, two.bottom_right);
634
        assert_eq!(two.top_right, two.bottom_left);
635
        assert_eq!(two.top_left, PixelValue::px(1.0));
636
        assert_eq!(two.top_right, PixelValue::px(2.0));
637

            
638
        // 3 values => TL = 1st, TR/BL = 2nd, BR = 3rd.
639
        let three = parse_style_border_radius("1px 2px 3px").unwrap();
640
        assert_eq!(three.top_left, PixelValue::px(1.0));
641
        assert_eq!(three.top_right, PixelValue::px(2.0));
642
        assert_eq!(three.bottom_left, PixelValue::px(2.0));
643
        assert_eq!(three.bottom_right, PixelValue::px(3.0));
644
    }
645

            
646
    #[test]
647
    fn shorthand_four_values_map_clockwise_from_top_left() {
648
        let four = parse_style_border_radius("1px 2px 3px 4px").unwrap();
649
        assert_eq!(four.top_left, PixelValue::px(1.0));
650
        assert_eq!(four.top_right, PixelValue::px(2.0));
651
        assert_eq!(four.bottom_right, PixelValue::px(3.0));
652
        assert_eq!(four.bottom_left, PixelValue::px(4.0));
653
        // No two corners aliased: a copy-paste slip in the match arm would
654
        // duplicate one of these.
655
        assert_ne!(four.top_left, four.top_right);
656
        assert_ne!(four.bottom_right, four.bottom_left);
657
    }
658

            
659
    #[test]
660
    fn shorthand_preserves_per_corner_units() {
661
        let radius = parse_style_border_radius("1px 2em 3% 4rem").unwrap();
662
        assert_eq!(radius.top_left, PixelValue::px(1.0));
663
        assert_eq!(radius.top_right, PixelValue::em(2.0));
664
        assert_eq!(radius.bottom_right, PixelValue::percent(3.0));
665
        assert_eq!(radius.bottom_left, PixelValue::rem(4.0));
666
    }
667

            
668
    #[test]
669
    fn shorthand_valid_minimal_input() {
670
        let radius = parse_style_border_radius("0").unwrap();
671
        assert_eq!(radius.top_left, PixelValue::px(0.0));
672
    }
673

            
674
    #[test]
675
    fn shorthand_tolerates_arbitrary_ascii_whitespace_runs() {
676
        let radius = parse_style_border_radius("\n\t 1px \t 2px\r\n").unwrap();
677
        assert_eq!(radius.top_left, PixelValue::px(1.0));
678
        assert_eq!(radius.top_right, PixelValue::px(2.0));
679
    }
680

            
681
    // ====================================================== longhand parsers ===
682

            
683
    #[test]
684
    fn longhands_reject_empty_whitespace_and_garbage() {
685
        for input in ["", " ", "\t\n"] {
686
            for result in all_longhands(input) {
687
                assert_eq!(result, Err(()), "{input:?} was accepted by a longhand");
688
            }
689
        }
690
        for input in GARBAGE {
691
            for result in all_longhands(input) {
692
                assert_eq!(result, Err(()), "{input:?} was accepted by a longhand");
693
            }
694
        }
695
    }
696

            
697
    #[test]
698
    fn longhands_reject_multiple_values() {
699
        // A longhand takes exactly one length; the shorthand list must not leak
700
        // through (e.g. by silently using the first value).
701
        for input in ["1px 2px", "1px 2px 3px 4px", "1px,2px"] {
702
            for result in all_longhands(input) {
703
                assert_eq!(result, Err(()), "{input:?} was accepted by a longhand");
704
            }
705
        }
706
    }
707

            
708
    #[test]
709
    fn all_four_longhands_agree_on_every_input() {
710
        // The four parsers are macro-free copies of each other; a copy-paste
711
        // bug (wrong metric, wrong field) would show up as a disagreement.
712
        let inputs = [
713
            "0", "10px", "-3.25em", "50%", "1.5rem", "2pt", "1in", "2.54cm", "10mm", "5vw", "5vh",
714
            "5vmax", "bad", "", "   ", "\u{1F600}", "1e30px", "NaNpx",
715
        ];
716
        for input in inputs {
717
            let [tl, tr, bl, br] = all_longhands(input);
718
            assert_eq!(tl, tr, "top-left vs top-right disagree on {input:?}");
719
            assert_eq!(tl, bl, "top-left vs bottom-left disagree on {input:?}");
720
            assert_eq!(tl, br, "top-left vs bottom-right disagree on {input:?}");
721
        }
722
    }
723

            
724
    #[test]
725
    fn longhands_agree_with_the_shorthand_on_single_values() {
726
        for input in ["0", "10px", "-3.25em", "50%", "1.5rem", "2pt"] {
727
            let shorthand = parse_style_border_radius(input).unwrap();
728
            let [tl, tr, bl, br] = all_longhands(input);
729
            assert_eq!(tl, Ok(shorthand.top_left), "{input:?}");
730
            assert_eq!(tr, Ok(shorthand.top_right), "{input:?}");
731
            assert_eq!(bl, Ok(shorthand.bottom_left), "{input:?}");
732
            assert_eq!(br, Ok(shorthand.bottom_right), "{input:?}");
733
        }
734
    }
735

            
736
    #[test]
737
    fn longhands_accept_a_gap_between_number_and_unit_but_the_shorthand_does_not() {
738
        // `parse_pixel_value` trims the value before parsing, so "10 px" is a
739
        // valid longhand — while the shorthand splits it into two components
740
        // and fails. Divergent, but deterministic; pinning both sides.
741
        for result in all_longhands("10 px") {
742
            assert_eq!(result, Ok(PixelValue::px(10.0)));
743
        }
744
        assert!(parse_style_border_radius("10 px").is_err());
745
    }
746

            
747
    #[test]
748
    fn longhands_survive_extremely_long_and_nested_input() {
749
        let long = format!("{}px", "9".repeat(100_000));
750
        for result in all_longhands(&long) {
751
            assert!(result.unwrap().number.get().is_finite());
752
        }
753
        let nested = format!("{}1px{}", "(".repeat(10_000), ")".repeat(10_000));
754
        for result in all_longhands(&nested) {
755
            assert_eq!(result, Err(()));
756
        }
757
    }
758

            
759
    #[test]
760
    fn longhands_boundary_numbers_stay_finite() {
761
        for input in [
762
            "0",
763
            "-0",
764
            "1e-30px",
765
            "1e30px",
766
            "-1e30px",
767
            "9223372036854775807px",
768
            "NaNpx",
769
            "infpx",
770
            "-infpx",
771
        ] {
772
            for result in all_longhands(input) {
773
                let value = result.unwrap_or_else(|()| panic!("{input:?} rejected"));
774
                let n = value.number.get();
775
                assert!(n.is_finite() && !n.is_nan(), "{input:?} produced {n}");
776
            }
777
        }
778
    }
779

            
780
    // =========================================================== round-trips ===
781

            
782
    #[test]
783
    fn print_as_css_value_round_trips_through_every_longhand_parser() {
784
        for metric in ROUNDTRIPPABLE_METRICS {
785
            for value in EXACT_VALUES {
786
                let pixel = PixelValue::from_metric(metric, value);
787

            
788
                let tl = StyleBorderTopLeftRadius { inner: pixel };
789
                let reparsed = parse_style_border_top_left_radius(&tl.print_as_css_value())
790
                    .unwrap_or_else(|e| panic!("{:?} did not re-parse: {e}", tl.print_as_css_value()));
791
                assert_eq!(reparsed, tl);
792

            
793
                let tr = StyleBorderTopRightRadius { inner: pixel };
794
                assert_eq!(
795
                    parse_style_border_top_right_radius(&tr.print_as_css_value()).unwrap(),
796
                    tr
797
                );
798

            
799
                let bl = StyleBorderBottomLeftRadius { inner: pixel };
800
                assert_eq!(
801
                    parse_style_border_bottom_left_radius(&bl.print_as_css_value()).unwrap(),
802
                    bl
803
                );
804

            
805
                let br = StyleBorderBottomRightRadius { inner: pixel };
806
                assert_eq!(
807
                    parse_style_border_bottom_right_radius(&br.print_as_css_value()).unwrap(),
808
                    br
809
                );
810
            }
811
        }
812
    }
813

            
814
    #[test]
815
    fn shorthand_round_trips_through_the_printed_corner_values() {
816
        let original = parse_style_border_radius("1px 2em 3% 4rem").unwrap();
817
        let printed = format!(
818
            "{} {} {} {}",
819
            StyleBorderTopLeftRadius {
820
                inner: original.top_left
821
            }
822
            .print_as_css_value(),
823
            StyleBorderTopRightRadius {
824
                inner: original.top_right
825
            }
826
            .print_as_css_value(),
827
            StyleBorderBottomRightRadius {
828
                inner: original.bottom_right
829
            }
830
            .print_as_css_value(),
831
            StyleBorderBottomLeftRadius {
832
                inner: original.bottom_left
833
            }
834
            .print_as_css_value(),
835
        );
836
        assert_eq!(parse_style_border_radius(&printed).unwrap(), original);
837
    }
838

            
839
    #[test]
840
    fn debug_output_matches_the_printed_css_value() {
841
        // The Debug impl is hand-written to delegate to `inner`; if it ever
842
        // reverts to the derived one, printed CSS and logs would diverge.
843
        let pixel = PixelValue::em(1.5);
844
        let tl = StyleBorderTopLeftRadius { inner: pixel };
845
        assert_eq!(format!("{tl:?}"), "1.5em");
846
        assert_eq!(format!("{tl:?}"), tl.print_as_css_value());
847
    }
848

            
849
    #[test]
850
    fn vmin_radius_parses() {
851
        // `border-radius: 3vmin` is valid CSS. parse_pixel_value used to check the
852
        // "in" suffix before "vmin", so "3vmin" stripped to "3vm" and failed to
853
        // parse as f32. The suffix table now orders "vmin"/"vmax" before "in"
854
        // (css/src/props/basic/pixel.rs), so this round-trips like every other metric.
855
        let printed = StyleBorderTopLeftRadius {
856
            inner: PixelValue::from_metric(SizeMetric::Vmin, 3.0),
857
        }
858
        .print_as_css_value();
859
        assert_eq!(printed, "3vmin");
860
        assert_eq!(
861
            parse_style_border_top_left_radius(&printed).unwrap().inner,
862
            PixelValue::from_metric(SizeMetric::Vmin, 3.0)
863
        );
864
    }
865

            
866
    // ================================================ constructors / getters ===
867

            
868
    #[test]
869
    fn default_and_zero_agree_for_every_corner_type() {
870
        assert_eq!(
871
            StyleBorderTopLeftRadius::default(),
872
            StyleBorderTopLeftRadius::zero()
873
        );
874
        assert_eq!(
875
            StyleBorderTopRightRadius::default(),
876
            StyleBorderTopRightRadius::zero()
877
        );
878
        assert_eq!(
879
            StyleBorderBottomLeftRadius::default(),
880
            StyleBorderBottomLeftRadius::zero()
881
        );
882
        assert_eq!(
883
            StyleBorderBottomRightRadius::default(),
884
            StyleBorderBottomRightRadius::zero()
885
        );
886
        assert_eq!(
887
            StyleBorderTopLeftRadius::default().inner,
888
            PixelValue::px(0.0)
889
        );
890
    }
891

            
892
    #[test]
893
    fn const_constructors_agree_with_the_float_constructors() {
894
        assert_eq!(
895
            StyleBorderTopLeftRadius::const_px(10),
896
            StyleBorderTopLeftRadius::px(10.0)
897
        );
898
        assert_eq!(
899
            StyleBorderTopLeftRadius::const_em(2),
900
            StyleBorderTopLeftRadius::em(2.0)
901
        );
902
        assert_eq!(
903
            StyleBorderTopLeftRadius::const_percent(50),
904
            StyleBorderTopLeftRadius::percent(50.0)
905
        );
906
        assert_eq!(
907
            StyleBorderTopLeftRadius::const_pt(-3),
908
            StyleBorderTopLeftRadius::pt(-3.0)
909
        );
910
        assert_eq!(
911
            StyleBorderTopLeftRadius::from_pixel_value(PixelValue::px(4.0)),
912
            StyleBorderTopLeftRadius::px(4.0)
913
        );
914
    }
915

            
916
    #[test]
917
    fn interpolate_returns_the_endpoints_for_matching_metrics() {
918
        let a = StyleBorderTopLeftRadius::px(0.0);
919
        let b = StyleBorderTopLeftRadius::px(10.0);
920
        assert_eq!(a.interpolate(&b, 0.0), a);
921
        assert_eq!(a.interpolate(&b, 1.0), b);
922
        assert_eq!(a.interpolate(&b, 0.5), StyleBorderTopLeftRadius::px(5.0));
923
    }
924

            
925
    #[test]
926
    fn interpolate_stays_finite_for_hostile_t_values() {
927
        let a = StyleBorderTopLeftRadius::px(0.0);
928
        let b = StyleBorderTopLeftRadius::px(10.0);
929
        for t in [
930
            -1.0,
931
            2.0,
932
            1e30,
933
            -1e30,
934
            f32::INFINITY,
935
            f32::NEG_INFINITY,
936
            f32::NAN,
937
        ] {
938
            let n = a.interpolate(&b, t).inner.number.get();
939
            assert!(n.is_finite(), "t={t} produced {n}");
940
            assert!(!n.is_nan(), "t={t} produced NaN");
941
        }
942
        // NaN t collapses to 0 rather than poisoning the value.
943
        assert_eq!(
944
            a.interpolate(&b, f32::NAN).inner.number.get(),
945
            0.0
946
        );
947
    }
948

            
949
    #[test]
950
    fn interpolate_across_metrics_converts_to_px() {
951
        // Mixed metrics fall back to px using the default 16px font size,
952
        // so 1em (=16px) -> 10px at t=0.5 is 13px.
953
        let em = StyleBorderTopLeftRadius::em(1.0);
954
        let px = StyleBorderTopLeftRadius::px(10.0);
955
        let mid = em.interpolate(&px, 0.5);
956
        assert_eq!(mid.inner.metric, SizeMetric::Px);
957
        assert_eq!(mid.inner.number.get(), 13.0);
958
        // Note: unlike the same-metric case, t=0 does NOT return `em` itself.
959
        assert_eq!(em.interpolate(&px, 0.0), StyleBorderTopLeftRadius::px(16.0));
960
    }
961

            
962
    // ========================================================= error getters ===
963

            
964
    #[test]
965
    fn shorthand_error_to_contained_round_trips_for_every_variant() {
966
        let float_err = "x".parse::<f32>().unwrap_err();
967
        let variants = [
968
            CssBorderRadiusParseError::TooManyValues("1px 2px 3px 4px 5px"),
969
            CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::EmptyString),
970
            CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::NoValueGiven(
971
                "px",
972
                SizeMetric::Px,
973
            )),
974
            CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::ValueParseErr(
975
                float_err, "bad",
976
            )),
977
            CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::InvalidPixelValue(
978
                "nope",
979
            )),
980
        ];
981
        for variant in variants {
982
            let owned = variant.to_contained();
983
            assert_eq!(owned.to_shared(), variant);
984
            // Display must survive the round-trip too.
985
            assert_eq!(format!("{}", owned.to_shared()), format!("{variant}"));
986
        }
987
    }
988

            
989
    #[test]
990
    fn shorthand_error_to_contained_handles_empty_unicode_and_huge_inputs() {
991
        let huge = "1px ".repeat(50_000);
992
        for input in ["", " ", "\u{1F600}\u{301}", "\0", huge.as_str()] {
993
            let err = CssBorderRadiusParseError::TooManyValues(input);
994
            let owned = err.to_contained();
995
            assert_eq!(owned.to_shared(), err);
996
            match owned {
997
                CssBorderRadiusParseErrorOwned::TooManyValues(s) => {
998
                    assert_eq!(s.as_str(), input);
999
                }
                CssBorderRadiusParseErrorOwned::PixelValue(_) => panic!("wrong variant"),
            }
        }
    }
    #[test]
    fn shorthand_errors_from_the_parser_round_trip() {
        // Same as above, but with errors the parser actually produces rather
        // than hand-built ones.
        for input in ["", "1px 2px 3px 4px 5px", "1px bad 3px", "px", "\u{1F600}"] {
            let err = parse_style_border_radius(input).unwrap_err();
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err);
        }
    }
    #[test]
    fn error_debug_delegates_to_display() {
        let err = CssBorderRadiusParseError::TooManyValues("a b c d e");
        assert_eq!(format!("{err:?}"), format!("{err}"));
        assert!(format!("{err}").contains("a b c d e"));
        let inner = CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::EmptyString);
        assert_eq!(
            format!("{inner}"),
            format!("{}", CssPixelValueParseError::EmptyString)
        );
    }
    #[test]
    fn longhand_error_types_round_trip() {
        // Each of the four longhand error enums has its own generated
        // to_contained/to_shared pair.
        let tl = parse_style_border_top_left_radius("bad").unwrap_err();
        assert_eq!(tl.to_contained().to_shared(), tl);
        let tr = parse_style_border_top_right_radius("").unwrap_err();
        assert_eq!(tr.to_contained().to_shared(), tr);
        let bl = parse_style_border_bottom_left_radius("px").unwrap_err();
        assert_eq!(bl.to_contained().to_shared(), bl);
        let br = parse_style_border_bottom_right_radius("\u{1F600}").unwrap_err();
        assert_eq!(br.to_contained().to_shared(), br);
        // ... and each keeps the underlying pixel error intact.
        assert_eq!(
            tl.to_contained(),
            StyleBorderTopLeftRadiusParseErrorOwned::PixelValue(
                CssPixelValueParseError::InvalidPixelValue("bad").to_contained()
            )
        );
    }
    #[test]
    fn owned_error_newtype_wrapper_preserves_the_inner_error() {
        let owned = CssBorderRadiusParseError::TooManyValues("1 2 3 4 5").to_contained();
        let wrapped = CssStyleBorderRadiusParseErrorOwned::from(owned.clone());
        assert_eq!(wrapped.inner, owned);
        assert_eq!(wrapped.inner.to_shared().to_contained(), owned);
    }
}