diff --git a/src/token/mod.rs b/src/token/mod.rs index 9882559..16c6784 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -21,7 +21,7 @@ use crate::token::walk::{BranchFold, Fold, FoldMap, Starting, TokenEntry}; use crate::{StrExt as _, PATHS_ARE_CASE_INSENSITIVE}; pub use crate::token::parse::{parse, ParseError, ROOT_SEPARATOR_EXPRESSION}; -pub use crate::token::variance::bound::NaturalRange; +pub use crate::token::variance::bound::{NaturalRange, VariantRange}; pub use crate::token::variance::invariant::{Breadth, Depth, GlobVariance, Invariant, Size, Text}; pub use crate::token::variance::Variance; diff --git a/src/walk/glob.rs b/src/walk/glob.rs index b668146..8437bc0 100644 --- a/src/walk/glob.rs +++ b/src/walk/glob.rs @@ -193,44 +193,48 @@ impl<'t> Glob<'t> { Some(prefix.into()) } }; - // Establish the root directory and any prefix in that root path that is not a part of the - // glob expression. The directory tree is traversed from `root`, which may include an - // invariant prefix from the glob. The `prefix` is an integer that specifies how many - // components from the end of the root path must be popped to get the portion of the root - // path that is not present in the glob. The prefix may be empty or may be the entirety of - // `root` depending on `directory` and the glob. + // Establish the root path and any pivot in that root path from the given directory and any + // invariant prefix in the glob. The file system is traversed from this root path. The + // pivot partitions the root path into the given directory and any invariant prefix by + // specifying how many components from the end of the root path must be popped to restore + // the given directory. The popped components form the invariant prefix of the glob. Either + // partition of the root path may be empty depending on the given directory and the glob + // pattern. In this way, any invariant prefix of the glob becomes a postfix in the root + // path. // - // Note that a rooted glob, like in `Path::join`, replaces `directory` when establishing - // the root path. In this case, there is no prefix, as the entire root path is present in - // the glob expression. - let (root, prefix) = match prefix { + // Note that a rooted glob, like in `Path::join`, replaces the given directory when + // establishing the root path. In this case, there is no invariant prefix (the pivot is + // zero), as the entire root path is present in the glob expression and the given directory + // is completely discarded. + let (root, pivot) = match prefix { Some(prefix) => directory.join_and_get_depth(prefix), _ => (directory, 0), }; - Anchor { root, prefix } + Anchor { root, pivot } } } -/// Root path and prefix of a `Glob` when walking a particular path. +/// Root path and pivot of a `Glob` when walking a particular directory. #[derive(Clone, Debug)] struct Anchor { - /// The root (starting) directory of the walk. + /// The root (starting) path of the walk. root: PathBuf, - // TODO: Is there a better name for this? This is a prefix w.r.t. a glob but is a suffix w.r.t. - // the root directory. This can be a bit confusing since either perspective is reasonable - // (and in some contexts one may be more intuitive than the other). - /// The number of components from the end of `root` that are present in the `Glob`'s - /// expression. - prefix: usize, + /// The number of components from the end of `root` that are present in any invariant prefix of + /// the glob expression. + /// + /// The pivot partitions the root path into a target directory and any invariant prefix in the + /// `Glob` (this prefix becomes a postfix in the root path or, when rooted, replaces any target + /// directory). + pivot: usize, } impl Anchor { pub fn root_prefix_paths(&self) -> (&Path, &Path) { - self.root.split_at_depth(self.prefix) + self.root.split_at_depth(self.pivot) } pub fn walk_with_behavior(self, behavior: impl Into) -> WalkTree { - WalkTree::with_prefix_and_behavior(self.root, self.prefix, behavior) + WalkTree::with_pivot_and_behavior(self.root, self.pivot, behavior) } } diff --git a/src/walk/mod.rs b/src/walk/mod.rs index d668c77..6626b16 100644 --- a/src/walk/mod.rs +++ b/src/walk/mod.rs @@ -69,6 +69,7 @@ mod glob; use std::fs::{FileType, Metadata}; use std::io; +use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use thiserror::Error; use walkdir::{self, DirEntry, WalkDir}; @@ -316,6 +317,139 @@ pub enum LinkBehavior { ReadTarget, } +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum DepthRoot { + Path(T), + Prefix(T), +} + +impl DepthRoot { + pub fn into_inner(self) -> T { + match self { + DepthRoot::Path(inner) | DepthRoot::Prefix(inner) => inner, + } + } + + pub fn map(self, f: F) -> DepthRoot + where + F: FnOnce(T) -> U, + { + match self { + DepthRoot::Path(inner) => DepthRoot::Path(f(inner)), + DepthRoot::Prefix(inner) => DepthRoot::Prefix(f(inner)), + } + } + + pub fn path(self) -> Option { + match self { + DepthRoot::Path(inner) => Some(inner), + _ => None, + } + } + + pub fn prefix(self) -> Option { + match self { + DepthRoot::Prefix(inner) => Some(inner), + _ => None, + } + } + + pub fn as_ref(&self) -> DepthRoot<&T> { + match self { + DepthRoot::Path(ref inner) => DepthRoot::Path(inner), + DepthRoot::Prefix(ref inner) => DepthRoot::Prefix(inner), + } + } +} + +impl DepthRoot { + fn min_max_at_pivot(self, pivot: usize) -> (usize, usize) { + match self { + DepthRoot::Path(minmax) => ( + minmax.min.get().saturating_sub(pivot), + minmax.max().get().saturating_sub(pivot), + ), + DepthRoot::Prefix(minmax) => (minmax.min.into(), minmax.max().into()), + } + } +} + +impl DepthRoot { + fn min_at_pivot(self, pivot: usize) -> usize { + match self { + DepthRoot::Path(min) => min.get().saturating_sub(pivot), + DepthRoot::Prefix(min) => min.into(), + } + } +} + +impl DepthRoot { + fn max_at_pivot(self, pivot: usize) -> usize { + match self { + DepthRoot::Path(max) => max.saturating_sub(pivot), + DepthRoot::Prefix(max) => max, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DepthMinMax { + pub min: NonZeroUsize, + pub extent: usize, +} + +impl DepthMinMax { + pub fn max(&self) -> NonZeroUsize { + self.min.saturating_add(self.extent) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum DepthBehavior { + #[default] + Unbounded, + Min(DepthRoot), + Max(DepthRoot), + MinMax(DepthRoot), +} + +impl DepthBehavior { + fn bounded( + root: DepthRoot<()>, + min: impl Into>, + max: impl Into>, + ) -> Option { + use DepthBehavior::{Max, Min, MinMax}; + + match (min.into(), max.into()) { + (Some(min), None) => NonZeroUsize::new(min).map(|min| root.map(|_| min)).map(Min), + (None, Some(max)) => Some(Max(root.map(|_| max))), + (Some(min), Some(max)) if min <= max => NonZeroUsize::new(min) + .map(|min| DepthMinMax { + min, + extent: max - min.get(), + }) + .map(|minmax| root.map(|_| minmax)) + .map(MinMax), + _ => None, + } + } + + pub fn bounded_from_path( + min: impl Into>, + max: impl Into>, + ) -> Option { + Self::bounded(DepthRoot::Path(()), min, max) + } + + pub fn bounded_from_prefix( + min: impl Into>, + max: impl Into>, + ) -> Option { + Self::bounded(DepthRoot::Prefix(()), min, max) + } +} + /// Configuration for walking directory trees. /// /// Determines the behavior of the traversal within a directory tree when using functions like @@ -343,7 +477,7 @@ pub enum LinkBehavior { /// ``` /// /// [`Glob::walk_with_behavior`]: crate::Glob::walk_with_behavior -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct WalkBehavior { // TODO: Consider using a dedicated type for this field. Using primitive types does not // interact well with conversions used in `walk` APIs. For example, if another `usize` @@ -363,7 +497,7 @@ pub struct WalkBehavior { /// [`Path`]: std::path::Path /// [`PathExt::walk`]: crate::walk::PathExt::walk /// [`usize::MAX`]: usize::MAX - pub depth: usize, + pub depth: DepthBehavior, /// Interpretation of symbolic links. /// /// Determines how symbolic links are interpreted when walking a directory tree. See @@ -387,14 +521,14 @@ pub struct WalkBehavior { /// [`link`]: crate::walk::WalkBehavior::link /// [`LinkBehavior::ReadFile`]: crate::walk::LinkBehavior::ReadFile /// [`usize::MAX`]: usize::MAX -impl Default for WalkBehavior { - fn default() -> Self { - WalkBehavior { - depth: usize::MAX, - link: LinkBehavior::default(), - } - } -} +//impl Default for WalkBehavior { +// fn default() -> Self { +// WalkBehavior { +// depth: usize::MAX, +// link: LinkBehavior::default(), +// } +// } +//} impl From<()> for WalkBehavior { fn from(_: ()) -> Self { @@ -402,19 +536,19 @@ impl From<()> for WalkBehavior { } } -impl From for WalkBehavior { - fn from(link: LinkBehavior) -> Self { +impl From for WalkBehavior { + fn from(depth: DepthBehavior) -> Self { WalkBehavior { - link, + depth, ..Default::default() } } } -impl From for WalkBehavior { - fn from(depth: usize) -> Self { +impl From for WalkBehavior { + fn from(link: LinkBehavior) -> Self { WalkBehavior { - depth, + link, ..Default::default() } } @@ -432,6 +566,14 @@ pub trait Entry { /// Gets the path of the file. fn path(&self) -> &Path; + // TODO: Refactor and rename these paths. There are _three_ parts of interest in a matched + // path: + // + // 1. The matched path. + // 2. The root. + // 3. The directory (target). + // + // (2) and (3) are currently captured by `Anchor`. /// Gets the root and relative paths. /// /// The root path is the path to the walked directory from which the file entry has been read. @@ -485,7 +627,7 @@ pub trait Entry { #[derive(Clone, Debug)] pub struct TreeEntry { entry: DirEntry, - prefix: usize, + pivot: usize, } impl Entry for TreeEntry { @@ -500,7 +642,7 @@ impl Entry for TreeEntry { fn root_relative_paths(&self) -> (&Path, &Path) { self.path().split_at_depth( self.depth() - .checked_add(self.prefix) + .checked_add(self.pivot) .expect("overflow determining root-relative paths"), ) } @@ -541,34 +683,40 @@ impl Entry for TreeEntry { /// [`PathExt::walk`]: crate::walk::PathExt::walk #[derive(Debug)] pub struct WalkTree { - prefix: usize, + pivot: usize, is_dir: bool, input: walkdir::IntoIter, } impl WalkTree { fn with_behavior(root: impl Into, behavior: impl Into) -> Self { - WalkTree::with_prefix_and_behavior(root, 0, behavior) + WalkTree::with_pivot_and_behavior(root, 0, behavior) } - fn with_prefix_and_behavior( + fn with_pivot_and_behavior( root: impl Into, - prefix: usize, + pivot: usize, behavior: impl Into, ) -> Self { let root = root.into(); let WalkBehavior { link, depth } = behavior.into(); - let builder = WalkDir::new(root.as_path()); + let builder = WalkDir::new(root.as_path()).follow_links(match link { + LinkBehavior::ReadFile => false, + LinkBehavior::ReadTarget => true, + }); + let builder = match depth { + DepthBehavior::Max(max) => builder.max_depth(max.max_at_pivot(pivot)), + DepthBehavior::Min(min) => builder.min_depth(min.min_at_pivot(pivot)), + DepthBehavior::MinMax(minmax) => { + let (min, max) = minmax.min_max_at_pivot(pivot); + builder.min_depth(min).max_depth(max) + }, + DepthBehavior::Unbounded => builder, + }; WalkTree { - prefix, + pivot, is_dir: false, - input: builder - .follow_links(match link { - LinkBehavior::ReadFile => false, - LinkBehavior::ReadTarget => true, - }) - .max_depth(depth) - .into_iter(), + input: builder.into_iter(), } } } @@ -594,7 +742,7 @@ impl Iterator for WalkTree { entry.file_type().is_dir(), Some(Ok(TreeEntry { entry, - prefix: self.prefix, + pivot: self.pivot, })), ), Err(error) => (false, Some(Err(error.into()))), @@ -1019,7 +1167,7 @@ mod tests { use crate::walk::filter::{HierarchicalIterator, Separation, TreeResidue}; use crate::walk::harness::{self, assert_set_eq, TempTree}; - use crate::walk::{Entry, FileIterator, LinkBehavior, PathExt, WalkBehavior}; + use crate::walk::{DepthBehavior, Entry, FileIterator, LinkBehavior, PathExt, WalkBehavior}; use crate::Glob; // TODO: Rust's testing framework does not provide a mechanism for maintaining shared state nor @@ -1274,15 +1422,51 @@ mod tests { harness::assert_walk_paths_eq( Glob::new("**").unwrap().walk_with_behavior( temptree.as_ref(), - WalkBehavior { - depth: 1, - ..Default::default() - }, + DepthBehavior::bounded_from_path(None, 1).unwrap(), ), temptree.join_all(["", "doc", "src", "tests", "README.md"]), ); } + #[rstest] + fn walk_glob_with_zero_max_depth_behavior_includes_only_root(temptree: TempTree) { + harness::assert_walk_paths_eq( + Glob::new("**").unwrap().walk_with_behavior( + temptree.as_ref(), + DepthBehavior::bounded_from_path(None, 0).unwrap(), + ), + [temptree.as_ref()], + ); + } + + #[rstest] + fn walk_glob_with_min_max_depth_from_path_behavior_excludes_ancestors_and_descendants( + temptree: TempTree, + ) { + harness::assert_walk_paths_eq( + Glob::new("tests/**").unwrap().walk_with_behavior( + temptree.as_ref(), + DepthBehavior::bounded_from_path(2, 2).unwrap(), + ), + temptree.join_all(["tests/harness", "tests/walk.rs"]), + ); + } + + // TODO: The test fixture does not have sufficient depth to demonstrate that the max depth + // behavior is not exceeded here. + #[rstest] + fn walk_glob_with_min_max_depth_from_prefix_behavior_excludes_ancestors_and_descendants( + temptree: TempTree, + ) { + harness::assert_walk_paths_eq( + Glob::new("tests/**").unwrap().walk_with_behavior( + temptree.as_ref(), + DepthBehavior::bounded_from_prefix(2, 2).unwrap(), + ), + temptree.join_all(["tests/harness/mod.rs"]), + ); + } + #[cfg(any(unix, windows))] #[rstest] fn walk_glob_with_read_link_file_behavior_includes_link_file(