Skip to content

Commit

Permalink
Prevent preview binary or large file (#939)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* fix Get and unwarp equivalent to referce of Index acess

* better preview implementation

* Rename Preview enum and vairant

Co-authored-by: Gokul Soumya <[email protected]>

* fixup! Rename Preview enum and vairant

* simplify long match

* Center text, add docs, fix formatting, refactor

Co-authored-by: Blaž Hrastnik <[email protected]>
Co-authored-by: Gokul Soumya <[email protected]>
  • Loading branch information
3 people authored Nov 4, 2021
1 parent 5b5d1b9 commit 70d21a9
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 20 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions helix-term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
123 changes: 103 additions & 20 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>,
Expand All @@ -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),
}
}
Expand All @@ -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])
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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(
Expand Down

0 comments on commit 70d21a9

Please sign in to comment.