diff options
author | diegodox | 2021-11-04 03:24:52 +0000 |
---|---|---|
committer | GitHub | 2021-11-04 03:24:52 +0000 |
commit | 70d21a903fef3ec0787c453f369d95e5223a2656 (patch) | |
tree | 27b1461ef8bc7e530c9d19c7293a9d3241685555 | |
parent | 5b5d1b9dfff6b522559174f7f8e99aeb82c674a9 (diff) |
Prevent preview binary or large file (#939)
* Prevent preview binary or large file (#847)
* fix wrong method name
* fix add use trait
* update lock file
* rename MAX_PREVIEW_SIZE from MAX_BYTE_PREVIEW
* read small bytes to determine cotent type
* [WIP] add preview struct to represent calcurated preveiw
* Refactor content type detection
- Remove unwraps
- Reuse a single read buffer to avoid 1kb reallocations between previews
* Refactor preview rendering so we don't construct docs when not necessary
* Replace unwarap whit Preview::NotFound
* Use index access to hide unwrap
Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
* fix Get and unwarp equivalent to referce of Index acess
* better preview implementation
* Rename Preview enum and vairant
Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
* fixup! Rename Preview enum and vairant
* simplify long match
* Center text, add docs, fix formatting, refactor
Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
-rw-r--r-- | Cargo.lock | 10 | ||||
-rw-r--r-- | helix-term/Cargo.toml | 2 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 123 |
3 files changed, 115 insertions, 20 deletions
@@ -111,6 +111,15 @@ dependencies = [ ] [[package]] +name = "content_inspector" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" +dependencies = [ + "memchr", +] + +[[package]] name = "crossbeam-utils" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -415,6 +424,7 @@ version = "0.5.0" dependencies = [ "anyhow", "chrono", + "content_inspector", "crossterm", "fern", "futures-util", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 45b4eb2c..a0079feb 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -46,6 +46,8 @@ fuzzy-matcher = "0.3" ignore = "0.4" # markdown doc rendering pulldown-cmark = { version = "0.8", default-features = false } +# file type detection +content_inspector = "0.2.4" # config toml = "0.5" diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 7fc6af0f..291f1f85 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; use tui::widgets::Widget; -use std::{borrow::Cow, collections::HashMap, path::PathBuf}; +use std::{ + borrow::Cow, + collections::HashMap, + io::Read, + path::{Path, PathBuf}, +}; use crate::ui::{Prompt, PromptEvent}; use helix_core::Position; @@ -23,18 +28,58 @@ use helix_view::{ }; pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; +/// Biggest file size to preview in bytes +pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; -/// File path and line number (used to align and highlight a line) +/// File path and range of lines (used to align and highlight lines) type FileLocation = (PathBuf, Option<(usize, usize)>); pub struct FilePicker<T> { picker: Picker<T>, /// Caches paths to documents - preview_cache: HashMap<PathBuf, Document>, + preview_cache: HashMap<PathBuf, CachedPreview>, + read_buffer: Vec<u8>, /// Given an item in the picker, return the file path and line number to display. file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>, } +pub enum CachedPreview { + Document(Document), + Binary, + LargeFile, + NotFound, +} + +// We don't store this enum in the cache so as to avoid lifetime constraints +// from borrowing a document already opened in the editor. +pub enum Preview<'picker, 'editor> { + Cached(&'picker CachedPreview), + EditorDocument(&'editor Document), +} + +impl Preview<'_, '_> { + fn document(&self) -> Option<&Document> { + match self { + Preview::EditorDocument(doc) => Some(doc), + Preview::Cached(CachedPreview::Document(doc)) => Some(doc), + _ => None, + } + } + + /// Alternate text to show for the preview. + fn placeholder(&self) -> &str { + match *self { + Self::EditorDocument(_) => "<File preview>", + Self::Cached(preview) => match preview { + CachedPreview::Document(_) => "<File preview>", + CachedPreview::Binary => "<Binary file>", + CachedPreview::LargeFile => "<File too large to preview>", + CachedPreview::NotFound => "<File not found>", + }, + } + } +} + impl<T> FilePicker<T> { pub fn new( options: Vec<T>, @@ -45,6 +90,7 @@ impl<T> FilePicker<T> { Self { picker: Picker::new(false, options, format_fn, callback_fn), preview_cache: HashMap::new(), + read_buffer: Vec::with_capacity(1024), file_fn: Box::new(preview_fn), } } @@ -60,14 +106,45 @@ impl<T> FilePicker<T> { }) } - fn calculate_preview(&mut self, editor: &Editor) { - if let Some((path, _line)) = self.current_file(editor) { - if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() { - // TODO: enable syntax highlighting; blocked by async rendering - let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap(); - self.preview_cache.insert(path, doc); - } + /// Get (cached) preview for a given path. If a document corresponding + /// to the path is already open in the editor, it is used instead. + fn get_preview<'picker, 'editor>( + &'picker mut self, + path: &Path, + editor: &'editor Editor, + ) -> Preview<'picker, 'editor> { + if let Some(doc) = editor.document_by_path(path) { + return Preview::EditorDocument(doc); + } + + if self.preview_cache.contains_key(path) { + return Preview::Cached(&self.preview_cache[path]); } + + let data = std::fs::File::open(path).and_then(|file| { + let metadata = file.metadata()?; + // Read up to 1kb to detect the content type + let n = file.take(1024).read_to_end(&mut self.read_buffer)?; + let content_type = content_inspector::inspect(&self.read_buffer[..n]); + self.read_buffer.clear(); + Ok((metadata, content_type)) + }); + let preview = data + .map( + |(metadata, content_type)| match (metadata.len(), content_type) { + (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary, + (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile, + _ => { + // TODO: enable syntax highlighting; blocked by async rendering + Document::open(path, None, Some(&editor.theme), None) + .map(CachedPreview::Document) + .unwrap_or(CachedPreview::NotFound) + } + }, + ) + .unwrap_or(CachedPreview::NotFound); + self.preview_cache.insert(path.to_owned(), preview); + Preview::Cached(&self.preview_cache[path]) } } @@ -79,12 +156,12 @@ impl<T: 'static> Component for FilePicker<T> { // |picker | | | // | | | | // +---------+ +---------+ - self.calculate_preview(cx.editor); let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; let area = inner_rect(area); // -- Render the frame: // clear area let background = cx.editor.theme.get("ui.background"); + let text = cx.editor.theme.get("ui.text"); surface.clear_with(area, background); let picker_width = if render_preview { @@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> { horizontal: 1, }; let inner = inner.inner(&margin); - block.render(preview_area, surface); - if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| { - cx.editor - .document_by_path(&path) - .or_else(|| self.preview_cache.get(&path)) - .zip(Some(range)) - }) { + if let Some((path, range)) = self.current_file(cx.editor) { + let preview = self.get_preview(&path, cx.editor); + let doc = match preview.document() { + Some(doc) => doc, + None => { + let alt_text = preview.placeholder(); + let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2; + let y = inner.y + inner.height / 2; + surface.set_stringn(x, y, alt_text, inner.width as usize, text); + return; + } + }; + // align to middle - let first_line = line + let first_line = range .map(|(start, end)| { let height = end.saturating_sub(start) + 1; let middle = start + (height.saturating_sub(1) / 2); @@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> { ); // highlight the line - if let Some((start, end)) = line { + if let Some((start, end)) = range { let offset = start.saturating_sub(first_line) as u16; surface.set_style( Rect::new( |