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