use uuid::Uuid;
use crate::error::JacsError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentId {
pub id: Uuid,
pub version: Uuid,
}
impl AgentId {
pub fn new(id: Uuid, version: Uuid) -> Self {
Self { id, version }
}
#[must_use]
pub fn to_full_id(&self) -> String {
format!("{}:{}", self.id, self.version)
}
#[must_use]
pub fn id_str(&self) -> String {
self.id.to_string()
}
#[must_use]
pub fn version_str(&self) -> String {
self.version.to_string()
}
}
impl std::fmt::Display for AgentId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.id, self.version)
}
}
#[must_use = "validation result should be checked"]
pub fn validate_agent_id(id: &str) -> Result<AgentId, JacsError> {
let (id_str, version_str) = split_agent_id(id).ok_or_else(|| {
JacsError::ValidationError(format!(
"Agent ID must be in format 'UUID:VERSION_UUID', got: '{}'",
id
))
})?;
let agent_uuid = Uuid::parse_str(id_str).map_err(|e| {
JacsError::ValidationError(format!("Invalid agent UUID '{}': {}", id_str, e))
})?;
let version_uuid = Uuid::parse_str(version_str).map_err(|e| {
JacsError::ValidationError(format!("Invalid version UUID '{}': {}", version_str, e))
})?;
Ok(AgentId::new(agent_uuid, version_uuid))
}
#[must_use = "parsing result should be checked"]
pub fn parse_agent_id(id: &str) -> Result<(Uuid, Uuid), JacsError> {
let agent_id = validate_agent_id(id)?;
Ok((agent_id.id, agent_id.version))
}
#[must_use]
pub fn split_agent_id(input: &str) -> Option<(&str, &str)> {
if input.is_empty() || !input.contains(':') {
return None;
}
let mut parts = input.splitn(2, ':');
match (parts.next(), parts.next()) {
(Some(first), Some(second)) if !first.is_empty() && !second.is_empty() => {
Some((first, second))
}
_ => None,
}
}
#[must_use]
pub fn normalize_agent_id(id: &str) -> &str {
id.split(':').next().unwrap_or(id)
}
#[must_use]
pub fn is_valid_agent_id(id: &str) -> bool {
validate_agent_id(id).is_ok()
}
#[must_use]
pub fn are_valid_uuid_parts(id: &str, version: &str) -> bool {
Uuid::parse_str(id).is_ok() && Uuid::parse_str(version).is_ok()
}
#[must_use]
pub fn format_agent_id(id: &str, version: &str) -> String {
format!("{}:{}", id, version)
}
pub fn require_relative_path_safe(path: &str) -> Result<(), JacsError> {
let bytes = path.as_bytes();
if bytes.len() >= 2
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes.len() == 2 || bytes[2] == b'/' || bytes[2] == b'\\')
{
return Err(JacsError::ValidationError(format!(
"Path '{}' contains a Windows drive prefix and is not a safe relative path",
path
)));
}
for segment in path.split(['/', '\\']) {
if segment.is_empty() {
return Err(JacsError::ValidationError(format!(
"Path '{}' contains empty segment",
path
)));
}
if segment == "." {
return Err(JacsError::ValidationError(format!(
"Path '{}' contains current-directory segment",
path
)));
}
if segment == ".." {
return Err(JacsError::ValidationError(format!(
"Path '{}' contains parent-directory segment (path traversal)",
path
)));
}
if segment.contains('\0') {
return Err(JacsError::ValidationError(format!(
"Path '{}' contains null byte",
path
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const VALID_UUID_1: &str = "550e8400-e29b-41d4-a716-446655440000";
const VALID_UUID_2: &str = "550e8400-e29b-41d4-a716-446655440001";
#[test]
fn test_validate_agent_id_success() {
let id = format!("{}:{}", VALID_UUID_1, VALID_UUID_2);
let result = validate_agent_id(&id);
assert!(result.is_ok());
let agent_id = result.unwrap();
assert_eq!(agent_id.id.to_string(), VALID_UUID_1);
assert_eq!(agent_id.version.to_string(), VALID_UUID_2);
}
#[test]
fn test_validate_agent_id_no_colon() {
let result = validate_agent_id(VALID_UUID_1);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, JacsError::ValidationError(_)));
}
#[test]
fn test_validate_agent_id_invalid_uuid() {
let result = validate_agent_id("invalid:uuid");
assert!(result.is_err());
}
#[test]
fn test_validate_agent_id_empty() {
let result = validate_agent_id("");
assert!(result.is_err());
}
#[test]
fn test_validate_agent_id_only_colon() {
let result = validate_agent_id(":");
assert!(result.is_err());
}
#[test]
fn test_parse_agent_id() {
let id = format!("{}:{}", VALID_UUID_1, VALID_UUID_2);
let (agent, version) = parse_agent_id(&id).unwrap();
assert_eq!(agent.to_string(), VALID_UUID_1);
assert_eq!(version.to_string(), VALID_UUID_2);
}
#[test]
fn test_split_agent_id() {
assert_eq!(split_agent_id("abc:123"), Some(("abc", "123")));
assert_eq!(split_agent_id("abc:123:456"), Some(("abc", "123:456")));
assert_eq!(split_agent_id("no-colon"), None);
assert_eq!(split_agent_id(""), None);
assert_eq!(split_agent_id(":empty-first"), None);
assert_eq!(split_agent_id("empty-second:"), None);
}
#[test]
fn test_normalize_agent_id() {
assert_eq!(normalize_agent_id("abc-123:v1"), "abc-123");
assert_eq!(normalize_agent_id("abc-123:v1:extra"), "abc-123");
assert_eq!(normalize_agent_id("abc-123"), "abc-123");
assert_eq!(normalize_agent_id(""), "");
}
#[test]
fn test_is_valid_agent_id() {
let valid_id = format!("{}:{}", VALID_UUID_1, VALID_UUID_2);
assert!(is_valid_agent_id(&valid_id));
assert!(!is_valid_agent_id("invalid"));
assert!(!is_valid_agent_id(""));
}
#[test]
fn test_are_valid_uuid_parts() {
assert!(are_valid_uuid_parts(VALID_UUID_1, VALID_UUID_2));
assert!(!are_valid_uuid_parts("invalid", VALID_UUID_2));
assert!(!are_valid_uuid_parts(VALID_UUID_1, "invalid"));
}
#[test]
fn test_format_agent_id() {
assert_eq!(
format_agent_id(VALID_UUID_1, VALID_UUID_2),
format!("{}:{}", VALID_UUID_1, VALID_UUID_2)
);
}
#[test]
fn test_agent_id_display() {
let agent_id = AgentId::new(
Uuid::parse_str(VALID_UUID_1).unwrap(),
Uuid::parse_str(VALID_UUID_2).unwrap(),
);
assert_eq!(
agent_id.to_string(),
format!("{}:{}", VALID_UUID_1, VALID_UUID_2)
);
}
#[test]
fn test_agent_id_to_full_id() {
let agent_id = AgentId::new(
Uuid::parse_str(VALID_UUID_1).unwrap(),
Uuid::parse_str(VALID_UUID_2).unwrap(),
);
assert_eq!(
agent_id.to_full_id(),
format!("{}:{}", VALID_UUID_1, VALID_UUID_2)
);
}
}