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}