diff --git a/rust/hg-core/src/errors.rs b/rust/hg-core/src/errors.rs --- a/rust/hg-core/src/errors.rs +++ b/rust/hg-core/src/errors.rs @@ -47,6 +47,8 @@ /// Details about where an I/O error happened #[derive(Debug)] pub enum IoErrorContext { + /// `std::fs::metadata` + ReadingMetadata(std::path::PathBuf), ReadingFile(std::path::PathBuf), WritingFile(std::path::PathBuf), RemovingFile(std::path::PathBuf), @@ -108,6 +110,9 @@ impl fmt::Display for IoErrorContext { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + IoErrorContext::ReadingMetadata(path) => { + write!(f, "when reading metadata of {}", path.display()) + } IoErrorContext::ReadingFile(path) => { write!(f, "when reading {}", path.display()) } 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 @@ -6,6 +6,7 @@ use crate::utils::SliceExt; use memmap::{Mmap, MmapOptions}; use std::collections::HashSet; +use std::io::ErrorKind; use std::path::{Path, PathBuf}; /// A repository on disk @@ -51,7 +52,7 @@ // ancestors() is inclusive: it first yields `current_directory` // as-is. for ancestor in current_directory.ancestors() { - if ancestor.join(".hg").is_dir() { + if is_dir(ancestor.join(".hg"))? { return Ok(ancestor.to_path_buf()); } } @@ -73,9 +74,9 @@ explicit_path: Option, ) -> Result { if let Some(root) = explicit_path { - if root.join(".hg").is_dir() { + if is_dir(root.join(".hg"))? { Self::new_at_path(root.to_owned(), config) - } else if root.is_file() { + } else if is_file(&root)? { Err(HgError::unsupported("bundle repository").into()) } else { Err(RepoError::NotFound { @@ -130,7 +131,7 @@ if relative { shared_path = dot_hg.join(shared_path) } - if !shared_path.is_dir() { + if !is_dir(&shared_path)? { return Err(HgError::corrupted(format!( ".hg/sharedpath points to nonexistent directory {}", shared_path.display() @@ -286,3 +287,29 @@ .with_context(|| IoErrorContext::RenamingFile { from, to }) } } + +fn fs_metadata( + path: impl AsRef, +) -> Result, HgError> { + let path = path.as_ref(); + match std::fs::metadata(path) { + 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), + }, + } +} + +fn is_dir(path: impl AsRef) -> Result { + Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir())) +} + +fn is_file(path: impl AsRef) -> Result { + Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_file())) +}