use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{PwrError, Result};
pub const PROTOCOL_VERSION: u8 = 0x01;
pub const CHUNK_SIZE: usize = 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum MessageType {
Handshake = 0x01,
HandshakeAck = 0x02,
ArchiveRequest = 0x10,
ArchiveAccept = 0x11,
ArchiveComplete = 0x12,
RestoreRequest = 0x20,
RestoreAccept = 0x21,
RestoreComplete = 0x22,
FileHeader = 0x30,
FileEnd = 0x31,
StatusRequest = 0x40,
StatusResponse = 0x41,
Error = 0xFF,
}
impl MessageType {
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0x01 => Some(Self::Handshake),
0x02 => Some(Self::HandshakeAck),
0x10 => Some(Self::ArchiveRequest),
0x11 => Some(Self::ArchiveAccept),
0x12 => Some(Self::ArchiveComplete),
0x20 => Some(Self::RestoreRequest),
0x21 => Some(Self::RestoreAccept),
0x22 => Some(Self::RestoreComplete),
0x30 => Some(Self::FileHeader),
0x31 => Some(Self::FileEnd),
0x40 => Some(Self::StatusRequest),
0x41 => Some(Self::StatusResponse),
0xFF => Some(Self::Error),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
Handshake(Handshake),
ArchiveRequest(ArchiveRequest),
ArchiveComplete(ArchiveComplete),
RestoreRequest(RestoreRequest),
StatusRequest(StatusRequest),
}
impl ClientMessage {
pub fn message_type(&self) -> MessageType {
match self {
Self::Handshake(_) => MessageType::Handshake,
Self::ArchiveRequest(_) => MessageType::ArchiveRequest,
Self::ArchiveComplete(_) => MessageType::ArchiveComplete,
Self::RestoreRequest(_) => MessageType::RestoreRequest,
Self::StatusRequest(_) => MessageType::StatusRequest,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerMessage {
HandshakeAck(HandshakeAck),
ArchiveAccept(ArchiveAccept),
RestoreAccept(RestoreAccept),
RestoreComplete(RestoreComplete),
StatusResponse(StatusResponse),
Error(ErrorMessage),
}
impl ServerMessage {
pub fn message_type(&self) -> MessageType {
match self {
Self::HandshakeAck(_) => MessageType::HandshakeAck,
Self::ArchiveAccept(_) => MessageType::ArchiveAccept,
Self::RestoreAccept(_) => MessageType::RestoreAccept,
Self::RestoreComplete(_) => MessageType::RestoreComplete,
Self::StatusResponse(_) => MessageType::StatusResponse,
Self::Error(_) => MessageType::Error,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Handshake {
pub version: u8,
pub client_id: String,
pub nonce: [u8; 32],
pub proof: [u8; 32],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandshakeAck {
pub success: bool,
pub server_version: String,
pub server_nonce: [u8; 32],
pub server_proof: [u8; 32],
#[serde(default)]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveRequest {
pub project_uuid: Uuid,
pub project_name: String,
pub total_size: u64,
pub file_count: u32,
pub compression: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveAccept {
pub session_id: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveComplete {
pub success: bool,
pub total_size: u64,
pub archive_hash: String,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreRequest {
pub project_uuid: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreAccept {
pub session_id: Uuid,
pub total_size: u64,
pub file_count: u32,
pub archive_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreComplete {
pub success: bool,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileHeader {
pub rel_path: String,
pub size: u64,
pub mode: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileEnd {
pub checksum: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusRequest {
#[serde(default)]
pub project_uuid: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusResponse {
pub projects: Vec<ProjectInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectInfo {
pub uuid: Uuid,
pub name: String,
pub size_bytes: u64,
pub file_count: u32,
pub created_at: DateTime<Utc>,
pub last_modified: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorMessage {
pub code: u32,
pub message: String,
}
pub fn decode_client_message(msg_type: MessageType, payload: &[u8]) -> Result<ClientMessage> {
match msg_type {
MessageType::Handshake => {
let m: Handshake = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad Handshake: {}", e)))?;
Ok(ClientMessage::Handshake(m))
}
MessageType::ArchiveRequest => {
let m: ArchiveRequest = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad ArchiveRequest: {}", e)))?;
Ok(ClientMessage::ArchiveRequest(m))
}
MessageType::ArchiveComplete => {
let m: ArchiveComplete = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad ArchiveComplete: {}", e)))?;
Ok(ClientMessage::ArchiveComplete(m))
}
MessageType::RestoreRequest => {
let m: RestoreRequest = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad RestoreRequest: {}", e)))?;
Ok(ClientMessage::RestoreRequest(m))
}
MessageType::StatusRequest => {
let m: StatusRequest = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad StatusRequest: {}", e)))?;
Ok(ClientMessage::StatusRequest(m))
}
other => Err(PwrError::Protocol(format!(
"Expected client message, got server message type {:?}", other
))),
}
}
pub fn decode_server_message(msg_type: MessageType, payload: &[u8]) -> Result<ServerMessage> {
match msg_type {
MessageType::HandshakeAck => {
let m: HandshakeAck = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad HandshakeAck: {}", e)))?;
Ok(ServerMessage::HandshakeAck(m))
}
MessageType::ArchiveAccept => {
let m: ArchiveAccept = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad ArchiveAccept: {}", e)))?;
Ok(ServerMessage::ArchiveAccept(m))
}
MessageType::RestoreAccept => {
let m: RestoreAccept = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad RestoreAccept: {}", e)))?;
Ok(ServerMessage::RestoreAccept(m))
}
MessageType::RestoreComplete => {
let m: RestoreComplete = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad RestoreComplete: {}", e)))?;
Ok(ServerMessage::RestoreComplete(m))
}
MessageType::StatusResponse => {
let m: StatusResponse = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad StatusResponse: {}", e)))?;
Ok(ServerMessage::StatusResponse(m))
}
MessageType::Error => {
let m: ErrorMessage = serde_json::from_slice(payload)
.map_err(|e| PwrError::Framing(format!("bad ErrorMessage: {}", e)))?;
Ok(ServerMessage::Error(m))
}
other => Err(PwrError::Protocol(format!(
"Expected server message, got client message type {:?}", other
))),
}
}
pub fn build_archive_request(
uuid: Uuid,
name: &str,
total_size: u64,
file_count: u32,
compression: bool,
) -> ClientMessage {
ClientMessage::ArchiveRequest(ArchiveRequest {
project_uuid: uuid,
project_name: name.to_string(),
total_size,
file_count,
compression,
})
}
pub fn build_archive_complete(total_size: u64, archive_hash: &str) -> ClientMessage {
ClientMessage::ArchiveComplete(ArchiveComplete {
success: true,
total_size,
archive_hash: archive_hash.to_string(),
error: None,
})
}
pub fn build_archive_failed(error: &str) -> ClientMessage {
ClientMessage::ArchiveComplete(ArchiveComplete {
success: false,
total_size: 0,
archive_hash: String::new(),
error: Some(error.to_string()),
})
}
pub fn build_restore_request(uuid: Uuid) -> ClientMessage {
ClientMessage::RestoreRequest(RestoreRequest {
project_uuid: uuid,
})
}
pub fn build_restore_accept(
session_id: Uuid,
total_size: u64,
file_count: u32,
archive_hash: &str,
) -> ServerMessage {
ServerMessage::RestoreAccept(RestoreAccept {
session_id,
total_size,
file_count,
archive_hash: archive_hash.to_string(),
})
}
pub fn build_restore_complete() -> ServerMessage {
ServerMessage::RestoreComplete(RestoreComplete {
success: true,
error: None,
})
}
pub fn build_restore_failed(error: &str) -> ServerMessage {
ServerMessage::RestoreComplete(RestoreComplete {
success: false,
error: Some(error.to_string()),
})
}
pub fn build_error(code: u32, message: &str) -> ServerMessage {
ServerMessage::Error(ErrorMessage {
code,
message: message.to_string(),
})
}
pub fn build_archive_accept(session_id: Uuid) -> ServerMessage {
ServerMessage::ArchiveAccept(ArchiveAccept { session_id })
}
pub fn build_handshake_ack_success(
server_version: &str,
server_nonce: [u8; 32],
server_proof: [u8; 32],
) -> ServerMessage {
ServerMessage::HandshakeAck(HandshakeAck {
success: true,
server_version: server_version.to_string(),
server_nonce,
server_proof,
reason: None,
})
}
pub fn build_handshake_ack_failed(reason: &str) -> ServerMessage {
ServerMessage::HandshakeAck(HandshakeAck {
success: false,
server_version: String::new(),
server_nonce: [0u8; 32],
server_proof: [0u8; 32],
reason: Some(reason.to_string()),
})
}
pub fn build_status_response(projects: Vec<ProjectInfo>) -> ServerMessage {
ServerMessage::StatusResponse(StatusResponse { projects })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_type_round_trip() {
let variants = [
MessageType::Handshake,
MessageType::HandshakeAck,
MessageType::ArchiveRequest,
MessageType::ArchiveAccept,
MessageType::ArchiveComplete,
MessageType::RestoreRequest,
MessageType::RestoreAccept,
MessageType::RestoreComplete,
MessageType::FileHeader,
MessageType::FileEnd,
MessageType::StatusRequest,
MessageType::StatusResponse,
MessageType::Error,
];
for v in &variants {
let byte = *v as u8;
let decoded = MessageType::from_byte(byte);
assert_eq!(decoded, Some(*v));
}
}
#[test]
fn test_invalid_message_type() {
assert_eq!(MessageType::from_byte(0x00), None);
assert_eq!(MessageType::from_byte(0x99), None);
}
#[test]
fn test_archive_request_serialization() {
let req = ArchiveRequest {
project_uuid: Uuid::new_v4(),
project_name: "testproj".into(),
total_size: 1_048_576,
file_count: 42,
compression: true,
};
let encoded = serde_json::to_vec(&req).unwrap();
let decoded: ArchiveRequest = serde_json::from_slice(&encoded).unwrap();
assert_eq!(decoded.project_name, "testproj");
assert_eq!(decoded.file_count, 42);
assert!(decoded.compression);
}
#[test]
fn test_client_message_round_trip() {
let msg = ClientMessage::ArchiveRequest(ArchiveRequest {
project_uuid: Uuid::new_v4(),
project_name: "roundtrip".into(),
total_size: 5000,
file_count: 10,
compression: false,
});
let encoded = serde_json::to_vec(&msg).unwrap();
let decoded: ClientMessage = serde_json::from_slice(&encoded).unwrap();
match decoded {
ClientMessage::ArchiveRequest(req) => {
assert_eq!(req.project_name, "roundtrip");
}
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_server_message_round_trip() {
let msg = ServerMessage::Error(ErrorMessage {
code: 3,
message: "project not found".into(),
});
let encoded = serde_json::to_vec(&msg).unwrap();
let decoded: ServerMessage = serde_json::from_slice(&encoded).unwrap();
match decoded {
ServerMessage::Error(e) => {
assert_eq!(e.code, 3);
}
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_decode_client_message_wrong_type() {
let payload = serde_json::to_vec(&ErrorMessage { code: 1, message: "err".into() }).unwrap();
let result = decode_client_message(MessageType::Error, &payload);
assert!(result.is_err());
}
#[test]
fn test_decode_server_message_wrong_type() {
let payload = serde_json::to_vec(&Handshake {
version: 1,
client_id: "x".into(),
nonce: [0; 32],
proof: [0; 32],
}).unwrap();
let result = decode_server_message(MessageType::Handshake, &payload);
assert!(result.is_err());
}
#[test]
fn test_project_info_timestamps() {
let info = ProjectInfo {
uuid: Uuid::new_v4(),
name: "test".into(),
size_bytes: 1000,
file_count: 10,
created_at: Utc::now(),
last_modified: Utc::now(),
};
let encoded = serde_json::to_vec(&info).unwrap();
let decoded: ProjectInfo = serde_json::from_slice(&encoded).unwrap();
assert_eq!(decoded.name, "test");
assert_eq!(decoded.size_bytes, 1000);
}
#[test]
fn test_client_message_type_mapping() {
let msg = ClientMessage::Handshake(Handshake {
version: 1,
client_id: "test".into(),
nonce: [0; 32],
proof: [0; 32],
});
assert_eq!(msg.message_type(), MessageType::Handshake);
let msg = ClientMessage::StatusRequest(StatusRequest { project_uuid: None });
assert_eq!(msg.message_type(), MessageType::StatusRequest);
}
#[test]
fn test_server_message_type_mapping() {
let msg = ServerMessage::HandshakeAck(HandshakeAck {
success: true,
server_version: "0.1.0".into(),
server_nonce: [0; 32],
server_proof: [0; 32],
reason: None,
});
assert_eq!(msg.message_type(), MessageType::HandshakeAck);
let msg = ServerMessage::Error(ErrorMessage { code: 1, message: "oops".into() });
assert_eq!(msg.message_type(), MessageType::Error);
}
}