stem_rs/
types.rs

1//! Type-safe wrappers for Tor-specific identifiers and values.
2//!
3//! This module provides newtype wrappers that enforce validation at
4//! construction time, preventing invalid values from being created.
5//! These types improve API safety and clarity by making invalid states
6//! unrepresentable.
7//!
8//! # Design Philosophy
9//!
10//! Following the library-rs reference implementation, these types:
11//! - Validate input at construction time
12//! - Provide infallible access after construction
13//! - Implement standard traits (Display, Debug, FromStr)
14//! - Use efficient internal representations
15//!
16//! # Available Types
17//!
18//! - [`Fingerprint`] - Relay identity fingerprint (40 hex chars)
19//! - [`Nickname`] - Relay nickname (1-19 alphanumeric)
20//! - [`Ed25519PublicKey`] - Ed25519 public key (base64)
21//! - [`Ed25519Identity`] - Ed25519 identity (32 bytes)
22//!
23//! # Example
24//!
25//! ```rust
26//! use stem_rs::types::{Fingerprint, Nickname};
27//! use std::str::FromStr;
28//!
29//! let fp = Fingerprint::from_str(
30//!     "9695DFC35FFEB861329B9F1AB04C46397020CE31"
31//! ).unwrap();
32//! println!("Fingerprint: {}", fp);
33//!
34//! let nick = Nickname::from_str("MyRelay").unwrap();
35//! println!("Nickname: {}", nick);
36//!
37//! let invalid = Nickname::from_str("invalid-name");
38//! assert!(invalid.is_err());
39//! ```
40
41use std::fmt;
42use std::str::FromStr;
43use thiserror::Error;
44
45const MAX_NICKNAME_LENGTH: usize = 19;
46const FINGERPRINT_LENGTH: usize = 40;
47const ED25519_PUBLIC_KEY_LENGTH: usize = 32;
48
49/// Errors that can occur when parsing or validating fingerprints.
50#[derive(Debug, Error)]
51pub enum FingerprintError {
52    /// Fingerprint is not exactly 40 characters long.
53    #[error("fingerprint must be exactly 40 hexadecimal characters")]
54    InvalidLength,
55    /// Fingerprint contains non-hexadecimal characters.
56    #[error("fingerprint contains invalid characters")]
57    InvalidCharacters,
58}
59
60/// Errors that can occur when parsing or validating nicknames.
61#[derive(Debug, Error)]
62pub enum NicknameError {
63    /// Nickname length is not between 1 and 19 characters.
64    #[error("nickname must be 1-19 characters long")]
65    InvalidLength,
66    /// Nickname contains non-alphanumeric characters.
67    #[error("nickname must contain only alphanumeric characters")]
68    InvalidCharacters,
69}
70
71/// Errors that can occur when parsing Ed25519 public keys.
72#[derive(Debug, Error)]
73pub enum Ed25519PublicKeyError {
74    /// Base64 decoding failed.
75    #[error("invalid base64 encoding")]
76    InvalidBase64,
77    /// Decoded key has wrong length.
78    #[error("invalid key length: expected 32 bytes, got {0}")]
79    InvalidLength(usize),
80}
81
82/// Errors that can occur when parsing Ed25519 identities.
83#[derive(Debug, Error)]
84pub enum Ed25519IdentityError {
85    /// Base64 decoding failed.
86    #[error("invalid base64 encoding")]
87    InvalidBase64,
88    /// Decoded identity has wrong length.
89    #[error("invalid identity length: expected 32 bytes, got {0}")]
90    InvalidLength(usize),
91}
92
93/// A validated relay fingerprint.
94///
95/// Fingerprints are 40-character hexadecimal strings representing the
96/// SHA-1 hash of a relay's RSA identity key. This type ensures the
97/// fingerprint is valid at construction time.
98///
99/// # Format
100///
101/// - Length: exactly 40 characters
102/// - Characters: hexadecimal (0-9, a-f, A-F)
103/// - Stored in uppercase for consistency
104///
105/// # Example
106///
107/// ```rust
108/// use stem_rs::types::Fingerprint;
109/// use std::str::FromStr;
110///
111/// let fp = Fingerprint::from_str(
112///     "9695DFC35FFEB861329B9F1AB04C46397020CE31"
113/// ).unwrap();
114/// assert_eq!(fp.as_str(), "9695DFC35FFEB861329B9F1AB04C46397020CE31");
115///
116/// let invalid = Fingerprint::from_str("ABCD");
117/// assert!(invalid.is_err());
118/// ```
119#[derive(Clone, PartialEq, Eq, Hash)]
120pub struct Fingerprint(String);
121
122impl Fingerprint {
123    /// Creates a new fingerprint from a string.
124    ///
125    /// The input is validated and converted to uppercase.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if the string is not exactly 40 hexadecimal characters.
130    pub fn new(s: impl Into<String>) -> Result<Self, FingerprintError> {
131        let s = s.into();
132        if s.len() != FINGERPRINT_LENGTH {
133            return Err(FingerprintError::InvalidLength);
134        }
135        if !s.chars().all(|c| c.is_ascii_hexdigit()) {
136            return Err(FingerprintError::InvalidCharacters);
137        }
138        Ok(Self(s.to_uppercase()))
139    }
140
141    /// Returns the fingerprint as a string slice.
142    pub fn as_str(&self) -> &str {
143        &self.0
144    }
145
146    /// Returns the fingerprint in lowercase.
147    pub fn to_lowercase(&self) -> String {
148        self.0.to_lowercase()
149    }
150}
151
152impl fmt::Display for Fingerprint {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "{}", self.0)
155    }
156}
157
158impl fmt::Debug for Fingerprint {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        write!(f, "Fingerprint({})", self.0)
161    }
162}
163
164impl FromStr for Fingerprint {
165    type Err = FingerprintError;
166
167    fn from_str(s: &str) -> Result<Self, Self::Err> {
168        Self::new(s)
169    }
170}
171
172/// A validated relay nickname.
173///
174/// Nicknames are human-readable identifiers for relays, consisting of
175/// 1 to 19 alphanumeric ASCII characters. This type ensures the nickname
176/// is valid at construction time.
177///
178/// # Format
179///
180/// - Length: 1 to 19 characters
181/// - Characters: ASCII alphanumeric only (a-z, A-Z, 0-9)
182///
183/// # Example
184///
185/// ```rust
186/// use stem_rs::types::Nickname;
187/// use std::str::FromStr;
188///
189/// let nick = Nickname::from_str("MyRelay").unwrap();
190/// assert_eq!(nick.as_str(), "MyRelay");
191///
192/// let invalid = Nickname::from_str("my-relay");
193/// assert!(invalid.is_err());
194/// ```
195#[derive(Clone, PartialEq, Eq, Hash)]
196pub struct Nickname(String);
197
198impl Nickname {
199    /// Creates a new nickname from a string.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if the string is not 1-19 alphanumeric characters.
204    pub fn new(s: impl Into<String>) -> Result<Self, NicknameError> {
205        let s = s.into();
206        let len = s.len();
207        if !(1..=MAX_NICKNAME_LENGTH).contains(&len) {
208            return Err(NicknameError::InvalidLength);
209        }
210        if !s.chars().all(|c| c.is_ascii_alphanumeric()) {
211            return Err(NicknameError::InvalidCharacters);
212        }
213        Ok(Self(s))
214    }
215
216    /// Returns the nickname as a string slice.
217    pub fn as_str(&self) -> &str {
218        &self.0
219    }
220}
221
222impl fmt::Display for Nickname {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        write!(f, "{}", self.0)
225    }
226}
227
228impl fmt::Debug for Nickname {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        write!(f, "Nickname({})", self.0)
231    }
232}
233
234impl FromStr for Nickname {
235    type Err = NicknameError;
236
237    fn from_str(s: &str) -> Result<Self, Self::Err> {
238        Self::new(s)
239    }
240}
241
242/// A validated Ed25519 public key.
243///
244/// Ed25519 public keys are 32-byte values used for modern Tor relay
245/// identity and signing operations. This type ensures the key is valid
246/// at construction time.
247///
248/// # Example
249///
250/// ```rust
251/// use stem_rs::types::Ed25519PublicKey;
252///
253/// let bytes = [42u8; 32];
254/// let key = Ed25519PublicKey::new(bytes);
255/// let base64 = key.to_base64();
256/// let decoded = Ed25519PublicKey::from_base64(&base64).unwrap();
257/// assert_eq!(key, decoded);
258/// ```
259#[derive(Clone, PartialEq, Eq)]
260pub struct Ed25519PublicKey([u8; ED25519_PUBLIC_KEY_LENGTH]);
261
262impl Ed25519PublicKey {
263    /// Creates a new Ed25519 public key from raw bytes.
264    pub fn new(bytes: [u8; ED25519_PUBLIC_KEY_LENGTH]) -> Self {
265        Self(bytes)
266    }
267
268    /// Creates a new Ed25519 public key from base64-encoded string.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if the base64 is invalid or decodes to wrong length.
273    pub fn from_base64(s: &str) -> Result<Self, Ed25519PublicKeyError> {
274        use base64::Engine;
275        let engine = base64::engine::general_purpose::STANDARD;
276        let bytes = engine
277            .decode(s)
278            .map_err(|_| Ed25519PublicKeyError::InvalidBase64)?;
279
280        if bytes.len() != ED25519_PUBLIC_KEY_LENGTH {
281            return Err(Ed25519PublicKeyError::InvalidLength(bytes.len()));
282        }
283
284        let mut array = [0u8; ED25519_PUBLIC_KEY_LENGTH];
285        array.copy_from_slice(&bytes);
286        Ok(Self(array))
287    }
288
289    /// Returns the key as a byte array reference.
290    pub fn as_bytes(&self) -> &[u8; ED25519_PUBLIC_KEY_LENGTH] {
291        &self.0
292    }
293
294    /// Encodes the key as base64.
295    pub fn to_base64(&self) -> String {
296        use base64::Engine;
297        let engine = base64::engine::general_purpose::STANDARD;
298        engine.encode(self.0)
299    }
300}
301
302impl fmt::Display for Ed25519PublicKey {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        write!(f, "{}", self.to_base64())
305    }
306}
307
308impl fmt::Debug for Ed25519PublicKey {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        write!(f, "Ed25519PublicKey({})", self.to_base64())
311    }
312}
313
314impl FromStr for Ed25519PublicKey {
315    type Err = Ed25519PublicKeyError;
316
317    fn from_str(s: &str) -> Result<Self, Self::Err> {
318        Self::from_base64(s)
319    }
320}
321
322/// A validated Ed25519 identity.
323///
324/// Ed25519 identities are 32-byte values that uniquely identify relays
325/// in the modern Tor network. This type ensures the identity is valid
326/// at construction time.
327///
328/// # Example
329///
330/// ```rust
331/// use stem_rs::types::Ed25519Identity;
332///
333/// let bytes = [99u8; 32];
334/// let identity = Ed25519Identity::new(bytes);
335/// let base64 = identity.to_base64();
336/// let decoded = Ed25519Identity::from_base64(&base64).unwrap();
337/// assert_eq!(identity, decoded);
338/// ```
339#[derive(Clone, PartialEq, Eq)]
340pub struct Ed25519Identity([u8; ED25519_PUBLIC_KEY_LENGTH]);
341
342impl Ed25519Identity {
343    /// Creates a new Ed25519 identity from raw bytes.
344    pub fn new(bytes: [u8; ED25519_PUBLIC_KEY_LENGTH]) -> Self {
345        Self(bytes)
346    }
347
348    /// Creates a new Ed25519 identity from base64-encoded string.
349    ///
350    /// # Errors
351    ///
352    /// Returns an error if the base64 is invalid or decodes to wrong length.
353    pub fn from_base64(s: &str) -> Result<Self, Ed25519IdentityError> {
354        use base64::Engine;
355        let engine = base64::engine::general_purpose::STANDARD;
356        let bytes = engine
357            .decode(s)
358            .map_err(|_| Ed25519IdentityError::InvalidBase64)?;
359
360        if bytes.len() != ED25519_PUBLIC_KEY_LENGTH {
361            return Err(Ed25519IdentityError::InvalidLength(bytes.len()));
362        }
363
364        let mut array = [0u8; ED25519_PUBLIC_KEY_LENGTH];
365        array.copy_from_slice(&bytes);
366        Ok(Self(array))
367    }
368
369    /// Returns the identity as a byte array reference.
370    pub fn as_bytes(&self) -> &[u8; ED25519_PUBLIC_KEY_LENGTH] {
371        &self.0
372    }
373
374    /// Encodes the identity as base64.
375    pub fn to_base64(&self) -> String {
376        use base64::Engine;
377        let engine = base64::engine::general_purpose::STANDARD;
378        engine.encode(self.0)
379    }
380}
381
382impl fmt::Display for Ed25519Identity {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        write!(f, "{}", self.to_base64())
385    }
386}
387
388impl fmt::Debug for Ed25519Identity {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        write!(f, "Ed25519Identity({})", self.to_base64())
391    }
392}
393
394impl FromStr for Ed25519Identity {
395    type Err = Ed25519IdentityError;
396
397    fn from_str(s: &str) -> Result<Self, Self::Err> {
398        Self::from_base64(s)
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    #[test]
407    fn test_fingerprint_valid() {
408        let fp = Fingerprint::from_str("9695DFC35FFEB861329B9F1AB04C46397020CE31").unwrap();
409        assert_eq!(fp.as_str(), "9695DFC35FFEB861329B9F1AB04C46397020CE31");
410    }
411
412    #[test]
413    fn test_fingerprint_lowercase() {
414        let fp = Fingerprint::from_str("9695dfc35ffeb861329b9f1ab04c46397020ce31").unwrap();
415        assert_eq!(fp.as_str(), "9695DFC35FFEB861329B9F1AB04C46397020CE31");
416    }
417
418    #[test]
419    fn test_fingerprint_invalid_length() {
420        let result = Fingerprint::from_str("ABCD");
421        assert!(matches!(result, Err(FingerprintError::InvalidLength)));
422    }
423
424    #[test]
425    fn test_fingerprint_invalid_chars() {
426        let result = Fingerprint::from_str("ZZZZDFC35FFEB861329B9F1AB04C46397020CE31");
427        assert!(matches!(result, Err(FingerprintError::InvalidCharacters)));
428    }
429
430    #[test]
431    fn test_nickname_valid() {
432        let nick = Nickname::from_str("MyRelay").unwrap();
433        assert_eq!(nick.as_str(), "MyRelay");
434    }
435
436    #[test]
437    fn test_nickname_single_char() {
438        let nick = Nickname::from_str("A").unwrap();
439        assert_eq!(nick.as_str(), "A");
440    }
441
442    #[test]
443    fn test_nickname_max_length() {
444        let nick = Nickname::from_str("1234567890123456789").unwrap();
445        assert_eq!(nick.as_str(), "1234567890123456789");
446    }
447
448    #[test]
449    fn test_nickname_too_long() {
450        let result = Nickname::from_str("12345678901234567890");
451        assert!(matches!(result, Err(NicknameError::InvalidLength)));
452    }
453
454    #[test]
455    fn test_nickname_empty() {
456        let result = Nickname::from_str("");
457        assert!(matches!(result, Err(NicknameError::InvalidLength)));
458    }
459
460    #[test]
461    fn test_nickname_invalid_chars() {
462        let result = Nickname::from_str("my-relay");
463        assert!(matches!(result, Err(NicknameError::InvalidCharacters)));
464    }
465
466    #[test]
467    fn test_ed25519_public_key_roundtrip() {
468        let bytes = [42u8; 32];
469        let key = Ed25519PublicKey::new(bytes);
470        let base64 = key.to_base64();
471        let decoded = Ed25519PublicKey::from_base64(&base64).unwrap();
472        assert_eq!(key, decoded);
473    }
474
475    #[test]
476    fn test_ed25519_identity_roundtrip() {
477        let bytes = [99u8; 32];
478        let identity = Ed25519Identity::new(bytes);
479        let base64 = identity.to_base64();
480        let decoded = Ed25519Identity::from_base64(&base64).unwrap();
481        assert_eq!(identity, decoded);
482    }
483}