datui_lib/formats/
nul_tail.rs1use std::fs::File;
9use std::io::{Read, Seek, SeekFrom};
10use std::path::Path;
11
12pub fn text_end(bytes: &[u8]) -> Option<usize> {
16 if bytes.last() != Some(&0) {
17 return None;
18 }
19 if bytes.starts_with(b"\xFF\xFE") || bytes.starts_with(b"\xFE\xFF") {
22 return None;
23 }
24 const COMPRESSED: [&[u8]; 4] = [b"\x1F\x8B", b"\x28\xB5\x2F\xFD", b"BZh", b"\xFD7zXZ\x00"];
26 if COMPRESSED.iter().any(|magic| bytes.starts_with(magic)) {
27 return None;
28 }
29 let end = bytes.iter().rposition(|&b| b != 0).map_or(0, |at| at + 1);
30 if end >= 2 && bytes[end - 2] == 0 {
31 return None;
32 }
33 Some(end)
34}
35
36fn mapped(file: &File, len: u64) -> std::io::Result<Option<(memmap2::Mmap, usize)>> {
39 if len == 0 || !ends_in_nul(file)? {
40 return Ok(None);
41 }
42 let map = unsafe { memmap2::Mmap::map(file)? };
45 Ok(text_end(&map).map(|end| (map, end)))
46}
47
48fn ends_in_nul(mut file: &File) -> std::io::Result<bool> {
50 let mut last = [1u8];
51 file.seek(SeekFrom::End(-1))?;
52 file.read_exact(&mut last)?;
53 file.rewind()?;
54 Ok(last[0] == 0)
55}
56
57pub fn text_len(file: &File) -> std::io::Result<Option<u64>> {
59 let len = file.metadata()?.len();
60 Ok(mapped(file, len)?.map(|(_, end)| end as u64))
61}
62
63pub fn holds_nothing(path: &Path) -> bool {
66 let Ok(file) = File::open(path) else {
67 return false;
68 };
69 match file.metadata() {
70 Ok(m) if m.len() == 0 => true,
71 Ok(_) => text_len(&file).is_ok_and(|len| len == Some(0)),
72 Err(_) => false,
73 }
74}
75
76pub fn text_buffer(path: &Path) -> std::io::Result<Option<polars_buffer::Buffer<u8>>> {
79 let file = File::open(path)?;
80 let len = file.metadata()?.len();
81 Ok(mapped(&file, len)?.map(|(map, end)| polars_buffer::Buffer::from_owner(map).sliced(..end)))
82}
83
84pub fn trim(bytes: &mut Vec<u8>) {
86 if let Some(end) = text_end(bytes) {
87 bytes.truncate(end);
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn a_trailing_run_of_nuls_is_cut() {
97 assert_eq!(text_end(b"a,b\n1,2\n\0\0\0\0"), Some(8));
98 assert_eq!(text_end(&[0u8; 4096]), Some(0), "all NULs: no text");
99 assert_eq!(text_end(b"a\n1\0"), Some(3));
100 }
101
102 #[test]
104 fn a_file_without_a_nul_tail_is_left_alone() {
105 assert_eq!(text_end(b"a,b\n1,2\n"), None);
106 assert_eq!(text_end(b""), None);
107 assert_eq!(
108 text_end(b"a,\0,b\n1,2"),
109 None,
110 "an interior NUL is the text's"
111 );
112 assert_eq!(
113 text_end(b"\x1F\x8B\x08\x00rest\x46\x00\x00\x00"),
114 None,
115 "gzip's length ends in zeros"
116 );
117 let dir = tempfile::tempdir().unwrap();
118 let path = dir.path().join("plain.csv");
119 std::fs::write(&path, "a,b\n1,2\n").unwrap();
120 assert!(text_buffer(&path).unwrap().is_none());
121 let empty = dir.path().join("empty.csv");
122 std::fs::write(&empty, "").unwrap();
123 assert!(text_buffer(&empty).unwrap().is_none());
124 }
125
126 #[test]
127 fn utf16_text_keeps_its_nuls() {
128 let le: Vec<u8> = "a,b\n1,2\n"
129 .encode_utf16()
130 .flat_map(u16::to_le_bytes)
131 .collect();
132 assert_eq!(le.last(), Some(&0));
133 assert_eq!(text_end(&le), None, "no byte-order mark");
134 let mut bom = b"\xFF\xFE".to_vec();
135 bom.extend_from_slice(&le);
136 bom.extend_from_slice(&[0, 0, 0, 0]);
137 assert_eq!(text_end(&bom), None, "a byte-order mark");
138 let be: Vec<u8> = "a,b\n".encode_utf16().flat_map(u16::to_be_bytes).collect();
139 assert_eq!(text_end(&be), None);
140 }
141
142 #[test]
143 fn a_padded_file_is_scanned_up_to_its_text() {
144 let dir = tempfile::tempdir().unwrap();
145 let path = dir.path().join("padded.csv");
146 let mut bytes = b"a,b\n1,2\n".to_vec();
147 bytes.resize(4096 + 8, 0);
148 std::fs::write(&path, &bytes).unwrap();
149 let buffer = text_buffer(&path).unwrap().expect("a NUL tail");
150 assert_eq!(buffer.as_slice(), b"a,b\n1,2\n");
151 let file = File::open(&path).unwrap();
152 assert_eq!(text_len(&file).unwrap(), Some(8));
153 let mut owned = bytes.clone();
154 trim(&mut owned);
155 assert_eq!(owned, b"a,b\n1,2\n");
156 }
157}