granit_parser/
char_traits.rs1#[inline]
5#[must_use]
6pub fn is_z(c: char) -> bool {
7 c == '\0'
8}
9
10#[inline]
12#[must_use]
13pub fn is_break(c: char) -> bool {
14 c == '\n' || c == '\r'
15}
16
17#[inline]
19#[must_use]
20pub fn is_breakz(c: char) -> bool {
21 is_break(c) || is_z(c)
22}
23
24#[inline]
26#[must_use]
27pub fn is_blank(c: char) -> bool {
28 c == ' ' || c == '\t'
29}
30
31#[inline]
35#[must_use]
36pub fn is_blank_or_breakz(c: char) -> bool {
37 is_blank(c) || is_breakz(c)
38}
39
40#[inline]
42#[must_use]
43pub fn is_digit(c: char) -> bool {
44 c.is_ascii_digit()
45}
46
47#[inline]
55#[must_use]
56pub fn is_alpha(c: char) -> bool {
57 matches!(c, '0'..='9' | 'a'..='z' | 'A'..='Z' | '_' | '-')
58}
59
60#[inline]
62#[must_use]
63pub fn is_hex(c: char) -> bool {
64 c.is_ascii_digit() || ('a'..='f').contains(&c) || ('A'..='F').contains(&c)
65}
66
67#[track_caller]
72#[inline]
73#[must_use]
74pub fn as_hex(c: char) -> u32 {
75 match c {
76 '0'..='9' => (c as u32) - ('0' as u32),
77 'a'..='f' => (c as u32) - ('a' as u32) + 10,
78 'A'..='F' => (c as u32) - ('A' as u32) + 10,
79 _ => unreachable!("as_hex called with a non-hexadecimal character"),
80 }
81}
82
83#[inline]
85#[must_use]
86pub fn is_flow(c: char) -> bool {
87 matches!(c, ',' | '[' | ']' | '{' | '}')
88}
89
90#[inline]
92#[must_use]
93pub fn is_bom(c: char) -> bool {
94 c == '\u{FEFF}'
95}
96
97#[inline]
99#[must_use]
100pub fn is_yaml_non_break(c: char) -> bool {
101 is_printable(c) && !is_break(c) && !is_bom(c)
102}
103
104#[inline]
106#[must_use]
107pub(crate) fn is_printable(c: char) -> bool {
108 matches!(
109 c as u32,
110 0x0009
111 | 0x000A
112 | 0x000D
113 | 0x0020..=0x007E
114 | 0x0085
115 | 0x00A0..=0xD7FF
116 | 0xE000..=0xFFFD
117 | 0x10000..=0x0010_FFFF
118 )
119}
120
121const PRINTABLE_ASCII_FAST_PATH_MIN_BYTES: usize = 32;
122const BYTE_LANES_ONES: u64 = 0x0101_0101_0101_0101;
123const BYTE_LANES_HIGH_BITS: u64 = 0x8080_8080_8080_8080;
124const BYTE_LANES_TOP_THREE_BITS: u64 = 0xe0e0_e0e0_e0e0_e0e0;
125const BYTE_LANES_DEL: u64 = 0x7f7f_7f7f_7f7f_7f7f;
126
127#[inline]
128fn has_zero_byte(word: u64) -> bool {
129 word.wrapping_sub(BYTE_LANES_ONES) & !word & BYTE_LANES_HIGH_BITS != 0
130}
131
132#[inline]
133fn is_suspicious_scalar_byte(byte: u8) -> bool {
134 (byte < 0x20 && !matches!(byte, b'\t' | b'\n' | b'\r')) || byte >= 0x7f
135}
136
137#[inline]
143pub(crate) fn find_non_printable(s: &str) -> Option<char> {
144 if s.len() < PRINTABLE_ASCII_FAST_PATH_MIN_BYTES {
145 return s.chars().find(|&character| !is_printable(character));
146 }
147
148 let bytes = s.as_bytes();
149 let mut chunks = bytes.chunks_exact(8);
150 let mut byte_offset = 0;
151 let mut suspicious_offset = None;
152
153 for chunk in &mut chunks {
154 let word = u64::from_ne_bytes(chunk.try_into().unwrap());
156 let may_have_suspicious_byte = word & BYTE_LANES_HIGH_BITS != 0
157 || has_zero_byte(word & BYTE_LANES_TOP_THREE_BITS)
158 || has_zero_byte(word ^ BYTE_LANES_DEL);
159
160 if may_have_suspicious_byte {
161 if let Some(chunk_offset) = chunk
162 .iter()
163 .position(|&byte| is_suspicious_scalar_byte(byte))
164 {
165 suspicious_offset = Some(byte_offset + chunk_offset);
166 break;
167 }
168 }
169 byte_offset += chunk.len();
170 }
171
172 let suspicious_offset = suspicious_offset.or_else(|| {
173 chunks
174 .remainder()
175 .iter()
176 .position(|&byte| is_suspicious_scalar_byte(byte))
177 .map(|remainder_offset| byte_offset + remainder_offset)
178 });
179
180 match suspicious_offset {
181 None => None,
182 Some(offset) if bytes[offset].is_ascii() => Some(char::from(bytes[offset])),
183 Some(offset) => s[offset..]
185 .chars()
186 .find(|&character| !is_printable(character)),
187 }
188}
189
190#[inline]
192#[must_use]
193pub fn is_yaml_non_space(c: char) -> bool {
194 is_yaml_non_break(c) && !is_blank(c)
195}
196
197#[inline]
199#[must_use]
200pub fn is_anchor_char(c: char) -> bool {
201 is_yaml_non_space(c) && !is_flow(c) && !is_z(c)
202}
203
204#[inline]
210#[must_use]
211pub fn is_word_char(c: char) -> bool {
212 is_alpha(c) && c != '_'
213}
214
215#[inline]
217#[must_use]
218pub fn is_uri_char(c: char) -> bool {
219 is_word_char(c) || "#;/?:@&=+$,_.!~*\'()[]%".contains(c)
220}
221
222#[inline]
224#[must_use]
225pub fn is_tag_char(c: char) -> bool {
226 is_uri_char(c) && !is_flow(c) && c != '!'
227}
228
229#[cfg(test)]
230mod tests {
231 use alloc::string::String;
232
233 use super::*;
234
235 #[test]
236 fn printable_ranges_include_private_and_supplementary_planes() {
237 assert!(is_printable('\u{E000}'));
238 assert!(is_printable('\u{10FFFF}'));
239 assert!(is_yaml_non_break('\u{10000}'));
240 assert!(!is_yaml_non_break('\u{FEFF}'));
241 assert!(!is_yaml_non_break('\n'));
242 }
243
244 #[test]
245 fn optimized_non_printable_search_matches_yaml_boundaries() {
246 let printable = [
247 '\t',
248 '\n',
249 '\r',
250 ' ',
251 '~',
252 '\u{85}',
253 '\u{a0}',
254 '\u{d7ff}',
255 '\u{e000}',
256 '\u{feff}',
257 '\u{fffd}',
258 '\u{10000}',
259 '\u{10ffff}',
260 ];
261 for character in printable {
262 let mut short = String::from("before");
263 short.push(character);
264 short.push_str("after");
265 assert_eq!(find_non_printable(&short), None, "rejected {character:?}");
266
267 let mut long = "x".repeat(80);
268 long.push(character);
269 long.push_str("after");
270 assert_eq!(find_non_printable(&long), None, "rejected {character:?}");
271 }
272
273 let non_printable = [
274 '\0', '\u{1}', '\u{8}', '\u{b}', '\u{c}', '\u{e}', '\u{1f}', '\u{7f}', '\u{80}',
275 '\u{84}', '\u{86}', '\u{9f}', '\u{fffe}', '\u{ffff}',
276 ];
277 for character in non_printable {
278 let mut short = String::from("before");
279 short.push(character);
280 short.push_str("after");
281 assert_eq!(
282 find_non_printable(&short),
283 Some(character),
284 "accepted {character:?}",
285 );
286
287 let mut long = "x".repeat(80);
288 long.push(character);
289 long.push_str("after");
290 assert_eq!(
291 find_non_printable(&long),
292 Some(character),
293 "accepted {character:?}",
294 );
295 }
296
297 let mut multiple = "x".repeat(80);
298 multiple.push('\u{80}');
299 multiple.push('\u{7f}');
300 assert_eq!(find_non_printable(&multiple), Some('\u{80}'));
301 }
302
303 #[test]
304 fn optimized_non_printable_search_matches_reference_across_chunk_boundaries() {
305 let suffixes = [
306 "plain",
307 "\tafter",
308 "\nafter",
309 "\rafter",
310 "éafter",
311 "\u{85}after",
312 "\u{80}after",
313 "\u{7f}after",
314 "é\u{7f}after",
315 "\u{85}\u{9f}after",
316 "\u{10000}\u{ffff}after",
317 ];
318
319 for prefix_len in 56..=80 {
320 for suffix in suffixes {
321 let input = "x".repeat(prefix_len) + suffix;
322 let expected = input.chars().find(|&character| !is_printable(character));
323 assert_eq!(
324 find_non_printable(&input),
325 expected,
326 "mismatch at prefix length {prefix_len} for {suffix:?}",
327 );
328 }
329 }
330 }
331
332 #[test]
333 fn word_uri_and_tag_character_sets_are_distinct() {
334 assert!(is_word_char('-'));
335 assert!(!is_word_char('_'));
336 assert!(is_uri_char('_'));
337 assert!(is_uri_char('%'));
338 assert!(!is_tag_char('!'));
339 assert!(!is_tag_char('['));
340 }
341}