wdl_modules/
module_walk.rs1use std::io;
12use std::path::Path;
13use std::path::PathBuf;
14
15use thiserror::Error;
16
17use crate::hash::NON_MODULE_CONTENT;
18
19#[derive(Debug, Error)]
21pub enum ModuleWalkError {
22 #[error("symbolic link `{0}` resolves outside the module root")]
24 SymlinkEscapesRoot(String),
25
26 #[error("symbolic link `{0}` targets a directory")]
31 DirectorySymlink(String),
32
33 #[error("symbolic link target under `{0}` is not UTF-8")]
35 NonUtf8SymlinkTarget(String),
36
37 #[error("symbolic link `{0}` targets non-module content")]
40 SymlinkTargetsMetadata(String),
41
42 #[error("i/o error at `{path}`")]
44 Io {
45 path: PathBuf,
47 #[source]
49 source: io::Error,
50 },
51}
52
53#[derive(Clone, Debug, Default)]
55pub struct TreeStats {
56 pub files: usize,
58 pub bytes: u64,
60}
61
62pub fn walk_module_tree<E>(
75 root: &Path,
76 visitor: &mut dyn FnMut(&Path, u64) -> Result<(), E>,
77) -> Result<TreeStats, WalkError<E>> {
78 let canonical_root = std::fs::canonicalize(root).map_err(|source| {
79 WalkError::Walk(ModuleWalkError::Io {
80 path: root.to_path_buf(),
81 source,
82 })
83 })?;
84 let mut stats = TreeStats::default();
85 walk_recursive(&canonical_root, root, visitor, &mut stats)?;
86 Ok(stats)
87}
88
89#[derive(Debug)]
92pub enum WalkError<E> {
93 Walk(ModuleWalkError),
95 Visitor(E),
97}
98
99impl<E> From<ModuleWalkError> for WalkError<E> {
100 fn from(e: ModuleWalkError) -> Self {
101 Self::Walk(e)
102 }
103}
104
105fn walk_recursive<E>(
107 module_root: &Path,
108 dir: &Path,
109 visitor: &mut dyn FnMut(&Path, u64) -> Result<(), E>,
110 stats: &mut TreeStats,
111) -> Result<(), WalkError<E>> {
112 let entries = std::fs::read_dir(dir).map_err(|source| {
113 WalkError::Walk(ModuleWalkError::Io {
114 path: dir.to_path_buf(),
115 source,
116 })
117 })?;
118 for entry in entries {
119 let entry = entry.map_err(|source| {
120 WalkError::Walk(ModuleWalkError::Io {
121 path: dir.to_path_buf(),
122 source,
123 })
124 })?;
125 let name = entry.file_name();
126 if NON_MODULE_CONTENT.iter().any(|s| *s == name) {
127 continue;
128 }
129 let path = entry.path();
130 let meta = std::fs::symlink_metadata(&path).map_err(|source| {
131 WalkError::Walk(ModuleWalkError::Io {
132 path: path.to_path_buf(),
133 source,
134 })
135 })?;
136 if meta.file_type().is_symlink() {
137 handle_symlink(module_root, &path, visitor, stats)?;
138 continue;
139 }
140 if meta.is_dir() {
141 walk_recursive(module_root, &path, visitor, stats)?;
142 } else if meta.is_file() {
143 stats.files += 1;
144 stats.bytes = stats.bytes.saturating_add(meta.len());
145 visitor(&path, meta.len()).map_err(WalkError::Visitor)?;
146 }
147 }
148 Ok(())
149}
150
151fn handle_symlink<E>(
153 module_root: &Path,
154 path: &Path,
155 visitor: &mut dyn FnMut(&Path, u64) -> Result<(), E>,
156 stats: &mut TreeStats,
157) -> Result<(), WalkError<E>> {
158 let target = std::fs::canonicalize(path).map_err(|source| {
159 WalkError::Walk(ModuleWalkError::Io {
160 path: path.to_path_buf(),
161 source,
162 })
163 })?;
164 if !target.starts_with(module_root) {
165 return Err(WalkError::Walk(ModuleWalkError::SymlinkEscapesRoot(
166 path.display().to_string(),
167 )));
168 }
169 if let Ok(rel) = target.strip_prefix(module_root) {
170 if rel.to_str().is_none() {
171 return Err(WalkError::Walk(ModuleWalkError::NonUtf8SymlinkTarget(
172 path.display().to_string(),
173 )));
174 }
175 let targets_metadata = rel
178 .components()
179 .any(|c| NON_MODULE_CONTENT.contains(&c.as_os_str().to_str().unwrap()));
180 if targets_metadata {
181 return Err(WalkError::Walk(ModuleWalkError::SymlinkTargetsMetadata(
182 path.display().to_string(),
183 )));
184 }
185 }
186 let target_meta = std::fs::metadata(path).map_err(|source| {
187 WalkError::Walk(ModuleWalkError::Io {
188 path: path.to_path_buf(),
189 source,
190 })
191 })?;
192 if target_meta.is_dir() {
193 return Err(WalkError::Walk(ModuleWalkError::DirectorySymlink(
194 path.display().to_string(),
195 )));
196 }
197 if target_meta.is_file() {
198 stats.files += 1;
199 stats.bytes = stats.bytes.saturating_add(target_meta.len());
200 visitor(path, target_meta.len()).map_err(WalkError::Visitor)?;
201 }
202 Ok(())
203}