From 70d21a903fef3ec0787c453f369d95e5223a2656 Mon Sep 17 00:00:00 2001 From: diegodox Date: Thu, 4 Nov 2021 12:24:52 +0900 Subject: 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 * fix Get and unwarp equivalent to referce of Index acess * better preview implementation * Rename Preview enum and vairant Co-authored-by: Gokul Soumya * fixup! Rename Preview enum and vairant * simplify long match * Center text, add docs, fix formatting, refactor Co-authored-by: Blaž Hrastnik Co-authored-by: Gokul Soumya --- Cargo.lock | 10 ++++ helix-term/Cargo.toml | 2 + helix-term/src/ui/picker.rs | 123 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 115 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ce8ee8f..e036828a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,15 @@ dependencies = [ "winapi", ] +[[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" @@ -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 { picker: Picker, /// Caches paths to documents - preview_cache: HashMap, + preview_cache: HashMap, + read_buffer: Vec, /// Given an item in the picker, return the file path and line number to display. file_fn: Box Option>, } +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(_) => "", + Self::Cached(preview) => match preview { + CachedPreview::Document(_) => "", + CachedPreview::Binary => "", + CachedPreview::LargeFile => "", + CachedPreview::NotFound => "", + }, + } + } +} + impl FilePicker { pub fn new( options: Vec, @@ -45,6 +90,7 @@ impl FilePicker { 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 FilePicker { }) } - 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 Component for FilePicker { // |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 Component for FilePicker { 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 Component for FilePicker { ); // 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( -- cgit v1.2.3-70-g09d2