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