use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
pub fn text_end(bytes: &[u8]) -> Option<usize> {
if bytes.last() != Some(&0) {
return None;
}
if bytes.starts_with(b"\xFF\xFE") || bytes.starts_with(b"\xFE\xFF") {
return None;
}
const COMPRESSED: [&[u8]; 4] = [b"\x1F\x8B", b"\x28\xB5\x2F\xFD", b"BZh", b"\xFD7zXZ\x00"];
if COMPRESSED.iter().any(|magic| bytes.starts_with(magic)) {
return None;
}
let end = bytes.iter().rposition(|&b| b != 0).map_or(0, |at| at + 1);
if end >= 2 && bytes[end - 2] == 0 {
return None;
}
Some(end)
}
fn mapped(file: &File, len: u64) -> std::io::Result<Option<(memmap2::Mmap, usize)>> {
if len == 0 || !ends_in_nul(file)? {
return Ok(None);
}
let map = unsafe { memmap2::Mmap::map(file)? };
Ok(text_end(&map).map(|end| (map, end)))
}
fn ends_in_nul(mut file: &File) -> std::io::Result<bool> {
let mut last = [1u8];
file.seek(SeekFrom::End(-1))?;
file.read_exact(&mut last)?;
file.rewind()?;
Ok(last[0] == 0)
}
pub fn text_len(file: &File) -> std::io::Result<Option<u64>> {
let len = file.metadata()?.len();
Ok(mapped(file, len)?.map(|(_, end)| end as u64))
}
pub fn holds_nothing(path: &Path) -> bool {
let Ok(file) = File::open(path) else {
return false;
};
match file.metadata() {
Ok(m) if m.len() == 0 => true,
Ok(_) => text_len(&file).is_ok_and(|len| len == Some(0)),
Err(_) => false,
}
}
pub fn text_buffer(path: &Path) -> std::io::Result<Option<polars_buffer::Buffer<u8>>> {
let file = File::open(path)?;
let len = file.metadata()?.len();
Ok(mapped(&file, len)?.map(|(map, end)| polars_buffer::Buffer::from_owner(map).sliced(..end)))
}
pub fn trim(bytes: &mut Vec<u8>) {
if let Some(end) = text_end(bytes) {
bytes.truncate(end);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_trailing_run_of_nuls_is_cut() {
assert_eq!(text_end(b"a,b\n1,2\n\0\0\0\0"), Some(8));
assert_eq!(text_end(&[0u8; 4096]), Some(0), "all NULs: no text");
assert_eq!(text_end(b"a\n1\0"), Some(3));
}
#[test]
fn a_file_without_a_nul_tail_is_left_alone() {
assert_eq!(text_end(b"a,b\n1,2\n"), None);
assert_eq!(text_end(b""), None);
assert_eq!(
text_end(b"a,\0,b\n1,2"),
None,
"an interior NUL is the text's"
);
assert_eq!(
text_end(b"\x1F\x8B\x08\x00rest\x46\x00\x00\x00"),
None,
"gzip's length ends in zeros"
);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("plain.csv");
std::fs::write(&path, "a,b\n1,2\n").unwrap();
assert!(text_buffer(&path).unwrap().is_none());
let empty = dir.path().join("empty.csv");
std::fs::write(&empty, "").unwrap();
assert!(text_buffer(&empty).unwrap().is_none());
}
#[test]
fn utf16_text_keeps_its_nuls() {
let le: Vec<u8> = "a,b\n1,2\n"
.encode_utf16()
.flat_map(u16::to_le_bytes)
.collect();
assert_eq!(le.last(), Some(&0));
assert_eq!(text_end(&le), None, "no byte-order mark");
let mut bom = b"\xFF\xFE".to_vec();
bom.extend_from_slice(&le);
bom.extend_from_slice(&[0, 0, 0, 0]);
assert_eq!(text_end(&bom), None, "a byte-order mark");
let be: Vec<u8> = "a,b\n".encode_utf16().flat_map(u16::to_be_bytes).collect();
assert_eq!(text_end(&be), None);
}
#[test]
fn a_padded_file_is_scanned_up_to_its_text() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("padded.csv");
let mut bytes = b"a,b\n1,2\n".to_vec();
bytes.resize(4096 + 8, 0);
std::fs::write(&path, &bytes).unwrap();
let buffer = text_buffer(&path).unwrap().expect("a NUL tail");
assert_eq!(buffer.as_slice(), b"a,b\n1,2\n");
let file = File::open(&path).unwrap();
assert_eq!(text_len(&file).unwrap(), Some(8));
let mut owned = bytes.clone();
trim(&mut owned);
assert_eq!(owned, b"a,b\n1,2\n");
}
}