subc_protocol/
machine_id.rs1use std::{error::Error, fmt, str::FromStr};
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
30#[serde(try_from = "String", into = "String")]
31pub struct MachineId(String);
32
33impl MachineId {
34 pub const HEX_LEN: usize = 32;
36
37 pub fn parse(value: &str) -> Result<Self, MachineIdError> {
41 if value.len() != Self::HEX_LEN {
42 return Err(MachineIdError::WrongLength { len: value.len() });
43 }
44 if let Some(position) = value
45 .bytes()
46 .position(|byte| !matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
47 {
48 return Err(MachineIdError::NotLowercaseHex { position });
49 }
50 Ok(Self(value.to_owned()))
51 }
52
53 pub fn from_bytes(bytes: [u8; 16]) -> Self {
56 Self(format!("{:032x}", u128::from_be_bytes(bytes)))
57 }
58
59 pub fn as_str(&self) -> &str {
61 &self.0
62 }
63}
64
65impl fmt::Display for MachineId {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 f.write_str(&self.0)
68 }
69}
70
71impl FromStr for MachineId {
72 type Err = MachineIdError;
73
74 fn from_str(value: &str) -> Result<Self, Self::Err> {
75 Self::parse(value)
76 }
77}
78
79impl TryFrom<String> for MachineId {
80 type Error = MachineIdError;
81
82 fn try_from(value: String) -> Result<Self, Self::Error> {
83 Self::parse(&value)
84 }
85}
86
87impl From<MachineId> for String {
88 fn from(value: MachineId) -> Self {
89 value.0
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum MachineIdError {
96 WrongLength { len: usize },
98 NotLowercaseHex { position: usize },
100}
101
102impl fmt::Display for MachineIdError {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 Self::WrongLength { len } => write!(
106 f,
107 "a machine id is exactly {} lowercase hex characters, got {len} bytes",
108 MachineId::HEX_LEN
109 ),
110 Self::NotLowercaseHex { position } => write!(
111 f,
112 "a machine id is exactly {} lowercase hex characters; byte {position} is not 0-9 or a-f",
113 MachineId::HEX_LEN
114 ),
115 }
116 }
117}
118
119impl Error for MachineIdError {}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn accepts_exactly_32_lowercase_hex() {
127 let id = MachineId::parse("0123456789abcdef0123456789abcdef").expect("valid");
128 assert_eq!(id.as_str(), "0123456789abcdef0123456789abcdef");
129 }
130
131 #[test]
132 fn refuses_wrong_length_uppercase_and_non_hex() {
133 assert_eq!(
134 MachineId::parse("abc"),
135 Err(MachineIdError::WrongLength { len: 3 })
136 );
137 assert_eq!(
138 MachineId::parse("0123456789abcdef0123456789abcdef\n"),
139 Err(MachineIdError::WrongLength { len: 33 })
140 );
141 assert_eq!(
142 MachineId::parse("0123456789ABCDEF0123456789abcdef"),
143 Err(MachineIdError::NotLowercaseHex { position: 10 })
144 );
145 assert_eq!(
146 MachineId::parse("0123456789abcdeg0123456789abcdef"),
147 Err(MachineIdError::NotLowercaseHex { position: 15 })
148 );
149 }
150
151 #[test]
152 fn from_bytes_renders_32_lowercase_hex_with_leading_zeros() {
153 let mut bytes = [0u8; 16];
154 bytes[15] = 0xab;
155 let id = MachineId::from_bytes(bytes);
156 assert_eq!(id.as_str(), "000000000000000000000000000000ab");
157 assert_eq!(MachineId::parse(id.as_str()), Ok(id));
158 }
159
160 #[test]
161 fn serde_refuses_a_malformed_value() {
162 let ok: MachineId =
163 serde_json::from_str("\"0123456789abcdef0123456789abcdef\"").expect("valid");
164 assert_eq!(
165 serde_json::to_string(&ok).unwrap(),
166 "\"0123456789abcdef0123456789abcdef\""
167 );
168 assert!(serde_json::from_str::<MachineId>("\"not-an-id\"").is_err());
169 }
170}