use core::fmt;
use super::Error;
use super::base64;
use super::error::Syntax;
use super::note::{KeyName, Note, NoteSignature};
use crate::log::{Hash, TreeHead};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Checkpoint {
origin: KeyName,
head: TreeHead,
extensions: Vec<String>,
}
impl Checkpoint {
#[must_use]
pub const fn new(origin: KeyName, head: TreeHead) -> Self {
Self {
origin,
head,
extensions: Vec::new(),
}
}
#[must_use]
pub const fn origin(&self) -> &KeyName {
&self.origin
}
#[must_use]
pub const fn head(&self) -> TreeHead {
self.head
}
#[must_use]
pub fn extensions(&self) -> &[String] {
&self.extensions
}
#[must_use]
pub fn text(&self) -> String {
let mut text = format!(
"{}\n{}\n{}\n",
self.origin,
self.head.size,
base64::encode(self.head.root.as_bytes())
);
for line in &self.extensions {
text.push_str(line);
text.push('\n');
}
text
}
pub fn parse_text(text: &str) -> Result<Self, Error> {
let malformed = |what, line| Error::Malformed { what, line };
if text.bytes().any(|byte| byte < 0x20 && byte != b'\n') {
return Err(malformed(Syntax::ControlChar, 0));
}
let body = text
.strip_suffix('\n')
.ok_or(malformed(Syntax::MissingNewline, 1))?;
let lines: Vec<&str> = body.split('\n').collect();
if let Some(at) = lines.iter().position(|line| line.is_empty()) {
return Err(malformed(Syntax::EmptyLine, line_number(at)));
}
let [origin, size, root, extensions @ ..] = lines.as_slice() else {
return Err(malformed(Syntax::EmptyLine, line_number(lines.len())));
};
let origin = KeyName::new(*origin)?;
let size = parse_decimal(size).ok_or(malformed(Syntax::Decimal, 2))?;
let root = base64::decode(root).ok_or(malformed(Syntax::Base64, 3))?;
let root: [u8; 32] = root
.try_into()
.map_err(|_| malformed(Syntax::RootLength, 3))?;
Ok(Self {
origin,
head: TreeHead {
size,
root: Hash::from_bytes(root),
},
extensions: extensions.iter().map(|line| (*line).to_owned()).collect(),
})
}
}
impl fmt::Display for Checkpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.text())
}
}
fn line_number(index: usize) -> u32 {
u32::try_from(index.saturating_add(1)).unwrap_or(u32::MAX)
}
pub(crate) fn parse_decimal(text: &str) -> Option<u64> {
let canonical = match text.as_bytes() {
[b'0'] => true,
[first, rest @ ..] => (b'1'..=b'9').contains(first) && rest.iter().all(u8::is_ascii_digit),
[] => false,
};
if canonical { text.parse().ok() } else { None }
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SignedCheckpoint {
checkpoint: Checkpoint,
note: Note,
}
impl SignedCheckpoint {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
let note = Note::parse(bytes)?;
let checkpoint = Checkpoint::parse_text(note.text())?;
Ok(Self { checkpoint, note })
}
#[cfg(feature = "witness")]
pub(crate) fn from_parts(
checkpoint: Checkpoint,
signatures: Vec<NoteSignature>,
) -> Result<Self, Error> {
let note = Note::new(checkpoint.text(), signatures)?;
Ok(Self { checkpoint, note })
}
#[must_use]
pub const fn checkpoint(&self) -> &Checkpoint {
&self.checkpoint
}
#[must_use]
pub fn signatures(&self) -> &[NoteSignature] {
self.note.signatures()
}
#[must_use]
pub const fn note(&self) -> &Note {
&self.note
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
self.note.to_bytes()
}
}
impl fmt::Display for SignedCheckpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.note, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decimals_are_canonical() {
for (text, value) in [
("0", Some(0)),
("7", Some(7)),
("18446744073709551615", Some(u64::MAX)),
] {
assert_eq!(parse_decimal(text), value, "{text}");
}
for bad in [
"",
"00",
"07",
"+7",
"-1",
" 7",
"7 ",
"18446744073709551616",
"1e3",
"٣",
] {
assert_eq!(parse_decimal(bad), None, "{bad}");
}
}
#[test]
fn checkpoint_text_rules() {
let root = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
assert!(Checkpoint::parse_text(&format!("o\n0\n{root}\n")).is_ok());
let with_extension = format!("o\n0\n{root}\next one\n");
assert_eq!(
Checkpoint::parse_text(&with_extension).unwrap().text(),
with_extension
);
for (text, what) in [
(format!("o\n0\n{root}"), Syntax::MissingNewline),
(format!("o\n0\n{root}\n\n"), Syntax::EmptyLine),
("o\n0\n".to_owned(), Syntax::EmptyLine),
(format!("o\n00\n{root}\n"), Syntax::Decimal),
("o\n0\nAAAA\n".to_owned(), Syntax::RootLength),
(format!("o\n0\n{root}x\n"), Syntax::Base64),
(format!("o\n0\n{root}\n\t\n"), Syntax::ControlChar),
] {
assert!(
matches!(Checkpoint::parse_text(&text), Err(Error::Malformed { what: w, .. }) if w == what),
"{text:?}"
);
}
assert_eq!(
Checkpoint::parse_text(&format!("a b\n0\n{root}\n")),
Err(Error::BadName)
);
}
}