motorcortex-rust 0.5.0

Motorcortex Rust: a Rust client for the Motorcortex Core real-time control system (async + blocking).
Documentation
//! Protobuf (de)serialisation helpers shared by every driver command
//! that sends or receives a `Hash`-tagged message.
//!
//! Every Motorcortex wire payload is `<u32 LE type hash><prost-encoded
//! message>`. The helpers here enforce that framing on both sides.

use prost::Message;

use crate::error::{MotorcortexError, Result};
use crate::msg::{Hash, get_hash, get_hash_size};

/// Encode `msg` with its compile-time type hash prefixed in
/// little-endian. The server demuxes on the hash to route the reply.
pub(crate) fn encode_with_hash<M: Message + Hash>(msg: &M) -> Result<Vec<u8>> {
    let mut buffer = Vec::with_capacity(get_hash_size() + msg.encoded_len());
    buffer.extend(get_hash::<M>().to_le_bytes());
    msg.encode(&mut buffer)
        .map_err(|e| MotorcortexError::Encode(e.to_string()))?;
    Ok(buffer)
}

/// Decode a hash-tagged wire payload back into `T`. Errors if the
/// buffer is shorter than the hash prefix, the hash doesn't match
/// `T`, or the body is not a valid `T`.
pub(crate) fn decode_message<T: Message + Default + Hash>(bytes: &[u8]) -> Result<T> {
    let hash_size = get_hash_size();
    if bytes.len() < hash_size {
        return Err(MotorcortexError::Decode(
            "Invalid message length, hash missing".into(),
        ));
    }
    let provided = u32::from_le_bytes(
        bytes[..hash_size]
            .try_into()
            .map_err(|_| MotorcortexError::Decode("Failed to extract hash".into()))?,
    );
    if provided != get_hash::<T>() {
        return Err(MotorcortexError::Decode("Invalid message hash".into()));
    }
    T::decode(&bytes[hash_size..]).map_err(MotorcortexError::from)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::msg::{LoginMsg, StatusCode, StatusMsg};

    #[test]
    fn round_trips_a_status_msg() {
        let msg = StatusMsg {
            header: None,
            status: StatusCode::Ok as i32,
        };
        let wire = encode_with_hash(&msg).unwrap();
        let decoded: StatusMsg = decode_message(&wire).unwrap();
        assert_eq!(msg, decoded);
    }

    #[test]
    fn round_trips_a_login_msg_with_payload() {
        let msg = LoginMsg {
            header: None,
            login: "operator".into(),
            password: "secret".into(),
        };
        let wire = encode_with_hash(&msg).unwrap();
        assert!(wire.len() > get_hash_size(), "hash + body");
        let decoded: LoginMsg = decode_message(&wire).unwrap();
        assert_eq!(msg, decoded);
    }

    #[test]
    fn decode_rejects_input_shorter_than_hash() {
        let err = decode_message::<StatusMsg>(&[0, 1]).expect_err("< 4 bytes must fail");
        assert!(matches!(err, MotorcortexError::Decode(_)));
    }

    #[test]
    fn decode_rejects_wrong_hash() {
        let err = decode_message::<StatusMsg>(&[0xFF, 0xFF, 0xFF, 0xFF])
            .expect_err("bad hash must fail");
        assert!(matches!(err, MotorcortexError::Decode(_)));
    }

    #[test]
    fn decode_rejects_malformed_body() {
        // Correct hash, unparseable body.
        let mut buf = get_hash::<StatusMsg>().to_le_bytes().to_vec();
        buf.push(0xFF); // wiretype 7 → prost rejects
        let err = decode_message::<StatusMsg>(&buf).expect_err("bad body must fail");
        assert!(matches!(err, MotorcortexError::Decode(_)));
    }
}