coap-zero 0.3.0

CoAP protocol implementation for no_std without alloc
Documentation
// Copyright Open Logistics Foundation
//
// Licensed under the Open Logistics Foundation License 1.3.
// For details on the licensing terms, see the LICENSE file.
// SPDX-License-Identifier: OLFL-1.3

//! The Message Module represents a decoded CoAP Message

pub mod codes;
pub mod encoded_message;
mod header;
pub mod options;
pub mod token;

use bondrewd::Bitfields;
use heapless::Vec;

use self::{
    codes::Code, encoded_message::EncodedMessage, header::MessageHeader, options::CoapOption,
    token::Token,
};

/// Version of the CoAP Protocol this library implements
pub const PROTOCOL_VERSION: u8 = 1;

/// Message Type as defined in <https://www.rfc-editor.org/rfc/rfc7252#page-8>
#[derive(Debug, Clone, Copy, bondrewd::BitfieldEnum, PartialEq, Eq)]
pub enum Type {
    /// From <https://www.rfc-editor.org/rfc/rfc7252#page-8>:
    /// "Some messages require an acknowledgement.  These messages are
    /// called "Confirmable".  When no packets are lost, each Confirmable
    /// message elicits exactly one return message of type Acknowledgement
    /// or type Reset."
    Confirmable = 0,
    /// From <https://www.rfc-editor.org/rfc/rfc7252#page-8>:
    /// "Some other messages do not require an acknowledgement.  This is
    /// particularly true for messages that are repeated regularly for
    /// application requirements, such as repeated readings from a sensor."
    NonConfirmable = 1,
    /// From <https://www.rfc-editor.org/rfc/rfc7252#page-8>:
    /// "An Acknowledgement message acknowledges that a specific
    /// Confirmable message arrived.  By itself, an Acknowledgement
    /// message does not indicate success or failure of any request
    /// encapsulated in the Confirmable message, but the Acknowledgement
    /// message may also carry a Piggybacked Response [...]."
    Acknowledgement = 2,
    /// From <https://www.rfc-editor.org/rfc/rfc7252#page-8>:
    /// "A Reset message indicates that a specific message (Confirmable or
    /// Non-confirmable) was received, but some context is missing to
    /// properly process it.  This condition is usually caused when the
    /// receiving node has rebooted and has forgotten some state that
    /// would be required to interpret the message.  Provoking a Reset
    /// message (e.g., by sending an Empty Confirmable message) is also
    /// useful as an inexpensive check of the liveness of an endpoint
    /// ("CoAP ping")."
    Reset = 3,
}

/// Combined error type for message encoding, parsing and checking
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
    /// Invalid Code used
    InvalidCode,
    /// Option could not be parsed
    InvalidOption(options::Error),
    /// Invalid token length (greater 8)
    TokenLength,
    /// The provided buffer is too small
    OutOfMemory,
    /// The message is shorter than 4 bytes which is the minimum CoAP message length (the length of
    /// the message header)
    MsgTooShort,
    /// Empty Non-Confirmable message (Non-Confirmable messages may only be requests or responses).
    EmptyNon,
    /// Request in an ACK (ACKs may only be piggybacked responses or empty).
    RequestAck,
    /// Request or response in a RST message (RSTs _must_ be empty).
    NonEmptyRst,
    /// Empty message (code 0.00) with token, options or payload (empty messages _must_ be actually
    /// empty, "An Empty message only contains the 4-byte header.")
    EmptyWithContent,
}

/// A CoAP Message. Use this as a builder structure and then encode the message
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message<'data, const MAX_OPTION_COUNT: usize = { crate::DEFAULT_MAX_OPTION_COUNT }> {
    version: u8,
    message_type: Type,
    code: Code,
    // The Message Id and the Token are stored as Options here so they can be set later when the
    // endpoint assigns a Message ID and a (possibly new) Token. The user should be able to
    // construct a Message without knowing the Message ID and Token yet so the Options allow to
    // defer these.
    message_id: u16,
    token: Token,
    options: Vec<CoapOption<'data>, MAX_OPTION_COUNT>,
    payload: Option<&'data [u8]>,
}

