1#[cfg(not(any(feature = "baid64", feature = "base85")))]
23compile_error!("either baid64 or base85 feature must be specified, you provide none of them.");
24
25#[cfg(all(feature = "baid64", feature = "base85"))]
26compile_error!("either baid64 or base85 feature must be specified, you provide both of them.");
27
28#[macro_use]
29extern crate amplify;
30
31use core::fmt::{self, Debug, Display, Formatter};
32use core::str::FromStr;
33
34#[cfg(feature = "strict")]
35use amplify::confinement::U24 as U24MAX;
36#[cfg(feature = "strict")]
37use amplify::confinement::{self, Confined};
38use amplify::num::u24;
39use amplify::{Bytes32, hex};
40use sha2::{Digest, Sha256};
41#[cfg(feature = "strict")]
42use strict_encoding::{StrictDeserialize, StrictSerialize};
43
44pub const ASCII_ARMOR_MAX_LEN: usize = u24::MAX.to_usize();
45pub const ASCII_ARMOR_ID: &str = "Id";
46pub const ASCII_ARMOR_CHECKSUM_SHA256: &str = "Check-SHA256";
47
48pub struct DisplayAsciiArmored<'a, A: AsciiArmor>(&'a A);
49
50impl<'a, A: AsciiArmor> DisplayAsciiArmored<'a, A> {
51 fn data_digest(&self) -> (Vec<u8>, Option<Bytes32>) {
52 let data = self.0.to_ascii_armored_data();
53 let digest = Sha256::digest(&data);
54 (data, Some(Bytes32::from_byte_array(digest)))
55 }
56}
57
58impl<'a, A: AsciiArmor> Display for DisplayAsciiArmored<'a, A> {
59 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
60 writeln!(f, "-----BEGIN {}-----", A::PLATE_TITLE)?;
61
62 let (data, digest) = self.data_digest();
63 for header in self.0.ascii_armored_headers() {
64 writeln!(f, "{header}")?;
65 }
66 if let Some(digest) = digest {
67 writeln!(f, "{ASCII_ARMOR_CHECKSUM_SHA256}: {digest}")?;
68 }
69 writeln!(f)?;
70
71 #[cfg(feature = "base85")]
72 let data = base85::encode(&data);
73 #[cfg(feature = "baid64")]
74 let data = {
75 use baid64::base64::Engine;
76 baid64::base64::prelude::BASE64_STANDARD.encode(&data)
77 };
78 let mut data = data.as_str();
79 while data.len() >= 80 {
80 let (line, rest) = data.split_at(80);
81 writeln!(f, "{}", line)?;
82 data = rest;
83 }
84 writeln!(f, "{}", data)?;
85
86 writeln!(f, "\n-----END {}-----", A::PLATE_TITLE)?;
87
88 Ok(())
89 }
90}
91
92#[derive(Clone, Eq, PartialEq, Hash, Debug)]
93pub struct ArmorHeader {
94 pub title: String,
95 pub values: Vec<String>,
96 pub params: Vec<(String, String)>,
97}
98
99impl ArmorHeader {
100 pub fn new(title: &'static str, value: String) -> Self {
101 ArmorHeader {
102 title: title.to_owned(),
103 values: vec![value],
104 params: none!(),
105 }
106 }
107 pub fn with(title: &'static str, values: impl IntoIterator<Item = String>) -> Self {
108 ArmorHeader {
109 title: title.to_owned(),
110 values: values.into_iter().collect(),
111 params: none!(),
112 }
113 }
114}
115
116impl Display for ArmorHeader {
117 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
118 let pfx = if self.values.len() == 1 { " " } else { "\n\t" };
119 write!(f, "{}:{}", self.title, pfx)?;
120 for (i, val) in self.values.iter().enumerate() {
121 if i > 0 {
122 f.write_str(",\n\t")?;
123 }
124 write!(f, "{}", val)?;
125 }
126
127 for (name, val) in &self.params {
128 write!(f, ";\n\t{name}={val}")?;
129 }
130 Ok(())
131 }
132}
133
134#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
135#[display(doc_comments)]
136pub enum ArmorParseError {
137 InvalidHeaderFormat(String),
139
140 InvalidHeaderParam(String, String),
142
143 WrongStructure,
145
146 Base85,
148
149 Base64,
151
152 NonEmptyChecksumParams,
155
156 MultipleChecksums,
158
159 #[from]
161 UnparsableChecksum(hex::Error),
162
163 MismatchedChecksum,
165
166 UnrecognizedHeader(String),
168}
169
170impl FromStr for ArmorHeader {
171 type Err = ArmorParseError;
172
173 fn from_str(s: &str) -> Result<Self, Self::Err> {
174 let (title, rest) =
175 s.split_once(':').ok_or_else(|| ArmorParseError::InvalidHeaderFormat(s.to_owned()))?;
176 let rest = rest.trim();
177 let mut split = rest.split(';');
178 let values =
179 split.next().ok_or_else(|| ArmorParseError::InvalidHeaderFormat(s.to_owned()))?.trim();
180 let mut params = vec![];
181 for param in split {
182 let (name, val) = s.split_once('=').ok_or_else(|| {
183 ArmorParseError::InvalidHeaderParam(title.to_owned(), param.to_owned())
184 })?;
185 params.push((name.trim().to_owned(), val.trim().to_owned()));
186 }
187 let values = values.split(',').map(|val| val.trim().to_owned()).collect();
188 Ok(ArmorHeader {
189 title: title.to_owned(),
190 values,
191 params,
192 })
193 }
194}
195
196pub trait AsciiArmor: Sized {
197 type Err: Debug + From<ArmorParseError>;
198
199 const PLATE_TITLE: &'static str;
200
201 fn to_ascii_armored_string(&self) -> String { format!("{}", self.display_ascii_armored()) }
202 fn display_ascii_armored(&self) -> DisplayAsciiArmored<'_, Self> { DisplayAsciiArmored(self) }
203 fn ascii_armored_headers(&self) -> Vec<ArmorHeader> { none!() }
204 fn ascii_armored_digest(&self) -> Option<Bytes32> { DisplayAsciiArmored(self).data_digest().1 }
205 fn to_ascii_armored_data(&self) -> Vec<u8>;
206
207 fn from_ascii_armored_str(s: &str) -> Result<Self, Self::Err> {
208 let first = format!("-----BEGIN {}-----", Self::PLATE_TITLE);
209 let last = format!("-----END {}-----", Self::PLATE_TITLE);
210
211 let mut lines = s.lines().skip_while(|line| line != &first);
212 lines.next();
213 let mut checksum = None;
214 let mut headers = vec![];
215 for line in lines.by_ref() {
216 if line.is_empty() {
217 break;
218 }
219 let header = ArmorHeader::from_str(line)?;
220 if header.title == ASCII_ARMOR_CHECKSUM_SHA256 {
221 if !header.params.is_empty() {
222 return Err(ArmorParseError::NonEmptyChecksumParams.into());
223 }
224 if header.values.is_empty() || header.values.len() > 1 {
225 return Err(ArmorParseError::MultipleChecksums.into());
226 }
227 checksum = Some(header.values[0].to_owned());
228 } else {
229 headers.push(header);
230 }
231 }
232 let armor = lines.take_while(|line| line != &last).collect::<String>();
233 if armor.trim().is_empty() {
234 return Err(ArmorParseError::WrongStructure.into());
235 }
236 #[cfg(feature = "base85")]
237 let data = base85::decode(&armor).map_err(|_| ArmorParseError::Base85)?;
238 #[cfg(feature = "baid64")]
239 let data = {
240 use baid64::base64::Engine;
241 baid64::base64::prelude::BASE64_STANDARD
242 .decode(&armor)
243 .map_err(|_| ArmorParseError::Base64)?
244 };
245 if let Some(checksum) = checksum {
246 let checksum =
247 Bytes32::from_str(&checksum).map_err(ArmorParseError::UnparsableChecksum)?;
248 let expected = Bytes32::from_byte_array(Sha256::digest(&data));
249 if checksum != expected {
250 return Err(ArmorParseError::MismatchedChecksum.into());
251 }
252 }
253 let me = Self::with_headers_data(headers, data)?;
254 Ok(me)
255 }
256
257 fn with_headers_data(headers: Vec<ArmorHeader>, data: Vec<u8>) -> Result<Self, Self::Err>;
258}
259
260#[cfg(feature = "strict")]
261#[derive(Debug, Display, Error, From)]
262#[display(doc_comments)]
263pub enum StrictArmorError {
264 MissedId,
266
267 MultipleIds,
269
270 #[from]
272 InvalidId(baid64::Baid64ParseError),
273
274 MismatchedId { actual: String, expected: String },
280
281 #[from]
283 Deserialize(strict_encoding::DeserializeError),
284
285 #[from(confinement::Error)]
287 TooLarge,
288
289 #[from]
290 #[display(inner)]
291 Armor(ArmorParseError),
292}
293
294#[cfg(feature = "strict")]
295pub trait StrictArmor: StrictSerialize + StrictDeserialize {
296 type Id: Copy + Eq + Debug + Display + FromStr<Err = baid64::Baid64ParseError>;
297
298 const PLATE_TITLE: &'static str;
299
300 fn armor_id(&self) -> Self::Id;
301 fn checksum_armor(&self) -> bool { false }
302 fn armor_headers(&self) -> Vec<ArmorHeader> { none!() }
303 fn parse_armor_headers(&mut self, _headers: Vec<ArmorHeader>) -> Result<(), StrictArmorError> {
304 Ok(())
305 }
306}
307
308#[cfg(feature = "strict")]
309impl<T> AsciiArmor for T
310where T: StrictArmor
311{
312 type Err = StrictArmorError;
313 const PLATE_TITLE: &'static str = <T as StrictArmor>::PLATE_TITLE;
314
315 fn ascii_armored_headers(&self) -> Vec<ArmorHeader> {
316 let mut headers = vec![ArmorHeader::new(ASCII_ARMOR_ID, self.armor_id().to_string())];
317 headers.extend(self.armor_headers());
318 headers
319 }
320
321 fn to_ascii_armored_data(&self) -> Vec<u8> {
322 self.to_strict_serialized::<U24MAX>().expect("data too large for ASCII armoring").release()
323 }
324
325 fn with_headers_data(headers: Vec<ArmorHeader>, data: Vec<u8>) -> Result<Self, Self::Err> {
326 let id =
327 headers.iter().find(|h| h.title == ASCII_ARMOR_ID).ok_or(StrictArmorError::MissedId)?;
328 if id.values.is_empty() || id.values.len() > 1 {
330 return Err(StrictArmorError::MultipleIds);
331 }
332 let expected = T::Id::from_str(&id.values[0]).map_err(StrictArmorError::from)?;
333 let data = Confined::try_from(data).map_err(StrictArmorError::from)?;
334 let mut me =
335 Self::from_strict_serialized::<U24MAX>(data).map_err(StrictArmorError::from)?;
336 me.parse_armor_headers(headers)?;
337 let actual = me.armor_id();
338 if expected != actual {
339 return Err(StrictArmorError::MismatchedId {
340 expected: expected.to_string(),
341 actual: actual.to_string(),
342 });
343 }
344 Ok(me)
345 }
346}
347
348impl AsciiArmor for Vec<u8> {
349 type Err = ArmorParseError;
350 const PLATE_TITLE: &'static str = "DATA";
351
352 fn to_ascii_armored_data(&self) -> Vec<u8> { self.clone() }
353
354 fn with_headers_data(headers: Vec<ArmorHeader>, data: Vec<u8>) -> Result<Self, Self::Err> {
355 assert!(headers.is_empty());
356 Ok(data)
357 }
358}
359
360#[cfg(test)]
361mod test {
362 use super::*;
363
364 #[test]
365 fn roundtrip() {
366 let noise = Sha256::digest("some test data");
367 let data = noise.as_slice().repeat(100).to_vec();
368 let armor = data.to_ascii_armored_string();
369 let data2 = Vec::<u8>::from_ascii_armored_str(&armor).unwrap();
370 let armor2 = data2.to_ascii_armored_string();
371 assert_eq!(data, data2);
372 assert_eq!(armor, armor2);
373 }
374
375 #[test]
376 fn format() {
377 let noise = Sha256::digest("some test data");
378 let data = noise.as_slice().repeat(100).to_vec();
379 let armored_context = data.to_ascii_armored_string();
380 let mut lines = armored_context.lines();
381 let mut current = lines.next().unwrap_or_default();
382 assert_eq!(current, "-----BEGIN DATA-----");
383 for line in lines {
384 assert!(line.len() <= 80, "a line should less than or equal 80 chars");
385 current = line;
386 }
387 assert_eq!(current, "-----END DATA-----");
388 }
389
390 #[cfg(feature = "strict")]
391 #[test]
392 fn strict_format() {
393 use strict_encoding::{DefaultBasedStrictDumb, StrictDecode, StrictEncode, StrictType};
394
395 #[derive(
396 Copy,
397 Clone,
398 Debug,
399 Default,
400 Display,
401 Eq,
402 StrictType,
403 StrictEncode,
404 StrictDecode,
405 PartialEq
406 )]
407 #[strict_type(lib = "ARMORtest")]
408 #[display("{inner}")]
409 pub struct Sid {
410 inner: u8,
411 }
412 impl DefaultBasedStrictDumb for Sid {}
413 impl FromStr for Sid {
414 type Err = baid64::Baid64ParseError;
415 fn from_str(s: &str) -> Result<Self, Self::Err> {
416 Ok(Self {
417 inner: s.len() as u8,
418 })
419 }
420 }
421
422 #[derive(Default, Debug, StrictType, StrictEncode, StrictDecode, PartialEq)]
423 #[strict_type(lib = "ARMORtest")]
424 struct S {
425 inner: u8,
426 }
427 impl DefaultBasedStrictDumb for S {}
428 impl StrictSerialize for S {}
429 impl StrictDeserialize for S {}
430 impl StrictArmor for S {
431 const PLATE_TITLE: &'static str = "S";
432 type Id = Sid;
433 fn armor_id(&self) -> Self::Id { Default::default() }
434 }
435
436 let s = S::default();
437 let display_ascii_armored = s.display_ascii_armored();
438
439 #[cfg(feature = "base85")]
440 assert_eq!(
441 s.to_ascii_armored_string(),
442 format!(
443 r#"-----BEGIN S-----
444Id: 0
445Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
446
44700
448
449-----END S-----
450"#
451 )
452 );
453 #[cfg(feature = "baid64")]
454 assert_eq!(
455 s.to_ascii_armored_string(),
456 format!(
457 r#"-----BEGIN S-----
458Id: 0
459Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
460
461AA==
462
463-----END S-----
464"#
465 )
466 );
467
468 assert_eq!(display_ascii_armored.data_digest().0, vec![0]);
469
470 #[cfg(feature = "base85")]
473 assert!(
474 S::from_ascii_armored_str(
475 r#"-----BEGIN S-----
476Id: 0
477Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01e
478
47900
480
481-----END S-----
482"#
483 )
484 .is_err()
485 );
486 #[cfg(feature = "baid64")]
487 assert!(
488 S::from_ascii_armored_str(
489 r#"-----BEGIN S-----
490Id: 0
491Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01e
492
493AA==
494
495-----END S-----
496"#
497 )
498 .is_err()
499 );
500
501 #[cfg(feature = "base85")]
504 assert!(
505 S::from_ascii_armored_str(&format!(
506 r#"-----BEGIN S-----
507Id: 1
508Check-SHA256: {}
509
51000
511
512-----END S-----
513"#,
514 display_ascii_armored.data_digest().1.unwrap_or_default()
515 ))
516 .is_err()
517 );
518 #[cfg(feature = "baid64")]
519 assert!(
520 S::from_ascii_armored_str(&format!(
521 r#"-----BEGIN S-----
522Id: 1
523Check-SHA256: {}
524
525AA==
526
527-----END S-----
528"#,
529 display_ascii_armored.data_digest().1.unwrap_or_default()
530 ))
531 .is_err()
532 );
533 }
534}