coap-scroll-ring-server 0.2.1

An CoAP handler for read access to a scroll-ring
Documentation
//! An implemenmtation of a [coap_handler::Handler] around a [scroll_ring::Buffer]
//!
//! ## Usage
//!
//! Wrap a buffer (typically some tasks's linear text output) through
//! [`BufferHandler::new(&buf)`](BufferHandler::new) and place it [`.at(&["output"],
//! ...)`](coap_handler_implementations::HandlerBuilder::at) some CoAP handler tree.
//!
//! ## Examples
//!
//! This crate comes with no example, but the
//! [coap-message-demos](https://crates.io/crates/coap-message-demos) crate makes ample use of it
//! for its logging adapter.
//!
//! ## Tools
//!
//! In the `tools/` directory of this crate there is a Python script, which should be useful for
//! speedy collection of a ring buffer server's output.
#![no_std]

use coap_message::{
    Code as _, MinimalWritableMessage, MutableWritableMessage, OptionNumber as _, ReadableMessage,
};
use coap_message_utils::Error;
use coap_numbers::{code, content_format, option};
use core::num::Wrapping;

/// CoAP handler for reading from a [Buffer](scroll_ring::Buffer)
///
/// This implements a variant of the
/// [proposed](https://forum.riot-os.org/t/coap-remote-shell/3340/5) stdio handling. It deviates in
/// some details, in particular:
///
/// * It uses CBOR streams rather than a response array, and no array pairs.
/// * It always sends offset and a single data item (effecively leaving it open whether the pattern
///   would be continued `[5, "hello ", 11, "world"]` or `[5, "hello ", "world"]`).
/// * GET always reports the oldest available data (that's probably underspecified in the original
///   proposal)
/// * GETs return immediately.
/// * FETCH only takes a single integer: where to start.
/// * No paired channel.
pub struct BufferHandler<'a, const N: usize>(&'a scroll_ring::Buffer<N>);

impl<'a, const N: usize> BufferHandler<'a, N> {
    /// Construct a handler for a buffer
    ///
    /// This adds nothing to the buffer other than the selection of the CoAP serialization; if it
    /// were not for separation of concerns, the [coap_handler] implementation could just as well
    /// be on the buffer itself.
    pub fn new(buffer: &'a scroll_ring::Buffer<N>) -> Self {
        Self(buffer)
    }
}

#[doc(hidden)]
pub enum RequestedScrollbufPosition {
    FromStart,
    StartingAt(Wrapping<u32>),
}

#[doc(hidden)]
#[derive(Debug)]
// This might be a candidate for inclusion in coap_handler_implementations.
pub enum BuildResponseError<SE: coap_message::error::RenderableOnMinimal + core::fmt::Debug> {
    Own(Error),
    Stack(SE),
}

impl<SE: coap_message::error::RenderableOnMinimal + core::fmt::Debug>
    coap_message::error::RenderableOnMinimal for BuildResponseError<SE>
{
    // We could try to come up with something more elaborate, but this is already an error type, so
    // it'd only be rendered as a double fault. If this were made available more generally, it
    // could try to be more precise here.
    type Error<IE: coap_message::error::RenderableOnMinimal + core::fmt::Debug> = Error;
    fn render<M: MinimalWritableMessage>(
        self,
        message: &mut M,
    ) -> Result<(), Self::Error<M::UnionError>> {
        match self {
            BuildResponseError::Stack(se) => se.render(message).map_err(|_| {
                coap_message_utils::Error::internal_server_error()
                    .with_title("Stack error rendering failed")
            })?,
            BuildResponseError::Own(e) => e.render(message).map_err(|_| {
                coap_message_utils::Error::internal_server_error()
                    .with_title("Own error rendering failed")
            })?,
        }
        Ok(())
    }
}

use RequestedScrollbufPosition::*;

impl<'a, const N: usize> coap_handler::Handler for BufferHandler<'a, N> {
    type RequestData = RequestedScrollbufPosition;

    type ExtractRequestError = Error;
    type BuildResponseError<M: MinimalWritableMessage> = BuildResponseError<M::UnionError>;

