Skip to main content

tor_persist/
hsnickname.rs

1//! `HsNickname` module itself is private, but `HsNickname` etc. are re-exported
2
3use std::fmt::{self, Display};
4use std::str::FromStr;
5
6use derive_more::{From, Into};
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::slug::Slug;
11
12/// Nickname (local identifier) for a Tor hidden service
13///
14/// Used to look up this services's
15/// keys, state, configuration, etc,
16/// and distinguish them from other services.
17///
18/// An `HsNickname` must be a valid [`Slug`].
19/// See [slug](crate::slug) for the syntactic requirements.
20//
21// NOTE: if at some point we decide HsNickname should have a more restrictive syntax/charset than
22// Slug, we should remember to also update `KeySpecifierComponent::from_component` (it
23// should return an error if the specified string is a valid Slug, but not a valid
24// HsNickname).
25#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] //
26#[derive(derive_more::Display, From, Into, Serialize, Deserialize)]
27#[serde(try_from = "String", into = "String")]
28pub struct HsNickname(Slug);
29
30impl FromStr for HsNickname {
31    type Err = InvalidNickname;
32
33    fn from_str(s: &str) -> Result<Self, Self::Err> {
34        Self::new(s.to_string())
35    }
36}
37
38/// Local nickname for Tor Hidden Service (`.onion` service) was syntactically invalid
39#[derive(Clone, Debug, Hash, Eq, PartialEq, Error)]
40#[non_exhaustive]
41#[error("Invalid syntax for hidden service nickname")]
42pub struct InvalidNickname {}
43
44impl HsNickname {
45    /// Create a new `HsNickname` from a `String`
46    ///
47    /// Returns an error if the syntax is not valid
48    pub fn new(s: String) -> Result<HsNickname, InvalidNickname> {
49        Ok(Self(s.try_into().map_err(|_| InvalidNickname {})?))
50    }
51}
52
53impl From<HsNickname> for String {
54    fn from(nick: HsNickname) -> String {
55        nick.0.into()
56    }
57}
58
59impl TryFrom<String> for HsNickname {
60    type Error = InvalidNickname;
61    fn try_from(s: String) -> Result<HsNickname, InvalidNickname> {
62        Self::new(s)
63    }
64}
65
66impl AsRef<str> for HsNickname {
67    fn as_ref(&self) -> &str {
68        self.0.as_ref()
69    }
70}
71
72#[cfg(feature = "state-dir")]
73impl crate::state_dir::InstanceIdentity for HsNickname {
74    fn kind() -> &'static str {
75        "hss"
76    }
77    fn write_identity(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        Display::fmt(self, f)
79    }
80}
81
82#[cfg(test)]
83mod test {
84    // @@ begin test lint list maintained by maint/add_warning @@
85    #![allow(clippy::bool_assert_comparison)]
86    #![allow(clippy::clone_on_copy)]
87    #![allow(clippy::dbg_macro)]
88    #![allow(clippy::mixed_attributes_style)]
89    #![allow(clippy::print_stderr)]
90    #![allow(clippy::print_stdout)]
91    #![allow(clippy::single_char_pattern)]
92    #![allow(clippy::unwrap_used)]
93    #![allow(clippy::unchecked_time_subtraction)]
94    #![allow(clippy::useless_vec)]
95    #![allow(clippy::needless_pass_by_value)]
96    #![allow(clippy::string_slice)] // See arti#2571
97    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
98    use super::*;
99
100    #[test]
101    fn mk() {
102        assert_eq!(HsNickname::new("".into()), Err(InvalidNickname {}));
103        assert_eq!(HsNickname::new("-a".into()), Err(InvalidNickname {}));
104        assert_eq!(HsNickname::new("b.".into()), Err(InvalidNickname {}));
105        assert_eq!(HsNickname::new("_c".into()).unwrap().to_string(), "_c");
106        assert_eq!(&HsNickname::new("x".into()).unwrap().to_string(), "x");
107    }
108
109    #[test]
110    fn serde() {
111        // TODO: clone-and-hack with tor_keymgr::::key_specifier::test::serde
112        #[derive(Serialize, Deserialize, Debug)]
113        struct T {
114            n: HsNickname,
115        }
116        let j = serde_json::from_str(r#"{ "n": "x" }"#).unwrap();
117        let t: T = serde_json::from_value(j).unwrap();
118        assert_eq!(&t.n.to_string(), "x");
119
120        assert_eq!(&serde_json::to_string(&t).unwrap(), r#"{"n":"x"}"#);
121
122        let j = serde_json::from_str(r#"{ "n": "!" }"#).unwrap();
123        let e = serde_json::from_value::<T>(j).unwrap_err();
124        assert!(e.to_string().contains("Invalid syntax"), "wrong msg {e:?}");
125    }
126
127    #[test]
128    fn empty_nickname() {
129        assert_eq!(
130            HsNickname::new("".to_string()).unwrap_err(),
131            InvalidNickname {}
132        );
133        assert_eq!(
134            HsNickname::try_from("".to_string()).unwrap_err(),
135            InvalidNickname {}
136        );
137        assert_eq!(HsNickname::from_str("").unwrap_err(), InvalidNickname {});
138    }
139}