//! A crate with file-system specific utilities.//!//! ## Examples//!//! ```//! # fn main() -> Result<(), Box<dyn std::error::Error>> {//! use std::path::{Path, PathBuf};//!//! use gix_fs::{//! stack::{Delegate, ToNormalPathComponents},//! Stack,//! };//!//! let components = "src/lib.rs"//! .to_normal_path_components()//! .collect::<Result<Vec<_>, _>>()?;//! assert_eq!(//! components//! .into_iter()//! .map(|component| component.to_string_lossy().into_owned())//! .collect::<Vec<_>>(),//! vec!["src", "lib.rs"]//! );//!//! #[derive(Default)]//! struct Recorder {//! directories: Vec<PathBuf>,//! paths: Vec<PathBuf>,//! }//!//! impl Delegate for Recorder {//! fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> {//! self.directories.push(stack.current_relative().to_path_buf());//! Ok(())//! }//!//! fn push(&mut self, _is_last_component: bool, stack: &Stack) -> std::io::Result<()> {//! self.paths.push(stack.current_relative().to_path_buf());//! Ok(())//! }//!//! fn pop_directory(&mut self) {}//! }//!//! # let dir = tempfile::tempdir()?;//! let capabilities = gix_fs::Capabilities::probe_dir(dir.path());//! let mut stack = Stack::new(dir.path().to_path_buf());//! let mut recorder = Recorder::default();//! stack.make_relative_path_current("src/lib.rs", &mut recorder)?;//!//! assert_eq!(stack.current_relative(), Path::new("src/lib.rs"));//! assert_eq!(recorder.directories[0], Path::new(""));//! assert_eq!(recorder.paths, vec![PathBuf::from("src"), PathBuf::from("src/lib.rs")]);//! assert_eq!(gix_fs::current_dir(capabilities.precompose_unicode)?, std::env::current_dir()?);//! # Ok(()) }//! ```#![deny(missing_docs)]#![forbid(unsafe_code)] use std::path::PathBuf; /// Common knowledge about the worktree that is needed across most interactions with the work tree#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]pub struct Capabilities { /// If `true`, the filesystem will consider the precomposed umlaut `ä` similar to its decomposed form `"a\u{308}"` and consider them the same. /// If `false`, the filesystem will only see bytes which means that the above example could live side-by-side. /// /// Even though a filesystem that treats both forms the same will still reproduce the exact same byte sequence during traversal for instance, /// this might also mean that we see paths in their decomposed form (this happens when creating directory `ä` in MacOS Finder for example). /// /// If Git would store such decomposed paths in the repository, which only sees bytes, on linux this might mean the path will look strange /// at best, which is why it prefers to store precomposed unicode on systems where it matters, like MacOS and Windows. /// /// For best compatibility, and with this value being `true`, we will turn decomposed paths and input like command-line arguments into their /// precomposed forms, so no decomposed byte sequences should end up in storage. pub precompose_unicode: bool, /// If true, the filesystem ignores the case of input, which makes `A` the same file as `a`. /// This is also called case-folding. pub ignore_case: bool, /// If true, we assume the executable bit is honored as part of the files mode. If false, we assume the file system /// ignores the executable bit, hence it will be reported as 'off' even though we just tried to set it to be on. pub executable_bit: bool, /// If true, the file system supports symbolic links and we should try to create them. Otherwise symbolic links will be checked /// out as files which contain the link as text. pub symlink: bool,}mod capabilities; mod snapshot;pub use snapshot::{FileSnapshot, SharedFileSnapshot, SharedFileSnapshotMut}; ///pub mod symlink; ///pub mod read_dir;pub use read_dir::function::read_dir; ///pub mod dir; /// Like [`std::env::current_dir()`], but it will `precompose_unicode` if that value is true, if the current directory/// is valid unicode and if there are decomposed unicode codepoints.////// Thus, it will turn `"a\u{308}"` into `ä` if `true`./// Keeping it `false` will not alter the output.////// Note that `precompose_unicode` most be set using the `core.precomposeUnicode` git configuration.pub fn current_dir(precompose_unicode: bool) -> std::io::Result<PathBuf> { let cwd = std::env::current_dir()?; Ok(if precompose_unicode { gix_utils::str::precompose_path(cwd.into()).into_owned() } else { cwd })} /// Adjust `permissions` according to Git's shared-repository permission policy.////// `permissions` should normally be the permissions of a newly created entry, read back after the operating system has/// applied the process umask. Apply this function before the file is made visible at its final path. Existing permission/// bits not governed by the policy are preserved./// Read-only files stay read-only, and executable files gain execute bits wherever the policy grants read access./// Directory permissions must retain the file-type bits returned by metadata: directories gain search permissions/// alongside read permissions, and setgid where the platform needs it to inherit the parent directory's group.////// `shared_repository_permissions` uses Git's compact signed encoding and usually comes from parsing the effective/// `core.sharedRepository` configuration value:////// - `0` leaves the permissions produced by the umask unchanged. An absent key, `umask`, boolean `false`, and legacy/// value `0` produce this./// - A positive mode is ORed into the post-umask mode. A bare key, `group`, boolean `true`, and legacy value `1` usually/// produce `0o660`; `all`, `world`, `everybody`, and legacy value `2` usually produce `0o664`./// - A negative mode replaces the low nine Unix permission bits with its absolute value. Git-compatible parsers use this/// for explicit octal configuration such as `0640`, represented here as `-0o640`, so it can remove bits allowed by the/// umask instead of merely adding bits.////// Callers must pass the parsed encoding, not an explicit octal configuration value directly. On non-Unix platforms the/// shared-repository mode has no portable representation in [`std::fs::Permissions`], so this function returns/// `permissions` unchanged.pub fn adjust_shared_repository_permissions( permissions: std::fs::Permissions, shared_repository_permissions: i32,) -> std::fs::Permissions { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if shared_repository_permissions == 0 { return permissions; } let mut permissions = permissions; let mode = permissions.mode(); // Follow Git's calc_shared_perm() in path.c. let mut tweak = shared_repository_permissions.unsigned_abs(); if mode & 0o200 == 0 { tweak &= !0o222; } if mode & 0o100 != 0 { tweak |= (tweak & 0o444) >> 2; } let mut adjusted = if shared_repository_permissions < 0 { (mode & !0o777) | tweak } else { mode | tweak }; // Directory handling follows Git's adjust_shared_perm() in path.c. if mode & 0o170000 == 0o040000 { adjusted |= (adjusted & 0o444) >> 2; // Git's DIR_HAS_BSD_GROUP_SEMANTICS defaults exclude Darwin. if !cfg!(any(target_os = "freebsd", target_os = "openbsd")) && adjusted & 0o060 != 0 { adjusted |= 0o2000; } } permissions.set_mode(adjusted); permissions } #[cfg(not(unix))] { let _ = shared_repository_permissions; permissions }} /// Apply the parsed Git sharing policy to an existing entry at `path`, following symbolic links.////// See [`adjust_shared_repository_permissions()`] for the policy encoding and directory handling./// No metadata is read or written for the default policy (`0`) or on non-Unix platforms.pub fn set_shared_repository_permissions( path: &std::path::Path, shared_repository_permissions: i32,) -> std::io::Result<()> { #[cfg(unix)] if shared_repository_permissions != 0 { let current = std::fs::metadata(path)?.permissions(); let adjusted = adjust_shared_repository_permissions(current.clone(), shared_repository_permissions); if current != adjusted { std::fs::set_permissions(path, adjusted)?; } } #[cfg(not(unix))] let _ = (path, shared_repository_permissions); Ok(())} /// A stack of path components with the delegation of side-effects as the currently set path changes, component by component.#[derive(Clone)]pub struct Stack { /// The prefix/root for all paths we handle. root: PathBuf, /// the most recent known cached that we know is valid. current: PathBuf, /// The relative portion of `valid` that was added previously. current_relative: PathBuf, /// The amount of path components of 'current' beyond the roots components. valid_components: usize, /// If set, we assume the `current` element is a directory to affect calls to `(push|pop)_directory()`. current_is_directory: bool,} #[cfg(unix)]/// Returns whether a file has the executable permission set.pub fn is_executable(metadata: &std::fs::Metadata) -> bool { use std::os::unix::fs::MetadataExt; (metadata.mode() & 0o111) != 0} /// Classifiers for IO-errors.pub mod io_err { use std::io::ErrorKind; /// Return `true` if `err` indicates that the entry doesn't exist on disk. pub fn is_not_found(err: ErrorKind, _raw_err: Option<i32>) -> bool { err == ErrorKind::NotFound || err == ErrorKind::NotADirectory }} #[cfg(not(unix))]/// Returns whether a file has the executable permission set.pub fn is_executable(_metadata: &std::fs::Metadata) -> bool { false} ///pub mod stack;
//! A crate with file-system specific utilities.
//!
//! ## Examples
//!
//! ```
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use std::path::{Path, PathBuf};
//!
//! use gix_fs::{
//! stack::{Delegate, ToNormalPathComponents},
//! Stack,
//! };
//!
//! let components = "src/lib.rs"
//! .to_normal_path_components()
//! .collect::<Result<Vec<_>, _>>()?;
//! assert_eq!(
//! components
//! .into_iter()
//! .map(|component| component.to_string_lossy().into_owned())
//! .collect::<Vec<_>>(),
//! vec!["src", "lib.rs"]
//! );
//!
//! #[derive(Default)]
//! struct Recorder {
//! directories: Vec<PathBuf>,
//! paths: Vec<PathBuf>,
//! }
//!
//! impl Delegate for Recorder {
//! fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> {
//! self.directories.push(stack.current_relative().to_path_buf());
//! Ok(())
//! }
//!
//! fn push(&mut self, _is_last_component: bool, stack: &Stack) -> std::io::Result<()> {
//! self.paths.push(stack.current_relative().to_path_buf());
//! Ok(())
//! }
//!
//! fn pop_directory(&mut self) {}
//! }
//!
//! # let dir = tempfile::tempdir()?;
//! let capabilities = gix_fs::Capabilities::probe_dir(dir.path());
//! let mut stack = Stack::new(dir.path().to_path_buf());
//! let mut recorder = Recorder::default();
//! stack.make_relative_path_current("src/lib.rs", &mut recorder)?;
//!
//! assert_eq!(stack.current_relative(), Path::new("src/lib.rs"));
//! assert_eq!(recorder.directories[0], Path::new(""));
//! assert_eq!(recorder.paths, vec![PathBuf::from("src"), PathBuf::from("src/lib.rs")]);
//! assert_eq!(gix_fs::current_dir(capabilities.precompose_unicode)?, std::env::current_dir()?);
//! # Ok(()) }
//! ```
#![deny(missing_docs)]
#![forbid(unsafe_code)]
use std::path::PathBuf;
/// Common knowledge about the worktree that is needed across most interactions with the work tree
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
pub struct Capabilities {
/// If `true`, the filesystem will consider the precomposed umlaut `ä` similar to its decomposed form `"a\u{308}"` and consider them the same.
/// If `false`, the filesystem will only see bytes which means that the above example could live side-by-side.
///
/// Even though a filesystem that treats both forms the same will still reproduce the exact same byte sequence during traversal for instance,
/// this might also mean that we see paths in their decomposed form (this happens when creating directory `ä` in MacOS Finder for example).
///
/// If Git would store such decomposed paths in the repository, which only sees bytes, on linux this might mean the path will look strange
/// at best, which is why it prefers to store precomposed unicode on systems where it matters, like MacOS and Windows.
///
/// For best compatibility, and with this value being `true`, we will turn decomposed paths and input like command-line arguments into their
/// precomposed forms, so no decomposed byte sequences should end up in storage.
pub precompose_unicode: bool,
/// If true, the filesystem ignores the case of input, which makes `A` the same file as `a`.
/// This is also called case-folding.
pub ignore_case: bool,
/// If true, we assume the executable bit is honored as part of the files mode. If false, we assume the file system
/// ignores the executable bit, hence it will be reported as 'off' even though we just tried to set it to be on.
pub executable_bit: bool,
/// If true, the file system supports symbolic links and we should try to create them. Otherwise symbolic links will be checked
/// out as files which contain the link as text.
pub symlink: bool,
}
mod capabilities;
mod snapshot;
pub use snapshot::{FileSnapshot, SharedFileSnapshot, SharedFileSnapshotMut};
///
pub mod symlink;
///
pub mod read_dir;
pub use read_dir::function::read_dir;
///
pub mod dir;
/// Like [`std::env::current_dir()`], but it will `precompose_unicode` if that value is true, if the current directory
/// is valid unicode and if there are decomposed unicode codepoints.
///
/// Thus, it will turn `"a\u{308}"` into `ä` if `true`.
/// Keeping it `false` will not alter the output.
///
/// Note that `precompose_unicode` most be set using the `core.precomposeUnicode` git configuration.
pub fn current_dir(precompose_unicode: bool) -> std::io::Result<PathBuf> {
let cwd = std::env::current_dir()?;
Ok(if precompose_unicode {
gix_utils::str::precompose_path(cwd.into()).into_owned()
} else {
cwd
})
}
/// Adjust `permissions` according to Git's shared-repository permission policy.
///
/// `permissions` should normally be the permissions of a newly created entry, read back after the operating system has
/// applied the process umask. Apply this function before the file is made visible at its final path. Existing permission
/// bits not governed by the policy are preserved.
/// Read-only files stay read-only, and executable files gain execute bits wherever the policy grants read access.
/// Directory permissions must retain the file-type bits returned by metadata: directories gain search permissions
/// alongside read permissions, and setgid where the platform needs it to inherit the parent directory's group.
///
/// `shared_repository_permissions` uses Git's compact signed encoding and usually comes from parsing the effective
/// `core.sharedRepository` configuration value:
///
/// - `0` leaves the permissions produced by the umask unchanged. An absent key, `umask`, boolean `false`, and legacy
/// value `0` produce this.
/// - A positive mode is ORed into the post-umask mode. A bare key, `group`, boolean `true`, and legacy value `1` usually
/// produce `0o660`; `all`, `world`, `everybody`, and legacy value `2` usually produce `0o664`.
/// - A negative mode replaces the low nine Unix permission bits with its absolute value. Git-compatible parsers use this
/// for explicit octal configuration such as `0640`, represented here as `-0o640`, so it can remove bits allowed by the
/// umask instead of merely adding bits.
///
/// Callers must pass the parsed encoding, not an explicit octal configuration value directly. On non-Unix platforms the
/// shared-repository mode has no portable representation in [`std::fs::Permissions`], so this function returns
/// `permissions` unchanged.
pub fn adjust_shared_repository_permissions(
permissions: std::fs::Permissions,
shared_repository_permissions: i32,
) -> std::fs::Permissions {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if shared_repository_permissions == 0 {
return permissions;
}
let mut permissions = permissions;
let mode = permissions.mode();
// Follow Git's calc_shared_perm() in path.c.
let mut tweak = shared_repository_permissions.unsigned_abs();
if mode & 0o200 == 0 {
tweak &= !0o222;
}
if mode & 0o100 != 0 {
tweak |= (tweak & 0o444) >> 2;
}
let mut adjusted = if shared_repository_permissions < 0 {
(mode & !0o777) | tweak
} else {
mode | tweak
};
// Directory handling follows Git's adjust_shared_perm() in path.c.
if mode & 0o170000 == 0o040000 {
adjusted |= (adjusted & 0o444) >> 2;
// Git's DIR_HAS_BSD_GROUP_SEMANTICS defaults exclude Darwin.
if !cfg!(any(target_os = "freebsd", target_os = "openbsd")) && adjusted & 0o060 != 0 {
adjusted |= 0o2000;
}
}
permissions.set_mode(adjusted);
permissions
}
#[cfg(not(unix))]
{
let _ = shared_repository_permissions;
permissions
}
}
/// Apply the parsed Git sharing policy to an existing entry at `path`, following symbolic links.
///
/// See [`adjust_shared_repository_permissions()`] for the policy encoding and directory handling.
/// No metadata is read or written for the default policy (`0`) or on non-Unix platforms.
pub fn set_shared_repository_permissions(
path: &std::path::Path,
shared_repository_permissions: i32,
) -> std::io::Result<()> {
#[cfg(unix)]
if shared_repository_permissions != 0 {
let current = std::fs::metadata(path)?.permissions();
let adjusted = adjust_shared_repository_permissions(current.clone(), shared_repository_permissions);
if current != adjusted {
std::fs::set_permissions(path, adjusted)?;
}
}
#[cfg(not(unix))]
let _ = (path, shared_repository_permissions);
Ok(())
}
/// A stack of path components with the delegation of side-effects as the currently set path changes, component by component.
#[derive(Clone)]
pub struct Stack {
/// The prefix/root for all paths we handle.
root: PathBuf,
/// the most recent known cached that we know is valid.
current: PathBuf,
/// The relative portion of `valid` that was added previously.
current_relative: PathBuf,
/// The amount of path components of 'current' beyond the roots components.
valid_components: usize,
/// If set, we assume the `current` element is a directory to affect calls to `(push|pop)_directory()`.
current_is_directory: bool,
}
#[cfg(unix)]
/// Returns whether a file has the executable permission set.
pub fn is_executable(metadata: &std::fs::Metadata) -> bool {
use std::os::unix::fs::MetadataExt;
(metadata.mode() & 0o111) != 0
}
/// Classifiers for IO-errors.
pub mod io_err {
use std::io::ErrorKind;
/// Return `true` if `err` indicates that the entry doesn't exist on disk.
pub fn is_not_found(err: ErrorKind, _raw_err: Option<i32>) -> bool {
err == ErrorKind::NotFound || err == ErrorKind::NotADirectory
}
}
#[cfg(not(unix))]
/// Returns whether a file has the executable permission set.
pub fn is_executable(_metadata: &std::fs::Metadata) -> bool {
false
}
///
pub mod stack;