1#[macro_use]
23extern crate amplify;
24pub extern crate base64;
25
26use std::error::Error;
27use std::fmt::{self, Display, Formatter};
28
29use base64::Engine;
30use sha2::Digest;
31
32pub const ID_MIN_LEN: usize = 4;
33pub const HRI_MAX_LEN: usize = 16;
34
35pub const BAID64_ALPHABET: &str =
36 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~";
37
38fn check<const LEN: usize>(hri: &'static str, payload: [u8; LEN]) -> [u8; 4] {
39 let key = sha2::Sha256::digest(hri.as_bytes());
40 let mut sha = sha2::Sha256::new_with_prefix(key);
41 sha.update(payload);
42 let sha = sha.finalize();
43 [sha[0], sha[1], sha[1], sha[2]]
44}
45
46pub trait DisplayBaid64<const LEN: usize = 32> {
47 const HRI: &'static str;
48 const CHUNKING: bool;
49 const PREFIX: bool;
50 const EMBED_CHECKSUM: bool;
51 const MNEMONIC: bool;
52 const CHUNK_FIRST: usize = 8;
53 const CHUNK_LEN: usize = 7;
54
55 fn to_baid64_payload(&self) -> [u8; LEN];
56 fn to_baid64_string(&self) -> String { self.display_baid64().to_string() }
57 fn to_baid64_mnemonic(&self) -> String { self.display_baid64().mnemonic }
58 fn display_baid64(&self) -> Baid64Display<LEN> {
59 Baid64Display::with(
60 Self::HRI,
61 self.to_baid64_payload(),
62 Self::CHUNKING,
63 Self::CHUNK_FIRST,
64 Self::CHUNK_LEN,
65 Self::PREFIX,
66 Self::MNEMONIC,
67 Self::EMBED_CHECKSUM,
68 )
69 }
70 fn fmt_baid64(&self, f: &mut Formatter) -> fmt::Result {
71 Display::fmt(&self.display_baid64(), f)
72 }
73}
74
75#[derive(Debug, Display, Error, From)]
76#[display(doc_comments)]
77pub enum Baid64ParseError {
78 InvalidHri(String, &'static str),
80
81 InvalidLen(String),
83
84 InvalidChecksum(String, u32, u32),
87
88 InvalidMnemonicLen(String),
90
91 #[from]
92 #[display(inner)]
93 InvalidMnemonic(mnemonic::Error),
94
95 #[from]
96 #[display(inner)]
97 Base64(base64::DecodeError),
98
99 InvalidPayload(String),
101}
102
103pub trait FromBaid64Str<const LEN: usize = 32>
104where
105 Self: DisplayBaid64<LEN> + TryFrom<[u8; LEN]>,
106 <Self as TryFrom<[u8; LEN]>>::Error: Error,
107{
108 fn from_baid64_str(mut s: &str) -> Result<Self, Baid64ParseError> {
109 let orig = s;
110
111 use base64::alphabet::Alphabet;
112 use base64::engine::GeneralPurpose;
113 use base64::engine::general_purpose::NO_PAD;
114
115 let mut checksum = None;
116
117 if let Some((hri, rest)) = s.rsplit_once(':') {
118 if hri != Self::HRI {
119 return Err(Baid64ParseError::InvalidHri(orig.to_owned(), Self::HRI));
120 }
121 s = rest;
122 }
123
124 if let Some((rest, sfx)) = s.split_once('#') {
125 let mut mnemo = Vec::<u8>::with_capacity(4);
126 mnemonic::decode(sfx, &mut mnemo)?;
127 if mnemo.len() != 4 {
128 return Err(Baid64ParseError::InvalidMnemonicLen(orig.to_string()));
129 }
130 checksum = Some([mnemo[0], mnemo[1], mnemo[2], mnemo[3]]);
131 s = rest;
132 }
133
134 let s = if s.contains('-') {
135 s.replace('-', "")
136 } else {
137 s.to_owned()
138 };
139
140 let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet");
141 let engine = GeneralPurpose::new(&alphabet, NO_PAD);
142 let data = engine.decode(s)?;
143
144 if data.len() != LEN && data.len() != LEN + 4 {
145 return Err(Baid64ParseError::InvalidLen(orig.to_owned()));
146 }
147 let mut payload = [0u8; LEN];
148 payload.copy_from_slice(&data[..LEN]);
149 if data.len() == LEN + 4 {
150 checksum = Some([data[LEN], data[LEN + 1], data[LEN + 2], data[LEN + 3]]);
151 }
152
153 let ck = check(Self::HRI, payload);
154 if matches!(checksum, Some(c) if c != ck) {
155 return Err(Baid64ParseError::InvalidChecksum(
156 orig.to_owned(),
157 u32::from_le_bytes(ck),
158 u32::from_le_bytes(checksum.unwrap()),
159 ));
160 }
161
162 Self::try_from(payload).map_err(|e| Baid64ParseError::InvalidPayload(e.to_string()))
163 }
164}
165
166#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
167pub struct Baid64Display<const LEN: usize = 32> {
168 hri: &'static str,
169 chunking: bool,
170 chunk_first: usize,
171 chunk_len: usize,
172 mnemonic: String,
173 prefix: bool,
174 suffix: bool,
175 embed_checksum: bool,
176 checksum: [u8; 4],
177 payload: [u8; LEN],
178}
179
180impl<const LEN: usize> Baid64Display<LEN> {
181 pub fn with(
182 hri: &'static str,
183 payload: [u8; LEN],
184 chunking: bool,
185 chunk_first: usize,
186 chunk_len: usize,
187 prefix: bool,
188 suffix: bool,
189 embed_checksum: bool,
190 ) -> Self {
191 debug_assert!(
192 hri.len() <= HRI_MAX_LEN,
193 "HRI is too long; it must not exceed {HRI_MAX_LEN} bytes"
194 );
195 debug_assert!(LEN > ID_MIN_LEN, "Baid64 id payload must be at least {ID_MIN_LEN} bytes");
196
197 let checksum = check(hri, payload);
198 let mnemonic = mnemonic::to_string(checksum);
199
200 Self {
201 hri,
202 chunking,
203 chunk_first,
204 chunk_len,
205 mnemonic,
206 prefix,
207 suffix,
208 embed_checksum,
209 checksum,
210 payload,
211 }
212 }
213
214 pub fn new(hri: &'static str, payload: [u8; LEN]) -> Self {
215 Self::with(hri, payload, false, 8, 7, false, false, false)
216 }
217 pub const fn use_hri(mut self) -> Self {
218 self.prefix = true;
219 self
220 }
221 pub const fn use_chunking(mut self) -> Self {
222 self.chunking = true;
223 self
224 }
225 pub const fn use_mnemonic(mut self) -> Self {
226 self.suffix = true;
227 self
228 }
229 pub const fn embed_checksum(mut self) -> Self {
230 self.embed_checksum = true;
231 self
232 }
233
234 pub const fn human_identifier(&self) -> &'static str { self.hri }
235
236 pub fn mnemonic(&self) -> &str { self.mnemonic.as_str() }
237 pub const fn checksum(&self) -> [u8; 4] { self.checksum }
238}
239
240impl<const LEN: usize> Display for Baid64Display<LEN> {
241 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
242 use base64::alphabet::Alphabet;
243 use base64::engine::GeneralPurpose;
244 use base64::engine::general_purpose::NO_PAD;
245
246 if (self.prefix && !f.sign_minus()) || (!self.prefix && f.sign_minus()) {
247 write!(f, "{}:", self.hri)?;
248 }
249
250 let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet");
251 let engine = GeneralPurpose::new(&alphabet, NO_PAD);
252
253 let mut payload = self.payload.to_vec();
254 if self.embed_checksum {
255 payload.extend(self.checksum);
256 }
257 let s = engine.encode(payload);
258
259 if self.chunking {
260 let bytes = s.as_bytes();
261 f.write_str(&String::from_utf8_lossy(&bytes[..self.chunk_first]))?;
262 for chunk in bytes[self.chunk_first..].chunks(self.chunk_len) {
263 write!(f, "-{}", &String::from_utf8_lossy(chunk))?;
264 }
265 } else {
266 f.write_str(&s)?;
267 }
268
269 if (self.suffix && !f.alternate()) || (!self.suffix && f.alternate()) {
270 write!(f, "#{}", self.mnemonic)?;
271 }
272
273 Ok(())
274 }
275}