// Copyright 2021 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. #![expect(missing_docs)] use std::borrow::Cow;use std::ffi::OsString;use std::fs;use std::fs::File;use std::io;use std::io::ErrorKind;use std::io::Write;use std::path::Component;use std::path::Path;use std::path::PathBuf; use futures::AsyncRead;use futures::AsyncReadExt as _;use tempfile::NamedTempFile;use tempfile::PersistError;use thiserror::Error; #[cfg(unix)]pub use self::platform::check_executable_bit_support;pub use self::platform::check_symlink_support;pub use self::platform::symlink_dir;pub use self::platform::symlink_file; #[derive(Debug, Error)]#[error("Cannot access {path}")]pub struct PathError { pub path: PathBuf, pub source: io::Error,} pub trait IoResultExt<T> { fn context(self, path: impl AsRef<Path>) -> Result<T, PathError>;} impl<T> IoResultExt<T> for io::Result<T> { fn context(self, path: impl AsRef<Path>) -> Result<T, PathError> { self.map_err(|error| PathError { path: path.as_ref().to_path_buf(), source: error, }) }} /// Creates a directory or does nothing if the directory already exists.////// Returns the underlying error if the directory can't be created./// The function will also fail if intermediate directories on the path do not/// already exist.pub fn create_or_reuse_dir(dirname: &Path) -> io::Result<()> { match fs::create_dir(dirname) { Ok(()) => Ok(()), Err(_) if dirname.is_dir() => Ok(()), Err(e) => Err(e), }} /// Removes all files in the directory, but not the directory itself.////// The directory must exist, and there should be no sub directories.pub fn remove_dir_contents(dirname: &Path) -> Result<(), PathError> { for entry in dirname.read_dir().context(dirname)? { let entry = entry.context(dirname)?; let path = entry.path(); fs::remove_file(&path).context(&path)?; } Ok(())} /// Checks if path points at an empty directory.pub fn is_empty_dir(path: &Path) -> Result<bool, PathError> { match path.read_dir() { Ok(mut entries) => Ok(entries.next().is_none()), Err(error) => match error.kind() { ErrorKind::NotADirectory => Ok(false), ErrorKind::NotFound => Ok(false), _ => Err(error).context(path)?, }, }} #[derive(Debug, Error)]#[error(transparent)]pub struct BadPathEncoding(platform::BadOsStrEncoding); /// Constructs [`Path`] from `bytes` in platform-specific manner.////// On Unix, this function never fails because paths are just bytes. On Windows,/// this may return error if the input wasn't well-formed UTF-8.pub fn path_from_bytes(bytes: &[u8]) -> Result<&Path, BadPathEncoding> { let s = platform::os_str_from_bytes(bytes).map_err(BadPathEncoding)?; Ok(Path::new(s))} /// Converts `path` to bytes in platform-specific manner.////// On Unix, this function never fails because paths are just bytes. On Windows,/// this may return error if the input wasn't well-formed UTF-8.////// The returned byte sequence can be considered a superset of ASCII (such as/// UTF-8 bytes.)pub fn path_to_bytes(path: &Path) -> Result<&[u8], BadPathEncoding> { platform::os_str_to_bytes(path.as_ref()).map_err(BadPathEncoding)} /// Expands "~/" to the user's home directory.pub fn expand_home_path(path_str: &str) -> PathBuf { if let Some(remainder) = path_str.strip_prefix("~/") && let Ok(home_dir) = etcetera::home_dir() { return home_dir.join(remainder); } PathBuf::from(path_str)} /// Turns the given `to` path into relative path starting from the `from` path.////// Both `from` and `to` paths are supposed to be absolute and normalized in the/// same manner. If `from` and `to` share no common prefix, the returned path is/// unchanged. This also means `relative_path(abs, rel)` will return `rel`.pub fn relative_path(from: &Path, to: &Path) -> PathBuf { let Some((from_suffix, to_suffix)) = strip_common_path_prefix(from, to) else { // No common prefix found. Return the original path. return to.to_owned(); }; let depth = from_suffix.components().count(); let mut relative = PathBuf::with_capacity(2 * depth + 1 + to_suffix.as_os_str().len()); for _ in 0..depth { relative.push(Component::ParentDir); } if !to_suffix.as_os_str().is_empty() { relative.push(to_suffix); } else if depth == 0 { relative.push(Component::CurDir); } relative} fn strip_common_path_prefix<'a, 'b>( path1: &'a Path, path2: &'b Path,) -> Option<(&'a Path, &'b Path)> { let mut components1 = path1.components(); let mut components2 = path2.components(); let mut suffix_paths = None; while let (Some(c1), Some(c2)) = (components1.next(), components2.next()) { if c1 != c2 { break; } suffix_paths = Some((components1.as_path(), components2.as_path())); } suffix_paths} /// Consumes as much `..` and `.` as possible without considering symlinks.pub fn normalize_path(path: &Path) -> PathBuf { let mut result = PathBuf::new(); for c in path.components() { match c { Component::CurDir => {} Component::ParentDir if matches!(result.components().next_back(), Some(Component::Normal(_))) => { // Do not pop ".." let popped = result.pop(); assert!(popped); } _ => { result.push(c); } } } if result.as_os_str().is_empty() { ".".into() } else { result }} /// Converts the given `path` to Unix-like path separated by "/".////// The returned path might not work on Windows if it was canonicalized. On/// Unix, this function is noop.pub fn slash_path(path: &Path) -> Cow<'_, Path> { if cfg!(windows) { Cow::Owned(to_slash_separated(path).into()) } else { Cow::Borrowed(path) }} fn to_slash_separated(path: &Path) -> OsString { let mut buf = OsString::with_capacity(path.as_os_str().len()); let mut components = path.components(); match components.next() { Some(c) => buf.push(c), None => return buf, } for c in components { buf.push("/"); buf.push(c); } buf} /// Persists the temporary file after synchronizing the content.////// After system crash, the persisted file should have a valid content if/// existed. However, the persisted file name (or directory entry) could be/// lost. It's up to caller to synchronize the directory entries.////// See also <https://lwn.net/Articles/457667/> for the behavior on Linux.pub fn persist_temp_file<P: AsRef<Path>>( temp_file: NamedTempFile, new_path: P,) -> io::Result<File> { // Ensure persisted file content is flushed to disk. temp_file.as_file().sync_data()?; temp_file .persist(new_path) .map_err(|PersistError { error, file: _ }| error)} /// Like [`persist_temp_file()`], but doesn't try to overwrite the existing/// target on Windows.pub fn persist_content_addressed_temp_file<P: AsRef<Path>>( temp_file: NamedTempFile, new_path: P,) -> io::Result<File> { // Ensure new file content is flushed to disk, so the old file content // wouldn't be lost if existed at the same location. temp_file.as_file().sync_data()?; if cfg!(windows) { // On Windows, overwriting file can fail if the file is opened without // FILE_SHARE_DELETE for example. We don't need to take a risk if the // file already exists. match temp_file.persist_noclobber(&new_path) { Ok(file) => Ok(file), Err(PersistError { error, file: _ }) => { if let Ok(existing_file) = File::open(new_path) { // TODO: Update mtime to help GC keep this file Ok(existing_file) } else { Err(error) } } } } else { // On Unix, rename() is atomic and should succeed even if the // destination file exists. Checking if the target exists might involve // non-atomic operation, so don't use persist_noclobber(). temp_file .persist(new_path) .map_err(|PersistError { error, file: _ }| error) }} /// Opaque value that can be tested to know whether file or directory paths/// point to the same filesystem entity.////// The primary use case is to detect file name aliases on case-insensitive/// filesystem. On Unix, device and inode numbers are compared.#[derive(Debug, Eq, Hash, PartialEq)]pub struct FileIdentity(platform::FileIdentity); impl FileIdentity { /// Queries file identity without following symlinks. pub fn from_symlink_path(path: impl AsRef<Path>) -> io::Result<Self> { platform::file_identity_from_symlink_path(path.as_ref()).map(Self) } /// Queries file identity of the given `file`. // TODO: do not consume file object pub fn from_file(file: File) -> io::Result<Self> { platform::file_identity_from_file(file).map(Self) }} /// Reads from an async source and writes to a sync destination. Does not spawn/// a task, so writes will block.pub async fn copy_async_to_sync<R: AsyncRead, W: Write + ?Sized>( reader: R, writer: &mut W,) -> io::Result<usize> { let mut buf = vec![0; 16 << 10]; let mut total_written_bytes = 0; let mut reader = std::pin::pin!(reader); loop { let written_bytes = reader.read(&mut buf).await?; if written_bytes == 0 { return Ok(total_written_bytes); } writer.write_all(&buf[0..written_bytes])?; total_written_bytes += written_bytes; }} #[cfg(unix)]mod platform { use std::convert::Infallible; use std::ffi::OsStr; use std::fs; use std::fs::File; use std::io; use std::os::unix::ffi::OsStrExt as _; use std::os::unix::fs::MetadataExt as _; use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::symlink; use std::path::Path; pub type BadOsStrEncoding = Infallible; pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> { Ok(OsStr::from_bytes(data)) } pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> { Ok(data.as_bytes()) } /// Whether changing executable bits is permitted on the filesystem of this /// directory, and whether attempting to flip one has an observable effect. pub fn check_executable_bit_support(path: impl AsRef<Path>) -> io::Result<bool> { // Get current permissions and try to flip just the user's executable bit. let temp_file = tempfile::tempfile_in(path)?; let old_mode = temp_file.metadata()?.permissions().mode(); let new_mode = old_mode ^ 0o100; let result = temp_file.set_permissions(PermissionsExt::from_mode(new_mode)); match result { // If permission was denied, we do not have executable bit support. Err(err) if err.kind() == io::ErrorKind::PermissionDenied => Ok(false), Err(err) => Err(err), Ok(()) => { // Verify that the permission change was not silently ignored. let mode = temp_file.metadata()?.permissions().mode(); Ok(mode == new_mode) } } } /// Symlinks are always available on Unix. pub fn check_symlink_support() -> io::Result<bool> { Ok(true) } /// Creates a new symlink `link` pointing to the `original` path. /// /// On Unix, the `original` path doesn't have to be a directory. pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> { symlink(original, link) } /// Creates a new symlink `link` pointing to the `original` path. /// /// On Unix, the `original` path doesn't have to be a file. pub fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> { symlink(original, link) } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct FileIdentity { // https://github.com/BurntSushi/same-file/blob/1.0.6/src/unix.rs#L30 dev: u64, ino: u64, } impl FileIdentity { fn from_metadata(metadata: fs::Metadata) -> Self { Self { dev: metadata.dev(), ino: metadata.ino(), } } } pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> { path.symlink_metadata().map(FileIdentity::from_metadata) } pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> { file.metadata().map(FileIdentity::from_metadata) }} #[cfg(windows)]mod platform { use std::fs::File; use std::fs::OpenOptions; use std::io; use std::os::windows::fs::OpenOptionsExt as _; pub use std::os::windows::fs::symlink_dir; pub use std::os::windows::fs::symlink_file; use std::path::Path; use winreg::RegKey; use winreg::enums::HKEY_LOCAL_MACHINE; pub use super::fallback::BadOsStrEncoding; pub use super::fallback::os_str_from_bytes; pub use super::fallback::os_str_to_bytes; /// Symlinks may or may not be enabled on Windows. They require the /// Developer Mode setting, which is stored in the registry key below. /// /// Note: If developer mode is not enabled, the error code of symlink /// creation will be 1314, `ERROR_PRIVILEGE_NOT_HELD`. pub fn check_symlink_support() -> io::Result<bool> { let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); let sideloading = hklm.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock")?; let developer_mode: u32 = sideloading.get_value("AllowDevelopmentWithoutDevLicense")?; Ok(developer_mode == 1) } pub type FileIdentity = same_file::Handle; pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> { // `same_file::Handle::from_path()` follows symlinks, because it opens // the path without `FILE_FLAG_OPEN_REPARSE_POINT`. Open the file the // same way it does (read access, plus `FILE_FLAG_BACKUP_SEMANTICS` so a // directory can be opened too), but add `FILE_FLAG_OPEN_REPARSE_POINT` // so the handle refers to the symlink itself instead of its target. // This matches the Unix implementation, which uses `symlink_metadata()`. // The reparse-point flag is ignored for paths that aren't reparse // points, so regular files and hard links are unaffected. const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000; const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000; let file = OpenOptions::new() .read(true) .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) .open(path)?; same_file::Handle::from_file(file) } pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> { same_file::Handle::from_file(file) }} #[cfg_attr(unix, expect(dead_code))]mod fallback { use std::ffi::OsStr; use thiserror::Error; // Define error per platform so we can explicitly say UTF-8 is expected. #[derive(Debug, Error)] #[error("Invalid UTF-8 sequence")] pub struct BadOsStrEncoding; pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> { Ok(str::from_utf8(data).map_err(|_| BadOsStrEncoding)?.as_ref()) } pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> { Ok(data.to_str().ok_or(BadOsStrEncoding)?.as_ref()) }} #[cfg(test)]mod tests { use std::io::Write as _; use futures::io::Cursor; use itertools::Itertools as _; use pollster::FutureExt as _; use test_case::test_case; use super::*; use crate::tests::TestResult; use crate::tests::new_temp_dir; #[test] #[cfg(unix)] fn exec_bit_support_in_temp_dir() -> TestResult { // Temporary directories on Unix should always have executable support. // Note that it would be problematic to test in a non-temp directory, as // a developer's filesystem may or may not have executable bit support. let dir = new_temp_dir(); let supported = check_executable_bit_support(dir.path())?; assert!(supported); Ok(()) }Lines 1–500 of 850. Load more
// Copyright 2021 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #![expect(missing_docs)] use std::borrow::Cow; use std::ffi::OsString; use std::fs; use std::fs::File; use std::io; use std::io::ErrorKind; use std::io::Write; use std::path::Component; use std::path::Path; use std::path::PathBuf; use futures::AsyncRead; use futures::AsyncReadExt as _; use tempfile::NamedTempFile; use tempfile::PersistError; use thiserror::Error; #[cfg(unix)] pub use self::platform::check_executable_bit_support; pub use self::platform::check_symlink_support; pub use self::platform::symlink_dir; pub use self::platform::symlink_file; #[derive(Debug, Error)] #[error("Cannot access {path}")] pub struct PathError { pub path: PathBuf, pub source: io::Error, } pub trait IoResultExt<T> { fn context(self, path: impl AsRef<Path>) -> Result<T, PathError>; } impl<T> IoResultExt<T> for io::Result<T> { fn context(self, path: impl AsRef<Path>) -> Result<T, PathError> { self.map_err(|error| PathError { path: path.as_ref().to_path_buf(), source: error, }) } } /// Creates a directory or does nothing if the directory already exists. /// /// Returns the underlying error if the directory can't be created. /// The function will also fail if intermediate directories on the path do not /// already exist. pub fn create_or_reuse_dir(dirname: &Path) -> io::Result<()> { match fs::create_dir(dirname) { Ok(()) => Ok(()), Err(_) if dirname.is_dir() => Ok(()), Err(e) => Err(e), } } /// Removes all files in the directory, but not the directory itself. /// /// The directory must exist, and there should be no sub directories. pub fn remove_dir_contents(dirname: &Path) -> Result<(), PathError> { for entry in dirname.read_dir().context(dirname)? { let entry = entry.context(dirname)?; let path = entry.path(); fs::remove_file(&path).context(&path)?; } Ok(()) } /// Checks if path points at an empty directory. pub fn is_empty_dir(path: &Path) -> Result<bool, PathError> { match path.read_dir() { Ok(mut entries) => Ok(entries.next().is_none()), Err(error) => match error.kind() { ErrorKind::NotADirectory => Ok(false), ErrorKind::NotFound => Ok(false), _ => Err(error).context(path)?, }, } } #[derive(Debug, Error)] #[error(transparent)] pub struct BadPathEncoding(platform::BadOsStrEncoding); /// Constructs [`Path`] from `bytes` in platform-specific manner. /// /// On Unix, this function never fails because paths are just bytes. On Windows, /// this may return error if the input wasn't well-formed UTF-8. pub fn path_from_bytes(bytes: &[u8]) -> Result<&Path, BadPathEncoding> { let s = platform::os_str_from_bytes(bytes).map_err(BadPathEncoding)?; Ok(Path::new(s)) } /// Converts `path` to bytes in platform-specific manner. /// /// On Unix, this function never fails because paths are just bytes. On Windows, /// this may return error if the input wasn't well-formed UTF-8. /// /// The returned byte sequence can be considered a superset of ASCII (such as /// UTF-8 bytes.) pub fn path_to_bytes(path: &Path) -> Result<&[u8], BadPathEncoding> { platform::os_str_to_bytes(path.as_ref()).map_err(BadPathEncoding) } /// Expands "~/" to the user's home directory. pub fn expand_home_path(path_str: &str) -> PathBuf { if let Some(remainder) = path_str.strip_prefix("~/") && let Ok(home_dir) = etcetera::home_dir() { return home_dir.join(remainder); } PathBuf::from(path_str) } /// Turns the given `to` path into relative path starting from the `from` path. /// /// Both `from` and `to` paths are supposed to be absolute and normalized in the /// same manner. If `from` and `to` share no common prefix, the returned path is /// unchanged. This also means `relative_path(abs, rel)` will return `rel`. pub fn relative_path(from: &Path, to: &Path) -> PathBuf { let Some((from_suffix, to_suffix)) = strip_common_path_prefix(from, to) else { // No common prefix found. Return the original path. return to.to_owned(); }; let depth = from_suffix.components().count(); let mut relative = PathBuf::with_capacity(2 * depth + 1 + to_suffix.as_os_str().len()); for _ in 0..depth { relative.push(Component::ParentDir); } if !to_suffix.as_os_str().is_empty() { relative.push(to_suffix); } else if depth == 0 { relative.push(Component::CurDir); } relative } fn strip_common_path_prefix<'a, 'b>( path1: &'a Path, path2: &'b Path, ) -> Option<(&'a Path, &'b Path)> { let mut components1 = path1.components(); let mut components2 = path2.components(); let mut suffix_paths = None; while let (Some(c1), Some(c2)) = (components1.next(), components2.next()) { if c1 != c2 { break; } suffix_paths = Some((components1.as_path(), components2.as_path())); } suffix_paths } /// Consumes as much `..` and `.` as possible without considering symlinks. pub fn normalize_path(path: &Path) -> PathBuf { let mut result = PathBuf::new(); for c in path.components() { match c { Component::CurDir => {} Component::ParentDir if matches!(result.components().next_back(), Some(Component::Normal(_))) => { // Do not pop ".." let popped = result.pop(); assert!(popped); } _ => { result.push(c); } } } if result.as_os_str().is_empty() { ".".into() } else { result } } /// Converts the given `path` to Unix-like path separated by "/". /// /// The returned path might not work on Windows if it was canonicalized. On /// Unix, this function is noop. pub fn slash_path(path: &Path) -> Cow<'_, Path> { if cfg!(windows) { Cow::Owned(to_slash_separated(path).into()) } else { Cow::Borrowed(path) } } fn to_slash_separated(path: &Path) -> OsString { let mut buf = OsString::with_capacity(path.as_os_str().len()); let mut components = path.components(); match components.next() { Some(c) => buf.push(c), None => return buf, } for c in components { buf.push("/"); buf.push(c); } buf } /// Persists the temporary file after synchronizing the content. /// /// After system crash, the persisted file should have a valid content if /// existed. However, the persisted file name (or directory entry) could be /// lost. It's up to caller to synchronize the directory entries. /// /// See also <https://lwn.net/Articles/457667/> for the behavior on Linux. pub fn persist_temp_file<P: AsRef<Path>>( temp_file: NamedTempFile, new_path: P, ) -> io::Result<File> { // Ensure persisted file content is flushed to disk. temp_file.as_file().sync_data()?; temp_file .persist(new_path) .map_err(|PersistError { error, file: _ }| error) } /// Like [`persist_temp_file()`], but doesn't try to overwrite the existing /// target on Windows. pub fn persist_content_addressed_temp_file<P: AsRef<Path>>( temp_file: NamedTempFile, new_path: P, ) -> io::Result<File> { // Ensure new file content is flushed to disk, so the old file content // wouldn't be lost if existed at the same location. temp_file.as_file().sync_data()?; if cfg!(windows) { // On Windows, overwriting file can fail if the file is opened without // FILE_SHARE_DELETE for example. We don't need to take a risk if the // file already exists. match temp_file.persist_noclobber(&new_path) { Ok(file) => Ok(file), Err(PersistError { error, file: _ }) => { if let Ok(existing_file) = File::open(new_path) { // TODO: Update mtime to help GC keep this file Ok(existing_file) } else { Err(error) } } } } else { // On Unix, rename() is atomic and should succeed even if the // destination file exists. Checking if the target exists might involve // non-atomic operation, so don't use persist_noclobber(). temp_file .persist(new_path) .map_err(|PersistError { error, file: _ }| error) } } /// Opaque value that can be tested to know whether file or directory paths /// point to the same filesystem entity. /// /// The primary use case is to detect file name aliases on case-insensitive /// filesystem. On Unix, device and inode numbers are compared. #[derive(Debug, Eq, Hash, PartialEq)] pub struct FileIdentity(platform::FileIdentity); impl FileIdentity { /// Queries file identity without following symlinks. pub fn from_symlink_path(path: impl AsRef<Path>) -> io::Result<Self> { platform::file_identity_from_symlink_path(path.as_ref()).map(Self) } /// Queries file identity of the given `file`. // TODO: do not consume file object pub fn from_file(file: File) -> io::Result<Self> { platform::file_identity_from_file(file).map(Self) } } /// Reads from an async source and writes to a sync destination. Does not spawn /// a task, so writes will block. pub async fn copy_async_to_sync<R: AsyncRead, W: Write + ?Sized>( reader: R, writer: &mut W, ) -> io::Result<usize> { let mut buf = vec![0; 16 << 10]; let mut total_written_bytes = 0; let mut reader = std::pin::pin!(reader); loop { let written_bytes = reader.read(&mut buf).await?; if written_bytes == 0 { return Ok(total_written_bytes); } writer.write_all(&buf[0..written_bytes])?; total_written_bytes += written_bytes; } } #[cfg(unix)] mod platform { use std::convert::Infallible; use std::ffi::OsStr; use std::fs; use std::fs::File; use std::io; use std::os::unix::ffi::OsStrExt as _; use std::os::unix::fs::MetadataExt as _; use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::symlink; use std::path::Path; pub type BadOsStrEncoding = Infallible; pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> { Ok(OsStr::from_bytes(data)) } pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> { Ok(data.as_bytes()) } /// Whether changing executable bits is permitted on the filesystem of this /// directory, and whether attempting to flip one has an observable effect. pub fn check_executable_bit_support(path: impl AsRef<Path>) -> io::Result<bool> { // Get current permissions and try to flip just the user's executable bit. let temp_file = tempfile::tempfile_in(path)?; let old_mode = temp_file.metadata()?.permissions().mode(); let new_mode = old_mode ^ 0o100; let result = temp_file.set_permissions(PermissionsExt::from_mode(new_mode)); match result { // If permission was denied, we do not have executable bit support. Err(err) if err.kind() == io::ErrorKind::PermissionDenied => Ok(false), Err(err) => Err(err), Ok(()) => { // Verify that the permission change was not silently ignored. let mode = temp_file.metadata()?.permissions().mode(); Ok(mode == new_mode) } } } /// Symlinks are always available on Unix. pub fn check_symlink_support() -> io::Result<bool> { Ok(true) } /// Creates a new symlink `link` pointing to the `original` path. /// /// On Unix, the `original` path doesn't have to be a directory. pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> { symlink(original, link) } /// Creates a new symlink `link` pointing to the `original` path. /// /// On Unix, the `original` path doesn't have to be a file. pub fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> { symlink(original, link) } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct FileIdentity { // https://github.com/BurntSushi/same-file/blob/1.0.6/src/unix.rs#L30 dev: u64, ino: u64, } impl FileIdentity { fn from_metadata(metadata: fs::Metadata) -> Self { Self { dev: metadata.dev(), ino: metadata.ino(), } } } pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> { path.symlink_metadata().map(FileIdentity::from_metadata) } pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> { file.metadata().map(FileIdentity::from_metadata) } } #[cfg(windows)] mod platform { use std::fs::File; use std::fs::OpenOptions; use std::io; use std::os::windows::fs::OpenOptionsExt as _; pub use std::os::windows::fs::symlink_dir; pub use std::os::windows::fs::symlink_file; use std::path::Path; use winreg::RegKey; use winreg::enums::HKEY_LOCAL_MACHINE; pub use super::fallback::BadOsStrEncoding; pub use super::fallback::os_str_from_bytes; pub use super::fallback::os_str_to_bytes; /// Symlinks may or may not be enabled on Windows. They require the /// Developer Mode setting, which is stored in the registry key below. /// /// Note: If developer mode is not enabled, the error code of symlink /// creation will be 1314, `ERROR_PRIVILEGE_NOT_HELD`. pub fn check_symlink_support() -> io::Result<bool> { let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); let sideloading = hklm.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock")?; let developer_mode: u32 = sideloading.get_value("AllowDevelopmentWithoutDevLicense")?; Ok(developer_mode == 1) } pub type FileIdentity = same_file::Handle; pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> { // `same_file::Handle::from_path()` follows symlinks, because it opens // the path without `FILE_FLAG_OPEN_REPARSE_POINT`. Open the file the // same way it does (read access, plus `FILE_FLAG_BACKUP_SEMANTICS` so a // directory can be opened too), but add `FILE_FLAG_OPEN_REPARSE_POINT` // so the handle refers to the symlink itself instead of its target. // This matches the Unix implementation, which uses `symlink_metadata()`. // The reparse-point flag is ignored for paths that aren't reparse // points, so regular files and hard links are unaffected. const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000; const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000; let file = OpenOptions::new() .read(true) .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) .open(path)?; same_file::Handle::from_file(file) } pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> { same_file::Handle::from_file(file) } } #[cfg_attr(unix, expect(dead_code))] mod fallback { use std::ffi::OsStr; use thiserror::Error; // Define error per platform so we can explicitly say UTF-8 is expected. #[derive(Debug, Error)] #[error("Invalid UTF-8 sequence")] pub struct BadOsStrEncoding; pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> { Ok(str::from_utf8(data).map_err(|_| BadOsStrEncoding)?.as_ref()) } pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> { Ok(data.to_str().ok_or(BadOsStrEncoding)?.as_ref()) } } #[cfg(test)] mod tests { use std::io::Write as _; use futures::io::Cursor; use itertools::Itertools as _; use pollster::FutureExt as _; use test_case::test_case; use super::*; use crate::tests::TestResult; use crate::tests::new_temp_dir; #[test] #[cfg(unix)] fn exec_bit_support_in_temp_dir() -> TestResult { // Temporary directories on Unix should always have executable support. // Note that it would be problematic to test in a non-temp directory, as // a developer's filesystem may or may not have executable bit support. let dir = new_temp_dir(); let supported = check_executable_bit_support(dir.path())?; assert!(supported); Ok(()) }