1use std::fs;
4use std::io;
5use std::path::{Component, Path, PathBuf};
6
7use crate::{Format, STDIN};
8
9pub use crate::glob::{GlobError, GlobPattern};
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
165impl GlobPattern {
166 pub fn matches_path(&self, path: &Path) -> bool {
168 let bytes = path.as_os_str().as_encoded_bytes();
169 #[cfg(windows)]
170 let bytes = bytes
171 .iter()
172 .map(|&byte| if byte == b'\\' { b'/' } else { byte })
173 .collect::<Vec<_>>();
174 self.matches(bytes)
175 }
176
177 pub fn matches_filename(&self, path: &Path) -> bool {
179 path.file_name()
180 .is_some_and(|name| self.matches(name.as_encoded_bytes()))
181 }
182}
183
184pub fn filter_paths<P: AsRef<Path>>(
189 files: &[P],
190 pattern: &GlobPattern,
191 filename_only: bool,
192) -> Vec<PathBuf> {
193 files
194 .iter()
195 .map(AsRef::as_ref)
196 .filter(|path| {
197 if filename_only {
198 pattern.matches_filename(path)
199 } else {
200 pattern.matches_path(path)
201 }
202 })
203 .map(Path::to_path_buf)
204 .collect()
205}