vfs.rs
1130 lines
| 36.0 KiB
| application/rls-services+xml
|
RustLexer
Raphaël Gomès
|
r53077 | use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt}; | ||
Raphaël Gomès
|
r52761 | use crate::exit_codes; | ||
Raphaël Gomès
|
r53065 | use crate::fncache::FnCache; | ||
Raphaël Gomès
|
r53075 | use crate::revlog::path_encode::path_encode; | ||
Raphaël Gomès
|
r53065 | use crate::utils::files::{get_bytes_from_path, get_path_from_bytes}; | ||
Raphaël Gomès
|
r52761 | use dyn_clone::DynClone; | ||
Raphaël Gomès
|
r53077 | use format_bytes::format_bytes; | ||
Simon Sapin
|
r48767 | use memmap2::{Mmap, MmapOptions}; | ||
Raphaël Gomès
|
r53065 | use rand::distributions::{Alphanumeric, DistString}; | ||
Raphaël Gomès
|
r53078 | use std::fs::{File, Metadata, OpenOptions}; | ||
use std::io::{ErrorKind, Read, Seek, Write}; | ||||
use std::os::fd::AsRawFd; | ||||
Raphaël Gomès
|
r53064 | use std::os::unix::fs::{MetadataExt, PermissionsExt}; | ||
Simon Sapin
|
r48764 | use std::path::{Path, PathBuf}; | ||
Raphaël Gomès
|
r53078 | #[cfg(test)] | ||
use std::sync::atomic::AtomicUsize; | ||||
#[cfg(test)] | ||||
use std::sync::atomic::Ordering; | ||||
Raphaël Gomès
|
r53064 | use std::sync::OnceLock; | ||
Simon Sapin
|
r48764 | |||
/// Filesystem access abstraction for the contents of a given "base" diretory | ||||
Raphaël Gomès
|
r52761 | #[derive(Clone)] | ||
pub struct VfsImpl { | ||||
pub(crate) base: PathBuf, | ||||
Raphaël Gomès
|
r53064 | pub readonly: bool, | ||
pub mode: Option<u32>, | ||||
Simon Sapin
|
r48764 | } | ||
Arseniy Alekseyev
|
r49013 | struct FileNotFound(std::io::Error, PathBuf); | ||
Raphaël Gomès
|
r53064 | /// 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); | ||||
mask & 0o777 | ||||
}) | ||||
} | ||||
/// 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, | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r52761 | impl VfsImpl { | ||
Raphaël Gomès
|
r53064 | 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? | ||||
Simon Sapin
|
r48764 | pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { | ||
self.base.join(relative_path) | ||||
} | ||||
Simon Sapin
|
r49168 | 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) | ||||
} | ||||
Simon Sapin
|
r48764 | 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) | ||||
} | ||||
Raphaël Gomès
|
r50380 | /// 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() { | ||||
Raphaël Gomès
|
r50809 | ErrorKind::NotFound => Ok(None), | ||
Raphaël Gomès
|
r50380 | _ => Err(e), | ||
}, | ||||
_ => Err(e), | ||||
}, | ||||
Ok(v) => Ok(Some(v)), | ||||
} | ||||
} | ||||
Arseniy Alekseyev
|
r49013 | 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, | ||||
}; | ||||
Raphaël Gomès
|
r52761 | // 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)`? | ||||
Arseniy Alekseyev
|
r49013 | 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()) | ||||
} | ||||
Simon Sapin
|
r48764 | pub fn mmap_open( | ||
&self, | ||||
relative_path: impl AsRef<Path>, | ||||
) -> Result<Mmap, HgError> { | ||||
Arseniy Alekseyev
|
r49013 | match self.mmap_open_gen(relative_path)? { | ||
Err(FileNotFound(err, path)) => Err(err).when_reading_file(&path), | ||||
Ok(res) => Ok(res), | ||||
} | ||||
Simon Sapin
|
r48764 | } | ||
Simon Sapin
|
r49245 | #[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) | ||||
Simon Sapin
|
r49246 | .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> { | ||||
Raphaël Gomès
|
r52761 | let mut tmp = tempfile::NamedTempFile::new_in(&self.base) | ||
.when_writing_file(&self.base)?; | ||||
Simon Sapin
|
r49246 | 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(()) | ||||
Simon Sapin
|
r49245 | } | ||
Simon Sapin
|
r48764 | } | ||
fn fs_metadata( | ||||
path: impl AsRef<Path>, | ||||
) -> Result<Option<std::fs::Metadata>, HgError> { | ||||
let path = path.as_ref(); | ||||
Raphaël Gomès
|
r53077 | match path.metadata() { | ||
Simon Sapin
|
r48764 | 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), | ||||
}, | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r53078 | /// 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. | ||||
fn is_filetime_ambiguous(new: &Metadata, old: &Metadata) -> bool { | ||||
new.ctime() == old.ctime() | ||||
} | ||||
Raphaël Gomès
|
r52761 | /// 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. | ||||
Raphaël Gomès
|
r53077 | #[derive(Debug)] | ||
Raphaël Gomès
|
r52761 | 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( | ||||
Raphaël Gomès
|
r53077 | 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( | ||||
Raphaël Gomès
|
r52761 | 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 { | ||||
Raphaël Gomès
|
r53077 | if let Ok(stat) = target.metadata() { | ||
Raphaël Gomès
|
r52761 | std::fs::rename(&self.temp_path, &target)?; | ||
Raphaël Gomès
|
r53078 | avoid_timestamp_ambiguity(&target, &stat); | ||
Raphaël Gomès
|
r52761 | } else { | ||
std::fs::rename(&self.temp_path, target)?; | ||||
} | ||||
} else { | ||||
Raphaël Gomès
|
r53077 | std::fs::rename(&self.temp_path, target)?; | ||
Raphaël Gomès
|
r52761 | } | ||
self.is_open = false; | ||||
Ok(()) | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r53077 | impl Seek for AtomicFile { | ||
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { | ||||
self.fp.seek(pos) | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r52761 | impl Drop for AtomicFile { | ||
fn drop(&mut self) { | ||||
if self.is_open { | ||||
Raphaël Gomès
|
r53076 | std::fs::remove_file(&self.temp_path).ok(); | ||
Raphaël Gomès
|
r52761 | } | ||
} | ||||
} | ||||
/// Abstracts over the VFS to allow for different implementations of the | ||||
/// filesystem layer (like passing one from Python). | ||||
pub trait Vfs: Sync + Send + DynClone { | ||||
Raphaël Gomès
|
r53191 | /// Open a [`VfsFile::Normal`] for reading the file at `filename`, | ||
/// relative to this VFS's root. | ||||
fn open(&self, filename: &Path) -> Result<VfsFile, HgError>; | ||||
Raphaël Gomès
|
r53079 | /// Open a [`VfsFile::Normal`] for writing and reading the file at | ||
/// `filename`, relative to this VFS's root. | ||||
Raphaël Gomès
|
r53191 | fn open_write(&self, filename: &Path) -> Result<VfsFile, HgError>; | ||
Raphaël Gomès
|
r53079 | /// 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`]. | ||||
Raphaël Gomès
|
r53078 | fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError>; | ||
Raphaël Gomès
|
r53079 | /// 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. | ||||
Raphaël Gomès
|
r53078 | fn create( | ||
Raphaël Gomès
|
r52761 | &self, | ||
filename: &Path, | ||||
Raphaël Gomès
|
r53078 | check_ambig: bool, | ||
) -> Result<VfsFile, HgError>; | ||||
Raphaël Gomès
|
r53079 | /// 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. | ||||
Raphaël Gomès
|
r52761 | fn create_atomic( | ||
&self, | ||||
filename: &Path, | ||||
check_ambig: bool, | ||||
Raphaël Gomès
|
r53078 | ) -> Result<VfsFile, HgError>; | ||
Raphaël Gomès
|
r53079 | /// Return the total file size in bytes of the open `file`. Errors are | ||
/// usual IO errors (invalid file handle, permissions, etc.) | ||||
Raphaël Gomès
|
r53078 | fn file_size(&self, file: &VfsFile) -> Result<u64, HgError>; | ||
Raphaël Gomès
|
r53079 | /// 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. | ||||
Raphaël Gomès
|
r52761 | fn exists(&self, filename: &Path) -> bool; | ||
Raphaël Gomès
|
r53079 | /// Remove the file at `filename` relative to this VFS's root. Errors | ||
/// are the usual IO errors (lacking permission, file does not exist, etc.) | ||||
Raphaël Gomès
|
r52761 | fn unlink(&self, filename: &Path) -> Result<(), HgError>; | ||
Raphaël Gomès
|
r53079 | /// 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`]. | ||||
Raphaël Gomès
|
r52761 | fn rename( | ||
&self, | ||||
from: &Path, | ||||
to: &Path, | ||||
check_ambig: bool, | ||||
) -> Result<(), HgError>; | ||||
Raphaël Gomès
|
r53079 | /// 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`]. | ||||
Raphaël Gomès
|
r52761 | fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError>; | ||
Raphaël Gomès
|
r53079 | /// Returns the absolute root path of this VFS, relative to which all | ||
/// operations are done. | ||||
Raphaël Gomès
|
r53064 | fn base(&self) -> &Path; | ||
Raphaël Gomès
|
r52761 | } | ||
/// 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 { | ||||
Raphaël Gomès
|
r53078 | fn open(&self, filename: &Path) -> Result<VfsFile, HgError> { | ||
Raphaël Gomès
|
r53191 | // 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> { | ||||
Raphaël Gomès
|
r53064 | 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)?; | ||||
Raphaël Gomès
|
r53078 | Ok(VfsFile::normal( | ||
OpenOptions::new() | ||||
.create(false) | ||||
.create_new(false) | ||||
.write(true) | ||||
.read(true) | ||||
.open(&path) | ||||
.when_writing_file(&path)?, | ||||
path.to_owned(), | ||||
)) | ||||
Raphaël Gomès
|
r52761 | } | ||
Raphaël Gomès
|
r53064 | |||
Raphaël Gomès
|
r53078 | fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError> { | ||
Raphaël Gomès
|
r53064 | 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)?; | ||||
Raphaël Gomès
|
r53078 | // 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(), | ||||
) | ||||
Raphaël Gomès
|
r52761 | } | ||
Raphaël Gomès
|
r53064 | |||
Raphaël Gomès
|
r53078 | fn create( | ||
&self, | ||||
filename: &Path, | ||||
check_ambig: bool, | ||||
) -> Result<VfsFile, HgError> { | ||||
Raphaël Gomès
|
r53064 | 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)?; | ||||
} | ||||
Raphaël Gomès
|
r53078 | Ok(VfsFile::Normal { | ||
file, | ||||
check_ambig: if check_ambig { | ||||
Some(path.metadata().when_reading_file(&path)?) | ||||
} else { | ||||
None | ||||
}, | ||||
path: path.to_owned(), | ||||
}) | ||||
Raphaël Gomès
|
r52761 | } | ||
Raphaël Gomès
|
r53064 | |||
Raphaël Gomès
|
r52761 | fn create_atomic( | ||
&self, | ||||
_filename: &Path, | ||||
_check_ambig: bool, | ||||
Raphaël Gomès
|
r53078 | ) -> Result<VfsFile, HgError> { | ||
Raphaël Gomès
|
r52761 | todo!() | ||
} | ||||
Raphaël Gomès
|
r53064 | |||
Raphaël Gomès
|
r53078 | fn file_size(&self, file: &VfsFile) -> Result<u64, HgError> { | ||
Raphaël Gomès
|
r52761 | Ok(file | ||
.metadata() | ||||
.map_err(|e| { | ||||
HgError::abort( | ||||
format!("Could not get file metadata: {}", e), | ||||
exit_codes::ABORT, | ||||
None, | ||||
) | ||||
})? | ||||
.size()) | ||||
} | ||||
Raphaël Gomès
|
r53064 | |||
fn exists(&self, filename: &Path) -> bool { | ||||
self.base.join(filename).exists() | ||||
Raphaël Gomès
|
r52761 | } | ||
Raphaël Gomès
|
r53064 | |||
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)) | ||||
Raphaël Gomès
|
r52761 | } | ||
Raphaël Gomès
|
r53064 | |||
Raphaël Gomès
|
r52761 | fn rename( | ||
&self, | ||||
Raphaël Gomès
|
r53064 | from: &Path, | ||
to: &Path, | ||||
Raphaël Gomès
|
r53078 | check_ambig: bool, | ||
Raphaël Gomès
|
r52761 | ) -> Result<(), HgError> { | ||
Raphaël Gomès
|
r53064 | if self.readonly { | ||
return Err(HgError::abort( | ||||
"write access in a readonly vfs", | ||||
exit_codes::ABORT, | ||||
None, | ||||
)); | ||||
} | ||||
Raphaël Gomès
|
r53078 | let old_stat = if check_ambig { | ||
Some( | ||||
from.metadata() | ||||
.when_reading_file(from) | ||||
.io_not_found_as_none()?, | ||||
) | ||||
} else { | ||||
None | ||||
}; | ||||
Raphaël Gomès
|
r53064 | let from = self.base.join(from); | ||
let to = self.base.join(to); | ||||
Raphaël Gomès
|
r53078 | 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(()) | ||||
Raphaël Gomès
|
r53064 | } | ||
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 | ||||
Raphaël Gomès
|
r52761 | } | ||
Raphaël Gomès
|
r53064 | } | ||
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)?; | ||||
Raphaël Gomès
|
r52761 | } | ||
Raphaël Gomès
|
r53064 | Ok(()) | ||
} | ||||
Raphaël Gomès
|
r53065 | /// 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 { | ||||
Raphaël Gomès
|
r53078 | fn open(&self, filename: &Path) -> Result<VfsFile, HgError> { | ||
Raphaël Gomès
|
r53065 | let encoded = path_encode(&get_bytes_from_path(filename)); | ||
Raphaël Gomès
|
r53191 | let filename = get_path_from_bytes(&encoded); | ||
self.inner.open(filename) | ||||
Raphaël Gomès
|
r53065 | } | ||
Raphaël Gomès
|
r53191 | fn open_write(&self, filename: &Path) -> Result<VfsFile, HgError> { | ||
Raphaël Gomès
|
r53065 | let encoded = path_encode(&get_bytes_from_path(filename)); | ||
Raphaël Gomès
|
r53191 | let encoded_path = get_path_from_bytes(&encoded); | ||
self.maybe_add_to_fncache(filename, encoded_path)?; | ||||
self.inner.open_write(encoded_path) | ||||
Raphaël Gomès
|
r53065 | } | ||
Raphaël Gomès
|
r53078 | fn open_check_ambig(&self, filename: &Path) -> Result<VfsFile, HgError> { | ||
Raphaël Gomès
|
r53065 | let encoded = path_encode(&get_bytes_from_path(filename)); | ||
let filename = get_path_from_bytes(&encoded); | ||||
self.inner.open_check_ambig(filename) | ||||
} | ||||
Raphaël Gomès
|
r53078 | fn create( | ||
&self, | ||||
filename: &Path, | ||||
check_ambig: bool, | ||||
) -> Result<VfsFile, HgError> { | ||||
Raphaël Gomès
|
r53065 | 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)?; | ||||
Raphaël Gomès
|
r53078 | self.inner.create(encoded_path, check_ambig) | ||
Raphaël Gomès
|
r53065 | } | ||
fn create_atomic( | ||||
&self, | ||||
filename: &Path, | ||||
check_ambig: bool, | ||||
Raphaël Gomès
|
r53078 | ) -> Result<VfsFile, HgError> { | ||
Raphaël Gomès
|
r53065 | let encoded = path_encode(&get_bytes_from_path(filename)); | ||
let filename = get_path_from_bytes(&encoded); | ||||
self.inner.create_atomic(filename, check_ambig) | ||||
} | ||||
Raphaël Gomès
|
r53078 | fn file_size(&self, file: &VfsFile) -> Result<u64, HgError> { | ||
Raphaël Gomès
|
r53065 | 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() | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r53064 | /// 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) | ||||
Raphaël Gomès
|
r52761 | } | ||
Simon Sapin
|
r48764 | 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())) | ||||
} | ||||
Raphaël Gomès
|
r51075 | |||
/// 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 | ||||
} | ||||
} | ||||
Dan Villiom Podlaski Christiansen
|
r51182 | |||
/// 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 | ||||
} | ||||
Raphaël Gomès
|
r53077 | |||
#[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()); | ||||
} | ||||
Raphaël Gomès
|
r53078 | |||
#[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. | ||||
} | ||||
Raphaël Gomès
|
r53077 | } | ||