enigma-packet 0.1.0

Canonical message packet format + serialization for Enigma secure messaging
Documentation
use crate::error::{EnigmaPacketError, Result};
use crate::types::{AttachmentKind, AttachmentMeta, Message, MessageMeta, MessageType};

pub const MAX_PARTICIPANT_LEN: usize = 64;

pub fn validate_message(message: &Message) -> Result<()> {
    validate_participants(message)?;
    if message.timestamp_ms == 0 {
        return Err(EnigmaPacketError::ValidationError(
            "timestamp must be positive".into(),
        ));
    }
    match message.msg_type {
        MessageType::Text => validate_text_message(message),
        MessageType::Voice
        | MessageType::Image
        | MessageType::Video
        | MessageType::File
        | MessageType::CallOffer
        | MessageType::CallAnswer
        | MessageType::CallHangup
        | MessageType::GroupInvite => validate_basic_payload(message),
        MessageType::AttachmentInit => validate_attachment_init(message),
        MessageType::AttachmentChunk => validate_attachment_chunk(message),
        MessageType::AttachmentEnd => validate_attachment_end(message),
        MessageType::AttachmentAbort => validate_attachment_abort(message),
        MessageType::AttachmentAck => validate_attachment_ack(message),
    }
}

fn validate_participants(message: &Message) -> Result<()> {
    if message.sender.is_empty() || message.sender.chars().count() > MAX_PARTICIPANT_LEN {
        return Err(EnigmaPacketError::ValidationError("invalid sender".into()));
    }
    if message.receiver.is_empty() || message.receiver.chars().count() > MAX_PARTICIPANT_LEN {
        return Err(EnigmaPacketError::ValidationError(
            "invalid receiver".into(),
        ));
    }
    Ok(())
}

fn validate_text_message(message: &Message) -> Result<()> {
    if std::str::from_utf8(&message.payload).is_err() {
        return Err(EnigmaPacketError::ValidationError(
            "text payload must be utf-8".into(),
        ));
    }
    match &message.meta {
        MessageMeta::None | MessageMeta::Basic { .. } => Ok(()),
        _ => Err(EnigmaPacketError::ValidationError(
            "invalid metadata for text".into(),
        )),
    }
}

fn validate_basic_payload(message: &Message) -> Result<()> {
    match &message.meta {
        MessageMeta::None | MessageMeta::Basic { .. } => Ok(()),
        _ => Err(EnigmaPacketError::ValidationError(
            "invalid metadata for inline message".into(),
        )),
    }
}

fn validate_attachment_init(message: &Message) -> Result<()> {
    if !message.payload.is_empty() {
        return Err(EnigmaPacketError::ValidationError(
            "attachment init payload must be empty".into(),
        ));
    }
    let meta = match &message.meta {
        MessageMeta::AttachmentInit(meta) => meta,
        _ => {
            return Err(EnigmaPacketError::ValidationError(
                "attachment init metadata missing".into(),
            ))
        }
    };
    validate_attachment_meta(meta)
}

fn validate_attachment_meta(meta: &AttachmentMeta) -> Result<()> {
    if meta.total_size == 0 {
        return Err(EnigmaPacketError::ValidationError(
            "total size must be positive".into(),
        ));
    }
    if meta.chunk_size == 0 {
        return Err(EnigmaPacketError::ValidationError(
            "chunk size must be positive".into(),
        ));
    }
    if meta.chunk_count == 0 {
        return Err(EnigmaPacketError::ValidationError(
            "chunk count must be positive".into(),
        ));
    }
    if let Some(created) = meta.created_at_ms {
        if created == 0 {
            return Err(EnigmaPacketError::ValidationError(
                "created_at must be positive".into(),
            ));
        }
    }
    if matches!(meta.kind, AttachmentKind::File) {
        match &meta.filename {
            Some(name) if !name.is_empty() => {}
            _ => {
                return Err(EnigmaPacketError::ValidationError(
                    "filename required for file attachments".into(),
                ))
            }
        }
    }
    let chunk_size = meta.chunk_size as u64;
    let expected = ceil_div(meta.total_size, chunk_size);
    if expected != meta.chunk_count as u64 {
        return Err(EnigmaPacketError::ValidationError(
            "chunk count mismatch".into(),
        ));
    }
    Ok(())
}

