1#[cfg(not(any(feature = "base64", feature = "base85")))]
23compile_error!("either base64 or base85 feature must be specified, you provide none of them.");
24
25#[cfg(all(feature = "base64", feature = "base85"))]
26compile_error!("either base64 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;
36use amplify::confinement::{self, Confined};
37use amplify::num::u24;
38use amplify::{Bytes32, hex};
39use sha2::{Digest, Sha256};
40#[cfg(feature = "strict")]
41use strict_encoding::{StrictDeserialize, StrictSerialize};
42
43pub const ASCII_ARMOR_MAX_LEN: usize = u24::MAX.to_usize();
44pub const ASCII_ARMOR_ID: &str = "Id";
45pub const ASCII_ARMOR_CHECKSUM_SHA256: &str = "Check-SHA256";
46
47pub struct DisplayAsciiArmored<'a, A: AsciiArmor>(&'a A);
48
49impl<'a, A: AsciiArmor> DisplayAsciiArmored<'a, A> {
50 fn data_digest(&self) -> (Vec<u8>, Option<Bytes32>) {
51 let data = self.0.to_ascii_armored_data();
52 let digest = Sha256::digest(&data);
53 (data, Some(Bytes32::from_byte_array(digest)))
54 }
55}
56
57impl<'a, A: AsciiArmor> Display for DisplayAsciiArmored<'a, A> {
58 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
59 writeln!(f, "-----BEGIN {}-----", A::PLATE_TITLE)?;
60
61 let (data, digest) = self.data_digest();
62 for header in self.0.ascii_armored_headers() {
63 writeln!(f, "{header}")?;
64 }
65 if let Some(digest) = digest {
66 writeln!(f, "{ASCII_ARMOR_CHECKSUM_SHA256}: {digest}")?;
67 }
68 writeln!(f)?;
69
70 #[cfg(feature = "base85")]
71 let data = base85::encode(&data);
72 #[cfg(feature = "base64")]
73 let data = {
74 use base64::Engine;
75 base64::prelude::BASE64_STANDARD.encode(&data)
76 };
77 let mut data = data.as_str();
78 while data.len() >= 80 {
79 let (line, rest) = data.split_at(80);
80 writeln!(f, "{}", line)?;
81 data = rest;
82 }
83 writeln!(f, "{}", data)?;
84
85 writeln!(f, "\n-----END {}-----", A::PLATE_TITLE)?;
86
87 Ok(())
88 }
89}
90
91#[derive(Clone, Eq, PartialEq, Hash, Debug)]
92pub struct ArmorHeader {
93 pub title: String,
94 pub values: Vec<String>,
95 pub params: Vec<(String, String)>,
96}
97
98impl ArmorHeader {
99 pub fn new(title: &'static str, value: String) -> Self {
100 ArmorHeader {
101 title: title.to_owned(),
102 values: vec![value],
103 params: none!(),
104 }
105 }
106 pub fn with(title: &'static str, values: impl IntoIterator<Item = String>) -> Self {
107 ArmorHeader {
108 title: title.to_owned(),
109 values: values.into_iter().collect(),
110 params: none!(),
111 }
112 }
113}
114
115impl Display for ArmorHeader {
116 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
117 let pfx = if self.values.len() == 1 { " " } else { "\n\t" };
118 write!(f, "{}:{}", self.title, pfx)?;
119 for (i, val) in self.values.iter().enumerate() {
120 if i > 0 {
121 f.write_str(",\n\t")?;
122 }
123 write!(f, "{}", val)?;
124 }
125
126 for (name, val) in &self.params {
127 write!(f, ";\n\t{name}={val}")?;
128 }
129 Ok(())
130 }
131}
132
133#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
134#[display(doc_comments)]
135pub enum ArmorParseError {
136 InvalidHeaderFormat(String),
138
139 InvalidHeaderParam(String, String),
141
142 WrongStructure,
144
145 Base85,
147
148 Base64,
150
151 NonEmptyChecksumParams,
154
155 MultipleChecksums,
157
158 #[from]
160 UnparsableChecksum(hex::Error),
161
162 MismatchedChecksum,
164
165 UnrecognizedHeader(String),
167}
168
169impl FromStr for ArmorHeader {
170 type Err = ArmorParseError;
171
172 fn from_str(s: &str) -> Result<Self, Self::Err> {
173 let (title, rest) =
174 s.split_once(':').ok_or_else(|| ArmorParseError::InvalidHeaderFormat(s.to_owned()))?;
175 let rest = rest.trim();
176 let mut split = rest.split(';');
177 let values =
178 split.next().ok_or_else(|| ArmorParseError::InvalidHeaderFormat(s.to_owned()))?.trim();
179 let mut params = vec![];
180 for param in split {
181 let (name, val) = s.split_once('=').ok_or_else(|| {
182 ArmorParseError::InvalidHeaderParam(title.to_owned(), param.to_owned())
183 })?;
184 params.push((name.trim().to_owned(), val.trim().to_owned()));
185 }
186 let values = values.split(',').map(|val| val.trim().to_owned()).collect();
187 Ok(ArmorHeader {
188 title: title.to_owned(),
189 values,
190 params,
191 })
192 }
193}
194
195pub trait AsciiArmor: Sized {
196 type Err: Debug + From<ArmorParseError>;
197
198 const PLATE_TITLE: &'static str;
199
200 fn to_ascii_armored_string(&self) -> String { format!("{}", self.display_ascii_armored()) }
201 fn display_ascii_armored(&self) -> DisplayAsciiArmored<Self> { DisplayAsciiArmored(self) }
202 fn ascii_armored_headers(&self) -> Vec<ArmorHeader> { none!() }
203 fn ascii_armored_digest(&self) -> Option<Bytes32> { DisplayAsciiArmored(self).data_digest().1 }
204 fn to_ascii_armored_data(&self) -> Vec<u8>;
205
206 fn from_ascii_armored_str(s: &str) -> Result<Self, Self::Err> {
207 let first = format!("-----BEGIN {}-----", Self::PLATE_TITLE);
208 let last = format!("-----END {}-----", Self::PLATE_TITLE);
209
210 let mut lines = s.lines().skip_while(|line| line != &first);
211 lines.next();
212 let mut checksum = None;
213 let mut headers = vec![];
214 for line in lines.by_ref() {
215 if line.is_empty() {
216 break;
217 }
218 let header = ArmorHeader::from_str(line)?;
219 if header.title == ASCII_ARMOR_CHECKSUM_SHA256 {
220 if !header.params.is_empty() {
221 return Err(ArmorParseError::NonEmptyChecksumParams.into());
222 }
223 if header.values.is_empty() || header.values.len() > 1 {
224 return Err(ArmorParseError::MultipleChecksums.into());
225 }
226 checksum = Some(header.values[0].to_owned());
227 } else {
228 headers.push(header);
229 }
230 }
231 let armor = lines.take_while(|line| line != &last).collect::<String>();
232 if armor.trim().is_empty() {
233 return Err(ArmorParseError::WrongStructure.into());
234 }
235 #[cfg(feature = "base85")]
236 let data = base85::decode(&armor).map_err(|_| ArmorParseError::Base85)?;
237 #[cfg(feature = "base64")]
238 let data = {
239 use baid64::base64::Engine;
240 baid64::base64::prelude::BASE64_STANDARD
241 .decode(&armor)
242 .map_err(|_| ArmorParseError::Base64)?
243 };
244 if let Some(checksum) = checksum {
245 let checksum =
246 Bytes32::from_str(&checksum).map_err(ArmorParseError::UnparsableChecksum)?;
247 let expected = Bytes32::from_byte_array(Sha256::digest(&data));
248 if checksum != expected {
249 return Err(ArmorParseError::MismatchedChecksum.into());
250 }
251 }
252 let me = Self::with_headers_data(headers, data)?;
253 Ok(me)
254 }
255
256 fn with_headers_data(headers: Vec<ArmorHeader>, data: Vec<u8>) -> Result<Self, Self::Err>;
257}
258
259#[cfg(feature = "strict")]
260#[derive(Debug, Display, Error, From)]
261#[display(doc_comments)]
262pub enum StrictArmorError {
263 MissedId,
265
266 MultipleIds,
268
269 #[cfg(feature = "baid64")]
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::{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 FromStr for Sid {
413 type Err = baid64::Baid64ParseError;
414 fn from_str(s: &str) -> Result<Self, Self::Err> {
415 Ok(Self {
416 inner: s.len() as u8,
417 })
418 }
419 }
420
421 #[derive(Default, Debug, StrictType, StrictEncode, StrictDecode, PartialEq)]
422 #[strict_type(lib = "ARMORtest")]
423 struct S {
424 inner: u8,
425 }
426 impl StrictSerialize for S {}
427 impl StrictDeserialize for S {}
428 impl StrictArmor for S {
429 const PLATE_TITLE: &'static str = "S";
430 type Id = Sid;
431 fn armor_id(&self) -> Self::Id { Default::default() }
432 }
433
434 let s = S::default();
435 let display_ascii_armored = s.display_ascii_armored();
436
437 #[cfg(feature = "base85")]
438 assert_eq!(
439 s.to_ascii_armored_string(),
440 format!(
441 r#"-----BEGIN S-----
442Id: 0
443Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
444
44500
446
447-----END S-----
448"#
449 )
450 );
451 #[cfg(feature = "base64")]
452 assert_eq!(
453 s.to_ascii_armored_string(),
454 format!(
455 r#"-----BEGIN S-----
456Id: 0
457Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
458
459AA==
460
461-----END S-----
462"#
463 )
464 );
465
466 assert_eq!(display_ascii_armored.data_digest().0, vec![0]);
467
468 #[cfg(feature = "base85")]
471 assert!(
472 S::from_ascii_armored_str(
473 r#"-----BEGIN S-----
474Id: 0
475Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01e
476
47700
478
479-----END S-----
480"#
481 )
482 .is_err()
483 );
484 #[cfg(feature = "base64")]
485 assert!(
486 S::from_ascii_armored_str(
487 r#"-----BEGIN S-----
488Id: 0
489Check-SHA256: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01e
490
491AA==
492
493-----END S-----
494"#
495 )
496 .is_err()
497 );
498
499 #[cfg(feature = "base85")]
502 assert!(
503 S::from_ascii_armored_str(&format!(
504 r#"-----BEGIN S-----
505Id: 1
506Check-SHA256: {}
507
50800
509
510-----END S-----
511"#,
512 display_ascii_armored.data_digest().1.unwrap_or_default()
513 ))
514 .is_err()
515 );
516 #[cfg(feature = "base64")]
517 assert!(
518 S::from_ascii_armored_str(&format!(
519 r#"-----BEGIN S-----
520Id: 1
521Check-SHA256: {}
522
523AA==
524
525-----END S-----
526"#,
527 display_ascii_armored.data_digest().1.unwrap_or_default()
528 ))
529 .is_err()
530 );
531 }
532}