Skip to main content

tdyne_peer_id/
lib.rs

1//! # Base type for BitTorrent peer IDs in Rust
2//!
3//! `tdyne_peer_id` is a newtype for BitTorrent peer IDs, represented as `[u8; 20]`.
4//! It's intentionally kept very minimalist to minimise the possibility of backwards-incompatible
5//! changes.
6//!
7//! Example:
8//!
9//! ```
10//! use tdyne_peer_id::PeerId;
11//! use tdyne_peer_id::errors::BadPeerIdLengthError;
12//!
13//! let byte_array: &[u8; 20] = b"-TR0000-*\x00\x01d7xkqq04n";
14//! let byte_slice: &[u8] = b"-TR0000-*\x00\x01d7xkqq04n";
15//! let short_byte_slice: &[u8] = b"-TR0000-";
16//!
17//! // creating a PeerId from an array is simple
18//! let peer_id = PeerId::from(b"-TR0000-*\x00\x01d7xkqq04n");
19//! assert_eq!(peer_id.to_string(), "-TR0000-???d7xkqq04n".to_string());
20//!
21//! // you can also create PeerId from a byte slice if its 20 bytes long
22//! _ = PeerId::try_from(byte_slice).expect("matching lengths");
23//!
24//! // …if it's not, you get an error
25//! let error = BadPeerIdLengthError(short_byte_slice.len());
26//! assert_eq!(PeerId::try_from(short_byte_slice).expect_err("lengths don't match"), error);
27//! ```
28//!
29//! ## Libraries and projects using `tdyne_peer_id`
30//! * [`tdyne_peer_id_registry`](https://crates.io/crates/tdyne-peer-id-registry), peer ID
31//!   database and parser
32
33
34pub mod errors;
35
36use crate::errors::BadPeerIdLengthError;
37use std::borrow::Cow;
38use std::fmt;
39
40
41/// Represents an unparsed peer ID. It's just a thin wrapper over `[u8; 20]`.
42#[repr(transparent)]
43#[derive(Debug, Clone, Copy)]
44pub struct PeerId(pub [u8; 20]);
45
46impl From<[u8; 20]> for PeerId {
47    fn from(value: [u8; 20]) -> Self {
48        Self(value)
49    }
50}
51
52impl From<&[u8; 20]> for PeerId {
53    fn from(value: &[u8; 20]) -> Self {
54        Self(value.to_owned())
55    }
56}
57
58impl AsRef<[u8; 20]> for PeerId {
59    fn as_ref(&self) -> &[u8; 20] {
60        &self.0
61    }
62}
63
64impl TryFrom<&[u8]> for PeerId {
65    type Error = BadPeerIdLengthError;
66
67    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
68        value
69            .try_into()
70            .map(Self)
71            .map_err(|_| BadPeerIdLengthError(value.len()))
72    }
73}
74
75impl fmt::Display for PeerId {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        write!(f, "{}", self.to_safe())
78    }
79}
80
81impl PeerId {
82    /// Renders the [`PeerId`] into a [`Cow<'_, str>`] with every character outside base64 range
83    /// (`0-9`, `a-z`, `A-Z`, `-`, `.`) transformed into ASCII `?`. Most clients only use those
84    /// characters in their peer IDs, so this representation is good enough, while being completely
85    /// safe to show in any environment without escaping.
86    ///
87    /// Returns [`Cow<'_, str>`] despite always allocating the string at the moment in anticipation
88    /// of a future optimisation.
89    ///
90    /// Reused in the [`Display`] implementation.
91    ///
92    /// [`Cow<'_, str>`]: std::borrow::Cow
93    /// [`Display`]: std::fmt::Display
94    ///
95    /// ```
96    /// # use tdyne_peer_id::PeerId;
97    /// let peer_id = PeerId::from(b"-TR0000-*\x00\x01d7xkqq04n");
98    /// assert_eq!(peer_id.to_safe(), "-TR0000-???d7xkqq04n");
99    /// ```
100    pub fn to_safe(&self) -> Cow<'_, str> {
101        // todo: don't allocate on the happy path
102        String::from_utf8_lossy(&self.0)
103            .chars()
104            .map(|c| match c {
105                'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '.' => c,
106                _ => '?',
107            })
108            .collect()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use pretty_assertions::assert_eq;
116    use std::str;
117
118    #[test]
119    fn length_error() {
120        let ok_vec = vec![0u8; 20];
121        assert!(PeerId::try_from(ok_vec.as_slice()).is_ok());
122
123        let bad_vec = vec![0u8; 21];
124        let e = PeerId::try_from(bad_vec.as_slice()).unwrap_err();
125        assert_eq!(e.0, 21);
126        assert!(e.to_string().contains("21"));
127    }
128
129    #[test]
130    fn to_safe() {
131        let bytes = b"-TR0072-abvd7xkqq04n";
132        let peer_id = PeerId::from(bytes);
133        assert_eq!(&peer_id.to_safe(), str::from_utf8(bytes).unwrap());
134
135        let bytes = b"-TR0072-*\x00\x01d7xkqq04n";
136        let safe = "-TR0072-???d7xkqq04n";
137        let peer_id = PeerId::from(bytes);
138        assert_eq!(&peer_id.to_safe(), safe);
139    }
140}