roughtime 0.1.0

A no_std-capable Roughtime secure time-sync client with pluggable crypto backends
Documentation
//! Parsing of the Roughtime wire format: a flat tag-length-value message encoding.
//!
//! A message begins with a `u32` tag count, followed by `count - 1` `u32` cumulative offsets
//! (the first value always starts at offset 0), followed by `count` `u32` tags, followed by
//! the concatenated tag values.

use alloc::vec::Vec;

use crate::error::Error;

/// A parsed Roughtime message: a list of `(tag, value)` pairs borrowed from the input buffer.
#[derive(Debug)]
pub struct RtMessage<'a> {
    tags: Vec<u32>,
    values: Vec<&'a [u8]>,
}

impl<'a> RtMessage<'a> {
    /// Parses a Roughtime message from `data`.
    ///
    /// # Errors
    ///
    /// Returns an error if `data` is too short, declares zero tags, or has out-of-bounds,
    /// decreasing, or misaligned tag offsets.
    pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
        if data.len() < 4 {
            return Err(Error::MessageTooShort);
        }
        let num_tags = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
        if num_tags == 0 {
            return Err(Error::NoTags);
        }
        let header_len = 8 * num_tags;
        if data.len() < header_len {
            return Err(Error::MessageTooShort);
        }

        let mut offsets = Vec::with_capacity(num_tags);
        offsets.push(0usize);
        for i in 0..num_tags.saturating_sub(1) {
            let idx = 4 + i * 4;
            offsets.push(u32::from_le_bytes([
                data[idx],
                data[idx + 1],
                data[idx + 2],
                data[idx + 3],
            ]) as usize);
        }

        let mut tags = Vec::with_capacity(num_tags);
        let tags_start = 4 + num_tags.saturating_sub(1) * 4;
        for i in 0..num_tags {
            let idx = tags_start + i * 4;
            tags.push(u32::from_le_bytes([
                data[idx],
                data[idx + 1],
                data[idx + 2],
                data[idx + 3],
            ]));
        }

        let values_start = 8 * num_tags;
        if data.len() < values_start {
            return Err(Error::MessageTooShort);
        }
        let values_len = data.len() - values_start;
        let values_data = &data[values_start..];

        for i in 0..num_tags {
            let start = offsets[i];
            let end = if i + 1 < num_tags {
                offsets[i + 1]
            } else {
                values_len
            };
            if start > end || end > values_len || start % 4 != 0 || end % 4 != 0 {
                return Err(Error::InvalidOffsets);
            }
        }

        let mut values = Vec::with_capacity(num_tags);
        for i in 0..num_tags {
            let start = offsets[i];
            let end = if i + 1 < num_tags {
                offsets[i + 1]
            } else {
                values_len
            };
            values.push(&values_data[start..end]);
        }

        Ok(Self { tags, values })
    }

    /// Returns the value associated with `tag`, if present.
    #[must_use]
    pub fn get(&self, tag: u32) -> Option<&'a [u8]> {
        self.tags
            .iter()
            .position(|&t| t == tag)
            .map(|i| self.values[i])
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used)]

    use super::*;
    use crate::tags::{TAG_NONC, TAG_PAD};

    #[test]
    fn parses_two_tag_message() {
        let mut msg_data = Vec::new();
        msg_data.extend_from_slice(&2u32.to_le_bytes());
        msg_data.extend_from_slice(&4u32.to_le_bytes());
        msg_data.extend_from_slice(&TAG_NONC.to_le_bytes());
        msg_data.extend_from_slice(&TAG_PAD.to_le_bytes());
        msg_data.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]);
        msg_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);

        let parsed = RtMessage::parse(&msg_data).expect("should parse message");
        assert_eq!(parsed.get(TAG_NONC), Some(&[0x11, 0x22, 0x33, 0x44][..]));
        assert_eq!(parsed.get(TAG_PAD), Some(&[0x00, 0x00, 0x00, 0x00][..]));
    }

    #[test]
    fn rejects_short_message() {
        assert!(RtMessage::parse(&[0x01, 0x00]).is_err());
    }

    #[test]
    fn rejects_zero_tags() {
        assert!(RtMessage::parse(&0u32.to_le_bytes()).is_err());
    }
}