1use std::fs;
4use std::io;
5use std::path::{Component, Path, PathBuf};
6
7use crate::{Format, STDIN};
8
9pub use fast_glob::Error as GlobError;
11
12#[derive(Debug, Clone)]
14pub struct AccumulateTarget {
15 path: PathBuf,
16 format: Format,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum AccumulateTargetError {
22 #[error("discovery does not accept stdin")]
24 Stdin,
25 #[error("discovery requires a relative target path without .. components")]
27 InvalidPath,
28 #[error("discovery requires a target with a JSON or TOML extension")]
30 UnknownExtension,
31}
32
33impl TryFrom<PathBuf> for AccumulateTarget {
34 type Error = AccumulateTargetError;
35
36 fn try_from(raw: PathBuf) -> Result<Self, Self::Error> {
37 if raw.as_os_str() == STDIN {
38 return Err(AccumulateTargetError::Stdin);
39 }
40 if raw.components().any(|component| {
41 matches!(
42 component,
43 Component::RootDir | Component::Prefix(_) | Component::ParentDir
44 )
45 }) {
46 return Err(AccumulateTargetError::InvalidPath);
47 }
48 let path: PathBuf = raw
49 .components()
50 .filter(|component| !matches!(component, Component::CurDir))
51 .collect();
52 let format = Format::from_path(&path).ok_or(AccumulateTargetError::UnknownExtension)?;
53 Ok(Self { path, format })
54 }
55}
56
57#[derive(Debug, thiserror::Error)]
59pub enum AccumulateError {
60 #[error("cannot determine the working directory")]
62 CurrentDirectory(#[source] io::Error),
63 #[error("cannot inspect a discovery path")]
65 Inspect {
66 path: PathBuf,
68 #[source]
70 source: io::Error,
71 },
72 #[error("cannot list a discovery directory")]
74 List {
75 path: PathBuf,
77 #[source]
79 source: io::Error,
80 },
81 #[error("discovery target is a directory")]
83 Directory {
84 path: PathBuf,
86 },
87 #[error("discovery target is not a regular file")]
89 NonRegular {
90 path: PathBuf,
92 },
93}
94
95pub fn accumulate(
105 target: &AccumulateTarget,
106 base_dir: Option<&Path>,
107) -> Result<Vec<PathBuf>, AccumulateError> {
108 let base = match base_dir {
109 None => PathBuf::new(),
110 Some(base) if base.is_absolute() => base.to_path_buf(),
111 Some(base) => std::path::absolute(if base.as_os_str().is_empty() {
114 Path::new(".")
115 } else {
116 base
117 })
118 .map_err(AccumulateError::CurrentDirectory)?,
119 };
120 let target_path = base.join(&target.path);
121 let metadata = fs::metadata(&target_path).map_err(|source| AccumulateError::Inspect {
122 path: target_path.clone(),
123 source,
124 })?;
125 if metadata.is_dir() {
126 return Err(AccumulateError::Directory { path: target_path });
127 }
128 if !metadata.is_file() {
129 return Err(AccumulateError::NonRegular { path: target_path });
130 }
131
132 let mut files = Vec::new();
133 let mut directory = base;
134 if let Some(parent) = target.path.parent() {
135 for component in parent.components() {
136 directory.push(component);
137 let listing_error = |source| AccumulateError::List {
138 path: directory.clone(),
139 source,
140 };
141 let entries = fs::read_dir(&directory).map_err(listing_error)?;
142 let mut matching = Vec::new();
143 for entry in entries {
144 let entry = entry.map_err(listing_error)?;
145 let path = entry.path();
146 if path == target_path || Format::from_path(&path) != Some(target.format) {
147 continue;
148 }
149 let metadata = fs::metadata(&path).map_err(|source| AccumulateError::Inspect {
150 path: path.clone(),
151 source,
152 })?;
153 if metadata.is_file() {
154 matching.push((entry.file_name(), path));
155 }
156 }
157 matching.sort_by(|left, right| left.0.cmp(&right.0));
158 files.extend(matching.into_iter().map(|(_, path)| path));
159 }
160 }
161 files.push(target_path);
162 Ok(files)
163}
164
165#[derive(Debug, Clone)]
167pub struct GlobPattern(String);
168
169impl std::str::FromStr for GlobPattern {
170 type Err = GlobError;
171
172 fn from_str(pattern: &str) -> Result<Self, Self::Err> {
173 fast_glob::validate(pattern)?;
174 Ok(Self(pattern.to_owned()))
175 }
176}
177
178impl GlobPattern {
179 pub fn matches_path(&self, path: &Path) -> bool {
181 let bytes = path.as_os_str().as_encoded_bytes();
182 #[cfg(windows)]
183 let bytes = bytes
184 .iter()
185 .map(|&byte| if byte == b'\\' { b'/' } else { byte })
186 .collect::<Vec<_>>();
187 fast_glob::glob_match(&self.0, bytes)
188 }
189
190 pub fn matches_filename(&self, path: &Path) -> bool {
192 path.file_name()
193 .is_some_and(|name| fast_glob::glob_match(&self.0, name.as_encoded_bytes()))
194 }
195}
196
197pub fn filter_paths<P: AsRef<Path>>(
202 files: &[P],
203 pattern: &GlobPattern,
204 filename_only: bool,
205) -> Vec<PathBuf> {
206 files
207 .iter()
208 .map(AsRef::as_ref)
209 .filter(|path| {
210 if filename_only {
211 pattern.matches_filename(path)
212 } else {
213 pattern.matches_path(path)
214 }
215 })
216 .map(Path::to_path_buf)
217 .collect()
218}