Skip to main content

dhttp_home/
identity.rs

1use std::path::{Path, PathBuf};
2
3use crate::DhttpHome;
4use snafu::{OptionExt, ResultExt, Snafu};
5
6use dhttp_identity::name::{DhttpName, InvalidDhttpName};
7
8#[cfg(feature = "settings")]
9pub mod settings;
10#[cfg(feature = "ssl")]
11pub mod ssl;
12
13/// A handle to a per-identity profile directory (e.g. `~/.dhttp/reimu.pilot/`).
14///
15/// `IdentityProfile` is one of N sibling directories living inside a `DhttpHome`.
16/// Each profile owns that identity's certificate, private key, server config,
17/// and access database. The type is a typed path; it performs no IO on
18/// construction.
19#[derive(Debug, Clone)]
20pub struct IdentityProfile {
21    pub(crate) path: PathBuf,
22    pub(crate) name: DhttpName<'static>,
23}
24
25#[derive(Debug, Snafu)]
26#[snafu(module)]
27pub enum IdentityProfileFromPathError {
28    #[snafu(display("identity profile path has no directory name: {}", path.display()))]
29    MissingFileName { path: PathBuf },
30    #[snafu(display("identity profile directory name is not valid unicode: {}", path.display()))]
31    NonUtf8FileName { path: PathBuf },
32    #[snafu(display("failed to parse identity profile directory name as dhttp name"))]
33    InvalidName { source: InvalidDhttpName },
34}
35
36impl IdentityProfile {
37    pub const LOGS_DIR: &'static str = "logs";
38    pub const ACCESS_LOG_FILE: &'static str = "access.log";
39    pub const DB_DIR: &'static str = "db";
40    pub const ACCESS_DB_FILE: &'static str = "access.db";
41    pub const SERVER_CONF_FILE: &'static str = "server.conf";
42
43    pub fn name(&self) -> &DhttpName<'static> {
44        &self.name
45    }
46
47    pub fn path(&self) -> &Path {
48        self.path.as_path()
49    }
50
51    pub fn join(&self, sub: impl AsRef<Path>) -> PathBuf {
52        self.path.join(sub)
53    }
54
55    pub fn logs_dir(&self) -> PathBuf {
56        self.join(Self::LOGS_DIR)
57    }
58
59    pub fn access_log_path(&self) -> PathBuf {
60        self.logs_dir().join(Self::ACCESS_LOG_FILE)
61    }
62
63    pub fn access_db_path(&self) -> PathBuf {
64        self.join(Self::DB_DIR).join(Self::ACCESS_DB_FILE)
65    }
66
67    pub fn server_conf_path(&self) -> PathBuf {
68        self.join(Self::SERVER_CONF_FILE)
69    }
70
71    fn try_from_path(path: PathBuf) -> Result<Self, IdentityProfileFromPathError> {
72        use identity_profile_from_path_error::*;
73
74        let file_name = path
75            .file_name()
76            .context(MissingFileNameSnafu { path: &path })?;
77        let file_name = file_name
78            .to_str()
79            .context(NonUtf8FileNameSnafu { path: &path })?;
80        let name = file_name.parse::<DhttpName>().context(InvalidNameSnafu)?;
81        Ok(Self { path, name })
82    }
83}
84
85impl TryFrom<PathBuf> for IdentityProfile {
86    type Error = IdentityProfileFromPathError;
87
88    fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
89        Self::try_from_path(path)
90    }
91}
92
93impl TryFrom<&Path> for IdentityProfile {
94    type Error = IdentityProfileFromPathError;
95
96    fn try_from(path: &Path) -> Result<Self, Self::Error> {
97        Self::try_from_path(path.to_path_buf())
98    }
99}
100
101impl DhttpHome {
102    pub fn join_identity_name(&self, name: DhttpName<'_>) -> PathBuf {
103        self.join(name.as_partial())
104    }
105
106    /// Construct an [`IdentityProfile`] handle for `name` without touching disk.
107    ///
108    /// Use this when you only need the typed path (for example to compute a
109    /// child file path). To verify that the directory actually exists, call
110    /// [`DhttpHome::resolve_identity_profile`] instead.
111    pub fn identity_profile(&self, name: DhttpName<'_>) -> IdentityProfile {
112        IdentityProfile {
113            path: self.join_identity_name(name.clone()),
114            name: name.to_owned(),
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn identity_profile_from_path_uses_directory_name_as_dhttp_name() {
125        let profile = IdentityProfile::try_from(PathBuf::from("/tmp/reimu.pilot")).unwrap();
126
127        assert_eq!(profile.path(), Path::new("/tmp/reimu.pilot"));
128        assert_eq!(profile.name().as_full(), "reimu.pilot.dhttp.net");
129    }
130
131    #[test]
132    fn identity_profile_from_path_rejects_path_without_directory_name() {
133        let error = IdentityProfile::try_from(Path::new("/")).unwrap_err();
134
135        assert!(matches!(
136            error,
137            IdentityProfileFromPathError::MissingFileName { .. }
138        ));
139    }
140
141    #[test]
142    fn identity_profile_from_path_rejects_invalid_directory_name() {
143        let error = IdentityProfile::try_from(Path::new("/tmp/123")).unwrap_err();
144
145        assert!(matches!(
146            error,
147            IdentityProfileFromPathError::InvalidName { .. }
148        ));
149    }
150}