fn ceil_div(total: u64, chunk: u64) -> u64 {
    let mut count = total / chunk;
    if total % chunk != 0 {
        count += 1;
    }
    count
}

fn validate_attachment_chunk(message: &Message) -> Result<()> {
    if message.payload.is_empty() {
        return Err(EnigmaPacketError::ValidationError(
            "attachment chunk payload must not be empty".into(),
        ));
    }
    let meta = match &message.meta {
        MessageMeta::AttachmentChunk(meta) => meta,
        _ => {
            return Err(EnigmaPacketError::ValidationError(
                "attachment chunk metadata missing".into(),
            ))
        }
    };
    if meta.chunk_size == 0 {
        return Err(EnigmaPacketError::ValidationError(
            "chunk size must be positive".into(),
        ));
    }
    let chunk_size = meta.chunk_size as u64;
    let expected_offset = (meta.index as u64)
        .checked_mul(chunk_size)
        .ok_or_else(|| EnigmaPacketError::ValidationError("chunk offset overflow".into()))?;
    if meta.offset != expected_offset {
        return Err(EnigmaPacketError::ValidationError(
            "chunk offset mismatch".into(),
        ));
    }
    if message.payload.len() as u64 > chunk_size {
        return Err(EnigmaPacketError::ValidationError(
            "chunk payload larger than declared chunk size".into(),
        ));
    }
    if let Some(total) = meta.total_size {
        if total == 0 {
            return Err(EnigmaPacketError::ValidationError(
                "chunk total size must be positive".into(),
            ));
        }
        if meta.offset > total {
            return Err(EnigmaPacketError::ValidationError(
                "chunk offset beyond total size".into(),
            ));
        }
        let end = meta
            .offset
            .checked_add(message.payload.len() as u64)
            .ok_or_else(|| EnigmaPacketError::ValidationError("chunk exceeds total size".into()))?;
        if end > total {
            return Err(EnigmaPacketError::ValidationError(
                "chunk exceeds total size".into(),
            ));
        }
    }
    Ok(())
}

fn validate_attachment_end(message: &Message) -> Result<()> {
    if !message.payload.is_empty() {
        return Err(EnigmaPacketError::ValidationError(
            "attachment end payload must be empty".into(),
        ));
    }
    match &message.meta {
        MessageMeta::AttachmentEnd {
            attachment_id: _,
            sha256: _,
            total_size,
            chunk_count,
        } => {
            if *total_size == 0 || *chunk_count == 0 {
                return Err(EnigmaPacketError::ValidationError(
                    "attachment end totals must be positive".into(),
                ));
            }
            Ok(())
        }
        _ => Err(EnigmaPacketError::ValidationError(
            "attachment end metadata missing".into(),
        )),
    }
}

fn validate_attachment_abort(message: &Message) -> Result<()> {
    if !message.payload.is_empty() {
        return Err(EnigmaPacketError::ValidationError(
            "attachment abort payload must be empty".into(),
        ));
    }
    match &message.meta {
        MessageMeta::AttachmentAbort { .. } => Ok(()),
        _ => Err(EnigmaPacketError::ValidationError(
            "attachment abort metadata missing".into(),
        )),
    }
}

fn validate_attachment_ack(message: &Message) -> Result<()> {
    if !message.payload.is_empty() {
        return Err(EnigmaPacketError::ValidationError(
            "attachment ack payload must be empty".into(),
        ));
    }
    match &message.meta {
        MessageMeta::AttachmentAck(_) => Ok(()),
        _ => Err(EnigmaPacketError::ValidationError(
            "attachment ack metadata missing".into(),
        )),
    }
}