wdl_modules/
module_walk.rs1use std::io;
10use std::path::Path;
11use std::path::PathBuf;
12
13use thiserror::Error;
14
15use crate::hash::NON_MODULE_CONTENT;
16
17#[derive(Debug, Error)]
19pub enum ModuleWalkError {
20 #[error("symbolic link `{0}` is not permitted in a module")]
23 Symlink(String),
24
25 #[error("i/o error at `{path}`")]
27 Io {
28 path: PathBuf,
30 #[source]
32 source: io::Error,
33 },
34}
35
36#[derive(Clone, Debug, Default)]
38pub struct TreeStats {
39 pub files: usize,
41 pub bytes: u64,
43}
44
45pub fn walk_module_tree<E>(
55 root: &Path,
56 visitor: &mut dyn FnMut(&Path, u64) -> Result<(), E>,
57) -> Result<TreeStats, WalkError<E>> {
58 let mut stats = TreeStats::default();
59 walk_recursive(root, visitor, &mut stats)?;
60 Ok(stats)
61}
62
63#[derive(Debug)]
66pub enum WalkError<E> {
67 Walk(ModuleWalkError),
69 Visitor(E),
71}
72
73impl<E> From<ModuleWalkError> for WalkError<E> {
74 fn from(e: ModuleWalkError) -> Self {
75 Self::Walk(e)
76 }
77}
78
79fn walk_recursive<E>(
81 dir: &Path,
82 visitor: &mut dyn FnMut(&Path, u64) -> Result<(), E>,
83 stats: &mut TreeStats,
84) -> Result<(), WalkError<E>> {
85 let entries = std::fs::read_dir(dir).map_err(|source| {
86 WalkError::Walk(ModuleWalkError::Io {
87 path: dir.to_path_buf(),
88 source,
89 })
90 })?;
91 for entry in entries {
92 let entry = entry.map_err(|source| {
93 WalkError::Walk(ModuleWalkError::Io {
94 path: dir.to_path_buf(),
95 source,
96 })
97 })?;
98 let name = entry.file_name();
99 let path = entry.path();
100 let meta = std::fs::symlink_metadata(&path).map_err(|source| {
101 WalkError::Walk(ModuleWalkError::Io {
102 path: path.to_path_buf(),
103 source,
104 })
105 })?;
106 if meta.file_type().is_symlink() {
108 return Err(WalkError::Walk(ModuleWalkError::Symlink(
109 path.display().to_string(),
110 )));
111 }
112 if NON_MODULE_CONTENT.iter().any(|s| *s == name) {
113 continue;
114 }
115 if meta.is_dir() {
116 walk_recursive(&path, visitor, stats)?;
117 } else if meta.is_file() {
118 stats.files += 1;
119 stats.bytes = stats.bytes.saturating_add(meta.len());
120 visitor(&path, meta.len()).map_err(WalkError::Visitor)?;
121 }
122 }
123 Ok(())
124}
125
126#[cfg(all(test, unix))]
127mod tests {
128 use std::convert::Infallible;
129 use std::os::unix::fs::symlink;
130
131 use tempfile::tempdir;
132
133 use super::*;
134
135 #[test]
136 fn rejects_symlink_in_excluded_directory() -> Result<(), Box<dyn std::error::Error>> {
137 let root = tempdir()?;
138 let outside = tempdir()?;
139 symlink(outside.path(), root.path().join(".sprocket"))?;
140
141 let result = walk_module_tree(root.path(), &mut |_, _| -> Result<(), Infallible> {
142 Ok(())
143 });
144
145 assert!(matches!(
146 result,
147 Err(WalkError::Walk(ModuleWalkError::Symlink(_)))
148 ));
149 Ok(())
150 }
151}