diff --git a/rust/Cargo.lock b/rust/Cargo.lock --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -180,7 +180,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -435,10 +435,16 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -467,6 +473,18 @@ dependencies = [ ] [[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] name = "flate2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -584,6 +602,8 @@ dependencies = [ "clap", "crossbeam-channel", "derive_more", + "dyn-clone", + "filetime", "flate2", "format-bytes", "hashbrown 0.13.1", @@ -752,6 +772,7 @@ checksum = "c0ff37bd590ca25063e35af745c3 dependencies = [ "bitflags 2.6.0", "libc", + "redox_syscall 0.5.3", ] [[package]] @@ -1123,6 +1144,15 @@ dependencies = [ ] [[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] name = "redox_users" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1348,7 +1378,7 @@ dependencies = [ "cfg-if", "fastrand", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "remove_dir_all", "winapi", ] @@ -1615,6 +1645,15 @@ dependencies = [ ] [[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1631,17 +1670,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1652,9 +1692,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1664,9 +1704,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219 [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1676,9 +1716,15 @@ checksum = "a75915e7def60c94dcef72200b9a [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1688,9 +1734,9 @@ checksum = "8f55c233f70c4b27f66c523580f7 [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1700,9 +1746,9 @@ checksum = "53d40abd2583d23e4718fddf1ebe [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1712,9 +1758,9 @@ checksum = "0b7b52767868a23d5bab768e390d [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1724,9 +1770,9 @@ checksum = "ed94fce61571a4006852b7389a06 [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wyz" diff --git a/rust/hg-core/Cargo.toml b/rust/hg-core/Cargo.toml --- a/rust/hg-core/Cargo.toml +++ b/rust/hg-core/Cargo.toml @@ -41,6 +41,8 @@ format-bytes = "0.3.0" once_cell = "1.16.0" bitvec = "1.0.1" chrono = "0.4.34" +dyn-clone = "1.0.16" +filetime = "0.2.23" # We don't use the `miniz-oxide` backend to not change rhg benchmarks and until # we have a clearer view of which backend is the fastest. diff --git a/rust/hg-core/src/lock.rs b/rust/hg-core/src/lock.rs --- a/rust/hg-core/src/lock.rs +++ b/rust/hg-core/src/lock.rs @@ -2,7 +2,7 @@ use crate::errors::HgError; use crate::errors::HgResultExt; -use crate::vfs::Vfs; +use crate::vfs::VfsImpl; use std::io; use std::io::ErrorKind; @@ -21,7 +21,7 @@ pub enum LockError { /// The return value of `f` is dropped in that case. If all is successful, the /// return value of `f` is forwarded. pub fn try_with_lock_no_wait( - hg_vfs: Vfs, + hg_vfs: &VfsImpl, lock_filename: &str, f: impl FnOnce() -> R, ) -> Result { @@ -57,7 +57,7 @@ pub fn try_with_lock_no_wait( Err(LockError::AlreadyHeld) } -fn break_lock(hg_vfs: Vfs, lock_filename: &str) -> Result<(), LockError> { +fn break_lock(hg_vfs: &VfsImpl, lock_filename: &str) -> Result<(), LockError> { try_with_lock_no_wait(hg_vfs, &format!("{}.break", lock_filename), || { // Check again in case some other process broke and // acquired the lock in the meantime @@ -71,7 +71,7 @@ fn break_lock(hg_vfs: Vfs, lock_filename #[cfg(unix)] fn make_lock( - hg_vfs: Vfs, + hg_vfs: &VfsImpl, lock_filename: &str, data: &str, ) -> Result<(), HgError> { @@ -82,7 +82,7 @@ fn make_lock( } fn read_lock( - hg_vfs: Vfs, + hg_vfs: &VfsImpl, lock_filename: &str, ) -> Result, HgError> { let link_target = @@ -98,7 +98,7 @@ fn read_lock( } } -fn unlock(hg_vfs: Vfs, lock_filename: &str) -> Result<(), HgError> { +fn unlock(hg_vfs: &VfsImpl, lock_filename: &str) -> Result<(), HgError> { hg_vfs.remove_file(lock_filename) } diff --git a/rust/hg-core/src/logging.rs b/rust/hg-core/src/logging.rs --- a/rust/hg-core/src/logging.rs +++ b/rust/hg-core/src/logging.rs @@ -1,5 +1,5 @@ use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt}; -use crate::vfs::Vfs; +use crate::vfs::VfsImpl; use std::io::Write; /// An utility to append to a log file with the given name, and optionally @@ -9,14 +9,14 @@ use std::io::Write; /// "example.log.1" to "example.log.2" etc up to the given maximum number of /// files. pub struct LogFile<'a> { - vfs: Vfs<'a>, + vfs: VfsImpl, name: &'a str, max_size: Option, max_files: u32, } impl<'a> LogFile<'a> { - pub fn new(vfs: Vfs<'a>, name: &'a str) -> Self { + pub fn new(vfs: VfsImpl, name: &'a str) -> Self { Self { vfs, name, @@ -87,8 +87,12 @@ impl<'a> LogFile<'a> { #[test] fn test_rotation() { let temp = tempfile::tempdir().unwrap(); - let vfs = Vfs { base: temp.path() }; - let logger = LogFile::new(vfs, "log").max_size(Some(3)).max_files(2); + let vfs = VfsImpl { + base: temp.path().to_owned(), + }; + let logger = LogFile::new(vfs.clone(), "log") + .max_size(Some(3)) + .max_files(2); logger.write(b"one\n").unwrap(); logger.write(b"two\n").unwrap(); logger.write(b"3\n").unwrap(); diff --git a/rust/hg-core/src/repo.rs b/rust/hg-core/src/repo.rs --- a/rust/hg-core/src/repo.rs +++ b/rust/hg-core/src/repo.rs @@ -18,7 +18,7 @@ use crate::utils::debug::debug_wait_for_ use crate::utils::files::get_path_from_bytes; use crate::utils::hg_path::HgPath; use crate::utils::SliceExt; -use crate::vfs::{is_dir, is_file, Vfs}; +use crate::vfs::{is_dir, is_file, VfsImpl}; use crate::{ requirements, NodePrefix, RevlogDataConfig, RevlogDeltaConfig, RevlogFeatureConfig, RevlogType, RevlogVersionOptions, UncheckedRevision, @@ -121,8 +121,10 @@ impl Repo { let mut repo_config_files = vec![dot_hg.join("hgrc"), dot_hg.join("hgrc-not-shared")]; - let hg_vfs = Vfs { base: &dot_hg }; - let mut reqs = requirements::load_if_exists(hg_vfs)?; + let hg_vfs = VfsImpl { + base: dot_hg.to_owned(), + }; + let mut reqs = requirements::load_if_exists(&hg_vfs)?; let relative = reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT); let shared = @@ -163,9 +165,10 @@ impl Repo { store_path = shared_path.join("store"); - let source_is_share_safe = - requirements::load(Vfs { base: &shared_path })? - .contains(requirements::SHARESAFE_REQUIREMENT); + let source_is_share_safe = requirements::load(VfsImpl { + base: shared_path.to_owned(), + })? + .contains(requirements::SHARESAFE_REQUIREMENT); if share_safe != source_is_share_safe { return Err(HgError::unsupported("share-safe mismatch").into()); @@ -176,7 +179,9 @@ impl Repo { } } if share_safe { - reqs.extend(requirements::load(Vfs { base: &store_path })?); + reqs.extend(requirements::load(VfsImpl { + base: store_path.to_owned(), + })?); } let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() { @@ -216,19 +221,23 @@ impl Repo { /// For accessing repository files (in `.hg`), except for the store /// (`.hg/store`). - pub fn hg_vfs(&self) -> Vfs<'_> { - Vfs { base: &self.dot_hg } + pub fn hg_vfs(&self) -> VfsImpl { + VfsImpl { + base: self.dot_hg.to_owned(), + } } /// For accessing repository store files (in `.hg/store`) - pub fn store_vfs(&self) -> Vfs<'_> { - Vfs { base: &self.store } + pub fn store_vfs(&self) -> VfsImpl { + VfsImpl { + base: self.store.to_owned(), + } } /// For accessing the working copy - pub fn working_directory_vfs(&self) -> Vfs<'_> { - Vfs { - base: &self.working_directory, + pub fn working_directory_vfs(&self) -> VfsImpl { + VfsImpl { + base: self.working_directory.to_owned(), } } @@ -236,7 +245,7 @@ impl Repo { &self, f: impl FnOnce() -> R, ) -> Result { - try_with_lock_no_wait(self.hg_vfs(), "wlock", f) + try_with_lock_no_wait(&self.hg_vfs(), "wlock", f) } /// Whether this repo should use dirstate-v2. diff --git a/rust/hg-core/src/requirements.rs b/rust/hg-core/src/requirements.rs --- a/rust/hg-core/src/requirements.rs +++ b/rust/hg-core/src/requirements.rs @@ -1,7 +1,7 @@ use crate::errors::{HgError, HgResultExt}; use crate::repo::Repo; use crate::utils::join_display; -use crate::vfs::Vfs; +use crate::vfs::VfsImpl; use std::collections::HashSet; fn parse(bytes: &[u8]) -> Result, HgError> { @@ -24,11 +24,13 @@ fn parse(bytes: &[u8]) -> Result Result, HgError> { +pub(crate) fn load(hg_vfs: VfsImpl) -> Result, HgError> { parse(&hg_vfs.read("requires")?) } -pub(crate) fn load_if_exists(hg_vfs: Vfs) -> Result, HgError> { +pub(crate) fn load_if_exists( + hg_vfs: &VfsImpl, +) -> Result, HgError> { if let Some(bytes) = hg_vfs.read("requires").io_not_found_as_none()? { parse(&bytes) } else { diff --git a/rust/hg-core/src/revlog/changelog.rs b/rust/hg-core/src/revlog/changelog.rs --- a/rust/hg-core/src/revlog/changelog.rs +++ b/rust/hg-core/src/revlog/changelog.rs @@ -13,7 +13,7 @@ use crate::revlog::Revision; use crate::revlog::{Node, NodePrefix}; use crate::revlog::{Revlog, RevlogEntry, RevlogError}; use crate::utils::hg_path::HgPath; -use crate::vfs::Vfs; +use crate::vfs::VfsImpl; use crate::{Graph, GraphError, RevlogOpenOptions, UncheckedRevision}; /// A specialized `Revlog` to work with changelog data format. @@ -25,7 +25,7 @@ pub struct Changelog { impl Changelog { /// Open the `changelog` of a repository given by its root. pub fn open( - store_vfs: &Vfs, + store_vfs: &VfsImpl, options: RevlogOpenOptions, ) -> Result { let revlog = Revlog::open(store_vfs, "00changelog.i", None, options)?; @@ -500,7 +500,7 @@ fn unescape_extra(bytes: &[u8]) -> Vec Result<(), RevlogError> { // an empty revlog will be enough for this case let temp = tempfile::tempdir().unwrap(); - let vfs = Vfs { base: temp.path() }; + let vfs = VfsImpl { + base: temp.path().to_owned(), + }; std::fs::write(temp.path().join("foo.i"), b"").unwrap(); std::fs::write(temp.path().join("foo.d"), b"").unwrap(); let revlog = Revlog::open( diff --git a/rust/hg-core/src/revlog/filelog.rs b/rust/hg-core/src/revlog/filelog.rs --- a/rust/hg-core/src/revlog/filelog.rs +++ b/rust/hg-core/src/revlog/filelog.rs @@ -29,7 +29,7 @@ impl Graph for Filelog { impl Filelog { pub fn open_vfs( - store_vfs: &crate::vfs::Vfs<'_>, + store_vfs: &crate::vfs::VfsImpl, file_path: &HgPath, options: RevlogOpenOptions, ) -> Result { diff --git a/rust/hg-core/src/revlog/manifest.rs b/rust/hg-core/src/revlog/manifest.rs --- a/rust/hg-core/src/revlog/manifest.rs +++ b/rust/hg-core/src/revlog/manifest.rs @@ -3,7 +3,7 @@ use crate::revlog::{Node, NodePrefix}; use crate::revlog::{Revlog, RevlogError}; use crate::utils::hg_path::HgPath; use crate::utils::SliceExt; -use crate::vfs::Vfs; +use crate::vfs::VfsImpl; use crate::{ Graph, GraphError, Revision, RevlogOpenOptions, UncheckedRevision, }; @@ -23,7 +23,7 @@ impl Graph for Manifestlog { impl Manifestlog { /// Open the `manifest` of a repository given by its root. pub fn open( - store_vfs: &Vfs, + store_vfs: &VfsImpl, options: RevlogOpenOptions, ) -> Result { let revlog = Revlog::open(store_vfs, "00manifest.i", None, options)?; diff --git a/rust/hg-core/src/revlog/mod.rs b/rust/hg-core/src/revlog/mod.rs --- a/rust/hg-core/src/revlog/mod.rs +++ b/rust/hg-core/src/revlog/mod.rs @@ -38,7 +38,7 @@ use crate::exit_codes; use crate::requirements::{ GENERALDELTA_REQUIREMENT, NARROW_REQUIREMENT, SPARSEREVLOG_REQUIREMENT, }; -use crate::vfs::Vfs; +use crate::vfs::VfsImpl; /// As noted in revlog.c, revision numbers are actually encoded in /// 4 bytes, and are liberally converted to ints, whence the i32 @@ -708,7 +708,8 @@ impl Revlog { /// It will also open the associated data file if index and data are not /// interleaved. pub fn open( - store_vfs: &Vfs, + // Todo use the `Vfs` trait here once we create a function for mmap + store_vfs: &VfsImpl, index_path: impl AsRef, data_path: Option<&Path>, options: RevlogOpenOptions, @@ -717,7 +718,8 @@ impl Revlog { } fn open_gen( - store_vfs: &Vfs, + // Todo use the `Vfs` trait here once we create a function for mmap + store_vfs: &VfsImpl, index_path: impl AsRef, data_path: Option<&Path>, options: RevlogOpenOptions, @@ -1298,7 +1300,9 @@ mod tests { #[test] fn test_empty() { let temp = tempfile::tempdir().unwrap(); - let vfs = Vfs { base: temp.path() }; + let vfs = VfsImpl { + base: temp.path().to_owned(), + }; std::fs::write(temp.path().join("foo.i"), b"").unwrap(); std::fs::write(temp.path().join("foo.d"), b"").unwrap(); let revlog = @@ -1320,7 +1324,9 @@ mod tests { #[test] fn test_inline() { let temp = tempfile::tempdir().unwrap(); - let vfs = Vfs { base: temp.path() }; + let vfs = VfsImpl { + base: temp.path().to_owned(), + }; let node0 = Node::from_hex("2ed2a3912a0b24502043eae84ee4b279c18b90dd") .unwrap(); let node1 = Node::from_hex("b004912a8510032a0350a74daa2803dadfb00e12") @@ -1387,7 +1393,9 @@ mod tests { #[test] fn test_nodemap() { let temp = tempfile::tempdir().unwrap(); - let vfs = Vfs { base: temp.path() }; + let vfs = VfsImpl { + base: temp.path().to_owned(), + }; // building a revlog with a forced Node starting with zeros // This is a corruption, but it does not preclude using the nodemap diff --git a/rust/hg-core/src/revlog/nodemap_docket.rs b/rust/hg-core/src/revlog/nodemap_docket.rs --- a/rust/hg-core/src/revlog/nodemap_docket.rs +++ b/rust/hg-core/src/revlog/nodemap_docket.rs @@ -3,7 +3,7 @@ use bytes_cast::{unaligned, BytesCast}; use memmap2::Mmap; use std::path::{Path, PathBuf}; -use crate::vfs::Vfs; +use crate::vfs::VfsImpl; const ONDISK_VERSION: u8 = 1; @@ -33,7 +33,7 @@ impl NodeMapDocket { /// * The docket file points to a missing (likely deleted) data file (this /// can happen in a rare race condition). pub fn read_from_file( - store_vfs: &Vfs, + store_vfs: &VfsImpl, index_path: &Path, ) -> Result, HgError> { let docket_path = index_path.with_extension("n"); diff --git a/rust/hg-core/src/vfs.rs b/rust/hg-core/src/vfs.rs --- a/rust/hg-core/src/vfs.rs +++ b/rust/hg-core/src/vfs.rs @@ -1,17 +1,21 @@ use crate::errors::{HgError, IoErrorContext, IoResultExt}; +use crate::exit_codes; +use dyn_clone::DynClone; use memmap2::{Mmap, MmapOptions}; +use std::fs::File; use std::io::{ErrorKind, Write}; +use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; /// Filesystem access abstraction for the contents of a given "base" diretory -#[derive(Clone, Copy)] -pub struct Vfs<'a> { - pub(crate) base: &'a Path, +#[derive(Clone)] +pub struct VfsImpl { + pub(crate) base: PathBuf, } struct FileNotFound(std::io::Error, PathBuf); -impl Vfs<'_> { +impl VfsImpl { pub fn join(&self, relative_path: impl AsRef) -> PathBuf { self.base.join(relative_path) } @@ -71,7 +75,12 @@ impl Vfs<'_> { } Ok(file) => file, }; - // TODO: what are the safety requirements here? + // Safety is "enforced" by locks and assuming other processes are + // well-behaved. If any misbehaving or malicious process does touch + // the index, it could lead to corruption. This is inherent + // to file-based `mmap`, though some platforms have some ways of + // mitigating. + // TODO linux: set the immutable flag with `chattr(1)`? let mmap = unsafe { MmapOptions::new().map(&file) } .when_reading_file(&path)?; Ok(Ok(mmap)) @@ -134,8 +143,8 @@ impl Vfs<'_> { relative_path: impl AsRef, contents: &[u8], ) -> Result<(), HgError> { - let mut tmp = tempfile::NamedTempFile::new_in(self.base) - .when_writing_file(self.base)?; + let mut tmp = tempfile::NamedTempFile::new_in(&self.base) + .when_writing_file(&self.base)?; tmp.write_all(contents) .and_then(|()| tmp.flush()) .when_writing_file(tmp.path())?; @@ -165,6 +174,174 @@ fn fs_metadata( } } +/// Writable file object that atomically updates a file +/// +/// All writes will go to a temporary copy of the original file. Call +/// [`Self::close`] when you are done writing, and [`Self`] will rename +/// the temporary copy to the original name, making the changes +/// visible. If the object is destroyed without being closed, all your +/// writes are discarded. +pub struct AtomicFile { + /// The temporary file to write to + fp: std::fs::File, + /// Path of the temp file + temp_path: PathBuf, + /// Used when stat'ing the file, is useful only if the target file is + /// guarded by any lock (e.g. repo.lock or repo.wlock). + check_ambig: bool, + /// Path of the target file + target_name: PathBuf, + /// Whether the file is open or not + is_open: bool, +} + +impl AtomicFile { + pub fn new( + fp: std::fs::File, + check_ambig: bool, + temp_name: PathBuf, + target_name: PathBuf, + ) -> Self { + Self { + fp, + check_ambig, + temp_path: temp_name, + target_name, + is_open: true, + } + } + + /// Write `buf` to the temporary file + pub fn write_all(&mut self, buf: &[u8]) -> Result<(), std::io::Error> { + self.fp.write_all(buf) + } + + fn target(&self) -> PathBuf { + self.temp_path + .parent() + .expect("should not be at the filesystem root") + .join(&self.target_name) + } + + /// Close the temporary file and rename to the target + pub fn close(mut self) -> Result<(), std::io::Error> { + self.fp.flush()?; + let target = self.target(); + if self.check_ambig { + if let Ok(stat) = std::fs::metadata(&target) { + std::fs::rename(&self.temp_path, &target)?; + let new_stat = std::fs::metadata(&target)?; + let ctime = new_stat.ctime(); + let is_ambiguous = ctime == stat.ctime(); + if is_ambiguous { + let advanced = + filetime::FileTime::from_unix_time(ctime + 1, 0); + filetime::set_file_times(target, advanced, advanced)?; + } + } else { + std::fs::rename(&self.temp_path, target)?; + } + } else { + std::fs::rename(&self.temp_path, target).unwrap(); + } + self.is_open = false; + Ok(()) + } +} + +impl Drop for AtomicFile { + fn drop(&mut self) { + if self.is_open { + std::fs::remove_file(self.target()).ok(); + } + } +} + +/// Abstracts over the VFS to allow for different implementations of the +/// filesystem layer (like passing one from Python). +pub trait Vfs: Sync + Send + DynClone { + fn open(&self, filename: &Path) -> Result; + fn open_read(&self, filename: &Path) -> Result; + fn open_check_ambig( + &self, + filename: &Path, + ) -> Result; + fn create(&self, filename: &Path) -> Result; + /// Must truncate the new file if exist + fn create_atomic( + &self, + filename: &Path, + check_ambig: bool, + ) -> Result; + fn file_size(&self, file: &File) -> Result; + fn exists(&self, filename: &Path) -> bool; + fn unlink(&self, filename: &Path) -> Result<(), HgError>; + fn rename( + &self, + from: &Path, + to: &Path, + check_ambig: bool, + ) -> Result<(), HgError>; + fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError>; +} + +/// These methods will need to be implemented once `rhg` (and other) non-Python +/// users of `hg-core` start doing more on their own, like writing to files. +impl Vfs for VfsImpl { + fn open(&self, _filename: &Path) -> Result { + todo!() + } + fn open_read(&self, filename: &Path) -> Result { + let path = self.base.join(filename); + std::fs::File::open(&path).when_reading_file(&path) + } + fn open_check_ambig( + &self, + _filename: &Path, + ) -> Result { + todo!() + } + fn create(&self, _filename: &Path) -> Result { + todo!() + } + fn create_atomic( + &self, + _filename: &Path, + _check_ambig: bool, + ) -> Result { + todo!() + } + fn file_size(&self, file: &File) -> Result { + Ok(file + .metadata() + .map_err(|e| { + HgError::abort( + format!("Could not get file metadata: {}", e), + exit_codes::ABORT, + None, + ) + })? + .size()) + } + fn exists(&self, _filename: &Path) -> bool { + todo!() + } + fn unlink(&self, _filename: &Path) -> Result<(), HgError> { + todo!() + } + fn rename( + &self, + _from: &Path, + _to: &Path, + _check_ambig: bool, + ) -> Result<(), HgError> { + todo!() + } + fn copy(&self, _from: &Path, _to: &Path) -> Result<(), HgError> { + todo!() + } +} + pub(crate) fn is_dir(path: impl AsRef) -> Result { Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir())) } diff --git a/rust/rhg/src/commands/status.rs b/rust/rhg/src/commands/status.rs --- a/rust/rhg/src/commands/status.rs +++ b/rust/rhg/src/commands/status.rs @@ -393,8 +393,8 @@ pub fn run(invocation: &crate::CliInvoca // + map_err + collect, so let's just inline some of the // logic. match unsure_is_modified( - working_directory_vfs, - store_vfs, + &working_directory_vfs, + &store_vfs, check_exec, &manifest, &to_check.path, @@ -748,8 +748,8 @@ enum UnsureOutcome { /// This meant to be used for those that the dirstate cannot resolve, due /// to time resolution limits. fn unsure_is_modified( - working_directory_vfs: hg::vfs::Vfs, - store_vfs: hg::vfs::Vfs, + working_directory_vfs: &hg::vfs::VfsImpl, + store_vfs: &hg::vfs::VfsImpl, check_exec: bool, manifest: &Manifest, hg_path: &HgPath, @@ -786,7 +786,7 @@ fn unsure_is_modified( return Ok(UnsureOutcome::Modified); } let filelog = hg::filelog::Filelog::open_vfs( - &store_vfs, + store_vfs, hg_path, revlog_open_options, )?;