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>
pull/950/head
diegodox 3 years ago committed by GitHub
parent 5b5d1b9dff
commit 70d21a903f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

10
Cargo.lock generated

@ -110,6 +110,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "content_inspector"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.5" version = "0.8.5"
@ -415,6 +424,7 @@ version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"content_inspector",
"crossterm", "crossterm",
"fern", "fern",
"futures-util", "futures-util",

@ -46,6 +46,8 @@ fuzzy-matcher = "0.3"
ignore = "0.4" ignore = "0.4"
# markdown doc rendering # markdown doc rendering
pulldown-cmark = { version = "0.8", default-features = false } pulldown-cmark = { version = "0.8", default-features = false }
# file type detection
content_inspector = "0.2.4"
# config # config
toml = "0.5" toml = "0.5"

@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget; 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 crate::ui::{Prompt, PromptEvent};
use helix_core::Position; use helix_core::Position;
@ -23,18 +28,58 @@ use helix_view::{
}; };
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; 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)>); type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> { pub struct FilePicker<T> {
picker: Picker<T>, picker: Picker<T>,
/// Caches paths to documents /// 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. /// Given an item in the picker, return the file path and line number to display.
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>, 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> { impl<T> FilePicker<T> {
pub fn new( pub fn new(
options: Vec<T>, options: Vec<T>,
@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
Self { Self {
picker: Picker::new(false, options, format_fn, callback_fn), picker: Picker::new(false, options, format_fn, callback_fn),
preview_cache: HashMap::new(), preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024),
file_fn: Box::new(preview_fn), file_fn: Box::new(preview_fn),
} }
} }
@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
}) })
} }
fn calculate_preview(&mut self, editor: &Editor) { /// Get (cached) preview for a given path. If a document corresponding
if let Some((path, _line)) = self.current_file(editor) { /// to the path is already open in the editor, it is used instead.
if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() { fn get_preview<'picker, 'editor>(
// TODO: enable syntax highlighting; blocked by async rendering &'picker mut self,
let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap(); path: &Path,
self.preview_cache.insert(path, doc); 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 | | | // |picker | | |
// | | | | // | | | |
// +---------+ +---------+ // +---------+ +---------+
self.calculate_preview(cx.editor);
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
let area = inner_rect(area); let area = inner_rect(area);
// -- Render the frame: // -- Render the frame:
// clear area // clear area
let background = cx.editor.theme.get("ui.background"); let background = cx.editor.theme.get("ui.background");
let text = cx.editor.theme.get("ui.text");
surface.clear_with(area, background); surface.clear_with(area, background);
let picker_width = if render_preview { let picker_width = if render_preview {
@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
horizontal: 1, horizontal: 1,
}; };
let inner = inner.inner(&margin); let inner = inner.inner(&margin);
block.render(preview_area, surface); block.render(preview_area, surface);
if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| { if let Some((path, range)) = self.current_file(cx.editor) {
cx.editor let preview = self.get_preview(&path, cx.editor);
.document_by_path(&path) let doc = match preview.document() {
.or_else(|| self.preview_cache.get(&path)) Some(doc) => doc,
.zip(Some(range)) 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 // align to middle
let first_line = line let first_line = range
.map(|(start, end)| { .map(|(start, end)| {
let height = end.saturating_sub(start) + 1; let height = end.saturating_sub(start) + 1;
let middle = start + (height.saturating_sub(1) / 2); let middle = start + (height.saturating_sub(1) / 2);
@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
); );
// highlight the line // highlight the line
if let Some((start, end)) = line { if let Some((start, end)) = range {
let offset = start.saturating_sub(first_line) as u16; let offset = start.saturating_sub(first_line) as u16;
surface.set_style( surface.set_style(
Rect::new( Rect::new(

Loading…
Cancel
Save