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_time_subtraction)]
207 #![allow(clippy::useless_vec)]
208 #![allow(clippy::needless_pass_by_value)]
209 #![allow(clippy::string_slice)] use super::*;
213
214 use assert_matches::assert_matches;
215 use io::Write;
216 #[cfg(unix)]
217 use std::os::unix::fs::PermissionsExt;
218
219 use crate::testing::tempdir;
220
221 fn write(dir: &Path, fname: &str, mode: u32, content: &str) -> PathBuf {
222 #[cfg(not(unix))]
223 let _ = mode;
224
225 let p: PathBuf = dir.join(fname);
226
227 let mut f = fs::File::create(&p).unwrap();
228 f.write_all(content.as_bytes()).unwrap();
229
230 #[cfg(unix)]
232 f.set_permissions(PermissionsExt::from_mode(mode)).unwrap();
233
234 p
235 }
236
237 const EXAMPLE_1: &str = r#"
238 [connect]
239socket = "inet:[::1]:9191"
240socket_canonical = "inet:[::1]:2020"
241
242auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
243"#;
244
245 const EXAMPLE_2: &str = r#"
246[connect]
247socket = "inet:[::1]:9000"
248socket_canonical = "inet:[::1]:2000"
249
250auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
251"#;
252
253 const EXAMPLE_3: &str = r#"
254[connect]
255socket = "inet:[::1]:413"
256socket_canonical = "inet:[::1]:612"
257
258auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
259"#;
260
261 fn assert_conn_pt_eq(a: &ParsedConnectPoint, b: &ParsedConnectPoint) {
263 assert_eq!(format!("{:?}", a), format!("{:?}", b));
264 }
265 fn assert_conn_pt_ne(a: &ParsedConnectPoint, b: &ParsedConnectPoint) {
267 assert_ne!(format!("{:?}", a), format!("{:?}", b));
268 }
269
270 #[test]
272 fn load_normally() {
273 let (_tmpdir, dir, m) = tempdir();
274
275 let fname1 = write(dir.as_ref(), "01-file.toml", 0o600, EXAMPLE_1);
276 let fname2 = write(dir.as_ref(), "02-file.toml", 0o600, EXAMPLE_2);
277 let _fname3 = write(dir.as_ref(), "03-junk.toml", 0o600, "not toml at all");
279 let _not_dot_toml = write(dir.as_ref(), "README.config", 0o600, "skip me");
281 #[cfg(unix)]
283 let _dotfile = write(dir.as_ref(), ".foo.toml", 0o600, "also skipped");
284
285 let subdirname = dir.join("subdir");
287 m.make_directory(&subdirname).unwrap();
288 let _in_subdir = write(subdirname.as_ref(), "hello.toml", 0o600, EXAMPLE_1);
289
290 let connpt1: ParsedConnectPoint = EXAMPLE_1.parse().unwrap();
291 let connpt2: ParsedConnectPoint = EXAMPLE_2.parse().unwrap();
292
293 let p = ParsedConnectPoint::load_file(fname1.as_ref(), &m).unwrap();
295 assert_conn_pt_eq(&p, &connpt1);
296 assert_conn_pt_ne(&p, &connpt2);
297
298 let err = ParsedConnectPoint::load_file(dir.as_ref(), &m).unwrap_err();
300 assert_matches!(err, LoadError::Access(fs_mistrust::Error::BadType(_)));
301
302 let err = ParsedConnectPoint::load_dir(fname2.as_ref(), &m, &HashMap::new()).unwrap_err();
304 assert_matches!(err, LoadError::NotADirectory);
305
306 let v: Vec<_> = ParsedConnectPoint::load_dir(dir.as_ref(), &m, &HashMap::new())
308 .unwrap()
309 .collect();
310 assert_eq!(v.len(), 3);
311 assert_eq!(v[0].0.file_name().unwrap().to_str(), Some("01-file.toml"));
312 assert_conn_pt_eq(v[0].1.as_ref().unwrap(), &connpt1);
313 assert_eq!(v[1].0.file_name().unwrap().to_str(), Some("02-file.toml"));
314 assert_conn_pt_eq(v[1].1.as_ref().unwrap(), &connpt2);
315 assert_eq!(v[2].0.file_name().unwrap().to_str(), Some("03-junk.toml"));
316 assert_matches!(&v[2].1, Err(LoadError::Parse(_)));
317
318 let options: HashMap<_, _> = [
320 (
321 PathBuf::from("01-file.toml"),
322 LoadOptions { disable: false },
323 ), (PathBuf::from("02-file.toml"), LoadOptions { disable: true }),
325 ]
326 .into_iter()
327 .collect();
328 let v: Vec<_> = ParsedConnectPoint::load_dir(dir.as_ref(), &m, &options)
329 .unwrap()
330 .collect();
331 assert_eq!(v.len(), 2);
332 assert_conn_pt_eq(v[0].1.as_ref().unwrap(), &connpt1);
333 assert_matches!(&v[1].1, Err(LoadError::Parse(_)));
334 }
335
336 #[test]
337 #[cfg(unix)]
338 fn bad_permissions() {
339 let (_tmpdir, dir, m) = tempdir();
340
341 let fname1 = write(dir.as_ref(), "01-file.toml", 0o600, EXAMPLE_1);
342 let fname2 = write(dir.as_ref(), "02-file.toml", 0o777, EXAMPLE_2);
344 let _fname3 = write(dir.as_ref(), "03-file.toml", 0o600, EXAMPLE_3);
346
347 let connpt1: ParsedConnectPoint = EXAMPLE_1.parse().unwrap();
348 let connpt3: ParsedConnectPoint = EXAMPLE_3.parse().unwrap();
349
350 let p = ParsedConnectPoint::load_file(fname1.as_ref(), &m).unwrap();
352 assert_conn_pt_eq(&p, &connpt1);
353
354 let err: LoadError = ParsedConnectPoint::load_file(fname2.as_ref(), &m).unwrap_err();
356 assert_matches!(
357 err,
358 LoadError::Access(fs_mistrust::Error::BadPermission(..))
359 );
360
361 let v: Vec<_> = ParsedConnectPoint::load_dir(dir.as_ref(), &m, &HashMap::new())
363 .unwrap()
364 .collect();
365 assert_eq!(v.len(), 3);
366 assert_conn_pt_eq(v[0].1.as_ref().unwrap(), &connpt1);
367 assert_matches!(
368 v[1].1.as_ref().unwrap_err(),
369 LoadError::Access(fs_mistrust::Error::BadPermission(..))
370 );
371 assert_conn_pt_eq(v[2].1.as_ref().unwrap(), &connpt3);
372 }
373
374 }