| use anyhow::Result; |
| use tokio::fs::File; |
| use tokio::io::AsyncReadExt; |
|
|
| |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| enum Encoding { |
| Utf8WithBom, |
| Utf16BE, |
| Utf16LE, |
| } |
|
|
| impl Encoding { |
| |
| pub fn detect(buffer: &[u8], bytes_read: usize) -> Option<Self> { |
| match (buffer.first(), buffer.get(1), buffer.get(2)) { |
| (Some(&0xEF), Some(&0xBB), Some(&0xBF)) if bytes_read >= 3 => { |
| Some(Encoding::Utf8WithBom) |
| } |
| (Some(&0xFE), Some(&0xFF), _) if bytes_read >= 2 => Some(Encoding::Utf16BE), |
| (Some(&0xFF), Some(&0xFE), _) if bytes_read >= 2 => Some(Encoding::Utf16LE), |
| _ => None, |
| } |
| } |
| } |
|
|
| |
| pub async fn is_binary<P: AsRef<std::path::Path>>(path: P) -> Result<bool> { |
| use anyhow::Context; |
| let path_ref = path.as_ref(); |
| let mut file = File::open(path_ref) |
| .await |
| .with_context(|| format!("Failed to open file {}", path_ref.display()))?; |
|
|
| let mut buffer = vec![0u8; 512]; |
| let bytes_read = file.read(&mut buffer).await?; |
| buffer.truncate(bytes_read); |
|
|
| Ok(is_binary_internal(&buffer, bytes_read)) |
| } |
|
|
| |
| fn is_binary_internal(buffer: &[u8], bytes_read: usize) -> bool { |
| |
| let encoding = Encoding::detect(buffer, bytes_read); |
|
|
| |
| |
| let mut seems_binary = false; |
| if encoding != Some(Encoding::Utf16BE) |
| && encoding != Some(Encoding::Utf16LE) |
| && !buffer.is_empty() |
| { |
| let mut could_be_utf16le = true; |
| let mut could_be_utf16be = true; |
| let mut contains_zero_byte = false; |
|
|
| |
| |
| |
| |
| |
| |
| const ZERO_BYTE_DETECTION_BUFFER_MAX_LEN: usize = 512; |
| for (i, &byte) in buffer |
| .iter() |
| .enumerate() |
| .take(bytes_read.min(ZERO_BYTE_DETECTION_BUFFER_MAX_LEN)) |
| { |
| let is_endian = i % 2 == 1; |
| let is_zero_byte = byte == 0; |
|
|
| if is_zero_byte { |
| contains_zero_byte = true; |
| } |
|
|
| |
| if could_be_utf16le && (is_endian && !is_zero_byte || !is_endian && is_zero_byte) { |
| could_be_utf16le = false; |
| } |
|
|
| |
| if could_be_utf16be && (is_endian && is_zero_byte || !is_endian && !is_zero_byte) { |
| could_be_utf16be = false; |
| } |
|
|
| |
| if is_zero_byte && !could_be_utf16le && !could_be_utf16be { |
| break; |
| } |
| } |
|
|
| |
| if contains_zero_byte && !could_be_utf16le && !could_be_utf16be { |
| seems_binary = true; |
| } |
| } |
|
|
| seems_binary |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use pretty_assertions::assert_eq; |
| use tempfile::NamedTempFile; |
| use tokio::fs; |
|
|
| use super::*; |
|
|
| async fn create_test_file_fixture(content: &[u8]) -> Result<NamedTempFile> { |
| let file = NamedTempFile::new()?; |
| fs::write(file.path(), content).await?; |
| Ok(file) |
| } |
|
|
| #[tokio::test] |
| async fn test_empty_file_is_binary() -> Result<()> { |
| let fixture = create_test_file_fixture(&[]).await?; |
| let actual = is_binary(fixture.path()).await?; |
| let expected = false; |
| assert_eq!(actual, expected); |
| Ok(()) |
| } |
|
|
| #[tokio::test] |
| async fn test_plain_text_file_is_binary() -> Result<()> { |
| let fixture = create_test_file_fixture(b"Hello, world!").await?; |
| let actual = is_binary(fixture.path()).await?; |
| let expected = false; |
| assert_eq!(actual, expected); |
| Ok(()) |
| } |
|
|
| #[tokio::test] |
| async fn test_binary_file_with_zero_bytes() -> Result<()> { |
| let content = vec![ |
| 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x00, 0x57, 0x6F, 0x72, 0x6C, 0x64, |
| ]; |
| let fixture = create_test_file_fixture(&content).await?; |
| let actual = is_binary(fixture.path()).await?; |
| let expected = true; |
| assert_eq!(actual, expected); |
| Ok(()) |
| } |
|
|
| #[tokio::test] |
| async fn test_buffer_limit_512_bytes() -> Result<()> { |
| |
| let mut content = vec![0x48; 600]; |
| content[599] = 0x00; |
|
|
| let fixture = create_test_file_fixture(&content).await?; |
| let actual = is_binary(fixture.path()).await?; |
|
|
| |
| let expected = false; |
| assert_eq!(actual, expected); |
| Ok(()) |
| } |
| } |
|
|