impl<'data, const MAX_OPTION_COUNT: usize> TryFrom<EncodedMessage<'data>>
    for Message<'data, MAX_OPTION_COUNT>
{
    type Error = Error;

    fn try_from(encoded: EncodedMessage<'data>) -> Result<Self, Error> {
        let mut options: Vec<CoapOption<'_>, MAX_OPTION_COUNT> = Vec::new();

        // The payload offset can only be determined after iterating over all options. Therefore,
        // we iterate first so that the EncodedMessage may remember the payload offset. So when we
        // request the payload (offset) afterwards, it should be available immediately.
        for option in encoded.options_iter()? {
            options.push(option.map_err(Error::InvalidOption)?).unwrap();
        }

        Ok(Self {
            version: encoded.version(),
            message_type: encoded.message_type(),
            code: encoded.code()?,
            message_id: encoded.message_id(),
            token: encoded.token()?,
            options,
            payload: encoded.payload()?,
        })
    }
}

impl<'data, const MAX_OPTION_COUNT: usize> Message<'data, MAX_OPTION_COUNT> {
    /// Initialize with minimum required message parameters.
    ///
    /// The Message_Id and the Token are handled internally by CoAP-Zero.
    /// Options and a Payload are not required for a valid CoAP Message,
    /// but both can be set by the user if needed.
    pub fn new(
        message_type: Type,
        code: Code,
        message_id: u16,
        token: Token,
        options: Vec<CoapOption<'data>, MAX_OPTION_COUNT>,
        payload: Option<&'data [u8]>,
    ) -> Self {
        let options = crate::message::options::sort_options_vec(options);
        Self {
            version: PROTOCOL_VERSION,
            message_type,
            code,
            message_id,
            token,
            options,
            payload,
        }
    }

    /// Convenience constructor for empty messages. Not public but used internally for the other
    /// convenience constructors.
    fn new_empty(message_type: Type, message_id: u16) -> Self {
        Self {
            version: PROTOCOL_VERSION,
            message_type,
            code: Code::Empty,
            message_id,
            token: Token::default(),
            options: Vec::new(),
            payload: None,
        }
    }

    /// Convenience constructor for empty ACKs (can not be used for piggybacked responses)
    pub fn new_ack(message_id: u16) -> Self {
        Self::new_empty(Type::Acknowledgement, message_id)
    }

    /// Convenience constructor for RSTs
    pub fn new_rst(message_id: u16) -> Self {
        Self::new_empty(Type::Reset, message_id)
    }

    /// Convenience constructor for pings
    pub fn new_ping(message_id: u16) -> Self {
        Self::new_empty(Type::Confirmable, message_id)
    }

    /// Returns the message type
    pub fn message_type(&self) -> Type {
        self.message_type
    }

    /// Returns the message code
    pub fn code(&self) -> Code {
        self.code
    }

    /// Returns the message ID
    pub fn message_id(&self) -> u16 {
        self.message_id
    }

    /// Returns the token
    pub fn token(&self) -> Token {
        self.token
    }

    /// Returns a reference to the options vector
    pub fn options(&self) -> &Vec<CoapOption<'_>, MAX_OPTION_COUNT> {
        &self.options
    }

    /// Returns a reference to the payload, if any
    pub fn payload(&self) -> Option<&'data [u8]> {
        self.payload
    }

    /// Returns true if the message code is [Code::Empty]
    pub fn is_empty(&self) -> bool {
        matches!(self.code, Code::Empty)
    }

    /// Returns true if the message has a code of [Code::Request]
    pub fn is_request(&self) -> bool {
        matches!(self.code, Code::Request(_))
    }

    /// Returns true if the message has a code of [Code::Response]
    pub fn is_response(&self) -> bool {
        matches!(self.code, Code::Response(_))
    }

    /// Encode the message into the passed byte-slice, returns the resulting size of the message
    pub(crate) fn encode<'buffer>(
        &self,
        buf: &'buffer mut [u8],
    ) -> Result<EncodedMessage<'buffer>, Error> {
        let header = MessageHeader::new(
            self.version,
            self.message_type,
            self.token.length,
            self.code.into(),
            self.message_id,
        );

        let header = header.into_bytes();

        let mut index = 0;

        for x in header {
            *buf.get_mut(index).ok_or(Error::OutOfMemory)? = x;
            index += 1;
        }

        for i in 0..self.token.length as usize {
            *buf.get_mut(index).ok_or(Error::OutOfMemory)? = self.token.bytes[i];
            index += 1;
        }

        index += options::encode_to_buf(&mut buf[index..], &self.options)?;

        if let Some(payload) = self.payload {
            *buf.get_mut(index).ok_or(Error::OutOfMemory)? = 0xFF; // Payload Marker
            index += 1;

            for x in payload {
                *buf.get_mut(index).ok_or(Error::OutOfMemory)? = *x;
                index += 1;
            }
        }

        let encoded = EncodedMessage::try_new(&buf[..index]).unwrap();
        encoded.set_payload_offset(index);

        Ok(encoded)
    }
}