Skip to content

Commit

Permalink
Check for external file modifications when writing (#5805)
Browse files Browse the repository at this point in the history
`:write` and other file-saving commands now check the file modification
time before writing to protect against overwriting external changes.

Co-authored-by: Gustavo Noronha Silva <[email protected]>
Co-authored-by: LeoniePhiline <[email protected]>
Co-authored-by: Pascal Kuthe <[email protected]>
  • Loading branch information
4 people authored Feb 8, 2023
1 parent 00ecc55 commit f386ff7
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 4 deletions.
2 changes: 1 addition & 1 deletion helix-term/tests/test/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
const RANGE: RangeInclusive<i32> = 1..=1000;

for i in RANGE {
let cmd = format!("%c{}<esc>:w<ret>", i);
let cmd = format!("%c{}<esc>:w!<ret>", i);
command.push_str(&cmd);
}

Expand Down
6 changes: 6 additions & 0 deletions helix-term/tests/test/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,12 @@ impl AppBuilder {
}
}

pub async fn run_event_loop_until_idle(app: &mut Application) {
let (_, rx) = tokio::sync::mpsc::unbounded_channel();
let mut rx_stream = UnboundedReceiverStream::new(rx);
app.event_loop_until_idle(&mut rx_stream).await;
}

pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> {
file.flush()?;
file.sync_all()?;
Expand Down
36 changes: 34 additions & 2 deletions helix-term/tests/test/write.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
io::{Read, Write},
io::{Read, Seek, SeekFrom, Write},
ops::RangeInclusive,
};

Expand Down Expand Up @@ -37,6 +37,38 @@ async fn test_write() -> anyhow::Result<()> {
Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_overwrite_protection() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;

helpers::run_event_loop_until_idle(&mut app).await;

file.as_file_mut()
.write_all(helpers::platform_line("extremely important content").as_bytes())?;

file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;

test_key_sequence(&mut app, Some(":x<ret>"), None, false).await?;

file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;

file.seek(SeekFrom::Start(0))?;
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;

assert_eq!(
helpers::platform_line("extremely important content"),
file_content
);

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_write_quit() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
Expand Down Expand Up @@ -76,7 +108,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
.build()?;

for i in RANGE {
let cmd = format!("%c{}<esc>:w<ret>", i);
let cmd = format!("%c{}<esc>:w!<ret>", i);
command.push_str(&cmd);
}

Expand Down
25 changes: 24 additions & 1 deletion helix-view/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::future::Future;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;

use helix_core::{
encoding,
Expand Down Expand Up @@ -135,6 +136,10 @@ pub struct Document {

pub savepoint: Option<Transaction>,

// Last time we wrote to the file. This will carry the time the file was last opened if there
// were no saves.
last_saved_time: SystemTime,

last_saved_revision: usize,
version: i32, // should be usize?
pub(crate) modified_since_accessed: bool,
Expand All @@ -160,6 +165,7 @@ impl fmt::Debug for Document {
.field("changes", &self.changes)
.field("old_state", &self.old_state)
// .field("history", &self.history)
.field("last_saved_time", &self.last_saved_time)
.field("last_saved_revision", &self.last_saved_revision)
.field("version", &self.version)
.field("modified_since_accessed", &self.modified_since_accessed)
Expand Down Expand Up @@ -382,6 +388,7 @@ impl Document {
version: 0,
history: Cell::new(History::default()),
savepoint: None,
last_saved_time: SystemTime::now(),
last_saved_revision: 0,
modified_since_accessed: false,
language_server: None,
Expand Down Expand Up @@ -577,9 +584,11 @@ impl Document {

let encoding = self.encoding;

let last_saved_time = self.last_saved_time;

// We encode the file according to the `Document`'s encoding.
let future = async move {
use tokio::fs::File;
use tokio::{fs, fs::File};
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
if !parent.exists() {
Expand All @@ -591,6 +600,17 @@ impl Document {
}
}

// Protect against overwriting changes made externally
if !force {
if let Ok(metadata) = fs::metadata(&path).await {
if let Ok(mtime) = metadata.modified() {
if last_saved_time < mtime {
bail!("file modified by an external process, use :w! to overwrite");
}
}
}
}

let mut file = File::create(&path).await?;
to_writer(&mut file, encoding, &text).await?;

Expand Down Expand Up @@ -668,6 +688,8 @@ impl Document {
self.append_changes_to_history(view);
self.reset_modified();

self.last_saved_time = SystemTime::now();

self.detect_indent_and_line_ending();

match provider_registry.get_diff_base(&path) {
Expand Down Expand Up @@ -1016,6 +1038,7 @@ impl Document {
rev
);
self.last_saved_revision = rev;
self.last_saved_time = SystemTime::now();
}

/// Get the document's latest saved revision.
Expand Down

0 comments on commit f386ff7

Please sign in to comment.