const SUSPICIOUS_CONTROL_BINARY_MIN: u64 = 4;
const BINARY_NUL_RUN: usize = 4;
fn append_lossy_view_if_non_ascii_utf16(utf16_text: String, raw: &[u8]) -> String {
let total = utf16_text.chars().count();
if total == 0 {
return utf16_text;
}
let ascii = utf16_text.chars().filter(|c| c.is_ascii()).count();
if ascii * 2 >= total {
return utf16_text;
}
let lossy = String::from_utf8_lossy(raw);
let mut out = utf16_text;
out.push('\n');
out.push_str(&lossy);
out
}
pub(crate) fn decode_text_file(bytes: &[u8]) -> Option<String> {
if has_binary_magic(bytes) {
return None;
}
if let Some(text) = decode_utf16(bytes) {
return Some(append_lossy_view_if_non_ascii_utf16(text, bytes));
}
let bytes = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes);
if let Ok(s) = std::str::from_utf8(bytes) {
if looks_binary_header_check(bytes) {
return None;
}
return Some(s.to_owned());
}
if looks_binary_header_check(bytes) || looks_binary(bytes) {
return None;
}
Some(String::from_utf8_lossy(bytes).into_owned())
}
pub(in crate::filesystem) fn decode_text_file_owned_or_bytes(
bytes: Vec<u8>,
) -> Result<String, Vec<u8>> {
if has_binary_magic(&bytes) {
return Err(bytes);
}
if let Some(text) = decode_utf16(&bytes) {
return Ok(append_lossy_view_if_non_ascii_utf16(text, &bytes));
}
let mut bytes = bytes;
let had_utf8_bom = bytes.starts_with(&[0xEF, 0xBB, 0xBF]);
if had_utf8_bom {
bytes.drain(..3);
}
match String::from_utf8(bytes) {
Ok(s) => {
if looks_binary_header_check(s.as_bytes()) {
let mut bytes = s.into_bytes();
if had_utf8_bom {
bytes.splice(0..0, [0xEF, 0xBB, 0xBF]);
}
return Err(bytes);
}
Ok(s)
}
Err(e) => {
let bytes = e.into_bytes();
if looks_binary_header_check(&bytes) || looks_binary(&bytes) {
let mut bytes = bytes;
if had_utf8_bom {
bytes.splice(0..0, [0xEF, 0xBB, 0xBF]);
}
return Err(bytes);
}
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
}
}
fn looks_binary_header_check(bytes: &[u8]) -> bool {
let window = &bytes[..bytes.len().min(4096)];
if window.is_empty() {
return false;
}
let mut suspicious: u32 = 0;
for &byte in window {
if byte < 0x20 && !matches!(byte, b'\n' | b'\r' | b'\t' | 0x0C) {
suspicious += 1;
if suspicious >= SUSPICIOUS_CONTROL_BINARY_MIN as u32
&& (suspicious as usize) * 20 > window.len()
{
return true;
}
}
}
false
}
pub(in crate::filesystem::read) fn looks_binary(bytes: &[u8]) -> bool {
if has_binary_magic(bytes) || has_utf16_nul_pattern(bytes) {
return true;
}
if has_repeated_nul_run(bytes) {
return true;
}
let total = bytes.len() as u64;
if total == 0 {
return false;
}
let mut suspicious: u64 = 0;
for (i, &byte) in bytes.iter().enumerate() {
let is_susp = byte < 0x20 && !matches!(byte, b'\n' | b'\r' | b'\t' | 0x0C);
if is_susp {
suspicious += 1;
if suspicious >= SUSPICIOUS_CONTROL_BINARY_MIN && suspicious * 20 > total {
return true;
}
}
if i & 0xFFF == 0xFFF {
let scanned = (i as u64) + 1;
let remaining = total - scanned;
if (suspicious + remaining) * 20 <= total {
return false;
}
}
}
suspicious >= SUSPICIOUS_CONTROL_BINARY_MIN && suspicious * 20 > total
}
pub(in crate::filesystem) fn looks_binary_prefix(bytes: &[u8]) -> bool {
has_unambiguous_prefix_magic(bytes)
|| crate::magic::has_bmp_header(bytes)
|| crate::magic::has_pe_header(bytes)
|| crate::magic::has_bzip2_header(bytes)
|| has_repeated_nul_run(bytes)
}
fn has_repeated_nul_run(bytes: &[u8]) -> bool {
memchr::memchr_iter(0, bytes).any(|index| {
index + BINARY_NUL_RUN <= bytes.len()
&& bytes[index..index + BINARY_NUL_RUN]
.iter()
.all(|&byte| byte == 0)
})
}
fn has_binary_magic(bytes: &[u8]) -> bool {
if crate::magic::has_bmp_header(bytes)
|| crate::magic::has_pe_header(bytes)
|| crate::magic::has_bzip2_header(bytes)
{
return true;
}
crate::magic::has_unambiguous_binary_prefix(bytes)
|| crate::magic::starts_with_python_pickle_protocol2(bytes)
}
fn has_unambiguous_prefix_magic(bytes: &[u8]) -> bool {
crate::magic::has_unambiguous_binary_prefix(bytes)
}
fn has_utf16_nul_pattern(bytes: &[u8]) -> bool {
bytes.len() >= 4
&& (bytes[0] == 0xFF && bytes[1] == 0xFE || bytes[0] == 0xFE && bytes[1] == 0xFF)
}
pub(in crate::filesystem::read) fn decode_utf16(bytes: &[u8]) -> Option<String> {
#[allow(clippy::question_mark)]
let (little_endian, payload) = if let Some(rest) = bytes.strip_prefix(&[0xFF, 0xFE]) {
(true, rest)
} else if let Some(rest) = bytes.strip_prefix(&[0xFE, 0xFF]) {
(false, rest)
} else {
return None;
};
let chunks = payload.chunks_exact(2);
let has_orphan_trailing_byte = !chunks.remainder().is_empty();
if has_orphan_trailing_byte && payload.len() == 1 {
return None;
}
let units = chunks.map(|chunk| {
if little_endian {
u16::from_le_bytes([chunk[0], chunk[1]])
} else {
u16::from_be_bytes([chunk[0], chunk[1]])
}
});
let mut out = String::with_capacity(payload.len() / 2);
let mut invalid = 0usize;
let mut total = 0usize;
for r in char::decode_utf16(units) {
total += 1;
match r {
Ok(c) => out.push(c),
Err(_error) => {
invalid += 1;
out.push('\u{FFFD}');
}
}
}
if has_orphan_trailing_byte {
invalid += 1;
total += 1;
out.push('\u{FFFD}');
}
if total > 0 && invalid.saturating_mul(4) > total {
return None;
}
Some(out)
}