1use std::{
5 collections::HashMap,
6 fs, io,
7 path::{Path, PathBuf},
8 sync::Arc,
9};
10
11use crate::{ClientErrorAction, HasClientErrorAction, ParsedConnectPoint};
12use fs_mistrust::{CheckedDir, Mistrust};
13
14type PathEntry = (PathBuf, Result<ParsedConnectPoint, LoadError>);
16
17impl ParsedConnectPoint {
18 pub fn load_dir<'a>(
35 path: &Path,
36 mistrust: &Mistrust,
37 options: &'a HashMap<PathBuf, LoadOptions>,
38 ) -> Result<ConnPointIterator<'a>, LoadError> {
39 let dir = match mistrust.verifier().permit_readable().secure_dir(path) {
40 Ok(checked_dir) => checked_dir,
41 Err(fs_mistrust::Error::BadType(_)) => return Err(LoadError::NotADirectory),
42 Err(other) => return Err(other.into()),
43 };
44
45 let mut entries: Vec<(PathBuf, fs::DirEntry)> = dir
47 .read_directory(".")?
48 .map(|res| {
49 let dirent = res?;
50 Ok::<_, io::Error>((dirent.file_name().into(), dirent))
51 })
52 .collect::<Result<Vec<_>, _>>()?;
53 entries.sort_unstable_by(|a, b| a.0.cmp(&b.0).reverse());
57
58 Ok(ConnPointIterator {
59 dir,
60 entries,
61 options,
62 })
63 }
64
65 pub fn load_file(path: &Path, mistrust: &Mistrust) -> Result<ParsedConnectPoint, LoadError> {
69 Ok(mistrust
70 .verifier()
71 .require_file()
72 .permit_readable()
73 .file_access()
74 .follow_final_links(true)
75 .read_to_string(path)?
76 .parse()?)
77 }
78}
79
80#[derive(Debug)]
82pub struct ConnPointIterator<'a> {
83 dir: CheckedDir,
85 entries: Vec<(PathBuf, fs::DirEntry)>,
91 options: &'a HashMap<PathBuf, LoadOptions>,
93}
94
95impl<'a> Iterator for ConnPointIterator<'a> {
96 type Item = PathEntry;
97
98 fn next(&mut self) -> Option<Self::Item> {
99 loop {
100 let (fname, entry) = self.entries.pop()?;
101 if let Some(outcome) =
102 load_dirent(&self.dir, &entry, fname.as_path(), self.options).transpose()
103 {
104 return Some((self.dir.as_path().join(fname), outcome));
105 }
106 }
107 }
108}
109
110fn load_dirent(
118 dir: &CheckedDir,
119 entry: &fs::DirEntry,
120 name: &Path,
121 overrides: &HashMap<PathBuf, LoadOptions>,
122) -> Result<Option<ParsedConnectPoint>, LoadError> {
123 let settings = overrides.get(name);
124 if matches!(settings, Some(LoadOptions { disable: true })) {
125 return Ok(None);
127 }
128 if name.extension() != Some("toml".as_ref()) {
129 return Ok(None);
131 }
132 #[cfg(unix)]
133 if name.to_string_lossy().starts_with('.') {
134 return Ok(None);
136 }
137 if !entry.file_type()?.is_file() {
138 return Ok(None);
140 }
141
142 let contents = dir
143 .file_access()
144 .follow_final_links(true)
145 .read_to_string(name)?;
146 Ok(Some(contents.parse()?))
147}
148
149#[derive(Clone, Debug, derive_builder::Builder)]
151pub struct LoadOptions {
152 #[builder(default)]
154 disable: bool,
155}
156
157#[derive(Clone, Debug, thiserror::Error)]
159#[non_exhaustive]
160pub enum LoadError {
161 #[error("Problem accessing file or directory")]
166 Access(#[from] fs_mistrust::Error),
167 #[error("IO error while loading a file or directory")]
169 Io(#[source] Arc<io::Error>),
170 #[error("Unable to parse connect point")]
172 Parse(#[from] crate::connpt::ParseError),
173 #[error("not a directory")]
175 NotADirectory,
176}
177impl From<io::Error> for LoadError {
178 fn from(value: io::Error) -> Self {
179 LoadError::Io(Arc::new(value))
180 }
181}
182impl HasClientErrorAction for LoadError {
183 fn client_action(&self) -> ClientErrorAction {
184 use ClientErrorAction as A;
185 use LoadError as E;
186 match self {
187 E::Access(error) => error.client_action(),
188 E::Io(error) => crate::fs_error_action(error),
189 E::Parse(error) => error.client_action(),
190 E::NotADirectory => A::Abort,
191 }
192 }
193}
194
195#[cfg(test)]
196mod test {
197 #![allow(clippy::bool_assert_comparison)]
199 #![allow(clippy::clone_on_copy)]
200 #![allow(clippy::dbg_macro)]
201 #![allow(clippy::mixed_attributes_style)]
202 #![allow(clippy::print_stderr)]
203 #![allow(clippy::print_stdout)]
204 #![allow(clippy::single_char_pattern)]
205 #![allow(clippy::unwrap_used)]
206 #![allow(clippy::unchecked_duration_subtraction)]
207 #![allow(clippy::useless_vec)]
208 #![allow(clippy::needless_pass_by_value)]
209 use super::*;
212
213 use assert_matches::assert_matches;
214 use io::Write;
215 #[cfg(unix)]
216 use std::os::unix::fs::PermissionsExt;
217
218 use crate::testing::tempdir;
219
220 fn write(dir: &Path, fname: &str, mode: u32, content: &str) -> PathBuf {
221 #[cfg(not(unix))]
222 let _ = mode;
223
224 let p: PathBuf = dir.join(fname);
225
226 let mut f = fs::File::create(&p).unwrap();
227 f.write_all(content.as_bytes()).unwrap();
228
229 #[cfg(unix)]
231 f.set_permissions(PermissionsExt::from_mode(mode)).unwrap();
232
233 p
234 }
235
236 const EXAMPLE_1: &str = r#"
237 [connect]
238socket = "inet:[::1]:9191"
239socket_canonical = "inet:[::1]:2020"
240
241auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
242"#;
243
244 const EXAMPLE_2: &str = r#"
245[connect]
246socket = "inet:[::1]:9000"
247socket_canonical = "inet:[::1]:2000"
248
249auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
250"#;
251
252 const EXAMPLE_3: &str = r#"
253[connect]
254socket = "inet:[::1]:413"
255socket_canonical = "inet:[::1]:612"
256
257auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
258"#;
259
260 fn assert_conn_pt_eq(a: &ParsedConnectPoint, b: &ParsedConnectPoint) {
262 assert_eq!(format!("{:?}", a), format!("{:?}", b));
263 }
264 fn assert_conn_pt_ne(a: &ParsedConnectPoint, b: &ParsedConnectPoint) {
266 assert_ne!(format!("{:?}", a), format!("{:?}", b));
267 }
268
269 #[test]
271 fn load_normally() {
272 let (_tmpdir, dir, m) = tempdir();
273
274 let fname1 = write(dir.as_ref(), "01-file.toml", 0o600, EXAMPLE_1);
275 let fname2 = write(dir.as_ref(), "02-file.toml", 0o600, EXAMPLE_2);
276 let _fname3 = write(dir.as_ref(), "03-junk.toml", 0o600, "not toml at all");
278 let _not_dot_toml = write(dir.as_ref(), "README.config", 0o600, "skip me");
280 #[cfg(unix)]
282 let _dotfile = write(dir.as_ref(), ".foo.toml", 0o600, "also skipped");
283
284 let subdirname = dir.join("subdir");
286 m.make_directory(&subdirname).unwrap();
287 let _in_subdir = write(subdirname.as_ref(), "hello.toml", 0o600, EXAMPLE_1);
288
289 let connpt1: ParsedConnectPoint = EXAMPLE_1.parse().unwrap();
290 let connpt2: ParsedConnectPoint = EXAMPLE_2.parse().unwrap();
291
292 let p = ParsedConnectPoint::load_file(fname1.as_ref(), &m).unwrap();
294 assert_conn_pt_eq(&p, &connpt1);
295 assert_conn_pt_ne(&p, &connpt2);
296
297 let err = ParsedConnectPoint::load_file(dir.as_ref(), &m).unwrap_err();
299 assert_matches!(err, LoadError::Access(fs_mistrust::Error::BadType(_)));
300
301 let err = ParsedConnectPoint::load_dir(fname2.as_ref(), &m, &HashMap::new()).unwrap_err();
303 assert_matches!(err, LoadError::NotADirectory);
304
305 let v: Vec<_> = ParsedConnectPoint::load_dir(dir.as_ref(), &m, &HashMap::new())
307 .unwrap()
308 .collect();
309 assert_eq!(v.len(), 3);
310 assert_eq!(v[0].0.file_name().unwrap().to_str(), Some("01-file.toml"));
311 assert_conn_pt_eq(v[0].1.as_ref().unwrap(), &connpt1);
312 assert_eq!(v[1].0.file_name().unwrap().to_str(), Some("02-file.toml"));
313 assert_conn_pt_eq(v[1].1.as_ref().unwrap(), &connpt2);
314 assert_eq!(v[2].0.file_name().unwrap().to_str(), Some("03-junk.toml"));
315 assert_matches!(&v[2].1, Err(LoadError::Parse(_)));
316
317 let options: HashMap<_, _> = [
319 (
320 PathBuf::from("01-file.toml"),
321 LoadOptions { disable: false },
322 ), (PathBuf::from("02-file.toml"), LoadOptions { disable: true }),
324 ]
325 .into_iter()
326 .collect();
327 let v: Vec<_> = ParsedConnectPoint::load_dir(dir.as_ref(), &m, &options)
328 .unwrap()
329 .collect();
330 assert_eq!(v.len(), 2);
331 assert_conn_pt_eq(v[0].1.as_ref().unwrap(), &connpt1);
332 assert_matches!(&v[1].1, Err(LoadError::Parse(_)));
333 }
334
335 #[test]
336 #[cfg(unix)]
337 fn bad_permissions() {
338 let (_tmpdir, dir, m) = tempdir();
339
340 let fname1 = write(dir.as_ref(), "01-file.toml", 0o600, EXAMPLE_1);
341 let fname2 = write(dir.as_ref(), "02-file.toml", 0o777, EXAMPLE_2);
343 let _fname3 = write(dir.as_ref(), "03-file.toml", 0o600, EXAMPLE_3);
345
346 let connpt1: ParsedConnectPoint = EXAMPLE_1.parse().unwrap();
347 let connpt3: ParsedConnectPoint = EXAMPLE_3.parse().unwrap();
348
349 let p = ParsedConnectPoint::load_file(fname1.as_ref(), &m).unwrap();
351 assert_conn_pt_eq(&p, &connpt1);
352
353 let err: LoadError = ParsedConnectPoint::load_file(fname2.as_ref(), &m).unwrap_err();
355 assert_matches!(
356 err,
357 LoadError::Access(fs_mistrust::Error::BadPermission(..))
358 );
359
360 let v: Vec<_> = ParsedConnectPoint::load_dir(dir.as_ref(), &m, &HashMap::new())
362 .unwrap()
363 .collect();
364 assert_eq!(v.len(), 3);
365 assert_conn_pt_eq(v[0].1.as_ref().unwrap(), &connpt1);
366 assert_matches!(
367 v[1].1.as_ref().unwrap_err(),
368 LoadError::Access(fs_mistrust::Error::BadPermission(..))
369 );
370 assert_conn_pt_eq(v[2].1.as_ref().unwrap(), &connpt3);
371 }
372
373 }