use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub image_id: Option<u32>,
pub image_number: Option<u32>,
pub placement_id: Option<u32>,
pub success: bool,
pub error: Option<String>,
}
impl Response {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < 6 {
return Err(Error::InvalidResponse(
String::from_utf8_lossy(data).into_owned(),
));
}
if data[0] != crate::ESC || data[1] != b'_' || data[2] != b'G' {
return Err(Error::InvalidResponse(
String::from_utf8_lossy(data).into_owned(),
));
}
let semicolon_pos = data
.iter()
.position(|&b| b == b';')
.ok_or_else(|| Error::InvalidResponse(String::from_utf8_lossy(data).into_owned()))?;
let control = &data[3..semicolon_pos];
let control_str = std::str::from_utf8(control).map_err(Error::from)?;
let end_pos = data
.iter()
.rposition(|&b| b == crate::ESC)
.ok_or_else(|| Error::InvalidResponse(String::from_utf8_lossy(data).into_owned()))?;
let message = &data[semicolon_pos + 1..end_pos];
let message_str = std::str::from_utf8(message).map_err(Error::from)?;
let mut image_id = None;
let mut image_number = None;
let mut placement_id = None;
for part in control_str.split(',') {
let parts: Vec<&str> = part.splitn(2, '=').collect();
if parts.len() == 2 {
match parts[0] {
"i" => image_id = parts[1].parse().ok(),
"I" => image_number = parts[1].parse().ok(),
"p" => placement_id = parts[1].parse().ok(),
_ => {}
}
}
}
let (success, error) = if message_str == "OK" {
(true, None)
} else if let Some(err_msg) = message_str.strip_prefix("ENOENT:") {
(false, Some(format!("Not found: {err_msg}")))
} else if let Some(err_msg) = message_str.strip_prefix("EINVAL:") {
(false, Some(format!("Invalid argument: {err_msg}")))
} else if let Some(err_msg) = message_str.strip_prefix("EIO:") {
(false, Some(format!("IO error: {err_msg}")))
} else if let Some(err_msg) = message_str.strip_prefix("ETOODEEP:") {
(false, Some(format!("Chain too deep: {err_msg}")))
} else if let Some(err_msg) = message_str.strip_prefix("ECYCLE:") {
(false, Some(format!("Cycle detected: {err_msg}")))
} else if let Some(err_msg) = message_str.strip_prefix("ENOPARENT:") {
(false, Some(format!("Parent not found: {err_msg}")))
} else {
(false, Some(message_str.to_string()))
};
Ok(Response {
image_id,
image_number,
placement_id,
success,
error,
})
}
pub fn is_ok(&self) -> bool {
self.success
}
pub fn is_error(&self) -> bool {
!self.success
}
pub fn error_message(&self) -> Option<&str> {
self.error.as_deref()
}
}
impl std::fmt::Display for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.success {
write!(f, "OK")?;
if let Some(id) = self.image_id {
write!(f, " (image_id={}", id)?;
if let Some(pid) = self.placement_id {
write!(f, ", placement_id={}", pid)?;
}
write!(f, ")")?;
}
} else if let Some(err) = &self.error {
write!(f, "ERROR: {}", err)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCode {
NotFound,
InvalidArgument,
IoError,
TooDeep,
Cycle,
NoParent,
Unknown,
}
impl ErrorCode {
pub fn from_message(msg: &str) -> Self {
if msg.starts_with("ENOENT") {
Self::NotFound
} else if msg.starts_with("EINVAL") {
Self::InvalidArgument
} else if msg.starts_with("EIO") {
Self::IoError
} else if msg.starts_with("ETOODEEP") {
Self::TooDeep
} else if msg.starts_with("ECYCLE") {
Self::Cycle
} else if msg.starts_with("ENOPARENT") {
Self::NoParent
} else {
Self::Unknown
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ok_response() {
let data = b"\x1b_Gi=42;OK\x1b\\";
let resp = Response::parse(data).unwrap();
assert!(resp.is_ok());
assert_eq!(resp.image_id, Some(42));
}
#[test]
fn test_parse_ok_response_with_placement() {
let data = b"\x1b_Gi=42,p=7;OK\x1b\\";
let resp = Response::parse(data).unwrap();
assert!(resp.is_ok());
assert_eq!(resp.image_id, Some(42));
assert_eq!(resp.placement_id, Some(7));
}
#[test]
fn test_parse_error_response() {
let data = b"\x1b_Gi=42;ENOENT:Image not found\x1b\\";
let resp = Response::parse(data).unwrap();
assert!(resp.is_error());
assert_eq!(resp.image_id, Some(42));
assert!(resp.error.unwrap().contains("Not found"));
}
#[test]
fn test_parse_response_with_image_number() {
let data = b"\x1b_Gi=99,I=13;OK\x1b\\";
let resp = Response::parse(data).unwrap();
assert!(resp.is_ok());
assert_eq!(resp.image_id, Some(99));
assert_eq!(resp.image_number, Some(13));
}
}