Mercurial > hg
view rust/hg-core/src/vfs.rs @ 52314:33d8cb64e9da default tip
rust: fix darwin build
author | Dan Villiom Podlaski Christiansen <danchr@gmail.com> |
---|---|
date | Mon, 18 Nov 2024 15:42:09 +0100 |
parents | a876ab6c3fd5 |
children |
line wrap: on
line source
use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt}; use crate::exit_codes; use crate::fncache::FnCache; use crate::revlog::path_encode::path_encode; use crate::utils::files::{get_bytes_from_path, get_path_from_bytes}; use dyn_clone::DynClone; use format_bytes::format_bytes; use memmap2::{Mmap, MmapOptions}; use rand::distributions::{Alphanumeric, DistString}; use std::fs::{File, Metadata, OpenOptions}; use std::io::{ErrorKind, Read, Seek, Write}; use std::os::fd::AsRawFd; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::{Path, PathBuf}; #[cfg(test)] use std::sync::atomic::AtomicUsize; #[cfg(test)] use std::sync::atomic::Ordering; use std::sync::OnceLock; /// Filesystem access abstraction for the contents of a given "base" diretory #[derive(Clone)] pub struct VfsImpl { pub(crate) base: PathBuf, pub readonly: bool, pub mode: Option<u32>, } struct FileNotFound(std::io::Error, PathBuf); /// Store the umask for the whole process since it's expensive to get. static UMASK: OnceLock<u32> = OnceLock::new(); fn get_umask() -> u32 { *UMASK.get_or_init(|| unsafe { // TODO is there any way of getting the umask without temporarily // setting it? Doesn't this affect all threads in this tiny window? let mask = libc::umask(0); libc::umask(mask); #[allow(clippy::useless_conversion)] (mask & 0o777).into() }) } /// Return the (unix) mode with which we will create/fix files fn get_mode(base: impl AsRef<Path>) -> Option<u32> { match base.as_ref().metadata() { Ok(meta) => { // files in .hg/ will be created using this mode let mode = meta.mode(); // avoid some useless chmods if (0o777 & !get_umask()) == (0o777 & mode) { None } else { Some(mode) } } Err(_) => None, } } impl VfsImpl { pub fn new(base: PathBuf, readonly: bool) -> Self { let mode = get_mode(&base); Self { base, readonly, mode, } } // XXX these methods are probably redundant with VFS trait? pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { self.base.join(relative_path) } pub fn symlink_metadata( &self, relative_path: impl AsRef<Path>, ) -> Result<std::fs::Metadata, HgError> { let path = self.join(relative_path); std::fs::symlink_metadata(&path).when_reading_file(&path) } pub fn read_link( &self, relative_path: impl AsRef<Path>, ) -> Result<PathBuf, HgError> { let path = self.join(relative_path); std::fs::read_link(&path).when_reading_file(&path) } pub fn read( &self, relative_path: impl AsRef<Path>, ) -> Result<Vec<u8>, HgError> { let path = self.join(relative_path); std::fs::read(&path).when_reading_file(&path) } /// Returns `Ok(None)` if the file does not exist. pub fn try_read( &self, relative_path: impl AsRef<Path>, ) -> Result<Option<Vec<u8>>, HgError> { match self.read(relative_path) { Err(e) => match &e { HgError::IoError { error, .. } => match error.kind() { ErrorKind::NotFound => Ok(None), _ => Err(e), }, _ => Err(e), }, Ok(v) => Ok(Some(v)), } } fn mmap_open_gen( &self, relative_path: impl AsRef<Path>, ) -> Result<Result<Mmap, FileNotFound>, HgError> { let path = self.join(relative_path); let file = match std::fs::File::open(&path) { Err(err) => { if let ErrorKind::NotFound = err.kind() { return Ok(Err(FileNotFound(err, path))); }; return (Err(err)).when_reading_file(&path); } Ok(file) => file, }; // 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)) } pub fn mmap_open_opt( &self, relative_path: impl AsRef<Path>, ) -> Result<Option<Mmap>, HgError> { self.mmap_open_gen(relative_path).map(|res| res.ok()) } pub fn mmap_open( &self, relative_path: impl AsRef<Path>, ) -> Result<Mmap, HgError> { match self.mmap_open_gen(relative_path)? { Err(FileNotFound(err, path)) => Err(err).when_reading_file(&path), Ok(res) => Ok(res), } } #[cfg(unix)] pub fn create_symlink( &self, relative_link_path: impl AsRef<Path>, target_path: impl AsRef<Path>, ) -> Result<(), HgError> { let link_path = self.join(relative_link_path); std::os::unix::fs::symlink(target_path, &link_path) .when_writing_file(&link_path) } /// Write `contents` into a temporary file, then rename to `relative_path`. /// This makes writing to a file "atomic": a reader opening that path will /// see either the previous contents of the file or the complete new /// content, never a partial write. pub fn atomic_write( &self, relative_path: impl AsRef<Path>, contents: &[u8], ) -> Result<(), HgError> { 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())?; let path = self.join(relative_path); tmp.persist(&path) .map_err(|e| e.error) .when_writing_file(&path)?; Ok(()) } } fn fs_metadata( path: impl AsRef<Path>, ) -> Result<Option<std::fs::Metadata>, HgError> { let path = path.as_ref(); match path.metadata() { Ok(meta) => Ok(Some(meta)), Err(error) => match error.kind() { // TODO: when we require a Rust version where `NotADirectory` is // stable, invert this logic and return None for it and `NotFound` // and propagate any other error. ErrorKind::PermissionDenied => Err(error).with_context(|| { IoErrorContext::ReadingMetadata(path.to_owned()) }), _ => Ok(None), }, } } /// Abstraction over the files handled by a [`Vfs`]. #[derive(Debug)] pub enum VfsFile { Atomic(AtomicFile), Normal { file: File, path: PathBuf, /// If `Some`, check (and maybe fix) this file's timestamp ambiguity. /// See [`is_filetime_ambiguous`]. check_ambig: Option<Metadata>, }, } impl VfsFile { pub fn normal(file: File, path: PathBuf) -> Self { Self::Normal { file, check_ambig: None, path, } } pub fn normal_check_ambig( file: File, path: PathBuf, ) -> Result<Self, HgError> { Ok(Self::Normal { file, check_ambig: Some(path.metadata().when_reading_file(&path)?), path, }) } pub fn try_clone(&self) -> Result<VfsFile, HgError> { Ok(match self { VfsFile::Atomic(AtomicFile { fp, temp_path, check_ambig, target_name, is_open, }) => Self::Atomic(AtomicFile { fp: fp.try_clone().when_reading_file(temp_path)?, temp_path: temp_path.clone(), check_ambig: *check_ambig, target_name: target_name.clone(), is_open: *is_open, }), VfsFile::Normal { file, check_ambig, path, } => Self::Normal { file: file.try_clone().when_reading_file(path)?, check_ambig: check_ambig.clone(), path: path.to_owned(), }, }) } pub fn set_len(&self, len: u64) -> Result<(), std::io::Error> { match self { VfsFile::Atomic(atomic_file) => atomic_file.fp.set_len(len), VfsFile::Normal { file, .. } => file.set_len(len), } } pub fn metadata(&self) -> Result<std::fs::Metadata, std::io::Error> { match self { VfsFile::Atomic(atomic_file) => atomic_file.fp.metadata(), VfsFile::Normal { file, .. } => file.metadata(), } } } impl AsRawFd for VfsFile { fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { match self { VfsFile::Atomic(atomic_file) => atomic_file.fp.as_raw_fd(), VfsFile::Normal { file, .. } => file.as_raw_fd(), } } } impl Seek for VfsFile { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { match self { VfsFile::Atomic(atomic_file) => atomic_file.seek(pos), VfsFile::Normal { file, .. } => file.seek(pos), } } } impl Read for VfsFile { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { match self { VfsFile::Atomic(atomic_file) => atomic_file.fp.read(buf), VfsFile::Normal { file, .. } => file.read(buf), } } } impl Write for VfsFile { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { match self { VfsFile::Atomic(atomic_file) => atomic_file.fp.write(buf), VfsFile::Normal { file, .. } => file.write(buf), } } fn flush(&mut self) -> std::io::Result<()> { match self { VfsFile::Atomic(atomic_file) => atomic_file.fp.flush(), VfsFile::Normal { file, .. } => file.flush(), } } } impl Drop for VfsFile { fn drop(&mut self) { if let VfsFile::Normal { path, check_ambig: Some(old), .. } = self { avoid_timestamp_ambiguity(path, old) } } } /// Records the number of times we've fixed a timestamp ambiguity, only /// applicable for tests. #[cfg(test)] static TIMESTAMP_FIXES_CALLS: AtomicUsize = AtomicUsize::new(0); fn avoid_timestamp_ambiguity(path: &Path, old: &Metadata) { if let Ok(new) = path.metadata() { let is_ambiguous = is_filetime_ambiguous(&new, old); if is_ambiguous { let advanced = filetime::FileTime::from_unix_time(old.mtime() + 1, 0); if filetime::set_file_times(path, advanced, advanced).is_ok() { #[cfg(test)] { TIMESTAMP_FIXES_CALLS.fetch_add(1, Ordering::Relaxed); } } } } } /// Examine whether new stat is ambiguous against old one /// /// `S[N]` below means stat of a file at `N`-th change: /// /// - `S[n-1].ctime < S[n].ctime`: can detect change of a file /// - `S[n-1].ctime == S[n].ctime` /// - `S[n-1].ctime < S[n].mtime`: means natural advancing (*1) /// - `S[n-1].ctime == S[n].mtime`: is ambiguous (*2) /// - `S[n-1].ctime > S[n].mtime`: never occurs naturally (don't care) /// - `S[n-1].ctime > S[n].ctime`: never occurs naturally (don't care) /// /// Case (*2) above means that a file was changed twice or more at /// same time in sec (= `S[n-1].ctime`), and comparison of timestamp /// is ambiguous. /// /// Base idea to avoid such ambiguity is "advance mtime 1 sec, if /// timestamp is ambiguous". /// /// But advancing mtime only in case (*2) doesn't work as /// expected, because naturally advanced `S[n].mtime` in case (*1) /// might be equal to manually advanced `S[n-1 or earlier].mtime`. /// /// Therefore, all `S[n-1].ctime == S[n].ctime` cases should be /// treated as ambiguous regardless of mtime, to avoid overlooking /// by confliction between such mtime. /// /// Advancing mtime `if isambig(new, old)` ensures `S[n-1].mtime != /// S[n].mtime`, even if size of a file isn't changed. pub fn is_filetime_ambiguous(new: &Metadata, old: &Metadata) -> bool { new.ctime() == old.ctime() } /// 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. #[derive(Debug)] 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( target_path: impl AsRef<Path>, empty: bool, check_ambig: bool, ) -> Result<Self, HgError> { let target_path = target_path.as_ref().to_owned(); let random_id = Alphanumeric.sample_string(&mut rand::thread_rng(), 12); let filename = target_path.file_name().expect("target has no filename"); let filename = get_bytes_from_path(filename); let temp_filename = format_bytes!(b".{}-{}~", filename, random_id.as_bytes()); let temp_path = target_path.with_file_name(get_path_from_bytes(&temp_filename)); if !empty { std::fs::copy(&target_path, &temp_path) .with_context(|| IoErrorContext::CopyingFile { from: target_path.to_owned(), to: temp_path.to_owned(), }) // If it doesn't exist, create it on open .io_not_found_as_none()?; } let fp = std::fs::OpenOptions::new() .write(true) .create(true) .truncate(empty) .open(&temp_path) .when_writing_file(&temp_path)?; Ok(Self { fp, temp_path, check_ambig, target_name: target_path, is_open: true, }) } pub fn from_file( 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) = target.metadata() { std::fs::rename(&self.temp_path, &target)?; avoid_timestamp_ambiguity(&target, &stat); } else { std::fs::rename(&self.temp_path, target)?; } } else { std::fs::rename(&self.temp_path, target)?; } self.is_open = false; Ok(()) } } impl Seek for AtomicFile { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { self.fp.seek(pos) } } impl Drop for AtomicFile { fn drop(&mut self) { if self.is_open { std::fs::remove_file(&self.temp_path).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 { /// Open a [`VfsFile::Normal`] for reading the file at `filename`, /// relative to this VFS's root. fn open(&self, filename: &Path) -> Result<VfsFile, HgError>; /// Open a [`VfsFile::Normal`] for writing and reading the file at /// `filename`, relative to this VFS's root. fn open_write(&self, filename: &Path) -> Result<VfsFile, HgError>; /// Open a [`VfsFile::Normal`] for reading and writing the file at /// `filename`, relative to this VFS's root. This file will be checked /// for an ambiguous mtime on [`drop`]. See [`is_filetime_ambiguous`]. fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError>; /// Create a [`VfsFile::Normal`] for reading and writing the file at /// `filename`, relative to this VFS's root. If the file already exists, /// it will be truncated to 0 bytes. fn create( &self, filename: &Path, check_ambig: bool, ) -> Result<VfsFile, HgError>; /// Create a [`VfsFile::Atomic`] for reading and writing the file at /// `filename`, relative to this VFS's root. If the file already exists, /// it will be truncated to 0 bytes. fn create_atomic( &self, filename: &Path, check_ambig: bool, ) -> Result<VfsFile, HgError>; /// Return the total file size in bytes of the open `file`. Errors are /// usual IO errors (invalid file handle, permissions, etc.) fn file_size(&self, file: &VfsFile) -> Result<u64, HgError>; /// Return `true` if `filename` exists relative to this VFS's root. Errors /// will coerce to `false`, to this also returns `false` if there are /// IO problems. This is fine because any operation that actually tries /// to do anything with this path will get the same error. fn exists(&self, filename: &Path) -> bool; /// Remove the file at `filename` relative to this VFS's root. Errors /// are the usual IO errors (lacking permission, file does not exist, etc.) fn unlink(&self, filename: &Path) -> Result<(), HgError>; /// Rename the file `from` to `to`, both relative to this VFS's root. /// Errors are the usual IO errors (lacking permission, file does not /// exist, etc.). If `check_ambig` is `true`, the VFS will check for an /// ambiguous mtime on rename. See [`is_filetime_ambiguous`]. fn rename( &self, from: &Path, to: &Path, check_ambig: bool, ) -> Result<(), HgError>; /// Rename the file `from` to `to`, both relative to this VFS's root. /// Errors are the usual IO errors (lacking permission, file does not /// exist, etc.). If `check_ambig` is passed, the VFS will check for an /// ambiguous mtime on rename. See [`is_filetime_ambiguous`]. fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError>; /// Returns the absolute root path of this VFS, relative to which all /// operations are done. fn base(&self) -> &Path; } /// 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<VfsFile, HgError> { // TODO auditpath let path = self.base.join(filename); Ok(VfsFile::normal( std::fs::File::open(&path).when_reading_file(&path)?, filename.to_owned(), )) } fn open_write(&self, filename: &Path) -> Result<VfsFile, HgError> { if self.readonly { return Err(HgError::abort( "write access in a readonly vfs", exit_codes::ABORT, None, )); } // TODO auditpath let path = self.base.join(filename); copy_in_place_if_hardlink(&path)?; Ok(VfsFile::normal( OpenOptions::new() .create(false) .create_new(false) .write(true) .read(true) .open(&path) .when_writing_file(&path)?, path.to_owned(), )) } fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError> { if self.readonly { return Err(HgError::abort( "write access in a readonly vfs", exit_codes::ABORT, None, )); } let path = self.base.join(filename); copy_in_place_if_hardlink(&path)?; // TODO auditpath VfsFile::normal_check_ambig( OpenOptions::new() .write(true) .read(true) // Can be used for reading to save on `open` calls .create(false) .open(&path) .when_reading_file(&path)?, path.to_owned(), ) } fn create( &self, filename: &Path, check_ambig: bool, ) -> Result<VfsFile, HgError> { if self.readonly { return Err(HgError::abort( "write access in a readonly vfs", exit_codes::ABORT, None, )); } // TODO auditpath let path = self.base.join(filename); let parent = path.parent().expect("file at root"); std::fs::create_dir_all(parent).when_writing_file(parent)?; let file = OpenOptions::new() .create(true) .truncate(true) .write(true) .read(true) .open(&path) .when_writing_file(&path)?; if let Some(mode) = self.mode { // Creating the file with the right permission (with `.mode()`) // may not work since umask takes effect for file creation. // So we need to fix the permission after creating the file. fix_directory_permissions(&self.base, &path, mode)?; let perm = std::fs::Permissions::from_mode(mode & 0o666); std::fs::set_permissions(&path, perm).when_writing_file(&path)?; } Ok(VfsFile::Normal { file, check_ambig: if check_ambig { Some(path.metadata().when_reading_file(&path)?) } else { None }, path: path.to_owned(), }) } fn create_atomic( &self, _filename: &Path, _check_ambig: bool, ) -> Result<VfsFile, HgError> { todo!() } fn file_size(&self, file: &VfsFile) -> Result<u64, HgError> { 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 { self.base.join(filename).exists() } fn unlink(&self, filename: &Path) -> Result<(), HgError> { if self.readonly { return Err(HgError::abort( "write access in a readonly vfs", exit_codes::ABORT, None, )); } let path = self.base.join(filename); std::fs::remove_file(&path) .with_context(|| IoErrorContext::RemovingFile(path)) } fn rename( &self, from: &Path, to: &Path, check_ambig: bool, ) -> Result<(), HgError> { if self.readonly { return Err(HgError::abort( "write access in a readonly vfs", exit_codes::ABORT, None, )); } let old_stat = if check_ambig { Some( from.metadata() .when_reading_file(from) .io_not_found_as_none()?, ) } else { None }; let from = self.base.join(from); let to = self.base.join(to); std::fs::rename(&from, &to).with_context(|| { IoErrorContext::RenamingFile { from, to: to.to_owned(), } })?; if let Some(Some(old)) = old_stat { avoid_timestamp_ambiguity(&to, &old); } Ok(()) } fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError> { let from = self.base.join(from); let to = self.base.join(to); std::fs::copy(&from, &to) .with_context(|| IoErrorContext::CopyingFile { from, to }) .map(|_| ()) } fn base(&self) -> &Path { &self.base } } fn fix_directory_permissions( base: &Path, path: &Path, mode: u32, ) -> Result<(), HgError> { let mut ancestors = path.ancestors(); ancestors.next(); // yields the path itself for ancestor in ancestors { if ancestor == base { break; } let perm = std::fs::Permissions::from_mode(mode); std::fs::set_permissions(ancestor, perm) .when_writing_file(ancestor)?; } Ok(()) } /// A VFS that understands the `fncache` store layout (file encoding), and /// adds new entries to the `fncache`. /// TODO Only works when using from Python for now. pub struct FnCacheVfs { inner: VfsImpl, fncache: Box<dyn FnCache>, } impl Clone for FnCacheVfs { fn clone(&self) -> Self { Self { inner: self.inner.clone(), fncache: dyn_clone::clone_box(&*self.fncache), } } } impl FnCacheVfs { pub fn new( base: PathBuf, readonly: bool, fncache: Box<dyn FnCache>, ) -> Self { let inner = VfsImpl::new(base, readonly); Self { inner, fncache } } fn maybe_add_to_fncache( &self, filename: &Path, encoded_path: &Path, ) -> Result<(), HgError> { let relevant_file = (filename.starts_with("data/") || filename.starts_with("meta/")) && is_revlog_file(filename); if relevant_file { let not_load = !self.fncache.is_loaded() && (self.exists(filename) && self .inner .join(encoded_path) .metadata() .when_reading_file(encoded_path)? .size() != 0); if !not_load { self.fncache.add(filename); } }; Ok(()) } } impl Vfs for FnCacheVfs { fn open(&self, filename: &Path) -> Result<VfsFile, HgError> { let encoded = path_encode(&get_bytes_from_path(filename)); let filename = get_path_from_bytes(&encoded); self.inner.open(filename) } fn open_write(&self, filename: &Path) -> Result<VfsFile, HgError> { let encoded = path_encode(&get_bytes_from_path(filename)); let encoded_path = get_path_from_bytes(&encoded); self.maybe_add_to_fncache(filename, encoded_path)?; self.inner.open_write(encoded_path) } fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError> { let encoded = path_encode(&get_bytes_from_path(filename)); let filename = get_path_from_bytes(&encoded); self.inner.open_check_ambig(filename) } fn create( &self, filename: &Path, check_ambig: bool, ) -> Result<VfsFile, HgError> { let encoded = path_encode(&get_bytes_from_path(filename)); let encoded_path = get_path_from_bytes(&encoded); self.maybe_add_to_fncache(filename, encoded_path)?; self.inner.create(encoded_path, check_ambig) } fn create_atomic( &self, filename: &Path, check_ambig: bool, ) -> Result<VfsFile, HgError> { let encoded = path_encode(&get_bytes_from_path(filename)); let filename = get_path_from_bytes(&encoded); self.inner.create_atomic(filename, check_ambig) } fn file_size(&self, file: &VfsFile) -> Result<u64, HgError> { self.inner.file_size(file) } fn exists(&self, filename: &Path) -> bool { let encoded = path_encode(&get_bytes_from_path(filename)); let filename = get_path_from_bytes(&encoded); self.inner.exists(filename) } fn unlink(&self, filename: &Path) -> Result<(), HgError> { let encoded = path_encode(&get_bytes_from_path(filename)); let filename = get_path_from_bytes(&encoded); self.inner.unlink(filename) } fn rename( &self, from: &Path, to: &Path, check_ambig: bool, ) -> Result<(), HgError> { let encoded = path_encode(&get_bytes_from_path(from)); let from = get_path_from_bytes(&encoded); let encoded = path_encode(&get_bytes_from_path(to)); let to = get_path_from_bytes(&encoded); self.inner.rename(from, to, check_ambig) } fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError> { let encoded = path_encode(&get_bytes_from_path(from)); let from = get_path_from_bytes(&encoded); let encoded = path_encode(&get_bytes_from_path(to)); let to = get_path_from_bytes(&encoded); self.inner.copy(from, to) } fn base(&self) -> &Path { self.inner.base() } } /// Detects whether `path` is a hardlink and does a tmp copy + rename erase /// to turn it into its own file. Revlogs are usually hardlinked when doing /// a local clone, and we don't want to modify the original repo. fn copy_in_place_if_hardlink(path: &Path) -> Result<(), HgError> { let metadata = path.metadata().when_writing_file(path)?; if metadata.nlink() > 0 { // If it's hardlinked, copy it and rename it back before changing it. let tmpdir = path.parent().expect("file at root"); let name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); let tmpfile = tmpdir.join(name); std::fs::create_dir_all(tmpfile.parent().expect("file at root")) .with_context(|| IoErrorContext::CopyingFile { from: path.to_owned(), to: tmpfile.to_owned(), })?; std::fs::copy(path, &tmpfile).with_context(|| { IoErrorContext::CopyingFile { from: path.to_owned(), to: tmpfile.to_owned(), } })?; std::fs::rename(&tmpfile, path).with_context(|| { IoErrorContext::RenamingFile { from: tmpfile, to: path.to_owned(), } })?; } Ok(()) } pub fn is_revlog_file(path: impl AsRef<Path>) -> bool { path.as_ref() .extension() .map(|ext| { ["i", "idx", "d", "dat", "n", "nd", "sda"] .contains(&ext.to_string_lossy().as_ref()) }) .unwrap_or(false) } pub(crate) fn is_dir(path: impl AsRef<Path>) -> Result<bool, HgError> { Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir())) } pub(crate) fn is_file(path: impl AsRef<Path>) -> Result<bool, HgError> { Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_file())) } /// Returns whether the given `path` is on a network file system. /// Taken from `cargo`'s codebase. #[cfg(target_os = "linux")] pub(crate) fn is_on_nfs_mount(path: impl AsRef<Path>) -> bool { use std::ffi::CString; use std::mem; use std::os::unix::prelude::*; let path = match CString::new(path.as_ref().as_os_str().as_bytes()) { Ok(path) => path, Err(_) => return false, }; unsafe { let mut buf: libc::statfs = mem::zeroed(); let r = libc::statfs(path.as_ptr(), &mut buf); r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32 } } /// Similar to what Cargo does; although detecting NFS (or non-local /// file systems) _should_ be possible on other operating systems, /// we'll just assume that mmap() works there, for now; after all, /// _some_ functionality is better than a compile error, i.e. none at /// all #[cfg(not(target_os = "linux"))] pub(crate) fn is_on_nfs_mount(_path: impl AsRef<Path>) -> bool { false } #[cfg(test)] mod tests { use super::*; #[test] fn test_atomic_file() { let dir = tempfile::tempdir().unwrap().into_path(); let target_path = dir.join("sometargetname"); for empty in [true, false] { let file = AtomicFile::new(&target_path, empty, false).unwrap(); assert!(file.is_open); let filename = file.temp_path.file_name().unwrap().to_str().unwrap(); // Make sure we have a coherent temp name assert_eq!(filename.len(), 29, "{}", filename); assert!(filename.contains("sometargetname")); // Make sure the temp file is created in the same folder assert_eq!(target_path.parent(), file.temp_path.parent()); } assert!(!target_path.exists()); std::fs::write(&target_path, "version 1").unwrap(); let mut file = AtomicFile::new(&target_path, false, false).unwrap(); file.write_all(b"version 2!").unwrap(); assert_eq!( std::fs::read(&target_path).unwrap(), b"version 1".to_vec() ); let temp_path = file.temp_path.to_owned(); // test that dropping the file should discard the temp file and not // affect the target path. drop(file); assert_eq!( std::fs::read(&target_path).unwrap(), b"version 1".to_vec() ); assert!(!temp_path.exists()); let mut file = AtomicFile::new(&target_path, false, false).unwrap(); file.write_all(b"version 2!").unwrap(); assert_eq!( std::fs::read(&target_path).unwrap(), b"version 1".to_vec() ); file.close().unwrap(); assert_eq!( std::fs::read(&target_path).unwrap(), b"version 2!".to_vec(), "{}", std::fs::read_to_string(&target_path).unwrap() ); assert!(target_path.exists()); assert!(!temp_path.exists()); } #[test] fn test_vfs_file_check_ambig() { let dir = tempfile::tempdir().unwrap().into_path(); let file_path = dir.join("file"); fn vfs_file_write(file_path: &Path, check_ambig: bool) { let file = std::fs::OpenOptions::new() .write(true) .open(file_path) .unwrap(); let old_stat = if check_ambig { Some(file.metadata().unwrap()) } else { None }; let mut vfs_file = VfsFile::Normal { file, path: file_path.to_owned(), check_ambig: old_stat, }; vfs_file.write_all(b"contents").unwrap(); } std::fs::OpenOptions::new() .write(true) .create(true) .truncate(false) .open(&file_path) .unwrap(); let number_of_writes = 3; // Try multiple times, because reproduction of an ambiguity depends // on "filesystem time" for _ in 0..5 { TIMESTAMP_FIXES_CALLS.store(0, Ordering::Relaxed); vfs_file_write(&file_path, false); let old_stat = file_path.metadata().unwrap(); if old_stat.ctime() != old_stat.mtime() { // subsequent changing never causes ambiguity continue; } // Repeat atomic write with `check_ambig == true`, to examine // whether the mtime is advanced multiple times as expected for _ in 0..number_of_writes { vfs_file_write(&file_path, true); } let new_stat = file_path.metadata().unwrap(); if !is_filetime_ambiguous(&new_stat, &old_stat) { // timestamp ambiguity was naturally avoided while repetition continue; } assert_eq!( TIMESTAMP_FIXES_CALLS.load(Ordering::Relaxed), number_of_writes ); assert_eq!( old_stat.mtime() + number_of_writes as i64, file_path.metadata().unwrap().mtime() ); break; } // If we've arrived here without breaking, we might not have // tested anything because the platform is too slow. This test will // still work on fast platforms. } }