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);
    }
}