Scan `source[start..end]` for the first invalid escape sequence. Returns `Some((invalid_char, byte_offset_in_source))` for the first invalid escape found, or `None` if all escapes are valid. When `is_bytes` is true, `\u`, `\U`, and `\N` are treated as invalid (bytes literals only support byte-oriented escapes). Only reports the **first** invalid escape per string literal, matching `_PyUnicode_De
(
source: &str,
start: usize,
end: usize,
is_bytes: bool,
)
| 90 | /// `_PyUnicode_DecodeUnicodeEscapeInternal2` which stores only the first |
| 91 | /// `first_invalid_escape_char`. |
| 92 | fn first_invalid_escape( |
| 93 | source: &str, |
| 94 | start: usize, |
| 95 | end: usize, |
| 96 | is_bytes: bool, |
| 97 | ) -> Option<(char, usize)> { |
| 98 | let raw = &source[start..end]; |
| 99 | let mut chars = raw.char_indices().peekable(); |
| 100 | while let Some((i, ch)) = chars.next() { |
| 101 | if ch != '\\' { |
| 102 | continue; |
| 103 | } |
| 104 | let Some((_, next)) = chars.next() else { |
| 105 | break; |
| 106 | }; |
| 107 | let valid = match next { |
| 108 | '\\' | '\'' | '"' | 'a' | 'b' | 'f' | 'n' | 'r' | 't' | 'v' => true, |
| 109 | '\n' => true, |
| 110 | '\r' => { |
| 111 | if matches!(chars.peek(), Some(&(_, '\n'))) { |
| 112 | chars.next(); |
| 113 | } |
| 114 | true |
| 115 | } |
| 116 | '0'..='7' => { |
| 117 | for _ in 0..2 { |
| 118 | if matches!(chars.peek(), Some(&(_, '0'..='7'))) { |
| 119 | chars.next(); |
| 120 | } else { |
| 121 | break; |
| 122 | } |
| 123 | } |
| 124 | true |
| 125 | } |
| 126 | 'x' | 'u' | 'U' => { |
| 127 | // \u and \U are only valid in string literals, not bytes |
| 128 | if is_bytes && next != 'x' { |
| 129 | false |
| 130 | } else { |
| 131 | let count = match next { |
| 132 | 'x' => 2, |
| 133 | 'u' => 4, |
| 134 | 'U' => 8, |
| 135 | _ => unreachable!(), |
| 136 | }; |
| 137 | for _ in 0..count { |
| 138 | if chars.peek().is_some_and(|&(_, c)| c.is_ascii_hexdigit()) { |
| 139 | chars.next(); |
| 140 | } else { |
| 141 | break; |
| 142 | } |
| 143 | } |
| 144 | true |
| 145 | } |
| 146 | } |
| 147 | 'N' => { |
| 148 | // \N{name} is only valid in string literals, not bytes |
| 149 | if is_bytes { |
no test coverage detected