Skip to main content

agentics_domain/
storage.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::path::{Component, Path};
4use std::str::FromStr;
5
6use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9pub type Result<T> = std::result::Result<T, StorageKeyError>;
10
11/// Storage-key parse failures before mapping to a storage backend error.
12#[derive(Debug, thiserror::Error)]
13pub enum StorageKeyError {
14    #[error(
15        "storage key must be a non-empty relative path with safe ASCII components and no `.` or `..` components"
16    )]
17    InvalidKey,
18}
19
20/// Opaque object key relative to the configured Agentics storage namespace.
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct StorageKey(String);
23
24impl StorageKey {
25    /// Parse and validate a storage-relative object key.
26    pub fn try_new(value: impl AsRef<str>) -> Result<Self> {
27        validate_storage_key(value.as_ref()).map(Self)
28    }
29
30    /// Borrow the storage key string.
31    pub fn as_str(&self) -> &str {
32        &self.0
33    }
34
35    /// Return the safe relative storage key as a path.
36    pub fn as_path(&self) -> &Path {
37        Path::new(&self.0)
38    }
39}
40
41impl fmt::Display for StorageKey {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(self.as_str())
44    }
45}
46
47impl FromStr for StorageKey {
48    type Err = StorageKeyError;
49
50    fn from_str(value: &str) -> Result<Self> {
51        Self::try_new(value)
52    }
53}
54
55impl From<StorageKeyError> for agentics_error::ServiceError {
56    fn from(error: StorageKeyError) -> Self {
57        agentics_error::ServiceError::BadRequest(error.to_string())
58    }
59}
60
61impl Serialize for StorageKey {
62    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
63    where
64        S: Serializer,
65    {
66        serializer.serialize_str(self.as_str())
67    }
68}
69
70impl<'de> Deserialize<'de> for StorageKey {
71    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
72    where
73        D: Deserializer<'de>,
74    {
75        let value = String::deserialize(deserializer)?;
76        Self::try_new(&value).map_err(serde::de::Error::custom)
77    }
78}
79
80impl JsonSchema for StorageKey {
81    fn inline_schema() -> bool {
82        true
83    }
84
85    fn schema_name() -> Cow<'static, str> {
86        "StorageKey".into()
87    }
88
89    fn json_schema(_: &mut SchemaGenerator) -> Schema {
90        json_schema!({
91            "type": "string",
92            "pattern": r"^(?!.*(?:^|/)\.{1,2}(?:/|$))[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*$"
93        })
94    }
95}
96
97fn validate_storage_key(value: &str) -> Result<String> {
98    if value.is_empty()
99        || value.trim() != value
100        || value.starts_with('/')
101        || value.ends_with('/')
102        || value.contains('\\')
103        || value
104            .bytes()
105            .any(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control())
106    {
107        return Err(StorageKeyError::InvalidKey);
108    }
109    let path = Path::new(value);
110    if path.is_absolute() {
111        return Err(StorageKeyError::InvalidKey);
112    }
113
114    let mut parts = Vec::new();
115    for component in path.components() {
116        match component {
117            Component::Normal(part) => {
118                let Some(part) = part.to_str() else {
119                    return Err(StorageKeyError::InvalidKey);
120                };
121                if part.is_empty()
122                    || !part.bytes().all(|byte| {
123                        byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')
124                    })
125                {
126                    return Err(StorageKeyError::InvalidKey);
127                }
128                parts.push(part);
129            }
130            _ => return Err(StorageKeyError::InvalidKey),
131        }
132    }
133    if parts.is_empty() || parts.join("/") != value {
134        return Err(StorageKeyError::InvalidKey);
135    }
136    Ok(value.to_string())
137}