falcon_library/binary_preview/
mod.rs1mod 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
25pub(crate) const MAX_ITEM: usize = 16 << 20;
27pub(crate) const MAX_SCAN: u64 = 1 << 30;
29pub(crate) const MAX_HITS: usize = 16;
31pub(crate) const HEAD: u64 = 16 << 20;
33
34pub fn handles(ext: &str) -> bool {
36 matches!(ext, "eps" | "ai" | "ait" | "indd" | "indt" | "blend" | "skp" | "dwg")
37}
38
39pub 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
53pub(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
60pub(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
72pub(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
77pub(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
85pub(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 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 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 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 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}