1pub fn starts_with_ascii_case_insensitive(text: &str, prefix: &str) -> bool {
15 prefix.is_ascii()
16 && text.len() >= prefix.len()
17 && text.as_bytes()[..prefix.len()]
18 .iter()
19 .zip(prefix.as_bytes())
20 .all(|(left, right)| left.eq_ignore_ascii_case(right))
21}
22
23pub fn strip_prefix_ascii_case_insensitive<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
28 starts_with_ascii_case_insensitive(text, prefix).then(|| &text[prefix.len()..])
29}
30
31pub fn find_ascii_case_insensitive(text: &str, needle: &str) -> Option<usize> {
34 find_ascii_case_insensitive_from(text, needle, 0)
35}
36
37pub fn rfind_ascii_case_insensitive(text: &str, needle: &str) -> Option<usize> {
40 if needle.is_empty() {
41 return Some(text.len());
42 }
43 if !needle.is_ascii() || needle.len() > text.len() {
44 return None;
45 }
46
47 let haystack = text.as_bytes();
48 let needle = needle.as_bytes();
49 (0..=haystack.len() - needle.len()).rev().find(|&position| {
50 haystack[position..position + needle.len()]
51 .iter()
52 .zip(needle)
53 .all(|(left, right)| left.eq_ignore_ascii_case(right))
54 })
55}
56
57pub fn find_ascii_case_insensitive_from(text: &str, needle: &str, start: usize) -> Option<usize> {
63 if needle.is_empty() {
64 return (start <= text.len()).then_some(start);
65 }
66 if !needle.is_ascii() || start > text.len() || needle.len() > text.len() - start {
67 return None;
68 }
69
70 let haystack = text.as_bytes();
71 let needle = needle.as_bytes();
72 (start..=haystack.len() - needle.len()).find(|&position| {
73 haystack[position..position + needle.len()]
74 .iter()
75 .zip(needle)
76 .all(|(left, right)| left.eq_ignore_ascii_case(right))
77 })
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn matches_ascii_without_mutating_unicode_input() {
86 let text = "ßSELECT ff from İstanbul";
87 let select = find_ascii_case_insensitive(text, "select").expect("ASCII match");
88 let from = rfind_ascii_case_insensitive(text, "FROM").expect("ASCII match");
89 assert_eq!(select, 2);
90 assert_eq!(from, 13);
91 assert_eq!(&text[select..select + 6], "SELECT");
92 assert_eq!(&text[from..from + 4], "from");
93 assert_eq!(find_ascii_case_insensitive(text, "istanbul"), None);
94 assert_eq!(find_ascii_case_insensitive(text, "ssselect"), None);
95 assert_eq!(find_ascii_case_insensitive(text, "ff"), None);
96 }
97
98 #[test]
99 fn rejects_non_ascii_needles() {
100 let text = "SELECT Straße";
101 assert!(!starts_with_ascii_case_insensitive(text, "ŚELECT"));
102 assert_eq!(strip_prefix_ascii_case_insensitive(text, "ŚELECT"), None);
103 assert_eq!(find_ascii_case_insensitive(text, "ß"), None);
104 assert_eq!(find_ascii_case_insensitive_from(text, "İ", 0), None);
105 assert_eq!(rfind_ascii_case_insensitive(text, "ff"), None);
106 }
107
108 #[test]
109 fn prefix_helpers_return_original_suffix() {
110 let text = "SeLeCt ß";
111 assert!(starts_with_ascii_case_insensitive(text, "select"));
112 assert_eq!(
113 strip_prefix_ascii_case_insensitive(text, "SELECT"),
114 Some(" ß")
115 );
116 assert_eq!(strip_prefix_ascii_case_insensitive(text, ""), Some(text));
117 assert!(starts_with_ascii_case_insensitive(text, ""));
118 assert!(!starts_with_ascii_case_insensitive(text, "select x"));
119 }
120
121 #[test]
122 fn empty_and_start_bounds_are_conventional() {
123 let text = "éX";
124 assert_eq!(find_ascii_case_insensitive(text, ""), Some(0));
125 assert_eq!(rfind_ascii_case_insensitive(text, ""), Some(text.len()));
126 assert_eq!(find_ascii_case_insensitive_from(text, "", 1), Some(1));
127 assert_eq!(
128 find_ascii_case_insensitive_from(text, "", text.len()),
129 Some(text.len())
130 );
131 assert_eq!(
132 find_ascii_case_insensitive_from(text, "", text.len() + 1),
133 None
134 );
135 assert_eq!(find_ascii_case_insensitive_from(text, "x", 1), Some(2));
136 assert_eq!(
137 find_ascii_case_insensitive_from(text, "x", text.len()),
138 None
139 );
140 }
141}