Skip to main content

falcon_library/binary_preview/
mod.rs

1//! The preview picture inside the binary files of EPS, Illustrator (PostScript
2//! flavour), InDesign, Blender, SketchUp and AutoCAD. Used at import when
3//! QuickLook has no render (no generator for the format is installed).
4//!
5//! Every file here is untrusted. Reads go through the helpers below: a bounded
6//! prefix, a checked range, or a linear scan that stops after [`MAX_SCAN`]
7//! bytes and [`MAX_HITS`] pictures. Pictures are decoded with size and memory
8//! limits, and anything malformed means "no preview" (`None`), never a panic.
9
10mod blend;
11mod dwg;
12mod eps;
13mod indesign;
14mod skp;
15
16use std::fs::File;
17use std::io::{Read, Seek, SeekFrom};
18use std::ops::ControlFlow;
19use std::path::Path;
20
21use image::DynamicImage;
22
23pub(crate) use crate::zip_preview::decode;
24
25/// The most bytes of one embedded picture or text block read.
26pub(crate) const MAX_ITEM: usize = 16 << 20;
27/// The most bytes a scan for an embedded picture reads from one file.
28pub(crate) const MAX_SCAN: u64 = 1 << 30;
29/// The most embedded pictures a scan offers before it stops.
30pub(crate) const MAX_HITS: usize = 16;
31/// The most bytes of the head of a PostScript or Blender file read.
32pub(crate) const HEAD: u64 = 16 << 20;
33
34/// The extensions this module reads a preview from.
35pub fn handles(ext: &str) -> bool {
36    matches!(ext, "eps" | "ai" | "ait" | "indd" | "indt" | "blend" | "skp" | "dwg")
37}
38
39/// The preview picture of the file at `path`; `None` when the format has none
40/// or the file is malformed.
41pub fn preview(path: &Path, ext: &str) -> Option<DynamicImage> {
42    let file = File::open(path).ok()?;
43    match ext {
44        "eps" | "ai" | "ait" => eps::read(file, path),
45        "indd" | "indt" => indesign::read(file, path),
46        "blend" => blend::read(file, path),
47        "skp" => skp::read(file, path),
48        "dwg" => dwg::read(file, path),
49        _ => None,
50    }
51}
52
53/// Up to `max` bytes from the current position of `r`.
54pub(crate) fn read_head<R: Read>(r: &mut R, max: u64) -> Option<Vec<u8>> {
55    let mut out = Vec::new();
56    r.take(max).read_to_end(&mut out).ok()?;
57    Some(out)
58}
59
60/// `len` bytes at `offset`, when that range is inside a file of `file_len`
61/// bytes and no bigger than `cap`. Nothing is allocated before those checks.
62pub(crate) fn read_at(file: &mut File, file_len: u64, offset: u64, len: u64, cap: usize) -> Option<Vec<u8>> {
63    if len == 0 || len > cap as u64 || offset.checked_add(len)? > file_len {
64        return None;
65    }
66    file.seek(SeekFrom::Start(offset)).ok()?;
67    let mut out = vec![0u8; len as usize];
68    file.read_exact(&mut out).ok()?;
69    Some(out)
70}
71
72/// The little-endian `u32` at `at`, when it is inside `d`.
73pub(crate) fn le32(d: &[u8], at: usize) -> Option<u32> {
74    Some(u32::from_le_bytes(d.get(at..at.checked_add(4)?)?.try_into().ok()?))
75}
76
77/// Where `needle` first starts in `hay`.
78pub(crate) fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
79    if needle.is_empty() || hay.len() < needle.len() {
80        return None;
81    }
82    hay.windows(needle.len()).position(|w| w == needle)
83}
84
85/// Offers `visit` each stretch of `r` that starts with `open` and ends with
86/// the next `close`, markers included, in order. When an opening comes again
87/// before the close (a cut-off picture), the stretch starts at the last one.
88///
89/// Linear in the bytes read: at most [`MAX_SCAN`] are read, at most
90/// [`MAX_ITEM`] held, a stretch that outgrows that is dropped, and the scan
91/// ends after [`MAX_HITS`] offers or when `visit` breaks.
92pub(crate) fn scan<R: Read>(mut r: R, open: &[u8], close: &[u8], mut visit: impl FnMut(&[u8]) -> ControlFlow<()>) {
93    if open.is_empty() || close.is_empty() {
94        return;
95    }
96    let mut chunk = vec![0u8; 1 << 20];
97    let mut window: Vec<u8> = Vec::new();
98    // `window` starts with `open`, and has no `close` before `searched`.
99    let mut opened = false;
100    let mut searched = 0;
101    let mut hits = 0;
102    let mut total = 0u64;
103    loop {
104        loop {
105            if !opened {
106                let Some(s) = find(&window, open) else {
107                    let keep = open.len() - 1;
108                    if window.len() > keep {
109                        window.drain(..window.len() - keep);
110                    }
111                    break;
112                };
113                window.drain(..s);
114                opened = true;
115                searched = open.len();
116            }
117            let Some(e) = find(&window[searched..], close) else {
118                if window.len() > MAX_ITEM + close.len() {
119                    // Too long to be a picture: look for the next opening.
120                    window.drain(..window.len() - (open.len() - 1));
121                    opened = false;
122                } else {
123                    searched = window.len().saturating_sub(close.len() - 1).max(open.len());
124                }
125                break;
126            };
127            let end = searched + e + close.len();
128            let start = window[..end].windows(open.len()).rposition(|w| w == open).unwrap_or(0);
129            if end - start <= MAX_ITEM {
130                hits += 1;
131                if visit(&window[start..end]).is_break() || hits >= MAX_HITS {
132                    return;
133                }
134            }
135            window.drain(..end);
136            opened = false;
137        }
138        if total >= MAX_SCAN {
139            return;
140        }
141        match r.read(&mut chunk) {
142            Ok(0) | Err(_) => return,
143            Ok(n) => {
144                window.extend_from_slice(&chunk[..n]);
145                total += n as u64;
146            }
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    fn hits(data: &[u8], open: &[u8], close: &[u8]) -> Vec<Vec<u8>> {
156        let mut out = Vec::new();
157        scan(std::io::Cursor::new(data), open, close, |h| {
158            out.push(h.to_vec());
159            ControlFlow::Continue(())
160        });
161        out
162    }
163
164    #[test]
165    fn scan_finds_stretches_with_their_markers() {
166        assert_eq!(hits(b"xx<a>1</a>yy<a>22</a>", b"<a>", b"</a>"), vec![b"<a>1</a>".to_vec(), b"<a>22</a>".to_vec()]);
167        assert!(hits(b"", b"<a>", b"</a>").is_empty());
168        assert!(hits(b"<a>no close", b"<a>", b"</a>").is_empty());
169        assert!(hits(b"</a>no open", b"<a>", b"</a>").is_empty());
170    }
171
172    #[test]
173    fn scan_starts_a_cut_off_stretch_at_the_last_opening() {
174        assert_eq!(hits(b"<a>cut<a>whole</a>", b"<a>", b"</a>"), vec![b"<a>whole</a>".to_vec()]);
175    }
176
177    #[test]
178    fn scan_finds_markers_across_read_chunks() {
179        let mut d = vec![b'.'; (1 << 20) - 2];
180        d.extend(b"<a>");
181        d.extend(vec![b'x'; (1 << 20) - 3]);
182        d.extend(b"</a>");
183        let got = hits(&d, b"<a>", b"</a>");
184        assert_eq!(got.len(), 1);
185        assert_eq!(got[0].len(), 3 + (1 << 20) - 3 + 4);
186    }
187
188    #[test]
189    fn scan_drops_an_overlong_stretch_and_finds_the_next() {
190        let mut d = b"<a>".to_vec();
191        d.resize(MAX_ITEM + (3 << 20), b'x');
192        d.extend(b"<a>ok</a>");
193        assert_eq!(hits(&d, b"<a>", b"</a>"), vec![b"<a>ok</a>".to_vec()]);
194    }
195
196    #[test]
197    fn scan_stops_at_the_hit_limit_and_when_told() {
198        let d = b"<a></a>".repeat(MAX_HITS * 3);
199        assert_eq!(hits(&d, b"<a>", b"</a>").len(), MAX_HITS);
200        let mut n = 0;
201        scan(std::io::Cursor::new(&d), b"<a>", b"</a>", |_| {
202            n += 1;
203            ControlFlow::Break(())
204        });
205        assert_eq!(n, 1);
206    }
207
208    #[test]
209    fn scan_of_endless_openings_finishes_quickly() {
210        // Would be quadratic if each opening searched the window again.
211        let d = b"<a>".repeat(8 << 20);
212        let t = std::time::Instant::now();
213        assert!(hits(&d, b"<a>", b"</a>").is_empty());
214        assert!(t.elapsed().as_secs() < 30, "took {:?}", t.elapsed());
215    }
216
217    #[test]
218    fn read_at_refuses_ranges_outside_the_file_or_over_the_cap() {
219        let dir = tempfile::tempdir().unwrap();
220        let p = dir.path().join("f");
221        std::fs::write(&p, b"0123456789").unwrap();
222        let mut f = File::open(&p).unwrap();
223        assert_eq!(read_at(&mut f, 10, 2, 3, 100).unwrap(), b"234");
224        assert!(read_at(&mut f, 10, 8, 3, 100).is_none());
225        assert!(read_at(&mut f, 10, u64::MAX, 3, 100).is_none());
226        assert!(read_at(&mut f, 10, 0, 0, 100).is_none());
227        assert!(read_at(&mut f, 10, 0, 5, 4).is_none());
228        // A stated length near the top of memory allocates nothing.
229        assert!(read_at(&mut f, u64::MAX, 0, u64::MAX, MAX_ITEM).is_none());
230    }
231
232    #[test]
233    fn unknown_extensions_are_not_handled() {
234        assert!(!handles("pdf") && !handles("mrw") && handles("dwg") && handles("blend"));
235        let dir = tempfile::tempdir().unwrap();
236        let p = dir.path().join("a.txt");
237        std::fs::write(&p, b"x").unwrap();
238        assert!(preview(&p, "txt").is_none());
239        assert!(preview(&dir.path().join("absent.eps"), "eps").is_none());
240    }
241}