use std::{
collections::HashMap,
fs, io,
path::{Path, PathBuf},
sync::Arc,
};
use crate::{ClientErrorAction, HasClientErrorAction, ParsedConnectPoint};
use fs_mistrust::{CheckedDir, Mistrust};
type PathEntry = (PathBuf, Result<ParsedConnectPoint, LoadError>);
impl ParsedConnectPoint {
pub fn load_dir<'a>(
path: &Path,
mistrust: &Mistrust,
options: &'a HashMap<PathBuf, LoadOptions>,
) -> Result<ConnPointIterator<'a>, LoadError> {
let dir = match mistrust.verifier().permit_readable().secure_dir(path) {
Ok(checked_dir) => checked_dir,
Err(fs_mistrust::Error::BadType(_)) => return Err(LoadError::NotADirectory),
Err(other) => return Err(other.into()),
};
let mut entries: Vec<(PathBuf, fs::DirEntry)> = dir
.read_directory(".")?
.map(|res| {
let dirent = res?;
Ok::<_, io::Error>((dirent.file_name().into(), dirent))
})
.collect::<Result<Vec<_>, _>>()?;
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0).reverse());
Ok(ConnPointIterator {
dir,
entries,
options,
})
}
pub fn load_file(path: &Path, mistrust: &Mistrust) -> Result<ParsedConnectPoint, LoadError> {
Ok(mistrust
.verifier()
.require_file()
.permit_readable()
.file_access()
.follow_final_links(true)
.read_to_string(path)?
.parse()?)
}
}
#[derive(Debug)]
pub struct ConnPointIterator<'a> {
dir: CheckedDir,
entries: Vec<(PathBuf, fs::DirEntry)>,
options: &'a HashMap<PathBuf, LoadOptions>,
}
impl<'a> Iterator for ConnPointIterator<'a> {
type Item = PathEntry;
fn next(&mut self) -> Option<Self::Item> {
loop {
let (fname, entry) = self.entries.pop()?;
if let Some(outcome) =
load_dirent(&self.dir, &entry, fname.as_path(), self.options).transpose()
{
return Some((self.dir.as_path().join(fname), outcome));
}
}
}
}
fn load_dirent(
dir: &CheckedDir,
entry: &fs::DirEntry,
name: &Path,
overrides: &HashMap<PathBuf, LoadOptions>,
) -> Result<Option<ParsedConnectPoint>, LoadError> {
let settings = overrides.get(name);
if matches!(settings, Some(LoadOptions { disable: true })) {
return Ok(None);
}
if name.extension() != Some("toml".as_ref()) {
return Ok(None);
}
#[cfg(unix)]
if name.to_string_lossy().starts_with('.') {
return Ok(None);
}
if !entry.file_type()?.is_file() {
return Ok(None);
}
let contents = dir
.file_access()
.follow_final_links(true)
.read_to_string(name)?;
Ok(Some(contents.parse()?))
}
#[derive(Clone, Debug, derive_builder::Builder)]
pub struct LoadOptions {
#[builder(default)]
disable: bool,
}
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LoadError {
#[error("Problem accessing file or directory")]
Access(#[from] fs_mistrust::Error),
#[error("IO error while loading a file or directory")]
Io(#[source] Arc<io::Error>),
#[error("Unable to parse connect point")]
Parse(#[from] crate::connpt::ParseError),
#[error("not a directory")]
NotADirectory,
}
impl From<io::Error> for LoadError {
fn from(value: io::Error) -> Self {
LoadError::Io(Arc::new(value))
}
}
impl HasClientErrorAction for LoadError {
fn client_action(&self) -> ClientErrorAction {
use ClientErrorAction as A;
use LoadError as E;
match self {
E::Access(error) => error.client_action(),
E::Io(error) => crate::fs_error_action(error),
E::Parse(error) => error.client_action(),
E::NotADirectory => A::Abort,
}
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use assert_matches::assert_matches;
use io::Write;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use crate::testing::tempdir;
fn write(dir: &Path, fname: &str, mode: u32, content: &str) -> PathBuf {
#[cfg(not(unix))]
let _ = mode;
let p: PathBuf = dir.join(fname);
let mut f = fs::File::create(&p).unwrap();
f.write_all(content.as_bytes()).unwrap();
#[cfg(unix)]
f.set_permissions(PermissionsExt::from_mode(mode)).unwrap();
p
}
const EXAMPLE_1: &str = r#"
[connect]
socket = "inet:[::1]:9191"
socket_canonical = "inet:[::1]:2020"
auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
"#;
const EXAMPLE_2: &str = r#"
[connect]
socket = "inet:[::1]:9000"
socket_canonical = "inet:[::1]:2000"
auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
"#;
const EXAMPLE_3: &str = r#"
[connect]
socket = "inet:[::1]:413"
socket_canonical = "inet:[::1]:612"
auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
"#;
fn assert_conn_pt_eq(a: &ParsedConnectPoint, b: &ParsedConnectPoint) {
assert_eq!(format!("{:?}", a), format!("{:?}", b));
}
fn assert_conn_pt_ne(a: &ParsedConnectPoint, b: &ParsedConnectPoint) {
assert_ne!(format!("{:?}", a), format!("{:?}", b));
}
#[test]
fn load_normally() {
let (_tmpdir, dir, m) = tempdir();
let fname1 = write(dir.as_ref(), "01-file.toml", 0o600, EXAMPLE_1);
let fname2 = write(dir.as_ref(), "02-file.toml", 0o600, EXAMPLE_2);
let _fname3 = write(dir.as_ref(), "03-junk.toml", 0o600, "not toml at all");
let _not_dot_toml = write(dir.as_ref(), "README.config", 0o600, "skip me");
#[cfg(unix)]
let _dotfile = write(dir.as_ref(), ".foo.toml", 0o600, "also skipped");
let subdirname = dir.join("subdir");
m.make_directory(&subdirname).unwrap();
let _in_subdir = write(subdirname.as_ref(), "hello.toml", 0o600, EXAMPLE_1);
let connpt1: ParsedConnectPoint = EXAMPLE_1.parse().unwrap();
let connpt2: ParsedConnectPoint = EXAMPLE_2.parse().unwrap();
let p = ParsedConnectPoint::load_file(fname1.as_ref(), &m).unwrap();
assert_conn_pt_eq(&p, &connpt1);
assert_conn_pt_ne(&p, &connpt2);
let err = ParsedConnectPoint::load_file(dir.as_ref(), &m).unwrap_err();
assert_matches!(err, LoadError::Access(fs_mistrust::Error::BadType(_)));
let err = ParsedConnectPoint::load_dir(fname2.as_ref(), &m, &HashMap::new()).unwrap_err();
assert_matches!(err, LoadError::NotADirectory);
let v: Vec<_> = ParsedConnectPoint::load_dir(dir.as_ref(), &m, &HashMap::new())
.unwrap()
.collect();
assert_eq!(v.len(), 3);
assert_eq!(v[0].0.file_name().unwrap().to_str(), Some("01-file.toml"));
assert_conn_pt_eq(v[0].1.as_ref().unwrap(), &connpt1);
assert_eq!(v[1].0.file_name().unwrap().to_str(), Some("02-file.toml"));
assert_conn_pt_eq(v[1].1.as_ref().unwrap(), &connpt2);
assert_eq!(v[2].0.file_name().unwrap().to_str(), Some("03-junk.toml"));
assert_matches!(&v[2].1, Err(LoadError::Parse(_)));
let options: HashMap<_, _> = [
(
PathBuf::from("01-file.toml"),
LoadOptions { disable: false },
), (PathBuf::from("02-file.toml"), LoadOptions { disable: true }),
]
.into_iter()
.collect();
let v: Vec<_> = ParsedConnectPoint::load_dir(dir.as_ref(), &m, &options)
.unwrap()
.collect();
assert_eq!(v.len(), 2);
assert_conn_pt_eq(v[0].1.as_ref().unwrap(), &connpt1);
assert_matches!(&v[1].1, Err(LoadError::Parse(_)));
}
#[test]
#[cfg(unix)]
fn bad_permissions() {
let (_tmpdir, dir, m) = tempdir();
let fname1 = write(dir.as_ref(), "01-file.toml", 0o600, EXAMPLE_1);
let fname2 = write(dir.as_ref(), "02-file.toml", 0o777, EXAMPLE_2);
let _fname3 = write(dir.as_ref(), "03-file.toml", 0o600, EXAMPLE_3);
let connpt1: ParsedConnectPoint = EXAMPLE_1.parse().unwrap();
let connpt3: ParsedConnectPoint = EXAMPLE_3.parse().unwrap();
let p = ParsedConnectPoint::load_file(fname1.as_ref(), &m).unwrap();
assert_conn_pt_eq(&p, &connpt1);
let err: LoadError = ParsedConnectPoint::load_file(fname2.as_ref(), &m).unwrap_err();
assert_matches!(
err,
LoadError::Access(fs_mistrust::Error::BadPermission(..))
);
let v: Vec<_> = ParsedConnectPoint::load_dir(dir.as_ref(), &m, &HashMap::new())
.unwrap()
.collect();
assert_eq!(v.len(), 3);
assert_conn_pt_eq(v[0].1.as_ref().unwrap(), &connpt1);
assert_matches!(
v[1].1.as_ref().unwrap_err(),
LoadError::Access(fs_mistrust::Error::BadPermission(..))
);
assert_conn_pt_eq(v[2].1.as_ref().unwrap(), &connpt3);
}
}