(source: &[u8])
| 114 | /// Returns the encoding name if found, or None for default (UTF-8). |
| 115 | #[cfg(feature = "parser")] |
| 116 | fn detect_source_encoding(source: &[u8]) -> Option<String> { |
| 117 | fn find_encoding_in_line(line: &[u8]) -> Option<String> { |
| 118 | // PEP 263: '#' must be preceded only by whitespace/formfeed |
| 119 | let hash_pos = line.iter().position(|&b| b == b'#')?; |
| 120 | if !line[..hash_pos] |
| 121 | .iter() |
| 122 | .all(|&b| b == b' ' || b == b'\t' || b == b'\x0c' || b == b'\r') |
| 123 | { |
| 124 | return None; |
| 125 | } |
| 126 | let after_hash = &line[hash_pos..]; |
| 127 | |
| 128 | // Find "coding" after the # |
| 129 | let coding_pos = after_hash.windows(6).position(|w| w == b"coding")?; |
| 130 | let after_coding = &after_hash[coding_pos + 6..]; |
| 131 | |
| 132 | // Next char must be ':' or '=' |
| 133 | let rest = if after_coding.first() == Some(&b':') || after_coding.first() == Some(&b'=') |
| 134 | { |
| 135 | &after_coding[1..] |
| 136 | } else { |
| 137 | return None; |
| 138 | }; |
| 139 | |
| 140 | // Skip whitespace |
| 141 | let rest = rest |
| 142 | .iter() |
| 143 | .copied() |
| 144 | .skip_while(|&b| b == b' ' || b == b'\t') |
| 145 | .collect::<Vec<_>>(); |
| 146 | |
| 147 | // Read encoding name: [-\w.]+ |
| 148 | let name: String = rest |
| 149 | .iter() |
| 150 | .take_while(|&&b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.') |
| 151 | .map(|&b| b as char) |
| 152 | .collect(); |
| 153 | |
| 154 | if name.is_empty() { None } else { Some(name) } |
| 155 | } |
| 156 | |
| 157 | // Split into lines (first two only) |
| 158 | let mut lines = source.splitn(3, |&b| b == b'\n'); |
| 159 | |
| 160 | if let Some(first) = lines.next() { |
| 161 | // Strip BOM if present |
| 162 | let first = first.strip_prefix(b"\xef\xbb\xbf").unwrap_or(first); |
| 163 | if let Some(enc) = find_encoding_in_line(first) { |
| 164 | return Some(enc); |
| 165 | } |
| 166 | // Only check second line if first line is blank or a comment |
| 167 | let trimmed = first |
| 168 | .iter() |
| 169 | .skip_while(|&&b| b == b' ' || b == b'\t' || b == b'\x0c' || b == b'\r') |
| 170 | .copied() |
| 171 | .collect::<Vec<_>>(); |
| 172 | if !trimmed.is_empty() && trimmed[0] != b'#' { |
| 173 | return None; |
no test coverage detected