summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiegodox2021-11-04 03:24:52 +0000
committerGitHub2021-11-04 03:24:52 +0000
commit70d21a903fef3ec0787c453f369d95e5223a2656 (patch)
tree27b1461ef8bc7e530c9d19c7293a9d3241685555
parent5b5d1b9dfff6b522559174f7f8e99aeb82c674a9 (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.lock10
-rw-r--r--helix-term/Cargo.toml2
-rw-r--r--helix-term/src/ui/picker.rs123
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
@@ -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(