1use alloc::string::{String, ToString};
4use crate::corety::AzString;
5
6use crate::props::formatter::PrintAsCssValue;
7
8#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[repr(C)]
11#[derive(Default)]
12pub struct CssDuration {
13 pub inner: u32,
15}
16
17
18impl PrintAsCssValue for CssDuration {
19 fn print_as_css_value(&self) -> String {
20 format!("{}ms", self.inner)
21 }
22}
23
24impl crate::codegen::format::FormatAsRustCode for CssDuration {
25 fn format_as_rust_code(&self, _tabs: usize) -> String {
26 format!("CssDuration {{ inner: {} }}", self.inner)
27 }
28}
29
30#[cfg(feature = "parser")]
32#[derive(Clone, PartialEq, Eq)]
33pub enum DurationParseError<'a> {
34 InvalidValue(&'a str),
35 ParseFloat(core::num::ParseFloatError),
36}
37
38#[cfg(feature = "parser")]
39impl_debug_as_display!(DurationParseError<'a>);
40#[cfg(feature = "parser")]
41impl_display! { DurationParseError<'a>, {
42 InvalidValue(v) => format!("Invalid time value: \"{}\"", v),
43 ParseFloat(e) => format!("Invalid number for time value: {}", e),
44}}
45
46#[cfg(feature = "parser")]
48#[derive(Debug, Clone, PartialEq, Eq)]
49#[repr(C, u8)]
50pub enum DurationParseErrorOwned {
51 InvalidValue(AzString),
52 ParseFloat(AzString),
53}
54
55#[cfg(feature = "parser")]
56impl DurationParseError<'_> {
57 #[must_use] pub fn to_contained(&self) -> DurationParseErrorOwned {
58 match self {
59 Self::InvalidValue(s) => DurationParseErrorOwned::InvalidValue((*s).to_string().into()),
60 Self::ParseFloat(e) => DurationParseErrorOwned::ParseFloat(e.to_string().into()),
61 }
62 }
63}
64
65#[cfg(feature = "parser")]
66impl DurationParseErrorOwned {
67 #[must_use] pub fn to_shared(&self) -> DurationParseError<'_> {
68 match self {
69 Self::InvalidValue(s) => DurationParseError::InvalidValue(s),
70 Self::ParseFloat(s) => DurationParseError::InvalidValue(s.as_str()),
71 }
72 }
73}
74
75#[cfg(feature = "parser")]
77pub fn parse_duration(input: &str) -> Result<CssDuration, DurationParseError<'_>> {
81 let trimmed = input.trim().to_lowercase();
82 if trimmed == "0" {
83 return Ok(CssDuration { inner: 0 });
84 }
85 if let Some(num_str) = trimmed.strip_suffix("ms") {
86 let ms = num_str
87 .parse::<f32>()
88 .map_err(DurationParseError::ParseFloat)?;
89 if ms < 0.0 {
90 return Err(DurationParseError::InvalidValue(input));
91 }
92 Ok(CssDuration { inner: crate::cast::f32_to_u32(ms) })
93 } else if let Some(num_str) = trimmed.strip_suffix('s') {
94 let s = num_str
95 .parse::<f32>()
96 .map_err(DurationParseError::ParseFloat)?;
97 if s < 0.0 {
98 return Err(DurationParseError::InvalidValue(input));
99 }
100 Ok(CssDuration {
101 inner: crate::cast::f32_to_u32(s * 1000.0),
102 })
103 } else {
104 Err(DurationParseError::InvalidValue(input))
105 }
106}
107
108#[cfg(test)]
109#[allow(clippy::unreadable_literal)]
110mod autotest_generated {
111 use super::*;
112 use crate::codegen::format::FormatAsRustCode;
113 use crate::props::formatter::PrintAsCssValue;
114
115 #[cfg(feature = "parser")]
119 const TWO_POW_24: u32 = 16_777_216;
120
121 #[cfg(feature = "parser")]
123 fn ms(input: &str) -> u32 {
124 parse_duration(input)
125 .unwrap_or_else(|e| panic!("expected {input:?} to parse, got {e}"))
126 .inner
127 }
128
129 #[cfg(feature = "parser")]
132 #[test]
133 fn valid_minimal_inputs_parse_to_expected_values() {
134 assert_eq!(ms("0"), 0);
135 assert_eq!(ms("0ms"), 0);
136 assert_eq!(ms("0s"), 0);
137 assert_eq!(ms("200ms"), 200);
138 assert_eq!(ms("1s"), 1000);
139 assert_eq!(ms("1.5s"), 1500);
140 assert_eq!(ms("0.5s"), 500);
141 assert_eq!(ms(".25s"), 250);
142 assert_eq!(ms("5e2ms"), 500);
143 assert_eq!(ms("+5ms"), 5);
144 }
145
146 #[cfg(feature = "parser")]
149 #[test]
150 fn ms_suffix_wins_over_s_suffix() {
151 assert_eq!(ms("5ms"), 5);
152 assert_ne!(ms("5ms"), ms("5s"));
153 assert_eq!(ms("5s"), 5000);
154 }
155
156 #[cfg(feature = "parser")]
157 #[test]
158 fn units_are_case_insensitive() {
159 assert_eq!(ms("200MS"), 200);
160 assert_eq!(ms("200Ms"), 200);
161 assert_eq!(ms("1S"), 1000);
162 assert_eq!(ms("1.5E1S"), 15000);
163 }
164
165 #[cfg(feature = "parser")]
169 #[test]
170 fn sub_millisecond_values_truncate_toward_zero() {
171 assert_eq!(ms("5.9ms"), 5);
172 assert_eq!(ms("0.9ms"), 0);
173 assert_eq!(ms("0.0009s"), 0); assert_eq!(ms("0.0015s"), 1); }
176
177 #[cfg(feature = "parser")]
180 #[test]
181 fn empty_input_is_rejected_without_panicking() {
182 assert_eq!(parse_duration(""), Err(DurationParseError::InvalidValue("")));
183 }
184
185 #[cfg(feature = "parser")]
186 #[test]
187 fn whitespace_only_input_is_rejected_and_error_keeps_the_raw_input() {
188 assert_eq!(
191 parse_duration(" "),
192 Err(DurationParseError::InvalidValue(" "))
193 );
194 assert_eq!(
195 parse_duration("\t\n"),
196 Err(DurationParseError::InvalidValue("\t\n"))
197 );
198 }
199
200 #[cfg(feature = "parser")]
203 #[test]
204 fn a_bare_unit_with_no_number_is_a_parse_float_error_not_a_panic() {
205 assert!(matches!(
206 parse_duration("ms"),
207 Err(DurationParseError::ParseFloat(_))
208 ));
209 assert!(matches!(
210 parse_duration("s"),
211 Err(DurationParseError::ParseFloat(_))
212 ));
213 }
214
215 #[cfg(feature = "parser")]
216 #[test]
217 fn unitless_numbers_other_than_literal_zero_are_rejected() {
218 assert_eq!(ms("0"), 0);
220 assert_eq!(
221 parse_duration("200"),
222 Err(DurationParseError::InvalidValue("200"))
223 );
224 assert_eq!(
225 parse_duration("1.5"),
226 Err(DurationParseError::InvalidValue("1.5"))
227 );
228 assert_eq!(
229 parse_duration("0.0"),
230 Err(DurationParseError::InvalidValue("0.0"))
231 );
232 assert_eq!(
233 parse_duration("00"),
234 Err(DurationParseError::InvalidValue("00"))
235 );
236 assert_eq!(
237 parse_duration("-0"),
238 Err(DurationParseError::InvalidValue("-0"))
239 );
240 }
241
242 #[cfg(feature = "parser")]
243 #[test]
244 fn garbage_and_junk_never_panic() {
245 for garbage in [
246 "abc", "!!!", "\0\0\0", "ms ms", "1,5s", "1 ms", "--5ms", "5mss", "5sms", "0x10ms",
247 "1e", "1e+", ".s", "-.ms", "s1", "ms200", "200ms;garbage", "200ms !important",
248 ] {
249 assert!(
252 parse_duration(garbage).is_err(),
253 "expected {garbage:?} to be rejected"
254 );
255 }
256 }
257
258 #[cfg(feature = "parser")]
259 #[test]
260 fn leading_and_trailing_whitespace_is_trimmed_but_interior_space_is_not() {
261 assert_eq!(ms(" 200ms "), 200);
262 assert_eq!(ms("\t\n1.5s\r\n"), 1500);
263 assert!(matches!(
265 parse_duration("200 ms"),
266 Err(DurationParseError::ParseFloat(_))
267 ));
268 assert!(matches!(
269 parse_duration("2 0 0ms"),
270 Err(DurationParseError::ParseFloat(_))
271 ));
272 }
273
274 #[cfg(feature = "parser")]
275 #[test]
276 fn trailing_junk_after_a_valid_value_is_rejected_not_silently_accepted() {
277 assert!(parse_duration("200ms;").is_err());
278 assert!(parse_duration("200msx").is_err());
279 assert!(matches!(
281 parse_duration("200msms"),
282 Err(DurationParseError::ParseFloat(_))
283 ));
284 }
285
286 #[cfg(feature = "parser")]
289 #[test]
290 fn negative_durations_are_rejected_in_both_units() {
291 assert_eq!(
292 parse_duration("-1ms"),
293 Err(DurationParseError::InvalidValue("-1ms"))
294 );
295 assert_eq!(
296 parse_duration("-0.5s"),
297 Err(DurationParseError::InvalidValue("-0.5s"))
298 );
299 assert_eq!(
300 parse_duration("-1e-30s"),
301 Err(DurationParseError::InvalidValue("-1e-30s"))
302 );
303 }
304
305 #[cfg(feature = "parser")]
306 #[test]
307 fn the_invalid_value_error_reports_the_original_untrimmed_uncased_input() {
308 assert_eq!(
310 parse_duration(" -1MS "),
311 Err(DurationParseError::InvalidValue(" -1MS "))
312 );
313 }
314
315 #[cfg(feature = "parser")]
318 #[test]
319 fn negative_zero_is_accepted_and_clamps_to_zero() {
320 assert_eq!(ms("-0ms"), 0);
321 assert_eq!(ms("-0.0s"), 0);
322 assert_eq!(ms("-0e10ms"), 0);
323 }
324
325 #[cfg(feature = "parser")]
328 #[test]
329 fn values_beyond_u32_max_saturate_instead_of_wrapping_or_panicking() {
330 assert_eq!(ms("4294967296ms"), u32::MAX); assert_eq!(ms("99999999999ms"), u32::MAX);
332 assert_eq!(ms("1e30s"), u32::MAX);
333 assert_eq!(ms("5000000s"), u32::MAX); }
335
336 #[cfg(feature = "parser")]
339 #[test]
340 fn float_overflow_to_infinity_saturates_to_u32_max() {
341 assert_eq!(ms("1e39ms"), u32::MAX); assert_eq!(ms("1e999999ms"), u32::MAX);
343 assert_eq!(ms("infms"), u32::MAX);
344 assert_eq!(ms("infinityms"), u32::MAX);
345 assert_eq!(ms("INFms"), u32::MAX);
346 assert_eq!(ms("infs"), u32::MAX);
347 }
348
349 #[cfg(feature = "parser")]
350 #[test]
351 fn negative_infinity_is_rejected_as_a_negative_duration() {
352 assert_eq!(
353 parse_duration("-infms"),
354 Err(DurationParseError::InvalidValue("-infms"))
355 );
356 assert_eq!(
357 parse_duration("-infinitys"),
358 Err(DurationParseError::InvalidValue("-infinitys"))
359 );
360 }
361
362 #[cfg(feature = "parser")]
366 #[test]
367 fn nan_is_accepted_and_degrades_to_zero_rather_than_panicking() {
368 assert_eq!(ms("nanms"), 0);
369 assert_eq!(ms("NaNms"), 0);
370 assert_eq!(ms("-nanms"), 0);
371 assert_eq!(ms("nans"), 0); }
373
374 #[cfg(feature = "parser")]
375 #[test]
376 fn underflow_to_zero_is_not_an_error() {
377 assert_eq!(ms("1e-30ms"), 0);
378 assert_eq!(ms("1e-999999s"), 0);
379 }
380
381 #[cfg(feature = "parser")]
382 #[test]
383 fn u32_max_and_f32_max_boundary_strings_are_handled() {
384 assert_eq!(ms("4294967295ms"), u32::MAX); assert_eq!(ms("4294967040ms"), 4294967040); let f32_max = format!("{}ms", f32::MAX);
388 assert_eq!(ms(&f32_max), u32::MAX);
389
390 let i64_max = format!("{}ms", i64::MAX);
391 assert_eq!(ms(&i64_max), u32::MAX);
392 }
393
394 #[cfg(feature = "parser")]
397 #[test]
398 fn extremely_long_digit_string_saturates_without_hanging() {
399 let mut input = "9".repeat(100_000);
400 input.push_str("ms");
401 assert_eq!(ms(&input), u32::MAX);
402 }
403
404 #[cfg(feature = "parser")]
405 #[test]
406 fn extremely_long_run_of_leading_zeros_still_parses_exactly() {
407 let mut input = "0".repeat(100_000);
408 input.push_str("1ms");
409 assert_eq!(ms(&input), 1);
410 }
411
412 #[cfg(feature = "parser")]
413 #[test]
414 fn extremely_long_garbage_is_rejected_without_hanging() {
415 let input = "x".repeat(100_000);
416 assert!(parse_duration(&input).is_err());
417
418 let padded = format!("{}200ms{}", " ".repeat(50_000), " ".repeat(50_000));
420 assert_eq!(ms(&padded), 200);
421 }
422
423 #[cfg(feature = "parser")]
424 #[test]
425 fn deeply_nested_brackets_do_not_stack_overflow() {
426 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
428 assert!(parse_duration(&nested).is_err());
429
430 let nested_with_unit = format!("{nested}s");
431 assert!(parse_duration(&nested_with_unit).is_err());
432 }
433
434 #[cfg(feature = "parser")]
437 #[test]
438 fn non_ascii_input_is_rejected_without_panicking() {
439 for input in [
440 "\u{1F600}", "\u{1F600}ms", "1\u{FF53}", "1s\u{0301}", "200ms", "\u{202E}200ms", "1\u{00A0}s", ] {
448 assert!(
449 parse_duration(input).is_err(),
450 "expected {input:?} to be rejected"
451 );
452 }
453 }
454
455 #[cfg(feature = "parser")]
457 #[test]
458 fn unicode_whitespace_around_a_valid_value_is_trimmed() {
459 assert_eq!(ms("\u{00A0}200ms\u{00A0}"), 200); assert_eq!(ms("\u{3000}1.5s\u{3000}"), 1500); }
462
463 #[cfg(feature = "parser")]
467 #[test]
468 fn lowercasing_that_changes_the_byte_length_does_not_panic() {
469 assert!(parse_duration("\u{0130}ms").is_err()); assert!(parse_duration("1\u{0130}s").is_err());
471 }
472
473 #[cfg(feature = "parser")]
476 #[test]
477 fn print_as_css_value_round_trips_through_parse_duration() {
478 for inner in [
479 0,
480 1,
481 2,
482 17,
483 999,
484 1000,
485 65_535,
486 1_000_000,
487 TWO_POW_24, 4_294_967_040, u32::MAX, ] {
491 let duration = CssDuration { inner };
492 let printed = duration.print_as_css_value();
493 assert_eq!(
494 parse_duration(&printed),
495 Ok(duration),
496 "round-trip failed for {inner}ms (printed as {printed:?})"
497 );
498 }
499 }
500
501 #[test]
502 fn print_as_css_value_always_emits_the_ms_unit() {
503 for inner in [0, 1, u32::MAX] {
504 let printed = CssDuration { inner }.print_as_css_value();
505 assert!(printed.ends_with("ms"), "{printed:?} lacks a unit");
506 assert_eq!(printed, format!("{inner}ms"));
507 }
508 }
509
510 #[cfg(feature = "parser")]
514 #[test]
515 fn round_trip_above_two_pow_24_is_lossy_but_bounded() {
516 let duration = CssDuration {
517 inner: TWO_POW_24 + 1,
518 };
519 let reparsed = parse_duration(&duration.print_as_css_value()).unwrap();
520 assert_ne!(reparsed.inner, duration.inner);
521 assert_eq!(reparsed.inner, TWO_POW_24);
522 assert!(reparsed.inner.abs_diff(duration.inner) <= 1);
523 }
524
525 #[cfg(feature = "parser")]
526 #[test]
527 fn seconds_and_milliseconds_agree_for_the_same_duration() {
528 assert_eq!(ms("2s"), ms("2000ms"));
529 assert_eq!(ms("0.001s"), ms("1ms"));
530 assert_eq!(ms("0s"), ms("0ms"));
531 }
532
533 #[test]
536 fn default_duration_is_zero() {
537 assert_eq!(CssDuration::default(), CssDuration { inner: 0 });
538 assert_eq!(CssDuration::default().inner, 0);
539 }
540
541 #[test]
542 fn ordering_and_equality_follow_the_inner_millisecond_count() {
543 let a = CssDuration { inner: 1 };
544 let b = CssDuration { inner: 2 };
545 let max = CssDuration { inner: u32::MAX };
546 assert!(a < b);
547 assert!(b < max);
548 assert_eq!(a, CssDuration { inner: 1 });
549 assert_eq!(a.max(b), b);
550 assert_eq!(CssDuration::default(), CssDuration { inner: 0 });
551 }
552
553 #[test]
554 fn format_as_rust_code_emits_a_constructor_and_ignores_indentation() {
555 let d = CssDuration { inner: 42 };
556 assert_eq!(d.format_as_rust_code(0), "CssDuration { inner: 42 }");
557 assert_eq!(d.format_as_rust_code(7), d.format_as_rust_code(0));
558 assert_eq!(
559 CssDuration { inner: u32::MAX }.format_as_rust_code(0),
560 "CssDuration { inner: 4294967295 }"
561 );
562 }
563
564 #[cfg(feature = "parser")]
567 fn parse_float_error() -> core::num::ParseFloatError {
568 "not-a-float".parse::<f32>().unwrap_err()
569 }
570
571 #[cfg(feature = "parser")]
572 #[test]
573 fn to_contained_preserves_an_invalid_value_payload() {
574 let owned = DurationParseError::InvalidValue("10px").to_contained();
575 match owned {
576 DurationParseErrorOwned::InvalidValue(s) => assert_eq!(s.as_str(), "10px"),
577 DurationParseErrorOwned::ParseFloat(_) => panic!("variant changed"),
578 }
579 }
580
581 #[cfg(feature = "parser")]
582 #[test]
583 fn to_contained_stringifies_the_float_error() {
584 let owned = DurationParseError::ParseFloat(parse_float_error()).to_contained();
585 match owned {
586 DurationParseErrorOwned::ParseFloat(s) => {
587 assert!(!s.as_str().is_empty(), "float error message was empty");
588 assert_eq!(s.as_str(), parse_float_error().to_string());
589 }
590 DurationParseErrorOwned::InvalidValue(_) => panic!("variant changed"),
591 }
592 }
593
594 #[cfg(feature = "parser")]
595 #[test]
596 fn to_contained_handles_empty_and_extreme_payloads() {
597 assert_eq!(
598 DurationParseError::InvalidValue("").to_contained(),
599 DurationParseErrorOwned::InvalidValue(String::new().into())
600 );
601
602 let huge = "x".repeat(100_000);
603 let owned = DurationParseError::InvalidValue(&huge).to_contained();
604 match owned {
605 DurationParseErrorOwned::InvalidValue(s) => assert_eq!(s.as_str().len(), 100_000),
606 DurationParseErrorOwned::ParseFloat(_) => panic!("variant changed"),
607 }
608
609 let unicode = "\u{1F600}\u{0301}";
611 assert_eq!(
612 DurationParseError::InvalidValue(unicode).to_contained(),
613 DurationParseErrorOwned::InvalidValue(unicode.to_string().into())
614 );
615 }
616
617 #[cfg(feature = "parser")]
618 #[test]
619 fn to_shared_preserves_an_invalid_value_payload() {
620 let owned = DurationParseErrorOwned::InvalidValue("garbage".to_string().into());
621 assert_eq!(owned.to_shared(), DurationParseError::InvalidValue("garbage"));
622 }
623
624 #[cfg(feature = "parser")]
630 #[test]
631 fn to_shared_downgrades_parse_float_to_invalid_value() {
632 let msg = parse_float_error().to_string();
633 let owned = DurationParseErrorOwned::ParseFloat(msg.clone().into());
634 let shared = owned.to_shared();
635
636 assert!(!matches!(shared, DurationParseError::ParseFloat(_)));
637 assert_eq!(shared, DurationParseError::InvalidValue(msg.as_str()));
638 }
639
640 #[cfg(feature = "parser")]
641 #[test]
642 fn to_shared_does_not_panic_on_empty_or_extreme_payloads() {
643 assert_eq!(
644 DurationParseErrorOwned::InvalidValue(String::new().into()).to_shared(),
645 DurationParseError::InvalidValue("")
646 );
647
648 let huge = "y".repeat(100_000);
649 let owned = DurationParseErrorOwned::InvalidValue(huge.clone().into());
650 assert_eq!(owned.to_shared(), DurationParseError::InvalidValue(&huge));
651
652 let empty_float = DurationParseErrorOwned::ParseFloat(String::new().into());
653 assert_eq!(empty_float.to_shared(), DurationParseError::InvalidValue(""));
654 }
655
656 #[cfg(feature = "parser")]
659 #[test]
660 fn invalid_value_survives_a_full_shared_owned_shared_round_trip() {
661 let input = "10px";
662 let err = parse_duration(input).unwrap_err();
663 assert_eq!(err, DurationParseError::InvalidValue(input));
664
665 let owned = err.to_contained();
666 assert_eq!(owned.to_shared(), DurationParseError::InvalidValue(input));
667 }
668
669 #[cfg(feature = "parser")]
673 #[test]
674 fn a_word_ending_in_s_is_treated_as_a_seconds_value() {
675 assert!(matches!(
676 parse_duration("200 nanoseconds"),
677 Err(DurationParseError::ParseFloat(_))
678 ));
679 assert!(matches!(
680 parse_duration("always"),
681 Err(DurationParseError::ParseFloat(_))
682 ));
683 assert_eq!(
685 parse_duration("200 nanosecond"),
686 Err(DurationParseError::InvalidValue("200 nanosecond"))
687 );
688 }
689
690 #[cfg(feature = "parser")]
691 #[test]
692 fn error_display_never_panics_and_mentions_the_offender() {
693 let invalid = DurationParseError::InvalidValue("\u{1F600}");
694 let printed = format!("{invalid}");
695 assert!(printed.contains('\u{1F600}'), "{printed:?}");
696
697 let float = DurationParseError::ParseFloat(parse_float_error());
698 assert!(!format!("{float}").is_empty());
699
700 assert!(!format!("{invalid:?}").is_empty());
702 assert!(!format!("{float:?}").is_empty());
703 }
704}