1use crate::corety::AzString;
8
9#[must_use] pub fn split_string_respect_comma(input: &str) -> Vec<&str> {
15 split_string_by_char(input, ',')
16}
17
18#[must_use] pub fn split_string_respect_whitespace(input: &str) -> Vec<&str> {
22 let mut items = Vec::<&str>::new();
23 let mut current_start = 0;
24 let mut depth = 0;
25 let input_bytes = input.as_bytes();
26
27 for (idx, &ch) in input_bytes.iter().enumerate() {
28 match ch {
29 b'(' => depth += 1,
30 b')' => depth -= 1,
31 b' ' | b'\t' | b'\n' | b'\r' if depth == 0 => {
32 if current_start < idx {
33 items.push(&input[current_start..idx]);
34 }
35 current_start = idx + 1;
36 }
37 _ => {}
38 }
39 }
40
41 if current_start < input.len() {
43 items.push(&input[current_start..]);
44 }
45
46 items
47}
48
49fn split_string_by_char(input: &str, target_char: char) -> Vec<&str> {
50 let mut comma_separated_items = Vec::<&str>::new();
51 let mut current_input = input;
52
53 'outer: loop {
54 let Some((skip_next_braces_result, character_was_found)) =
55 skip_next_braces(current_input, target_char)
56 else {
57 break 'outer;
58 };
59 if character_was_found {
60 comma_separated_items.push(¤t_input[..skip_next_braces_result]);
61 current_input = ¤t_input[(skip_next_braces_result + 1)..];
62 } else {
63 comma_separated_items.push(current_input);
64 break 'outer;
65 }
66 }
67
68 comma_separated_items
69}
70
71fn skip_next_braces(input: &str, target_char: char) -> Option<(usize, bool)> {
73 let mut depth = 0;
74 let mut last_character: Option<usize> = None;
75 let mut character_was_found = false;
76
77 if input.is_empty() {
78 return None;
79 }
80
81 for (idx, ch) in input.char_indices() {
82 last_character = Some(idx);
83 match ch {
84 '(' => {
85 depth += 1;
86 }
87 ')' => {
88 depth -= 1;
89 }
90 c => {
91 if c == target_char && depth == 0 {
92 character_was_found = true;
93 break;
94 }
95 }
96 }
97 }
98
99 last_character.map(|lc| (lc, character_was_found))
100}
101
102#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
103pub enum ParenthesisParseError<'a> {
104 UnclosedBraces,
105 NoOpeningBraceFound,
106 NoClosingBraceFound,
107 StopWordNotFound(&'a str),
108 EmptyInput,
109}
110
111impl_display! { ParenthesisParseError<'a>, {
112 UnclosedBraces => format!("Unclosed parenthesis"),
113 NoOpeningBraceFound => format!("Expected value in parenthesis (missing \"(\")"),
114 NoClosingBraceFound => format!("Missing closing parenthesis (missing \")\")"),
115 StopWordNotFound(e) => format!("Stopword not found, found: \"{}\"", e),
116 EmptyInput => format!("Empty parenthesis"),
117}}
118#[allow(variant_size_differences)] #[derive(Debug, Clone, PartialEq, Eq)]
121#[repr(C, u8)]
122pub enum ParenthesisParseErrorOwned {
123 UnclosedBraces,
124 NoOpeningBraceFound,
125 NoClosingBraceFound,
126 StopWordNotFound(AzString),
127 EmptyInput,
128}
129
130impl ParenthesisParseError<'_> {
131 #[must_use] pub fn to_contained(&self) -> ParenthesisParseErrorOwned {
132 match self {
133 ParenthesisParseError::UnclosedBraces => ParenthesisParseErrorOwned::UnclosedBraces,
134 ParenthesisParseError::NoOpeningBraceFound => {
135 ParenthesisParseErrorOwned::NoOpeningBraceFound
136 }
137 ParenthesisParseError::NoClosingBraceFound => {
138 ParenthesisParseErrorOwned::NoClosingBraceFound
139 }
140 ParenthesisParseError::StopWordNotFound(s) => {
141 ParenthesisParseErrorOwned::StopWordNotFound((*s).to_string().into())
142 }
143 ParenthesisParseError::EmptyInput => ParenthesisParseErrorOwned::EmptyInput,
144 }
145 }
146}
147
148impl ParenthesisParseErrorOwned {
149 #[must_use] pub fn to_shared(&self) -> ParenthesisParseError<'_> {
150 match self {
151 Self::UnclosedBraces => ParenthesisParseError::UnclosedBraces,
152 Self::NoOpeningBraceFound => {
153 ParenthesisParseError::NoOpeningBraceFound
154 }
155 Self::NoClosingBraceFound => {
156 ParenthesisParseError::NoClosingBraceFound
157 }
158 Self::StopWordNotFound(s) => {
159 ParenthesisParseError::StopWordNotFound(s.as_str())
160 }
161 Self::EmptyInput => ParenthesisParseError::EmptyInput,
162 }
163 }
164}
165
166pub fn parse_parentheses<'a>(
196 input: &'a str,
197 stopwords: &[&'static str],
198) -> Result<(&'static str, &'a str), ParenthesisParseError<'a>> {
199 use self::ParenthesisParseError::{EmptyInput, NoOpeningBraceFound, StopWordNotFound, NoClosingBraceFound};
200
201 let input = input.trim();
202 if input.is_empty() {
203 return Err(EmptyInput);
204 }
205
206 let first_open_brace = input.find('(').ok_or(NoOpeningBraceFound)?;
207 let found_stopword = &input[..first_open_brace];
208
209 let mut validated_stopword = None;
211 for stopword in stopwords {
212 if found_stopword == *stopword {
213 validated_stopword = Some(stopword);
214 break;
215 }
216 }
217
218 let validated_stopword = validated_stopword.ok_or(StopWordNotFound(found_stopword))?;
219 let last_closing_brace = input.rfind(')').ok_or(NoClosingBraceFound)?;
220
221 Ok((
222 validated_stopword,
223 &input[(first_open_brace + 1)..last_closing_brace],
224 ))
225}
226
227#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
229pub struct UnclosedQuotesError<'a>(pub &'a str);
230
231impl<'a> From<UnclosedQuotesError<'a>> for CssImageParseError<'a> {
232 fn from(err: UnclosedQuotesError<'a>) -> Self {
233 CssImageParseError::UnclosedQuotes(err.0)
234 }
235}
236
237#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
239pub struct QuoteStripped<'a>(pub &'a str);
240
241pub fn strip_quotes(input: &str) -> Result<QuoteStripped<'_>, UnclosedQuotesError<'_>> {
262 let mut double_quote_iter = input.splitn(2, '"');
263 double_quote_iter.next();
264 let mut single_quote_iter = input.splitn(2, '\'');
265 single_quote_iter.next();
266
267 let first_double_quote = double_quote_iter.next();
268 let first_single_quote = single_quote_iter.next();
269 if first_double_quote.is_some() && first_single_quote.is_some() {
270 return Err(UnclosedQuotesError(input));
271 }
272 if let Some(quote_contents) = first_double_quote {
273 if !quote_contents.ends_with('"') {
274 return Err(UnclosedQuotesError(quote_contents));
275 }
276 Ok(QuoteStripped(quote_contents.trim_end_matches('"')))
277 } else if let Some(quote_contents) = first_single_quote {
278 if !quote_contents.ends_with('\'') {
279 return Err(UnclosedQuotesError(input));
280 }
281 Ok(QuoteStripped(quote_contents.trim_end_matches('\'')))
282 } else {
283 Err(UnclosedQuotesError(input))
284 }
285}
286
287#[derive(Copy, Clone, PartialEq, Eq)]
288pub enum CssImageParseError<'a> {
289 UnclosedQuotes(&'a str),
290}
291
292impl_debug_as_display!(CssImageParseError<'a>);
293impl_display! {CssImageParseError<'a>, {
294 UnclosedQuotes(e) => format!("Unclosed quotes: \"{}\"", e),
295}}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
299#[repr(C, u8)]
300pub enum CssImageParseErrorOwned {
301 UnclosedQuotes(AzString),
302}
303
304impl CssImageParseError<'_> {
305 #[must_use] pub fn to_contained(&self) -> CssImageParseErrorOwned {
307 match self {
308 CssImageParseError::UnclosedQuotes(s) => {
309 CssImageParseErrorOwned::UnclosedQuotes((*s).to_string().into())
310 }
311 }
312 }
313}
314
315impl CssImageParseErrorOwned {
316 #[must_use] pub fn to_shared(&self) -> CssImageParseError<'_> {
318 match self {
319 Self::UnclosedQuotes(s) => {
320 CssImageParseError::UnclosedQuotes(s.as_str())
321 }
322 }
323 }
324}
325
326pub fn parse_image(input: &str) -> Result<AzString, CssImageParseError<'_>> {
332 Ok(strip_quotes(input).map_or_else(|_| input.trim().into(), |stripped| stripped.0.into()))
333}
334
335#[cfg(all(test, feature = "parser"))]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_strip_quotes() {
341 assert_eq!(strip_quotes("'hello'").unwrap(), QuoteStripped("hello"));
342 assert_eq!(strip_quotes("\"world\"").unwrap(), QuoteStripped("world"));
343 assert_eq!(
344 strip_quotes("\" spaced \"").unwrap(),
345 QuoteStripped(" spaced ")
346 );
347 assert!(strip_quotes("'unclosed").is_err());
348 assert!(strip_quotes("\"mismatched'").is_err());
349 assert!(strip_quotes("no-quotes").is_err());
350 }
351
352 #[test]
353 fn test_parse_parentheses() {
354 assert_eq!(
355 parse_parentheses("url(image.png)", &["url"]),
356 Ok(("url", "image.png"))
357 );
358 assert_eq!(
359 parse_parentheses("linear-gradient(red, blue)", &["linear-gradient"]),
360 Ok(("linear-gradient", "red, blue"))
361 );
362 assert_eq!(
363 parse_parentheses("var(--my-var, 10px)", &["var"]),
364 Ok(("var", "--my-var, 10px"))
365 );
366 assert_eq!(
367 parse_parentheses(" rgb( 255, 0, 0 ) ", &["rgb", "rgba"]),
368 Ok(("rgb", " 255, 0, 0 "))
369 );
370 }
371
372 #[test]
373 fn test_parse_parentheses_errors() {
374 assert!(parse_parentheses("rgba(255,0,0,1)", &["rgb"]).is_err());
376 assert!(parse_parentheses("url'image.png'", &["url"]).is_err());
378 assert!(parse_parentheses("url(image.png", &["url"]).is_err());
380 }
381
382 #[test]
383 fn test_split_string_respect_comma() {
384 let simple = "one, two, three";
386 assert_eq!(
387 split_string_respect_comma(simple),
388 vec!["one", " two", " three"]
389 );
390
391 let with_parens = "rgba(255, 0, 0, 1), #ff00ff";
393 assert_eq!(
394 split_string_respect_comma(with_parens),
395 vec!["rgba(255, 0, 0, 1)", " #ff00ff"]
396 );
397
398 let multi_parens =
400 "linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1)), url(image.png)";
401 assert_eq!(
402 split_string_respect_comma(multi_parens),
403 vec![
404 "linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1))",
405 " url(image.png)"
406 ]
407 );
408
409 let no_commas = "rgb(0,0,0)";
411 assert_eq!(split_string_respect_comma(no_commas), vec!["rgb(0,0,0)"]);
412 }
413}
414
415#[cfg(test)]
416mod autotest_generated {
417 use super::*;
418
419 #[test]
424 fn skip_next_braces_empty_input_returns_none() {
425 assert_eq!(skip_next_braces("", ','), None);
426 assert_eq!(skip_next_braces("", '('), None);
427 assert_eq!(skip_next_braces("", '\0'), None);
428 }
429
430 #[test]
431 fn skip_next_braces_not_found_yields_last_char_start_not_len() {
432 assert_eq!(skip_next_braces("abc", ','), Some((2, false)));
435 assert_eq!(skip_next_braces("a", ','), Some((0, false)));
436 assert_eq!(skip_next_braces("\u{1F600}", ','), Some((0, false)));
438 }
439
440 #[test]
441 fn skip_next_braces_finds_target_only_at_depth_zero() {
442 assert_eq!(skip_next_braces("a,b", ','), Some((1, true)));
443 assert_eq!(skip_next_braces("(a,b)", ','), Some((4, false)));
445 assert_eq!(skip_next_braces("(a,b),c", ','), Some((5, true)));
447 }
448
449 #[test]
450 fn skip_next_braces_unbalanced_closing_paren_drives_depth_negative() {
451 assert_eq!(skip_next_braces(")a,b", ','), Some((3, false)));
454 assert_eq!(skip_next_braces("))))", ','), Some((3, false)));
455 }
456
457 #[test]
458 fn skip_next_braces_paren_as_target_char_can_never_match() {
459 assert_eq!(skip_next_braces("a(b", '('), Some((2, false)));
462 assert_eq!(skip_next_braces("a)b", ')'), Some((2, false)));
463 }
464
465 #[test]
466 fn skip_next_braces_whitespace_only() {
467 assert_eq!(skip_next_braces(" ", ','), Some((2, false)));
468 assert_eq!(skip_next_braces("\t\n", ','), Some((1, false)));
469 assert_eq!(skip_next_braces(" ", ' '), Some((0, true)));
470 }
471
472 #[test]
473 fn skip_next_braces_boundary_number_strings() {
474 assert_eq!(skip_next_braces("0", ','), Some((0, false)));
475 assert_eq!(skip_next_braces("-0", ','), Some((1, false)));
476 assert_eq!(
477 skip_next_braces("9223372036854775807", ','),
478 Some((18, false))
479 );
480 assert_eq!(skip_next_braces("NaN,inf", ','), Some((3, true)));
481 assert_eq!(skip_next_braces("1e309,-1e309", ','), Some((5, true)));
482 }
483
484 #[test]
485 fn skip_next_braces_unicode_indices_stay_on_char_boundaries() {
486 assert_eq!(skip_next_braces("e\u{0301},x", ','), Some((3, true)));
488 assert_eq!(skip_next_braces("\u{1F600},x", ','), Some((4, true)));
490 let s = "\u{1F600}\u{0301}\u{4E2D}";
491 let (idx, found) = skip_next_braces(s, ',').expect("non-empty input");
492 assert!(!found);
493 assert!(s.is_char_boundary(idx));
494 }
495
496 #[test]
497 fn skip_next_braces_extremely_long_input_terminates() {
498 let mut input = "a".repeat(1_000_000);
499 input.push(',');
500 assert_eq!(skip_next_braces(&input, ','), Some((1_000_000, true)));
501 }
502
503 #[test]
504 fn skip_next_braces_deeply_nested_does_not_stack_overflow() {
505 let input = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
506 assert_eq!(skip_next_braces(&input, ','), Some((19_999, false)));
508 }
509
510 #[test]
515 fn split_string_by_char_empty_input_yields_empty_vec() {
516 assert!(split_string_by_char("", ',').is_empty());
518 assert!(split_string_by_char("", ';').is_empty());
519 }
520
521 #[test]
522 fn split_string_by_char_respects_nesting_for_any_ascii_separator() {
523 assert_eq!(
524 split_string_by_char("a;b(c;d);e", ';'),
525 vec!["a", "b(c;d)", "e"]
526 );
527 assert_eq!(split_string_by_char("a b(c d) e", ' '), vec!["a", "b(c d)", "e"]);
528 }
529
530 #[test]
531 fn split_string_by_char_paren_separator_never_splits() {
532 assert_eq!(split_string_by_char("a(b)c", '('), vec!["a(b)c"]);
533 assert_eq!(split_string_by_char("a(b)c", ')'), vec!["a(b)c"]);
534 }
535
536 #[test]
541 fn split_comma_empty_and_separator_only_inputs() {
542 assert!(split_string_respect_comma("").is_empty());
543 assert_eq!(split_string_respect_comma(","), vec![""]);
544 assert_eq!(split_string_respect_comma(",,"), vec!["", ""]);
545 assert_eq!(split_string_respect_comma("a,,b"), vec!["a", "", "b"]);
546 }
547
548 #[test]
549 fn split_comma_trailing_separator_drops_the_empty_tail() {
550 assert_eq!(split_string_respect_comma("a,"), vec!["a"]);
553 assert_eq!(split_string_respect_comma(",a"), vec!["", "a"]);
554 }
555
556 #[test]
557 fn split_comma_unbalanced_closing_paren_swallows_separators() {
558 assert_eq!(split_string_respect_comma("a),b"), vec!["a),b"]);
560 assert_eq!(split_string_respect_comma("a(b,c"), vec!["a(b,c"]);
561 }
562
563 #[test]
564 fn split_comma_respects_balanced_nesting() {
565 assert_eq!(
566 split_string_respect_comma("rgba(1,2,3),url(a,b)"),
567 vec!["rgba(1,2,3)", "url(a,b)"]
568 );
569 assert_eq!(
570 split_string_respect_comma("f(g(h(1,2),3),4),5"),
571 vec!["f(g(h(1,2),3),4)", "5"]
572 );
573 }
574
575 #[test]
576 fn split_comma_unicode_segments_are_valid_utf8() {
577 assert_eq!(
578 split_string_respect_comma("\u{1F600},h\u{E9}llo,\u{FC}"),
579 vec!["\u{1F600}", "h\u{E9}llo", "\u{FC}"]
580 );
581 assert_eq!(
583 split_string_respect_comma("e\u{0301},a\u{0308}"),
584 vec!["e\u{0301}", "a\u{0308}"]
585 );
586 }
587
588 #[test]
589 fn split_comma_garbage_input_never_panics() {
590 for garbage in [
591 "\0", "\u{FFFD}", ";;;", "((((", "))))", "()", ",()", "()," , "\\\"'`",
592 "\u{200B},\u{200B}", "--,--", "\t,\n,\r",
593 ] {
594 let parts = split_string_respect_comma(garbage);
595 for p in &parts {
597 assert!(garbage.contains(p));
598 }
599 }
600 }
601
602 #[test]
603 fn split_comma_extremely_long_inputs_do_not_hang() {
604 let no_comma = "a".repeat(1_000_000);
605 assert_eq!(split_string_respect_comma(&no_comma), vec![no_comma.as_str()]);
606
607 let all_commas = ",".repeat(100_000);
608 let parts = split_string_respect_comma(&all_commas);
609 assert_eq!(parts.len(), 100_000);
610 assert!(parts.iter().all(|p| p.is_empty()));
611 }
612
613 #[test]
614 fn split_comma_deeply_nested_does_not_stack_overflow() {
615 let nested = format!("{}1,2{}", "(".repeat(10_000), ")".repeat(10_000));
616 assert_eq!(split_string_respect_comma(&nested), vec![nested.as_str()]);
618 }
619
620 #[test]
621 fn split_comma_round_trips_via_join_when_no_trailing_separator() {
622 for input in [
623 "a,b,c",
624 "one, two, three",
625 "rgba(1,2,3),x",
626 "a,,b",
627 ",a",
628 "rgb(0,0,0)",
629 ] {
630 assert_eq!(split_string_respect_comma(input).join(","), input);
631 }
632 }
633
634 #[test]
639 fn split_whitespace_empty_and_blank_inputs_yield_nothing() {
640 assert!(split_string_respect_whitespace("").is_empty());
641 assert!(split_string_respect_whitespace(" ").is_empty());
642 assert!(split_string_respect_whitespace("\t\n\r").is_empty());
643 }
644
645 #[test]
646 fn split_whitespace_valid_minimal_and_run_collapsing() {
647 assert_eq!(
648 split_string_respect_whitespace("translateX(10px) rotate(90deg)"),
649 vec!["translateX(10px)", "rotate(90deg)"]
650 );
651 assert_eq!(split_string_respect_whitespace(" a\t\tb\n"), vec!["a", "b"]);
652 }
653
654 #[test]
655 fn split_whitespace_respects_balanced_nesting() {
656 assert_eq!(
657 split_string_respect_whitespace("translate( 10px , 20px ) scale(2)"),
658 vec!["translate( 10px , 20px )", "scale(2)"]
659 );
660 }
661
662 #[test]
663 fn split_whitespace_unbalanced_closing_paren_disables_splitting() {
664 assert_eq!(split_string_respect_whitespace("a) b"), vec!["a) b"]);
665 assert_eq!(split_string_respect_whitespace("a( b"), vec!["a( b"]);
666 }
667
668 #[test]
669 fn split_whitespace_unicode_is_split_on_ascii_bytes_only() {
670 assert_eq!(
673 split_string_respect_whitespace("h\u{E9}llo w\u{F6}rld \u{1F600}"),
674 vec!["h\u{E9}llo", "w\u{F6}rld", "\u{1F600}"]
675 );
676 assert_eq!(
678 split_string_respect_whitespace("a\u{A0}b"),
679 vec!["a\u{A0}b"]
680 );
681 }
682
683 #[test]
684 fn split_whitespace_garbage_input_never_panics() {
685 for garbage in ["\0", "((((", "))))", ")(", "\u{FFFD} \u{FFFD}", " ) ( "] {
686 for p in &split_string_respect_whitespace(garbage) {
687 assert!(garbage.contains(p));
688 }
689 }
690 }
691
692 #[test]
693 fn split_whitespace_extremely_long_inputs_do_not_hang() {
694 let blanks = " ".repeat(1_000_000);
695 assert!(split_string_respect_whitespace(&blanks).is_empty());
696
697 let word = "a".repeat(1_000_000);
698 assert_eq!(split_string_respect_whitespace(&word), vec![word.as_str()]);
699 }
700
701 #[test]
702 fn split_whitespace_deeply_nested_does_not_stack_overflow() {
703 let nested = format!("{}a{}", "(".repeat(10_000), ")".repeat(10_000));
704 let input = format!("{nested} z");
705 assert_eq!(
706 split_string_respect_whitespace(&input),
707 vec![nested.as_str(), "z"]
708 );
709 }
710
711 #[test]
716 fn parse_parentheses_empty_and_whitespace_only_input() {
717 assert_eq!(
718 parse_parentheses("", &["url"]),
719 Err(ParenthesisParseError::EmptyInput)
720 );
721 assert_eq!(
722 parse_parentheses(" ", &["url"]),
723 Err(ParenthesisParseError::EmptyInput)
724 );
725 assert_eq!(
726 parse_parentheses("\t\n", &["url"]),
727 Err(ParenthesisParseError::EmptyInput)
728 );
729 assert_eq!(
731 parse_parentheses("url(a)", &[]),
732 Err(ParenthesisParseError::StopWordNotFound("url"))
733 );
734 }
735
736 #[test]
737 fn parse_parentheses_valid_minimal_positive_control() {
738 assert_eq!(parse_parentheses("a(b)", &["a"]), Ok(("a", "b")));
739 assert_eq!(parse_parentheses("abc()", &["abc"]), Ok(("abc", "")));
740 assert_eq!(
741 parse_parentheses("abc(def(g))", &["abc", "def"]),
742 Ok(("abc", "def(g)"))
743 );
744 }
745
746 #[test]
747 fn parse_parentheses_missing_braces_and_stopword() {
748 assert_eq!(
749 parse_parentheses("abc", &["abc"]),
750 Err(ParenthesisParseError::NoOpeningBraceFound)
751 );
752 assert_eq!(
753 parse_parentheses("url(image.png", &["url"]),
754 Err(ParenthesisParseError::NoClosingBraceFound)
755 );
756 assert_eq!(
757 parse_parentheses("rgba(1,2,3,4)", &["rgb"]),
758 Err(ParenthesisParseError::StopWordNotFound("rgba"))
759 );
760 }
761
762 #[test]
763 fn parse_parentheses_stopword_must_directly_abut_the_brace() {
764 assert_eq!(
767 parse_parentheses("url (x)", &["url"]),
768 Err(ParenthesisParseError::StopWordNotFound("url "))
769 );
770 assert_eq!(
771 parse_parentheses("URL(x)", &["url"]),
772 Err(ParenthesisParseError::StopWordNotFound("URL"))
773 );
774 }
775
776 #[test]
777 fn parse_parentheses_uses_last_closing_brace_and_drops_trailing_junk() {
778 assert_eq!(parse_parentheses("url(a)b)", &["url"]), Ok(("url", "a)b")));
780 assert_eq!(
781 parse_parentheses("url(a);garbage", &["url"]),
782 Ok(("url", "a"))
783 );
784 assert_eq!(
786 parse_parentheses(" rgb( 1 ) ", &["rgb", "rgba"]),
787 Ok(("rgb", " 1 "))
788 );
789 }
790
791 #[test]
792 fn parse_parentheses_boundary_number_strings_pass_through_verbatim() {
793 for n in [
794 "0",
795 "-0",
796 "NaN",
797 "inf",
798 "-inf",
799 "9223372036854775807",
800 "-9223372036854775808",
801 "1e309",
802 "0.0000000000000000000001",
803 ] {
804 let input = format!("translate({n})");
805 assert_eq!(parse_parentheses(&input, &["translate"]), Ok(("translate", n)));
806 }
807 assert_eq!(
809 parse_parentheses("9223372036854775807", &["translate"]),
810 Err(ParenthesisParseError::NoOpeningBraceFound)
811 );
812 }
813
814 #[test]
815 fn parse_parentheses_unicode_stopword_and_payload() {
816 assert_eq!(
818 parse_parentheses("url(\u{1F600}.png)", &["url"]),
819 Ok(("url", "\u{1F600}.png"))
820 );
821 assert_eq!(
822 parse_parentheses("\u{FC}(\u{1F600})", &["\u{FC}"]),
823 Ok(("\u{FC}", "\u{1F600}"))
824 );
825 assert_eq!(
826 parse_parentheses("\u{1F600}(x)", &["url"]),
827 Err(ParenthesisParseError::StopWordNotFound("\u{1F600}"))
828 );
829 }
830
831 #[test]
832 fn parse_parentheses_garbage_never_panics() {
833 for garbage in ["(", ")", ")(", "()", "((((", "))))", "\0(\0)", "\u{FFFD}"] {
834 if let Ok((_, inner)) = parse_parentheses(garbage, &["", "\u{FFFD}"]) {
837 assert!(garbage.contains(inner));
838 }
839 }
840 }
841
842 #[test]
843 fn parse_parentheses_extremely_long_input_does_not_hang() {
844 let payload = "a".repeat(1_000_000);
845 let input = format!("url({payload})");
846 assert_eq!(
847 parse_parentheses(&input, &["url"]),
848 Ok(("url", payload.as_str()))
849 );
850 }
851
852 #[test]
853 fn parse_parentheses_deeply_nested_does_not_stack_overflow() {
854 let inner = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
855 let input = format!("abc({inner})");
856 assert_eq!(parse_parentheses(&input, &["abc"]), Ok(("abc", inner.as_str())));
858 }
859
860 #[test]
865 fn parenthesis_error_to_contained_maps_each_variant() {
866 assert_eq!(
867 ParenthesisParseError::UnclosedBraces.to_contained(),
868 ParenthesisParseErrorOwned::UnclosedBraces
869 );
870 assert_eq!(
871 ParenthesisParseError::NoOpeningBraceFound.to_contained(),
872 ParenthesisParseErrorOwned::NoOpeningBraceFound
873 );
874 assert_eq!(
875 ParenthesisParseError::NoClosingBraceFound.to_contained(),
876 ParenthesisParseErrorOwned::NoClosingBraceFound
877 );
878 assert_eq!(
879 ParenthesisParseError::EmptyInput.to_contained(),
880 ParenthesisParseErrorOwned::EmptyInput
881 );
882 assert_eq!(
883 ParenthesisParseError::StopWordNotFound("abc").to_contained(),
884 ParenthesisParseErrorOwned::StopWordNotFound("abc".into())
885 );
886 }
887
888 #[test]
889 fn parenthesis_error_round_trips_through_owned() {
890 let huge = "x".repeat(100_000);
891 let cases = [
892 ParenthesisParseError::UnclosedBraces,
893 ParenthesisParseError::NoOpeningBraceFound,
894 ParenthesisParseError::NoClosingBraceFound,
895 ParenthesisParseError::EmptyInput,
896 ParenthesisParseError::StopWordNotFound(""),
897 ParenthesisParseError::StopWordNotFound("url"),
898 ParenthesisParseError::StopWordNotFound("\u{1F600}\u{0301}"),
899 ParenthesisParseError::StopWordNotFound("\0"),
900 ParenthesisParseError::StopWordNotFound(huge.as_str()),
901 ];
902 for case in cases {
903 let owned = case.to_contained();
904 assert_eq!(owned.to_shared(), case, "round-trip must be lossless");
905 assert_eq!(owned.to_shared().to_contained(), owned);
907 }
908 }
909
910 #[test]
911 fn parenthesis_error_owned_to_shared_borrows_the_payload() {
912 let owned = ParenthesisParseErrorOwned::StopWordNotFound("linear-gradient".into());
913 match owned.to_shared() {
914 ParenthesisParseError::StopWordNotFound(s) => assert_eq!(s, "linear-gradient"),
915 other => panic!("expected StopWordNotFound, got {other:?}"),
916 }
917 }
918
919 #[test]
920 fn parenthesis_error_display_never_panics_on_extreme_payloads() {
921 for e in [
922 ParenthesisParseError::EmptyInput,
923 ParenthesisParseError::StopWordNotFound(""),
924 ParenthesisParseError::StopWordNotFound("\u{1F600}"),
925 ] {
926 assert!(!format!("{e}").is_empty());
927 }
928 }
929
930 #[test]
935 fn strip_quotes_valid_minimal_positive_control() {
936 assert_eq!(strip_quotes("\"Helvetica\""), Ok(QuoteStripped("Helvetica")));
937 assert_eq!(strip_quotes("'Arial'"), Ok(QuoteStripped("Arial")));
938 assert_eq!(strip_quotes("\"\""), Ok(QuoteStripped("")));
940 assert_eq!(strip_quotes("''"), Ok(QuoteStripped("")));
941 }
942
943 #[test]
944 fn strip_quotes_empty_blank_and_unquoted_inputs_error() {
945 assert_eq!(strip_quotes(""), Err(UnclosedQuotesError("")));
946 assert_eq!(strip_quotes(" "), Err(UnclosedQuotesError(" ")));
947 assert_eq!(strip_quotes("\t\n"), Err(UnclosedQuotesError("\t\n")));
948 assert_eq!(strip_quotes("no-quotes"), Err(UnclosedQuotesError("no-quotes")));
949 }
950
951 #[test]
952 fn strip_quotes_mixed_quote_kinds_are_rejected() {
953 assert_eq!(strip_quotes("\"Arial'"), Err(UnclosedQuotesError("\"Arial'")));
954 assert_eq!(
956 strip_quotes("\"Bob's Font\""),
957 Err(UnclosedQuotesError("\"Bob's Font\""))
958 );
959 }
960
961 #[test]
962 fn strip_quotes_unclosed_error_payload_is_asymmetric_between_branches() {
963 assert_eq!(strip_quotes("'unclosed"), Err(UnclosedQuotesError("'unclosed")));
965 assert_eq!(strip_quotes("'"), Err(UnclosedQuotesError("'")));
966 assert_eq!(strip_quotes("\"unclosed"), Err(UnclosedQuotesError("unclosed")));
969 assert_eq!(strip_quotes("\""), Err(UnclosedQuotesError("")));
970 }
971
972 #[test]
973 fn strip_quotes_surrounding_whitespace_defeats_stripping() {
974 assert_eq!(
976 strip_quotes(" \"Arial\" "),
977 Err(UnclosedQuotesError("Arial\" "))
978 );
979 assert_eq!(
980 strip_quotes(" 'Arial' "),
981 Err(UnclosedQuotesError(" 'Arial' "))
982 );
983 assert_eq!(strip_quotes("\" spaced \""), Ok(QuoteStripped(" spaced ")));
985 }
986
987 #[test]
988 fn strip_quotes_trims_the_entire_trailing_quote_run() {
989 assert_eq!(strip_quotes("\"\"\""), Ok(QuoteStripped("")));
991 assert_eq!(strip_quotes("\"ab\"\"\""), Ok(QuoteStripped("ab")));
992 assert_eq!(strip_quotes("\"a\"b\""), Ok(QuoteStripped("a\"b")));
994 }
995
996 #[test]
997 fn strip_quotes_unicode_payload() {
998 assert_eq!(
999 strip_quotes("\"\u{1F600}\u{E9}\""),
1000 Ok(QuoteStripped("\u{1F600}\u{E9}"))
1001 );
1002 assert_eq!(
1003 strip_quotes("'e\u{0301}\u{4E2D}'"),
1004 Ok(QuoteStripped("e\u{0301}\u{4E2D}"))
1005 );
1006 }
1007
1008 #[test]
1009 fn strip_quotes_boundary_number_strings() {
1010 for n in ["0", "-0", "NaN", "inf", "9223372036854775807", "1e309"] {
1011 assert_eq!(strip_quotes(&format!("\"{n}\"")), Ok(QuoteStripped(n)));
1012 }
1013 }
1014
1015 #[test]
1016 fn strip_quotes_garbage_never_panics() {
1017 for garbage in ["\0", "\\", "`", "\u{FFFD}", "\"\0\"", "((\"))"] {
1018 let _ = strip_quotes(garbage);
1019 }
1020 assert_eq!(strip_quotes("\"\0\""), Ok(QuoteStripped("\0")));
1021 }
1022
1023 #[test]
1024 fn strip_quotes_extremely_long_and_deeply_nested_inputs() {
1025 let payload = "a".repeat(1_000_000);
1026 let input = format!("\"{payload}\"");
1027 assert_eq!(strip_quotes(&input), Ok(QuoteStripped(payload.as_str())));
1028
1029 let nested = format!("{}x{}", "(".repeat(10_000), ")".repeat(10_000));
1030 let quoted = format!("'{nested}'");
1031 assert_eq!(strip_quotes("ed), Ok(QuoteStripped(nested.as_str())));
1032 }
1033
1034 #[test]
1035 fn strip_quotes_round_trips_quote_free_payloads() {
1036 for payload in [
1037 "Helvetica",
1038 "",
1039 " spaced ",
1040 "url(a,b)",
1041 "\u{1F600}",
1042 "0",
1043 "a\nb",
1044 ] {
1045 assert_eq!(
1046 strip_quotes(&format!("\"{payload}\"")),
1047 Ok(QuoteStripped(payload)),
1048 "double-quote round-trip"
1049 );
1050 assert_eq!(
1051 strip_quotes(&format!("'{payload}'")),
1052 Ok(QuoteStripped(payload)),
1053 "single-quote round-trip"
1054 );
1055 }
1056 }
1057
1058 #[test]
1063 fn css_image_error_round_trips_through_owned() {
1064 let huge = "x".repeat(100_000);
1065 for payload in ["", "a.png", "\u{1F600}\u{0301}", "\0", huge.as_str()] {
1066 let shared = CssImageParseError::UnclosedQuotes(payload);
1067 let owned = shared.to_contained();
1068 assert_eq!(owned, CssImageParseErrorOwned::UnclosedQuotes(payload.into()));
1069 assert_eq!(owned.to_shared(), shared, "round-trip must be lossless");
1070 assert_eq!(owned.to_shared().to_contained(), owned);
1071 }
1072 }
1073
1074 #[test]
1075 fn css_image_error_display_includes_the_payload() {
1076 let e = CssImageParseError::UnclosedQuotes("\u{1F600}");
1077 assert!(format!("{e}").contains('\u{1F600}'));
1078 assert!(!format!("{:?}", CssImageParseError::UnclosedQuotes("")).is_empty());
1080 }
1081
1082 #[test]
1083 fn unclosed_quotes_error_converts_into_css_image_error() {
1084 let e: CssImageParseError<'_> = UnclosedQuotesError("bad").into();
1085 assert_eq!(e, CssImageParseError::UnclosedQuotes("bad"));
1086 }
1087
1088 #[test]
1093 fn parse_image_is_infallible_for_every_adversarial_input() {
1094 let huge = "a".repeat(1_000_000);
1097 let nested = format!("{}x{}", "(".repeat(10_000), ")".repeat(10_000));
1098 for input in [
1099 "",
1100 " ",
1101 "\t\n",
1102 "\0",
1103 "\"",
1104 "'",
1105 "\"mixed'",
1106 "no-quotes",
1107 "url(a.png)",
1108 "9223372036854775807",
1109 "NaN",
1110 "\u{1F600}",
1111 huge.as_str(),
1112 nested.as_str(),
1113 ] {
1114 assert!(parse_image(input).is_ok(), "parse_image({input:?}) must be Ok");
1115 }
1116 }
1117
1118 #[test]
1119 fn parse_image_valid_minimal_positive_control() {
1120 assert_eq!(parse_image("\"image.png\"").unwrap().as_str(), "image.png");
1121 assert_eq!(parse_image("'image.png'").unwrap().as_str(), "image.png");
1122 assert_eq!(parse_image(" image.png ").unwrap().as_str(), "image.png");
1124 assert_eq!(parse_image("").unwrap().as_str(), "");
1125 assert_eq!(parse_image(" ").unwrap().as_str(), "");
1126 }
1127
1128 #[test]
1129 fn parse_image_quoted_payload_is_not_trimmed() {
1130 assert_eq!(parse_image("\" a \"").unwrap().as_str(), " a ");
1133 assert_eq!(parse_image(" a ").unwrap().as_str(), "a");
1134 }
1135
1136 #[test]
1137 fn parse_image_malformed_quotes_fall_back_to_the_raw_trimmed_input() {
1138 assert_eq!(parse_image("\"unclosed").unwrap().as_str(), "\"unclosed");
1141 assert_eq!(parse_image("'unclosed").unwrap().as_str(), "'unclosed");
1142 assert_eq!(parse_image("\"mixed'").unwrap().as_str(), "\"mixed'");
1143 assert_eq!(parse_image(" \"a\" ").unwrap().as_str(), "\"a\"");
1145 }
1146
1147 #[test]
1148 fn parse_image_does_not_unwrap_url_functions() {
1149 assert_eq!(parse_image("url(a.png)").unwrap().as_str(), "url(a.png)");
1151 }
1152
1153 #[test]
1154 fn parse_image_unicode_and_extremely_long_inputs() {
1155 assert_eq!(
1156 parse_image("'\u{1F600}.png'").unwrap().as_str(),
1157 "\u{1F600}.png"
1158 );
1159 let payload = "a".repeat(1_000_000);
1160 let input = format!("\"{payload}\"");
1161 assert_eq!(parse_image(&input).unwrap().as_str().len(), 1_000_000);
1162 }
1163
1164 #[test]
1165 fn parse_image_round_trips_quote_free_payloads() {
1166 for payload in ["a.png", "", "\u{1F600}", "some/deep/path.jpeg", "0"] {
1167 assert_eq!(
1168 parse_image(&format!("\"{payload}\"")).unwrap().as_str(),
1169 payload
1170 );
1171 assert_eq!(
1172 parse_image(&format!("'{payload}'")).unwrap().as_str(),
1173 payload
1174 );
1175 }
1176 }
1177}