use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::DaemonError;
pub const MAX_MESSAGE_BYTES: u32 = 8 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CachedArtifact {
pub image_ref: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WarmStatus {
AlreadyLocal,
FetchScheduled,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DaemonRequest {
Get { content_hash: String },
Put { content_hash: String, artifact: CachedArtifact },
Warm { content_hash: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DaemonResponse {
Get { artifact: Option<CachedArtifact> },
Put { ok: bool },
Warm { status: WarmStatus },
Error { message: String },
}
pub async fn write_message<W, T>(w: &mut W, message: &T) -> Result<(), DaemonError>
where
W: AsyncWrite + Unpin,
T: Serialize,
{
let body = serde_json::to_vec(message).map_err(|e| DaemonError::Protocol(e.to_string()))?;
let len: u32 = body
.len()
.try_into()
.map_err(|_| DaemonError::Protocol("message body too large to frame".to_string()))?;
if len > MAX_MESSAGE_BYTES {
return Err(DaemonError::Protocol(format!(
"message body {len} bytes exceeds cap {MAX_MESSAGE_BYTES}"
)));
}
w.write_all(&len.to_le_bytes()).await?;
w.write_all(&body).await?;
w.flush().await?;
Ok(())
}
pub async fn read_message<R, T>(r: &mut R) -> Result<T, DaemonError>
where
R: AsyncRead + Unpin,
T: DeserializeOwned,
{
let mut len_buf = [0u8; 4];
r.read_exact(&mut len_buf).await?;
let len = u32::from_le_bytes(len_buf);
if len > MAX_MESSAGE_BYTES {
return Err(DaemonError::Protocol(format!(
"declared message length {len} exceeds cap {MAX_MESSAGE_BYTES}"
)));
}
let mut body = vec![0u8; len as usize];
r.read_exact(&mut body).await?;
serde_json::from_slice(&body).map_err(|e| DaemonError::Protocol(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::duplex;
#[tokio::test]
async fn request_round_trips_over_a_duplex_pipe() {
let (mut a, mut b) = duplex(4096);
let req = DaemonRequest::Get { content_hash: "abc123".to_string() };
write_message(&mut a, &req).await.unwrap();
let got: DaemonRequest = read_message(&mut b).await.unwrap();
assert_eq!(got, req);
}
#[tokio::test]
async fn response_round_trips_over_a_duplex_pipe() {
let (mut a, mut b) = duplex(4096);
let resp = DaemonResponse::Get {
artifact: Some(CachedArtifact { image_ref: "example/image:cached".to_string() }),
};
write_message(&mut a, &resp).await.unwrap();
let got: DaemonResponse = read_message(&mut b).await.unwrap();
assert_eq!(got, resp);
}
#[test]
fn cached_artifact_json_roundtrip() {
let artifact = CachedArtifact { image_ref: "example/image:v1".to_string() };
let json = serde_json::to_string(&artifact).unwrap();
let parsed: CachedArtifact = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, artifact);
}
}