agentics_domain/
storage.rs1use 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#[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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct StorageKey(String);
23
24impl StorageKey {
25 pub fn try_new(value: impl AsRef<str>) -> Result<Self> {
27 validate_storage_key(value.as_ref()).map(Self)
28 }
29
30 pub fn as_str(&self) -> &str {
32 &self.0
33 }
34
35 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}