on master help sign in
Check for external file modifications when writing (#5805)

`: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 <gustavo@noronha.dev.br> Co-authored-by: LeoniePhiline <22329650+LeoniePhiline@users.noreply.github.com> Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

commitf386ff795d4833cce02d57de921999284aadded3 authorClément Delafargue <clement@delafargue.name> dateWed 8 Feb 2023, 17:09 (4 years ago) parent00ecc55
4 files changed+65−4
helix-term/tests/test/commands.rs+1−1 helix-term/tests/test/helpers.rs+6 helix-term/tests/test/write.rs+34−2 helix-view/src/document.rs+24−1
helix-term/tests/test/commands.rs+1−1
@@ -67,7 +67,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
6767 const RANGE: RangeInclusive<i32> = 1..=1000;
6868
6969 for i in RANGE {
70− let cmd = format!("%c{}<esc>:w<ret>", i);
70+ let cmd = format!("%c{}<esc>:w!<ret>", i);
7171 command.push_str(&cmd);
7272 }
7373
helix-term/tests/test/helpers.rs+6
@@ -319,6 +319,12 @@ impl AppBuilder {
319319 }
320320}
321321
322+pub async fn run_event_loop_until_idle(app: &mut Application) {
323+ let (_, rx) = tokio::sync::mpsc::unbounded_channel();
324+ let mut rx_stream = UnboundedReceiverStream::new(rx);
325+ app.event_loop_until_idle(&mut rx_stream).await;
326+}
327+
322328pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> {
323329 file.flush()?;
324330 file.sync_all()?;
helix-term/tests/test/write.rs+34−2
@@ -1,5 +1,5 @@
11use std::{
2− io::{Read, Write},
2+ io::{Read, Seek, SeekFrom, Write},
33 ops::RangeInclusive,
44};
55
@@ -37,6 +37,38 @@ async fn test_write() -> anyhow::Result<()> {
3737 Ok(())
3838}
3939
40+#[tokio::test(flavor = "multi_thread")]
41+async fn test_overwrite_protection() -> anyhow::Result<()> {
42+ let mut file = tempfile::NamedTempFile::new()?;
43+ let mut app = helpers::AppBuilder::new()
44+ .with_file(file.path(), None)
45+ .build()?;
46+
47+ helpers::run_event_loop_until_idle(&mut app).await;
48+
49+ file.as_file_mut()
50+ .write_all(helpers::platform_line("extremely important content").as_bytes())?;
51+
52+ file.as_file_mut().flush()?;
53+ file.as_file_mut().sync_all()?;
54+
55+ test_key_sequence(&mut app, Some(":x<ret>"), None, false).await?;
56+
57+ file.as_file_mut().flush()?;
58+ file.as_file_mut().sync_all()?;
59+
60+ file.seek(SeekFrom::Start(0))?;
61+ let mut file_content = String::new();
62+ file.as_file_mut().read_to_string(&mut file_content)?;
63+
64+ assert_eq!(
65+ helpers::platform_line("extremely important content"),
66+ file_content
67+ );
68+
69+ Ok(())
70+}
71+
4072#[tokio::test(flavor = "multi_thread")]
4173async fn test_write_quit() -> anyhow::Result<()> {
4274 let mut file = tempfile::NamedTempFile::new()?;
@@ -76,7 +108,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
76108 .build()?;
77109
78110 for i in RANGE {
79− let cmd = format!("%c{}<esc>:w<ret>", i);
111+ let cmd = format!("%c{}<esc>:w!<ret>", i);
80112 command.push_str(&cmd);
81113 }
82114
helix-view/src/document.rs+24−1
@@ -19,6 +19,7 @@ use std::future::Future;
1919use std::path::{Path, PathBuf};
2020use std::str::FromStr;
2121use std::sync::Arc;
22+use std::time::SystemTime;
2223
2324use helix_core::{
2425 encoding,
@@ -135,6 +136,10 @@ pub struct Document {
135136
136137 pub savepoint: Option<Transaction>,
137138
139+ // Last time we wrote to the file. This will carry the time the file was last opened if there
140+ // were no saves.
141+ last_saved_time: SystemTime,
142+
138143 last_saved_revision: usize,
139144 version: i32, // should be usize?
140145 pub(crate) modified_since_accessed: bool,
@@ -160,6 +165,7 @@ impl fmt::Debug for Document {
160165 .field("changes", &self.changes)
161166 .field("old_state", &self.old_state)
162167 // .field("history", &self.history)
168+ .field("last_saved_time", &self.last_saved_time)
163169 .field("last_saved_revision", &self.last_saved_revision)
164170 .field("version", &self.version)
165171 .field("modified_since_accessed", &self.modified_since_accessed)
@@ -382,6 +388,7 @@ impl Document {
382388 version: 0,
383389 history: Cell::new(History::default()),
384390 savepoint: None,
391+ last_saved_time: SystemTime::now(),
385392 last_saved_revision: 0,
386393 modified_since_accessed: false,
387394 language_server: None,
@@ -577,9 +584,11 @@ impl Document {
577584
578585 let encoding = self.encoding;
579586
587+ let last_saved_time = self.last_saved_time;
588+
580589 // We encode the file according to the `Document`'s encoding.
581590 let future = async move {
582− use tokio::fs::File;
591+ use tokio::{fs, fs::File};
583592 if let Some(parent) = path.parent() {
584593 // TODO: display a prompt asking the user if the directories should be created
585594 if !parent.exists() {
@@ -591,6 +600,17 @@ impl Document {
591600 }
592601 }
593602
603+ // Protect against overwriting changes made externally
604+ if !force {
605+ if let Ok(metadata) = fs::metadata(&path).await {
606+ if let Ok(mtime) = metadata.modified() {
607+ if last_saved_time < mtime {
608+ bail!("file modified by an external process, use :w! to overwrite");
609+ }
610+ }
611+ }
612+ }
613+
594614 let mut file = File::create(&path).await?;
595615 to_writer(&mut file, encoding, &text).await?;
596616
@@ -668,6 +688,8 @@ impl Document {
668688 self.append_changes_to_history(view);
669689 self.reset_modified();
670690
691+ self.last_saved_time = SystemTime::now();
692+
671693 self.detect_indent_and_line_ending();
672694
673695 match provider_registry.get_diff_base(&path) {
@@ -1016,6 +1038,7 @@ impl Document {
10161038 rev
10171039 );
10181040 self.last_saved_revision = rev;
1041+ self.last_saved_time = SystemTime::now();
10191042 }
10201043
10211044 /// Get the document's latest saved revision.