lxmf-wire 0.2.0

Core LXMF wire format, message primitives, and identity helpers for LXMF-rs.
Documentation
use crate::message::Message;
use crate::LxmfError;
use alloc::string::String;
use alloc::vec::Vec;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InboundPayloadMode {
    FullWire,
    DestinationStripped,
}

#[derive(Debug, Clone)]
pub struct DecodedInboundMessage {
    pub id: String,
    pub source: [u8; 16],
    pub destination: [u8; 16],
    pub title: Vec<u8>,
    pub content: Vec<u8>,
    pub timestamp_f64: f64,
    pub fields: Option<rmpv::Value>,
}

pub fn decode_inbound_message(
    fallback_destination: [u8; 16],
    payload: &[u8],
    mode: InboundPayloadMode,
) -> Result<DecodedInboundMessage, LxmfError> {
    let wire = match mode {
        InboundPayloadMode::FullWire => payload.to_vec(),
        InboundPayloadMode::DestinationStripped => {
            let mut with_destination_prefix = Vec::with_capacity(16 + payload.len());
            with_destination_prefix.extend_from_slice(&fallback_destination);
            with_destination_prefix.extend_from_slice(payload);
            with_destination_prefix
        }
    };

    let message = Message::from_wire(&wire)?;
    let source = message.source_hash.unwrap_or([0u8; 16]);
    let destination = message.destination_hash.unwrap_or(fallback_destination);
    let id = wire_message_id_hex(&wire).unwrap_or_else(|| hex::encode(destination));
    Ok(DecodedInboundMessage {
        id,
        source,
        destination,
        title: message.title,
        content: message.content,
        timestamp_f64: message.timestamp.unwrap_or(0.0),
        fields: message.fields,
    })
}

fn wire_message_id_hex(candidate: &[u8]) -> Option<String> {
    const SIGNATURE_LEN: usize = 64;
    const HEADER_LEN: usize = 16 + 16 + SIGNATURE_LEN;
    if candidate.len() <= HEADER_LEN {
        return None;
    }
    let mut destination = [0u8; 16];
    destination.copy_from_slice(&candidate[..16]);
    let mut source = [0u8; 16];
    source.copy_from_slice(&candidate[16..32]);
    let payload_value = rmp_serde::from_slice::<rmpv::Value>(&candidate[HEADER_LEN..]).ok()?;
    let rmpv::Value::Array(items) = payload_value else {
        return None;
    };
    let payload_without_stamp = payload_without_stamp_bytes(&items)?;
    Some(compute_message_id_hex(destination, source, &payload_without_stamp))
}

fn payload_without_stamp_bytes(items: &[rmpv::Value]) -> Option<Vec<u8>> {
    if items.len() < 4 || items.len() > 5 {
        return None;
    }
    let mut trimmed = items.to_vec();
    if trimmed.len() == 5 {
        trimmed.pop();
    }
    rmp_serde::to_vec(&rmpv::Value::Array(trimmed)).ok()
}

fn compute_message_id_hex(
    destination: [u8; 16],
    source: [u8; 16],
    payload_without_stamp: &[u8],
) -> String {
    use sha2::Digest as _;
    let mut hasher = sha2::Sha256::new();
    hasher.update(destination);
    hasher.update(source);
    hasher.update(payload_without_stamp);
    hex::encode(hasher.finalize())
}