coap-message-utils 0.3.9

Utilities for using coap-message traits
Documentation
//! Traits and types that represent serializations of options in encoded options

use coap_message::{MessageOption, ReadableMessage};
use coap_numbers::option;

/// A trait semantically similar to TryFrom, but rather than being generic over the source, this
/// trait is specialized in being from any impl of MessageOption.
///
/// Types that implement this implicitly encode an extra piece of information, their option number:
/// Options passed in that don't have a matching number need to be ignored with a `None` return
/// value.
///
/// In passing, we also introduce a lifetime for the option (we work `from(&O)` instead of
/// `from(O)`) because this is always used with data copied out (as the MessageOption doesn't allow
/// long-term references into it anyway).
///
/// This uses an Option and not a Result, because the main way in which it is used is through
/// `take_into`, which leaves unprocessable options in the stream for later failing when critical
/// options are rejected. This pattern of usage also means that non-critical options that should
/// cause errors need to implement `TryFromOption for Result<Good, Bad>`, and raise an error of
/// their own when `Some(Bad(_))` is found.
pub trait TryFromOption: Sized {
    fn try_from(value: &impl MessageOption) -> Option<Self>;
}

/// The information model for block options when used in a way that the M flag does not matter.
///
/// In serialization, the M bit is unset and ignored (RFC7959: "MUST be set as zero and ignored on
/// reception").
struct BlockDataWithoutM {
    // Validity constraint: using only up to 20 bit
    blknum: u32,
    szx: u8,
}

/// The information model for block options when used in a way that the M flag *does* matter.
struct BlockDataWithM {
    coordinates: BlockDataWithoutM,
    m: bool,
}

impl core::ops::Deref for BlockDataWithM {
    type Target = BlockDataWithoutM;
    fn deref(&self) -> &BlockDataWithoutM {
        &self.coordinates
    }
}

/// Block1 option data (as used in either the request or the response)
pub struct Block1Data(BlockDataWithM);

/// Request data from a Block2 request
///
/// As the M flag is unused in requests, it is not captured in here (and ignored at construction).
pub struct Block2RequestData(BlockDataWithoutM);

/// Error that occurs when constructing a `Block2RequestData` from a message or an option.
///
/// It is singular and contains no details (for there is no usable action from them), but usually
/// stems from either the option being repeated or having an excessively large value.
#[derive(Debug)]
pub struct BadBlock2Option;

const M_BIT: u32 = 0x08;
const SZX_MASK: u32 = 0x07;
// Block options are only up to 3 bytes long
const BLOCK_MAX: u32 = 0xffffff;

impl BlockDataWithoutM {
    fn from_u32(o: u32) -> Option<Self> {
        if o > BLOCK_MAX {
            return None;
        }
        Some(Self {
            szx: (o & SZX_MASK) as u8,
            blknum: o >> 4,
        })
    }

    fn to_u32(&self) -> u32 {
        (self.blknum << 4) | self.szx as u32
    }

    /// Size of a single block
    pub fn size(&self) -> u16 {
        1 << (4 + self.szx)
    }

    /// Number of bytes before the indicated block
    pub fn start(&self) -> u32 {
        self.size() as u32 * self.blknum
    }
}

impl BlockDataWithM {
    fn from_u32(o: u32) -> Option<Self> {
        Some(Self {
            coordinates: BlockDataWithoutM::from_u32(o)?,
            m: o & M_BIT != 0,
        })
    }

    fn to_u32(&self) -> u32 {
        self.coordinates.to_u32() | if self.m { M_BIT } else { 0 }
    }
}

impl Block2RequestData {
    /// Extract a request block 2 value from a request message.
    ///
    /// Absence of the option is not an error and results in the default value to be returned;
    /// exceeding length or duplicate entries are an error and are indicated by returning an error,
    /// which should be responded to with a Bad Option error.
    pub fn from_message(message: &impl ReadableMessage) -> Result<Self, BadBlock2Option> {
        let mut b2options = message.options().filter(|o| o.number() == option::BLOCK2);

        match b2options.next() {
            None => Ok(Self::default()),
            Some(o) => {
                if b2options.next().is_none() {
                    Self::from_option(&o)
                } else {
                    Err(BadBlock2Option)
                }
            }
        }
    }

    /// Extract a request block 2 value from a single option. An error is indicated on a malformed
    /// (ie. overly long) option.
    ///
    /// Compared to [Block2RequestData::from_message()], this can easily be packed into a single
    /// loop that processes all options and fails on unknown critical ones; on the other hand, this
    /// does not automate the check for duplicate options.
    ///
    /// # Panics
    ///
    /// In debug mode if the option is not Block2
    pub fn from_option(option: &impl MessageOption) -> Result<Self, BadBlock2Option> {
        debug_assert!(option.number() == option::BLOCK2);
        let o: u32 = option.value_uint().ok_or(BadBlock2Option)?;
        BlockDataWithoutM::from_u32(o)
            .map(Self)
            .ok_or(BadBlock2Option)
    }

    pub fn to_option_value(&self, more: bool) -> u32 {
        self.0.to_u32() | if more { 0x08 } else { 0 }
    }

    /// Size of a single block
    pub fn size(&self) -> u16 {
        self.0.size()
    }

    /// Number of bytes before the indicated block
    pub fn start(&self) -> usize {
        self.0.start() as _
    }

    /// Return a block that has identical .start(), but a block size smaller or equal to the given
    /// one.
    ///
    /// Returns None if the given size is not expressible as a CoAP block (ie. is less than 16).
    pub fn shrink(mut self, size: u16) -> Option<Self> {
        while self.size() > size {
            if self.0.szx == 0 {
                return None;
            }
            self.0.szx -= 1;
            self.0.blknum *= 2;
        }
        Some(self)
    }
}

impl Block1Data {
    /// Number of bytes before the indicated block
    pub fn start(&self) -> usize {
        self.0.start() as _
    }

    pub fn more(&self) -> bool {
        self.0.m
    }

    pub fn to_option_value(&self) -> u32 {
        self.0.to_u32()
    }
}

impl Default for Block2RequestData {
    fn default() -> Self {
        Self(BlockDataWithoutM { szx: 6, blknum: 0 })
    }
}

impl TryFromOption for Block2RequestData {
    fn try_from(value: &impl MessageOption) -> Option<Self> {
        if value.number() != coap_numbers::option::BLOCK2 {
            return None;
        }

        Self::from_option(value).ok()
    }
}

impl TryFromOption for Block1Data {
    fn try_from(o: &impl MessageOption) -> Option<Self> {
        if o.number() != coap_numbers::option::BLOCK1 {
            return None;
        }

        Some(Self(BlockDataWithM::from_u32(o.value_uint()?)?))
    }
}