#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
#![warn(noop_method_call)]
#![warn(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unchecked_time_subtraction)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::mod_module_files)]
#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] #![allow(mismatched_lifetime_syntaxes)] #![allow(clippy::collapsible_if)] #![deny(clippy::unused_async)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
#[cfg(feature = "expand-paths")]
use {directories::BaseDirs, std::sync::LazyLock};
use tor_error::{ErrorKind, HasKind};
#[cfg(all(test, feature = "expand-paths"))]
use std::ffi::OsStr;
#[cfg(feature = "address")]
pub mod addr;
#[cfg(feature = "arti-client")]
mod arti_client_paths;
#[cfg(feature = "arti-client")]
pub use arti_client_paths::arti_client_base_resolver;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(transparent)]
pub struct CfgPath(PathInner);
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(untagged)]
enum PathInner {
Literal(LiteralPath),
Shell(String),
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
struct LiteralPath {
literal: PathBuf,
}
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
#[cfg_attr(test, derive(PartialEq))]
pub enum CfgPathError {
#[error("Unrecognized variable {0} in path")]
UnknownVar(String),
#[error(
"Couldn't determine XDG Project Directories, needed to resolve a path; probably, unable to determine HOME directory"
)]
NoProjectDirs,
#[error("Can't construct base directories to resolve a path element")]
NoBaseDirs,
#[error("Can't find the path to the current binary")]
NoProgramPath,
#[error("Can't find the directory of the current binary")]
NoProgramDir,
#[error("Invalid path string: {0:?}")]
InvalidString(String),
#[error(
"Variable interpolation $ is not supported (tor-config/expand-paths feature disabled)); $ must still be doubled"
)]
VariableInterpolationNotSupported(String),
#[error("Home dir ~/ is not supported (tor-config/expand-paths feature disabled)")]
HomeDirInterpolationNotSupported(String),
}
impl HasKind for CfgPathError {
fn kind(&self) -> ErrorKind {
use CfgPathError as E;
use ErrorKind as EK;
match self {
E::UnknownVar(_) | E::InvalidString(_) => EK::InvalidConfig,
E::NoProjectDirs | E::NoBaseDirs => EK::NoHomeDirectory,
E::NoProgramPath | E::NoProgramDir => EK::InvalidConfig,
E::VariableInterpolationNotSupported(_) | E::HomeDirInterpolationNotSupported(_) => {
EK::FeatureDisabled
}
}
}
}
#[derive(Clone, Debug, Default)]
pub struct CfgPathResolver {
vars: HashMap<String, Result<Cow<'static, Path>, CfgPathError>>,
}
impl CfgPathResolver {
#[cfg(feature = "expand-paths")]
fn get_var(&self, var: &str) -> Result<Cow<'static, Path>, CfgPathError> {
match self.vars.get(var) {
Some(val) => val.clone(),
None => Err(CfgPathError::UnknownVar(var.to_owned())),
}
}
pub fn set_var(
&mut self,
var: impl Into<String>,
val: Result<Cow<'static, Path>, CfgPathError>,
) {
self.vars.insert(var.into(), val);
}
#[cfg(all(test, feature = "expand-paths"))]
fn from_pairs<K, V>(vars: impl IntoIterator<Item = (K, V)>) -> CfgPathResolver
where
K: Into<String>,
V: AsRef<OsStr>,
{
let mut path_resolver = CfgPathResolver::default();
for (name, val) in vars.into_iter() {
let val = Path::new(val.as_ref()).to_owned();
path_resolver.set_var(name, Ok(val.into()));
}
path_resolver
}
}
impl CfgPath {
pub fn new(s: String) -> Self {
CfgPath(PathInner::Shell(s))
}
pub fn new_literal<P: Into<PathBuf>>(path: P) -> Self {
CfgPath(PathInner::Literal(LiteralPath {
literal: path.into(),
}))
}
pub fn path(&self, path_resolver: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
match &self.0 {
PathInner::Shell(s) => expand(s, path_resolver),
PathInner::Literal(LiteralPath { literal }) => Ok(literal.clone()),
}
}
pub fn as_unexpanded_str(&self) -> Option<&str> {
match &self.0 {
PathInner::Shell(s) => Some(s),
PathInner::Literal(_) => None,
}
}
pub fn as_literal_path(&self) -> Option<&Path> {
match &self.0 {
PathInner::Shell(_) => None,
PathInner::Literal(LiteralPath { literal }) => Some(literal),
}
}
}
impl std::fmt::Display for CfgPath {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0 {
PathInner::Literal(LiteralPath { literal }) => write!(fmt, "{:?} [exactly]", literal),
PathInner::Shell(s) => s.fmt(fmt),
}
}
}
#[cfg(feature = "expand-paths")]
pub fn home() -> Result<&'static Path, CfgPathError> {
static HOME_DIR: LazyLock<Option<PathBuf>> =
LazyLock::new(|| Some(BaseDirs::new()?.home_dir().to_owned()));
HOME_DIR
.as_ref()
.map(PathBuf::as_path)
.ok_or(CfgPathError::NoBaseDirs)
}
#[cfg(feature = "expand-paths")]
fn expand(s: &str, path_resolver: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
let path = shellexpand::path::full_with_context(
s,
|| home().ok(),
|x| path_resolver.get_var(x).map(Some),
);
Ok(path.map_err(|e| e.cause)?.into_owned())
}
#[cfg(not(feature = "expand-paths"))]
fn expand(input: &str, _: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
if input.starts_with('~') {
return Err(CfgPathError::HomeDirInterpolationNotSupported(input.into()));
}
let mut out = String::with_capacity(input.len());
let mut s = input;
while let Some((lhs, rhs)) = s.split_once('$') {
if let Some(rhs) = rhs.strip_prefix('$') {
out += lhs;
out += "$";
s = rhs;
} else {
return Err(CfgPathError::VariableInterpolationNotSupported(
input.into(),
));
}
}
out += s;
Ok(out.into())
}
#[cfg(all(test, feature = "expand-paths"))]
mod test {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn expand_no_op() {
let r = CfgPathResolver::from_pairs([("FOO", "foo")]);
let p = CfgPath::new("Hello/world".to_string());
assert_eq!(p.to_string(), "Hello/world".to_string());
assert_eq!(p.path(&r).unwrap().to_str(), Some("Hello/world"));
let p = CfgPath::new("/usr/local/foo".to_string());
assert_eq!(p.to_string(), "/usr/local/foo".to_string());
assert_eq!(p.path(&r).unwrap().to_str(), Some("/usr/local/foo"));
}
#[cfg(not(target_family = "windows"))]
#[test]
fn expand_home() {
let r = CfgPathResolver::from_pairs([("USER_HOME", home().unwrap())]);
let p = CfgPath::new("~/.arti/config".to_string());
assert_eq!(p.to_string(), "~/.arti/config".to_string());
let expected = dirs::home_dir().unwrap().join(".arti/config");
assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
let p = CfgPath::new("${USER_HOME}/.arti/config".to_string());
assert_eq!(p.to_string(), "${USER_HOME}/.arti/config".to_string());
assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
}
#[cfg(target_family = "windows")]
#[test]
fn expand_home() {
let r = CfgPathResolver::from_pairs([("USER_HOME", home().unwrap())]);
let p = CfgPath::new("~\\.arti\\config".to_string());
assert_eq!(p.to_string(), "~\\.arti\\config".to_string());
let expected = dirs::home_dir().unwrap().join(".arti\\config");
assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
let p = CfgPath::new("${USER_HOME}\\.arti\\config".to_string());
assert_eq!(p.to_string(), "${USER_HOME}\\.arti\\config".to_string());
assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
}
#[test]
fn expand_bogus() {
let r = CfgPathResolver::from_pairs([("FOO", "foo")]);
let p = CfgPath::new("${ARTI_WOMBAT}/example".to_string());
assert_eq!(p.to_string(), "${ARTI_WOMBAT}/example".to_string());
assert!(matches!(p.path(&r), Err(CfgPathError::UnknownVar(_))));
assert_eq!(
&p.path(&r).unwrap_err().to_string(),
"Unrecognized variable ARTI_WOMBAT in path"
);
}
#[test]
fn literal() {
let r = CfgPathResolver::from_pairs([("ARTI_CACHE", "foo")]);
let p = CfgPath::new_literal(PathBuf::from("${ARTI_CACHE}/literally"));
assert_eq!(
p.path(&r).unwrap().to_str().unwrap(),
"${ARTI_CACHE}/literally"
);
assert_eq!(p.to_string(), "\"${ARTI_CACHE}/literally\" [exactly]");
}
#[test]
#[cfg(feature = "expand-paths")]
fn program_dir() {
let current_exe = std::env::current_exe().unwrap();
let r = CfgPathResolver::from_pairs([("PROGRAM_DIR", current_exe.parent().unwrap())]);
let p = CfgPath::new("${PROGRAM_DIR}/foo".to_string());
let mut this_binary = current_exe;
this_binary.pop();
this_binary.push("foo");
let expanded = p.path(&r).unwrap();
assert_eq!(expanded, this_binary);
}
#[test]
#[cfg(not(feature = "expand-paths"))]
fn rejections() {
let r = CfgPathResolver::from_pairs([("PROGRAM_DIR", std::env::current_exe().unwrap())]);
let chk_err = |s: &str, mke: &dyn Fn(String) -> CfgPathError| {
let p = CfgPath::new(s.to_string());
assert_eq!(p.path(&r).unwrap_err(), mke(s.to_string()));
};
let chk_ok = |s: &str, exp| {
let p = CfgPath::new(s.to_string());
assert_eq!(p.path(&r), Ok(PathBuf::from(exp)));
};
chk_err(
"some/${PROGRAM_DIR}/foo",
&CfgPathError::VariableInterpolationNotSupported,
);
chk_err("~some", &CfgPathError::HomeDirInterpolationNotSupported);
chk_ok("some$$foo$$bar", "some$foo$bar");
chk_ok("no dollars", "no dollars");
}
}
#[cfg(test)]
mod test_serde {
#![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 std::ffi::OsString;
use std::fmt::Debug;
use derive_builder::Builder;
use tor_config::load::TopLevel;
use tor_config::{ConfigBuildError, impl_standard_builder};
#[derive(Serialize, Deserialize, Builder, Eq, PartialEq, Debug)]
#[builder(derive(Serialize, Deserialize, Debug))]
#[builder(build_fn(error = "ConfigBuildError"))]
struct TestConfigFile {
p: CfgPath,
}
impl_standard_builder! { TestConfigFile: !Default }
impl TopLevel for TestConfigFile {
type Builder = TestConfigFileBuilder;
}
fn deser_json(json: &str) -> CfgPath {
dbg!(json);
let TestConfigFile { p } = serde_json::from_str(json).expect("deser json failed");
p
}
fn deser_toml(toml: &str) -> CfgPath {
dbg!(toml);
let TestConfigFile { p } = toml::from_str(toml).expect("deser toml failed");
p
}
fn deser_toml_cfg(toml: &str) -> CfgPath {
dbg!(toml);
let mut sources = tor_config::ConfigurationSources::new_empty();
sources.push_source(
tor_config::ConfigurationSource::from_verbatim(toml.to_string()),
tor_config::sources::MustRead::MustRead,
);
let cfg = sources.load().unwrap();
dbg!(&cfg);
let TestConfigFile { p } = tor_config::load::resolve(cfg).expect("cfg resolution failed");
p
}
#[test]
fn test_parse() {
fn desers(toml: &str, json: &str) -> Vec<CfgPath> {
vec![deser_toml(toml), deser_toml_cfg(toml), deser_json(json)]
}
for cp in desers(r#"p = "string""#, r#"{ "p": "string" }"#) {
assert_eq!(cp.as_unexpanded_str(), Some("string"));
assert_eq!(cp.as_literal_path(), None);
}
for cp in desers(
r#"p = { literal = "lit" }"#,
r#"{ "p": {"literal": "lit"} }"#,
) {
assert_eq!(cp.as_unexpanded_str(), None);
assert_eq!(cp.as_literal_path(), Some(&*PathBuf::from("lit")));
}
}
fn non_string_path() -> PathBuf {
#[cfg(target_family = "unix")]
{
use std::os::unix::ffi::OsStringExt;
return PathBuf::from(OsString::from_vec(vec![0x80_u8]));
}
#[cfg(target_family = "windows")]
{
use std::os::windows::ffi::OsStringExt;
return PathBuf::from(OsString::from_wide(&[0xD800_u16]));
}
#[allow(unreachable_code)]
PathBuf::default()
}
fn test_roundtrip_cases<SER, S, DESER, E, F>(ser: SER, deser: DESER)
where
SER: Fn(&TestConfigFile) -> Result<S, E>,
DESER: Fn(&S) -> Result<TestConfigFile, F>,
S: Debug,
E: Debug,
F: Debug,
{
let case = |easy, p| {
let input = TestConfigFile { p };
let s = match ser(&input) {
Ok(s) => s,
Err(e) if easy => panic!("ser failed {:?} e={:?}", &input, &e),
Err(_) => return,
};
dbg!(&input, &s);
let output = deser(&s).expect("deser failed");
assert_eq!(&input, &output, "s={:?}", &s);
};
case(true, CfgPath::new("string".into()));
case(true, CfgPath::new_literal(PathBuf::from("nice path")));
case(true, CfgPath::new_literal(PathBuf::from("path with ✓")));
case(false, CfgPath::new_literal(non_string_path()));
}
#[test]
fn roundtrip_json() {
test_roundtrip_cases(
|input| serde_json::to_string(&input),
|json| serde_json::from_str(json),
);
}
#[test]
fn roundtrip_toml() {
test_roundtrip_cases(|input| toml::to_string(&input), |toml| toml::from_str(toml));
}
#[test]
fn roundtrip_mpack() {
test_roundtrip_cases(
|input| rmp_serde::to_vec(&input),
|mpack| rmp_serde::from_slice(mpack),
);
}
}