1use std::fs;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, thiserror::Error)]
6pub enum WorkspaceMemberError {
7 #[error("failed to read directory {path}: {source}")]
9 ReadDir {
10 path: Box<str>,
12 #[source]
14 source: std::io::Error,
15 },
16}
17
18pub fn resolve_workspace_members(
24 workspace_root: &Path,
25 members: &[Box<str>],
26) -> Result<Vec<PathBuf>, WorkspaceMemberError> {
27 let mut dirs: Vec<PathBuf> = members
28 .iter()
29 .map(|member| expand_member(workspace_root, member))
30 .collect::<Result<Vec<_>, _>>()?
31 .into_iter()
32 .flatten()
33 .filter(|path| path.join("Cargo.toml").is_file())
34 .collect();
35 dirs.sort();
36 dirs.dedup();
37 Ok(dirs)
38}
39
40fn expand_member(
41 workspace_root: &Path,
42 member: &str,
43) -> Result<Vec<PathBuf>, WorkspaceMemberError> {
44 match member.contains('*') {
45 true => expand_glob_member(workspace_root, member),
46 false => Ok(vec![workspace_root.join(member)]),
47 }
48}
49
50fn expand_glob_member(
51 workspace_root: &Path,
52 member: &str,
53) -> Result<Vec<PathBuf>, WorkspaceMemberError> {
54 let (scan_root, pattern) = scan_root_for_member(workspace_root, member);
55 if !scan_root.is_dir() {
56 return Ok(Vec::new());
57 }
58
59 let max_depth = member_path_segments(pattern.as_ref()).len();
60 let mut matches = Vec::new();
61 collect_matching_dirs(
62 &scan_root,
63 &scan_root,
64 pattern.as_ref(),
65 max_depth,
66 &mut matches,
67 )?;
68 Ok(matches)
69}
70
71fn scan_root_for_member(workspace_root: &Path, member: &str) -> (PathBuf, Box<str>) {
72 let member_segments = member_path_segments(member);
73 let split_index = member_segments
74 .iter()
75 .position(|segment| segment.contains('*'))
76 .unwrap_or(member_segments.len());
77 let mut scan_root = workspace_root.to_path_buf();
78 for segment in &member_segments[..split_index] {
79 scan_root.push(segment);
80 }
81 let pattern = member_segments[split_index..].join("/").into_boxed_str();
82 (scan_root, pattern)
83}
84
85fn collect_matching_dirs(
86 pattern_root: &Path,
87 current_dir: &Path,
88 member: &str,
89 max_depth: usize,
90 matches: &mut Vec<PathBuf>,
91) -> Result<(), WorkspaceMemberError> {
92 for entry in read_directory(current_dir)? {
93 let path = entry.path();
94 if !path.is_dir() {
95 continue;
96 }
97 if !matches_member_prefix(pattern_root, &path, member) {
98 continue;
99 }
100
101 add_matching_dir(pattern_root, &path, member, matches);
102 if relative_depth(pattern_root, &path) < max_depth {
103 collect_matching_dirs(pattern_root, &path, member, max_depth, matches)?;
104 }
105 }
106 Ok(())
107}
108
109fn add_matching_dir(workspace_root: &Path, path: &Path, member: &str, matches: &mut Vec<PathBuf>) {
110 if matches_member_pattern(workspace_root, path, member) {
111 matches.push(path.to_path_buf());
112 }
113}
114
115fn read_directory(path: &Path) -> Result<Vec<fs::DirEntry>, WorkspaceMemberError> {
116 fs::read_dir(path)
117 .map_err(|source| WorkspaceMemberError::ReadDir {
118 path: path.to_string_lossy().into(),
119 source,
120 })?
121 .collect::<Result<Vec<_>, _>>()
122 .map_err(|source| WorkspaceMemberError::ReadDir {
123 path: path.to_string_lossy().into(),
124 source,
125 })
126}
127
128fn relative_depth(workspace_root: &Path, path: &Path) -> usize {
129 path.strip_prefix(workspace_root)
130 .ok()
131 .map(path_component_count)
132 .unwrap_or(0)
133}
134
135fn matches_member_pattern(workspace_root: &Path, path: &Path, member: &str) -> bool {
136 let relative = match path.strip_prefix(workspace_root) {
137 Ok(relative) => relative,
138 Err(_) => return false,
139 };
140 let path_segments = path_segments(relative);
141 let member_segments = member_path_segments(member);
142 match path_segments.len() == member_segments.len() {
143 true => path_segments
144 .iter()
145 .zip(member_segments.iter())
146 .all(|(path_segment, member_segment)| segment_matches(path_segment, member_segment)),
147 false => false,
148 }
149}
150
151fn matches_member_prefix(workspace_root: &Path, path: &Path, member: &str) -> bool {
152 let relative = match path.strip_prefix(workspace_root) {
153 Ok(relative) => relative,
154 Err(_) => return false,
155 };
156 let path_segments = path_segments(relative);
157 let member_segments = member_path_segments(member);
158 match path_segments.len() <= member_segments.len() {
159 true => path_segments
160 .iter()
161 .zip(member_segments.iter())
162 .all(|(path_segment, member_segment)| segment_matches(path_segment, member_segment)),
163 false => false,
164 }
165}
166
167fn path_component_count(path: &Path) -> usize {
168 path.components().count()
169}
170
171fn path_segments(path: &Path) -> Vec<Box<str>> {
172 path.iter()
173 .map(|segment| segment.to_string_lossy().into_owned().into_boxed_str())
174 .collect()
175}
176
177fn member_path_segments(member: &str) -> Vec<&str> {
178 member
179 .split('/')
180 .filter(|segment| !segment.is_empty())
181 .collect()
182}
183
184fn segment_matches(path_segment: &str, pattern_segment: &str) -> bool {
185 let parts = pattern_segment.split('*').collect::<Vec<_>>();
186 match parts.len() {
187 1 => path_segment == pattern_segment,
188 _ => wildcard_segment_matches(path_segment, &parts, pattern_segment.starts_with('*')),
189 }
190}
191
192fn wildcard_segment_matches(
193 path_segment: &str,
194 parts: &[&str],
195 starts_with_wildcard: bool,
196) -> bool {
197 let mut remaining = path_segment;
198 for (index, part) in parts.iter().enumerate() {
199 if part.is_empty() {
200 continue;
201 }
202 let found = match (index == 0, starts_with_wildcard) {
203 (true, false) => remaining.strip_prefix(part),
204 _ => remaining
205 .find(part)
206 .map(|offset| &remaining[offset + part.len()..]),
207 };
208 match found {
209 Some(next) => remaining = next,
210 None => return false,
211 }
212 }
213 pattern_segment_ends_with_wildcard(parts, remaining)
214}
215
216fn pattern_segment_ends_with_wildcard(parts: &[&str], remaining: &str) -> bool {
217 match parts.last() {
218 Some(&"") => true,
219 Some(_) => remaining.is_empty(),
220 None => false,
221 }
222}