use crate::ContainerError;
use strop_workspace::ContainerId;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ContainerIdentity {
pub id: String,
pub name: String,
pub image: String,
pub started_at: String,
pub user: String,
pub workdir: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerRef {
id: ContainerId,
started_at: String,
}
impl ContainerRef {
pub fn of(identity: &ContainerIdentity) -> Result<Self, ContainerError> {
let id =
ContainerId::canonical(identity.id.clone()).map_err(|_| ContainerError::Protocol {
detail: format!(
"inspect id {:?} is not the canonical 64-hex id",
identity.id
),
})?;
Ok(Self {
id,
started_at: identity.started_at.clone(),
})
}
pub fn id(&self) -> &ContainerId {
&self.id
}
pub fn started_at(&self) -> &str {
&self.started_at
}
pub(crate) fn incarnation(&self) -> String {
format!("{}@{}", self.id, self.started_at)
}
}
pub(crate) fn validate_name(name: &str) -> Result<(), ContainerError> {
let valid = !name.is_empty()
&& name.len() <= 255
&& name
.bytes()
.next()
.is_some_and(|b| b.is_ascii_alphanumeric())
&& name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b'-'));
if valid {
Ok(())
} else {
Err(ContainerError::PoisonedName {
name: name.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn identity(id: String) -> ContainerIdentity {
ContainerIdentity {
id,
name: "fixture".into(),
image: "busybox".into(),
started_at: "2026-09-10T08:00:00Z".into(),
user: String::new(),
workdir: String::new(),
}
}
#[test]
fn names_are_validated_before_the_engine_sees_them() {
assert!(validate_name("web").is_ok());
assert!(validate_name("web_1.2-alpine").is_ok());
assert!(validate_name(&"a".repeat(64)).is_ok(), "hex id");
assert!(validate_name("f00dbabe").is_ok(), "id prefix");
for bad in [
"", "-rf", "--format", "a b", "a/b", "a:b", ".hidden", "_lead", "é",
] {
assert!(
matches!(validate_name(bad), Err(ContainerError::PoisonedName { .. })),
"{bad:?} must be refused"
);
}
assert!(validate_name(&"a".repeat(256)).is_err(), "length bound");
}
#[test]
fn references_carry_only_canonical_ids() {
let reference = ContainerRef::of(&identity("a".repeat(64))).unwrap();
assert_eq!(reference.id().as_str(), &"a".repeat(64));
assert_eq!(reference.started_at(), "2026-09-10T08:00:00Z");
assert!(ContainerRef::of(&identity("web".into())).is_err());
assert!(ContainerRef::of(&identity("A".repeat(64))).is_err());
}
}