1use std::path::Path;
2
3const BINARY_EXTENSIONS: &[&str] = &[
4 "parquet",
6 "avro",
7 "orc",
8 "arrow",
9 "feather",
10 "hdf5",
11 "h5",
12 "npy",
13 "npz",
14 "db",
16 "sqlite",
17 "sqlite3",
18 "mdb",
19 "accdb",
20 "ldb",
21 "zip",
23 "gz",
24 "tar",
25 "bz2",
26 "xz",
27 "7z",
28 "rar",
29 "zst",
30 "lz4",
31 "lzma",
32 "png",
34 "jpg",
35 "jpeg",
36 "gif",
37 "webp",
38 "bmp",
39 "ico",
40 "tiff",
41 "tif",
42 "psd",
45 "raw",
46 "cr2",
47 "nef",
48 "heic",
49 "heif",
50 "avif",
51 "mp3",
53 "mp4",
54 "wav",
55 "flac",
56 "ogg",
57 "avi",
58 "mkv",
59 "mov",
60 "webm",
61 "m4a",
62 "exe",
64 "dll",
65 "so",
66 "dylib",
67 "o",
68 "a",
69 "obj",
70 "lib",
71 "pdb",
72 "class",
73 "jar",
74 "war",
75 "ear",
76 "pyc",
78 "pyo",
79 "whl",
80 "egg",
81 "beam",
82 "wasm",
83 "wast",
84 "model",
86 "onnx",
87 "pt",
88 "pth",
89 "safetensors",
90 "gguf",
91 "ggml",
92 "tflite",
93 "pb",
94 "h5",
95 "keras",
96 "pkl",
98 "pickle",
99 "bin",
100 "dat",
101 "protobuf",
102 "pdf",
104 "doc",
105 "docx",
106 "xls",
107 "xlsx",
108 "ppt",
109 "pptx",
110 "odt",
111 "ods",
112 "ttf",
114 "otf",
115 "woff",
116 "woff2",
117 "eot",
118 "iso",
120 "img",
121 "vmdk",
122 "qcow2",
123];
124
125const LLM_VIEWABLE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp"];
128
129pub const IMAGE_MAX_BYTES: u64 = 20 * 1024 * 1024;
131
132pub fn has_binary_extension(path: &str) -> bool {
134 Path::new(path)
135 .extension()
136 .and_then(|e| e.to_str())
137 .map(str::to_ascii_lowercase)
138 .is_some_and(|ext| BINARY_EXTENSIONS.contains(&ext.as_str()))
139}
140
141fn has_binary_content(path: &str) -> bool {
144 let Ok(file) = std::fs::File::open(path) else {
145 return false;
146 };
147 use std::io::Read;
148 let mut buf = [0u8; 8192];
149 let mut reader = std::io::BufReader::new(file);
150 let Ok(n) = reader.read(&mut buf) else {
151 return false;
152 };
153 crate::core::text_decode::looks_binary(&buf[..n])
154}
155
156pub fn is_binary_file(path: &str) -> bool {
159 if has_binary_extension(path) {
160 return true;
161 }
162 has_binary_content(path)
163}
164
165pub fn is_llm_viewable_image(path: &str) -> bool {
167 Path::new(path)
168 .extension()
169 .and_then(|e| e.to_str())
170 .map(str::to_ascii_lowercase)
171 .is_some_and(|ext| LLM_VIEWABLE_EXTENSIONS.contains(&ext.as_str()))
172}
173
174pub fn image_mime_type(path: &str) -> Option<&'static str> {
176 let ext = Path::new(path)
177 .extension()
178 .and_then(|e| e.to_str())?
179 .to_ascii_lowercase();
180 match ext.as_str() {
181 "png" => Some("image/png"),
182 "jpg" | "jpeg" => Some("image/jpeg"),
183 "gif" => Some("image/gif"),
184 "webp" => Some("image/webp"),
185 _ => None,
186 }
187}
188
189fn file_type_label(path: &str) -> &'static str {
191 let ext = Path::new(path)
192 .extension()
193 .and_then(|e| e.to_str())
194 .unwrap_or("");
195 match ext.to_ascii_lowercase().as_str() {
196 "parquet" | "avro" | "orc" | "arrow" | "feather" => "columnar data file",
197 "hdf5" | "h5" | "npy" | "npz" => "scientific data file",
198 "db" | "sqlite" | "sqlite3" => "database file",
199 "zip" | "gz" | "tar" | "bz2" | "xz" | "7z" | "rar" | "zst" => "compressed archive",
200 "png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "ico" | "heic" => "image file",
201 "mp3" | "mp4" | "wav" | "flac" | "ogg" | "avi" | "mkv" | "mov" => "media file",
202 "exe" | "dll" | "so" | "dylib" => "native binary",
203 "wasm" => "WebAssembly binary",
204 "pdf" => "PDF document",
205 "onnx" | "pt" | "pth" | "safetensors" | "gguf" | "ggml" => "ML model file",
206 "pkl" | "pickle" => "serialized object",
207 "pyc" | "pyo" => "Python bytecode",
208 "class" | "jar" | "war" => "Java bytecode",
209 _ => "binary file",
210 }
211}
212
213pub fn binary_file_message(path: &str) -> String {
215 let ext = Path::new(path)
216 .extension()
217 .and_then(|e| e.to_str())
218 .unwrap_or("unknown");
219 let label = file_type_label(path);
220 if matches!(
221 ext.to_ascii_lowercase().as_str(),
222 "parquet" | "avro" | "orc" | "arrow" | "feather"
223 ) {
224 let size_info = std::fs::metadata(path).ok().map_or_else(
225 || "unknown size".to_string(),
226 |metadata| format_file_size(metadata.len()),
227 );
228 return format!(
229 "Binary {label} (.{ext}, {size_info}). \
230 Cannot read as text. \
231 Inspect with: duckdb \"SELECT * FROM '{path}' LIMIT 5\" \
232 or: python -c \"import pyarrow.parquet as pq; print(pq.read_schema('{path}'))\""
233 );
234 }
235 format!(
236 "Binary file detected (.{ext}, {label}). \
237 lean-ctx cannot read binary files as text. \
238 Use a specialized tool for this file type."
239 )
240}
241
242fn format_file_size(bytes: u64) -> String {
243 if bytes >= 1_073_741_824 {
244 format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
245 } else if bytes >= 1_048_576 {
246 format!("{:.1} MB", bytes as f64 / 1_048_576.0)
247 } else if bytes >= 1024 {
248 format!("{:.1} KB", bytes as f64 / 1024.0)
249 } else {
250 format!("{bytes} B")
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::{
257 binary_file_message, has_binary_content, has_binary_extension, image_mime_type,
258 is_llm_viewable_image,
259 };
260
261 #[test]
262 fn detects_binary_extensions() {
263 assert!(has_binary_extension("data.parquet"));
264 assert!(has_binary_extension("model.onnx"));
265 assert!(has_binary_extension("archive.tar.gz"));
266 assert!(has_binary_extension("photo.PNG"));
267 assert!(has_binary_extension("/path/to/file.sqlite3"));
268 }
269
270 #[test]
271 fn rejects_text_extensions() {
272 assert!(!has_binary_extension("main.rs"));
273 assert!(!has_binary_extension("config.toml"));
274 assert!(!has_binary_extension("README.md"));
275 assert!(!has_binary_extension("script.py"));
276 assert!(!has_binary_extension("icons/logo.svg"));
277 assert!(!has_binary_extension("ICON.SVG"));
278 }
279
280 #[test]
281 fn message_includes_type() {
282 let msg = binary_file_message("data.parquet");
283 assert!(msg.contains("columnar data file"));
284 assert!(msg.contains(".parquet"));
285 }
286
287 #[test]
288 fn parquet_message_includes_tool_suggestion() {
289 let msg = binary_file_message("data.parquet");
290 assert!(msg.contains("duckdb"));
291 assert!(msg.contains("columnar data"));
292 }
293
294 #[test]
295 fn message_for_unknown_binary() {
296 let msg = binary_file_message("file.xyz");
297 assert!(msg.contains("binary file"));
298 }
299
300 #[test]
301 fn null_byte_detection() {
302 let dir = std::env::temp_dir().join("lean_ctx_binary_test");
303 std::fs::create_dir_all(&dir).ok();
304
305 let bin_path = dir.join("test.bin");
306 std::fs::write(&bin_path, b"\x00\x01\x02\x03").unwrap();
307 assert!(has_binary_content(bin_path.to_str().unwrap()));
308
309 let txt_path = dir.join("test.txt");
310 std::fs::write(&txt_path, b"hello world").unwrap();
311 assert!(!has_binary_content(txt_path.to_str().unwrap()));
312
313 std::fs::remove_dir_all(&dir).ok();
314 }
315
316 #[test]
317 fn llm_viewable_detects_supported_formats() {
318 assert!(is_llm_viewable_image("photo.png"));
319 assert!(is_llm_viewable_image("photo.PNG"));
320 assert!(is_llm_viewable_image("image.jpg"));
321 assert!(is_llm_viewable_image("image.jpeg"));
322 assert!(is_llm_viewable_image("anim.gif"));
323 assert!(is_llm_viewable_image("modern.webp"));
324 assert!(is_llm_viewable_image("/path/to/file.JPEG"));
325 }
326
327 #[test]
328 fn llm_viewable_rejects_unsupported() {
329 assert!(!is_llm_viewable_image("icon.svg"));
330 assert!(!is_llm_viewable_image("photo.heic"));
331 assert!(!is_llm_viewable_image("image.tiff"));
332 assert!(!is_llm_viewable_image("design.psd"));
333 assert!(!is_llm_viewable_image("photo.avif"));
334 assert!(!is_llm_viewable_image("code.rs"));
335 assert!(!is_llm_viewable_image("data.bin"));
336 }
337
338 #[test]
339 fn mime_type_correct() {
340 assert_eq!(image_mime_type("x.png"), Some("image/png"));
341 assert_eq!(image_mime_type("x.jpg"), Some("image/jpeg"));
342 assert_eq!(image_mime_type("x.jpeg"), Some("image/jpeg"));
343 assert_eq!(image_mime_type("x.gif"), Some("image/gif"));
344 assert_eq!(image_mime_type("x.webp"), Some("image/webp"));
345 assert_eq!(image_mime_type("x.svg"), None);
346 assert_eq!(image_mime_type("x.rs"), None);
347 }
348}