Skip to main content

trail/preview/
binary.rs

1//! Binary file preview provider.
2//!
3//! Displays file metadata (size, type, modification timestamp) for non-text,
4//! non-image binary files, from the metadata the directory listing already
5//! collected — so this provider performs no I/O of its own.
6//!
7//! Entry routing:
8//! - `ImageProvider` matches image files first (registered before
9//!   `BinaryProvider` in the registry).
10//! - `TextProvider` takes every file *not yet* classified, because the worker it
11//!   spawns reads the file and can classify it on the way past.
12//! - `BinaryProvider` therefore serves files already known to be binary: the
13//!   second and later previews of one, after `workers::merge` cached what that
14//!   worker found.
15
16use std::path::Path;
17
18use crate::app::state::{Entry, EntryKind};
19use crate::preview::provider::{PreviewContent, PreviewCtx, PreviewOutcome, PreviewProvider};
20
21/// Known image extensions handled by `ImageProvider`.
22///
23/// `BinaryProvider::can_handle` returns `false` for these so the image provider
24/// gets first pick.
25const IMAGE_EXTENSIONS: &[&str] = &[
26    "png", "jpg", "jpeg", "gif", "bmp", "ico", "tiff", "tif", "webp", "avif", "svg",
27];
28
29/// Preview provider for binary (non-text, non-image) files.
30///
31/// Returns file metadata (size, kind, modification time) synchronously from
32/// cached `Entry::metadata`. No worker task is spawned because the metadata
33/// was already collected by the directory listing.
34pub struct BinaryProvider;
35
36impl PreviewProvider for BinaryProvider {
37    fn can_handle(&self, entry: &Entry) -> bool {
38        if entry.kind != EntryKind::File {
39            return false;
40        }
41        // Defer to ImageProvider for image files.
42        if is_image_path(&entry.path) {
43            return false;
44        }
45        // Only a file already *known* to be binary. `TextProvider` runs first
46        // and takes every unclassified file, so this arm is reached on the
47        // second and later previews of a binary file — the highlight worker
48        // classified it the first time and `workers::merge` cached that.
49        entry.is_text == Some(false)
50    }
51
52    fn preview(&self, entry: &Entry, _ctx: &PreviewCtx) -> PreviewOutcome {
53        let content = build_binary_preview(&entry.path, entry.metadata.as_ref());
54        PreviewOutcome::Ready(content)
55    }
56}
57
58/// Returns `true` if `path` has a known image file extension.
59fn is_image_path(path: &Path) -> bool {
60    path.extension()
61        .and_then(|e| e.to_str())
62        .map(|ext| IMAGE_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
63        .unwrap_or(false)
64}
65
66/// Builds a `PreviewContent::Binary` describing `path`.
67///
68/// Reads metadata from the already-cached `metadata` if present, or performs a
69/// fresh `fs::metadata` call if not. Returns `PreviewContent::Empty` only if
70/// neither source works.
71pub fn build_binary_preview(path: &Path, metadata: Option<&std::fs::Metadata>) -> PreviewContent {
72    let owned;
73    let meta: &std::fs::Metadata = match metadata {
74        Some(m) => m,
75        None => {
76            owned = match std::fs::metadata(path) {
77                Ok(m) => m,
78                Err(e) => {
79                    return PreviewContent::Binary(vec![format!("  Cannot read metadata: {e}")])
80                }
81            };
82            &owned
83        }
84    };
85
86    // The same formatters the listing's details column uses, so a file cannot
87    // be described one way in the pane and another in the list.
88    let size_str = crate::metafmt::size(meta.len());
89
90    let ext = path
91        .extension()
92        .and_then(|e| e.to_str())
93        .unwrap_or("unknown")
94        .to_uppercase();
95
96    let modified = crate::metafmt::modified(meta.modified().ok());
97
98    let mut lines = vec![
99        format!("  Type     : {} binary", ext),
100        format!("  Size     : {}", size_str),
101        format!("  Modified : {}", modified),
102    ];
103
104    // Include a hex dump hint if the file is non-empty.
105    if meta.len() > 0 {
106        lines.push(String::new());
107        lines.push("  (binary content — no text preview)".to_owned());
108    }
109
110    PreviewContent::Binary(lines)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use std::io::Write;
117    use tempfile::NamedTempFile;
118
119    #[test]
120    fn binary_preview_includes_size() {
121        let mut f = NamedTempFile::new().unwrap();
122        f.write_all(&[0u8; 1024]).unwrap();
123        let meta = std::fs::metadata(f.path()).unwrap();
124        let content = build_binary_preview(f.path(), Some(&meta));
125        if let PreviewContent::Binary(lines) = content {
126            let combined = lines.join("\n");
127            assert!(
128                combined.contains("1.02 kB")
129                    || combined.contains("1 kB")
130                    || combined.contains("1024"),
131                "expected size info in: {combined}"
132            );
133        } else {
134            panic!("expected Binary variant");
135        }
136    }
137
138    #[test]
139    fn is_image_path_recognises_png() {
140        let p = Path::new("photo.PNG");
141        assert!(is_image_path(p));
142    }
143
144    #[test]
145    fn is_image_path_ignores_rs() {
146        let p = Path::new("main.rs");
147        assert!(!is_image_path(p));
148    }
149}