urma-core 0.3.0

Canonical URMA V0 codecs, crypto and proof validation without I/O
Documentation
use crate::error::{Error, ensure};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Topics {
    pub wire: String,
    pub hashtags: Vec<String>,
}

impl Topics {
    pub const MAX_LABEL_BYTES: usize = 32;
    pub const MAX_HASHTAGS: usize = 8;

    pub fn validate(&self) -> Result<(), Error> {
        if !self.wire.is_empty() {
            Self::validate_label(&self.wire)?;
        }
        ensure!(
            self.hashtags.len() <= Self::MAX_HASHTAGS,
            "at most 8 hashtags"
        );
        for tag in &self.hashtags {
            Self::validate_label(tag)?;
        }
        ensure!(
            self.hashtags.windows(2).all(|pair| pair[0] < pair[1]),
            "hashtags must be unique and sorted"
        );
        Ok(())
    }

    pub fn validate_label(label: &str) -> Result<(), Error> {
        let bytes = label.as_bytes();
        ensure!(
            (1..=Self::MAX_LABEL_BYTES).contains(&bytes.len()),
            "label must be 1..32 bytes"
        );
        ensure!(
            bytes[0].is_ascii_lowercase() || bytes[0].is_ascii_digit(),
            "label must start with a lowercase letter or digit"
        );
        ensure!(
            bytes.iter().all(|byte| byte.is_ascii_lowercase()
                || byte.is_ascii_digit()
                || *byte == b'-'
                || *byte == b'_'),
            "label permits only lowercase ASCII letters, digits, - and _"
        );
        Ok(())
    }

    pub fn encode_body(&self, text: &str) -> Result<Vec<u8>, Error> {
        self.validate()?;
        let mut bytes = Vec::new();
        bytes.push(u8::try_from(self.wire.len())?);
        bytes.extend_from_slice(self.wire.as_bytes());
        bytes.push(u8::try_from(self.hashtags.len())?);
        for tag in &self.hashtags {
            bytes.push(u8::try_from(tag.len())?);
            bytes.extend_from_slice(tag.as_bytes());
        }
        bytes.extend_from_slice(&u16::try_from(text.len())?.to_le_bytes());
        bytes.extend_from_slice(text.as_bytes());
        Ok(bytes)
    }

    pub fn decode_body(mut bytes: &[u8]) -> Result<(Self, String), Error> {
        let wire_length = usize::from(take(&mut bytes, 1)?[0]);
        ensure!(
            wire_length <= Self::MAX_LABEL_BYTES,
            "wire exceeds 32 bytes"
        );
        let wire = std::str::from_utf8(take(&mut bytes, wire_length)?)?.to_owned();
        let count = usize::from(take(&mut bytes, 1)?[0]);
        ensure!(count <= Self::MAX_HASHTAGS, "at most 8 hashtags");
        let mut hashtags = Vec::with_capacity(count);
        for _ in 0..count {
            hashtags.push(read_label(&mut bytes)?);
        }
        let length = usize::from(u16::from_le_bytes(take(&mut bytes, 2)?.try_into()?));
        let text = std::str::from_utf8(take(&mut bytes, length)?)?.to_owned();
        ensure!(bytes.is_empty(), "trailing structured record bytes");
        let topics = Self { wire, hashtags };
        topics.validate()?;
        Ok((topics, text))
    }
}

fn take<'a>(bytes: &mut &'a [u8], length: usize) -> Result<&'a [u8], Error> {
    ensure!(bytes.len() >= length, "truncated structured record");
    let (head, tail) = bytes.split_at(length);
    *bytes = tail;
    Ok(head)
}

fn read_label(bytes: &mut &[u8]) -> Result<String, Error> {
    let length = usize::from(take(bytes, 1)?[0]);
    ensure!(
        (1..=Topics::MAX_LABEL_BYTES).contains(&length),
        "label must be 1..32 bytes"
    );
    Ok(std::str::from_utf8(take(bytes, length)?)?.to_owned())
}