    fn extract_request_data<M: ReadableMessage>(
        &mut self,
        req: &M,
    ) -> Result<RequestedScrollbufPosition, Error> {
        use coap_message::MessageOption;
        use coap_message_utils::OptionsExt;

        req.options()
            .filter(|o| {
                !(o.number() == option::ACCEPT
                    && o.value_uint::<u16>()
                        == Some(content_format::from_str("application/cbor-seq").unwrap()))
            })
            // Not processing Content-Format: it's elective anyway
            .ignore_elective_others()?;

        Ok(match req.code().into() {
            code::GET => FromStart,
            code::FETCH => StartingAt(Wrapping({
                let payload = req.payload();
                // Quick and dirty parsing of a single CBOR uint up to 32bit
                let (start, bytes_consumed) = match payload.get(0) {
                    Some(i @ 0..=23) => (Some((*i).into()), 1),
                    Some(24) => (payload.get(1).map(|&n| n.into()), 2),
                    Some(25) => (
                        payload
                            .get(1..3)
                            .map(|b| u16::from_be_bytes(b.try_into().unwrap()))
                            .map(u32::from),
                        3,
                    ),
                    Some(26) => (
                        payload
                            .get(1..5)
                            .map(|b| u32::from_be_bytes(b.try_into().unwrap())),
                        5,
                    ),
                    Some(_) => {
                        return Err(
                            Error::bad_request_with_rbep(0).with_title("Expected cursor position")
                        )
                    }
                    None => return Err(Error::bad_request().with_title("Expected some CBOR")),
                };
                let Some(start) = start else {
                    return Err(Error::bad_request_with_rbep(payload.len())
                        .with_title("Integer out of range"));
                };
                if bytes_consumed != payload.len() {
                    return Err(Error::bad_request_with_rbep(bytes_consumed)
                        .with_title("Data after CBOR item"));
                }
                start
            })),
            _ => return Err(Error::method_not_allowed()),
        })
    }

    fn estimate_length(&mut self, _: &<Self as coap_handler::Handler>::RequestData) -> usize {
        1100
    }
    fn build_response<M: MutableWritableMessage>(
        &mut self,
        res: &mut M,
        mode: RequestedScrollbufPosition,
    ) -> Result<(), Self::BuildResponseError<M>> {
        res.set_code(M::Code::new(code::CONTENT).map_err(|e| BuildResponseError::Stack(e.into()))?);

        res.add_option_uint(
            M::OptionNumber::new(option::CONTENT_FORMAT)
                .map_err(|e| BuildResponseError::Stack(e.into()))?,
            content_format::from_str("application/cbor-seq").unwrap(),
        )
        .map_err(|e| BuildResponseError::Stack(e.into()))?;

        // N + 8 as in data_area. Using an upper bound is useful on stacks that don't provide one
        // of their own. (Those may be buggy because even CoAP-over-TCP should respect the CSM, but
        // there's nothing fundamentally limiting the size on the general transport).
        let len = core::cmp::min(res.available_space() - 1, N + 8);
        let msg_buf = res
            .payload_mut_with_len(len)
            .map_err(|e| BuildResponseError::Stack(e.into()))?;

        assert!(len > 8);
        msg_buf[0] = 0x1a; // u32; we'll get the cursor later
        msg_buf[5] = 0x59; // byte string of u16 length; we'll get the length later

        let data_area = &mut msg_buf[8..];

        let (cursor, bytes) = match mode {
            FromStart => match self.0.read_earliest(data_area) {
                Ok((cursor, bytes)) => (cursor, bytes),
                _ => {
                    // This is always "it's currently locked", as read_earliest can try to access
                    // lost data.
                    return Err(BuildResponseError::Own(
                        Error::service_unavailable()
                            .with_max_age(0)
                            .with_title("Buffer busy"),
                    ));
                }
            },
            StartingAt(n) => match self.0.read_from_cursor(n, data_area) {
                Ok(bytes) => (n, bytes),
                Err(scroll_ring::ReadErr::BufferUnavailable) => {
                    return Err(BuildResponseError::Own(
                        Error::service_unavailable()
                            .with_max_age(0)
                            .with_title("Buffer busy"),
                    ));
                }
                Err(scroll_ring::ReadErr::DataUnavailable) => {
                    // It's either not available any more or not available yet, and given the ring
                    // semantics that's kind of the same.
                    return Err(BuildResponseError::Own(
                        Error::bad_request().with_title("Data unavailable"),
                    ));
                }
            },
        };

        let bytes_written = core::cmp::min(bytes, data_area.len());
        msg_buf[1..5].copy_from_slice(&cursor.0.to_be_bytes());
        msg_buf[6..8].copy_from_slice(&(bytes_written as u16).to_be_bytes());
        res.truncate(8 + bytes_written)
            .map_err(|e| BuildResponseError::Stack(e.into()))?;

        Ok(())
    }
}

#[doc(hidden)]
pub struct BufferHandlerRecord(());

impl<'a, const N: usize> coap_handler::Reporting for BufferHandler<'a, N> {
    type Record<'b>
        = BufferHandlerRecord
    where
        Self: 'b;
    type Reporter<'b>
        = core::iter::Once<BufferHandlerRecord>
    where
        Self: 'b;
    fn report(&self) -> Self::Reporter<'_> {
        core::iter::once(BufferHandlerRecord(()))
    }
}

impl<'a> coap_handler::Record for BufferHandlerRecord {
    type PathElement = &'static &'static str;
    type PathElements = core::iter::Empty<&'static &'static str>;
    type Attributes = core::iter::Once<coap_handler::Attribute>;

    fn path(&self) -> Self::PathElements {
        core::iter::empty()
    }
    fn rel(&self) -> Option<&str> {
        None
    }
    fn attributes(&self) -> Self::Attributes {
        core::iter::once(coap_handler::Attribute::Interface(
            "tag:riot-os.org,2021:ser-out",
        ))
    }
}