clnrm_core/backend/
volume.rs1use crate::config::VolumeConfig;
6use crate::error::{CleanroomError, Result};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone)]
11pub struct VolumeMount {
12 host_path: PathBuf,
14 container_path: PathBuf,
16 read_only: bool,
18}
19
20impl VolumeMount {
21 pub fn new(
47 host_path: impl AsRef<Path>,
48 container_path: impl AsRef<Path>,
49 read_only: bool,
50 ) -> Result<Self> {
51 let host_path = host_path.as_ref();
52 let container_path = container_path.as_ref();
53
54 if !host_path.is_absolute() {
56 return Err(CleanroomError::validation_error(format!(
57 "Host path must be absolute: {}",
58 host_path.display()
59 )));
60 }
61
62 if !host_path.exists() {
64 return Err(CleanroomError::validation_error(format!(
65 "Host path does not exist: {}",
66 host_path.display()
67 )));
68 }
69
70 let host_path = host_path.canonicalize().map_err(|e| {
72 CleanroomError::validation_error(format!(
73 "Failed to canonicalize host path {}: {}",
74 host_path.display(),
75 e
76 ))
77 })?;
78
79 if !container_path.is_absolute() {
81 return Err(CleanroomError::validation_error(format!(
82 "Container path must be absolute: {}",
83 container_path.display()
84 )));
85 }
86
87 Ok(Self {
88 host_path,
89 container_path: container_path.to_path_buf(),
90 read_only,
91 })
92 }
93
94 pub fn from_config(config: &VolumeConfig) -> Result<Self> {
96 let read_only = config.read_only.unwrap_or(false);
97 Self::new(&config.host_path, &config.container_path, read_only)
98 }
99
100 pub fn host_path(&self) -> &Path {
102 &self.host_path
103 }
104
105 pub fn container_path(&self) -> &Path {
107 &self.container_path
108 }
109
110 pub fn is_read_only(&self) -> bool {
112 self.read_only
113 }
114}
115
116#[derive(Debug, Clone)]
118pub struct VolumeValidator {
119 whitelist: Vec<PathBuf>,
121}
122
123impl VolumeValidator {
124 pub fn new(whitelist: Vec<PathBuf>) -> Self {
138 Self { whitelist }
139 }
140
141 pub fn validate(&self, mount: &VolumeMount) -> Result<()> {
147 if self.whitelist.is_empty() {
149 return Ok(());
150 }
151
152 let host_path = mount.host_path();
153
154 for allowed in &self.whitelist {
156 if host_path.starts_with(allowed) {
157 return Ok(());
158 }
159 }
160
161 Err(CleanroomError::validation_error(format!(
162 "Host path {} is not in whitelist. Allowed directories: {}",
163 host_path.display(),
164 self.whitelist
165 .iter()
166 .map(|p| p.display().to_string())
167 .collect::<Vec<_>>()
168 .join(", ")
169 )))
170 }
171
172 pub fn validate_all(&self, mounts: &[VolumeMount]) -> Result<()> {
174 for mount in mounts {
175 self.validate(mount)?;
176 }
177 Ok(())
178 }
179}
180
181impl Default for VolumeValidator {
182 fn default() -> Self {
184 let mut whitelist = vec![PathBuf::from("/tmp"), PathBuf::from("/var/tmp")];
185
186 whitelist.push(std::env::temp_dir());
188
189 if let Ok(current_dir) = std::env::current_dir() {
191 whitelist.push(current_dir);
192 }
193
194 Self::new(whitelist)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use std::fs;
202
203 #[test]
204 fn test_volume_mount_creation() -> Result<()> {
205 let temp_dir = std::env::temp_dir();
207 let host_path = temp_dir.join("test_volume");
208 fs::create_dir_all(&host_path)?;
209
210 let mount = VolumeMount::new(&host_path, "/data", false)?;
211 assert_eq!(mount.container_path(), Path::new("/data"));
212 assert!(!mount.is_read_only());
213
214 fs::remove_dir(&host_path)?;
216 Ok(())
217 }
218
219 #[test]
220 fn test_volume_mount_read_only() -> Result<()> {
221 let temp_dir = std::env::temp_dir();
222 let host_path = temp_dir.join("test_volume_ro");
223 fs::create_dir_all(&host_path)?;
224
225 let mount = VolumeMount::new(&host_path, "/data", true)?;
226 assert!(mount.is_read_only());
227
228 fs::remove_dir(&host_path)?;
229 Ok(())
230 }
231
232 #[test]
233 fn test_volume_mount_nonexistent_path() {
234 let result = VolumeMount::new("/nonexistent/path/xyz123", "/data", false);
235 assert!(result.is_err());
236 }
237
238 #[test]
239 fn test_volume_mount_relative_host_path() {
240 let result = VolumeMount::new("relative/path", "/data", false);
241 assert!(result.is_err());
242 }
243
244 #[test]
245 fn test_volume_mount_relative_container_path() -> Result<()> {
246 let temp_dir = std::env::temp_dir();
247 let result = VolumeMount::new(&temp_dir, "relative/path", false);
248 assert!(result.is_err());
249 Ok(())
250 }
251
252 #[test]
253 fn test_validator_whitelist() -> Result<()> {
254 let temp_dir = std::env::temp_dir();
255 let host_path = temp_dir.join("test_validator");
256 fs::create_dir_all(&host_path)?;
257
258 let validator = VolumeValidator::new(vec![temp_dir.clone()]);
259 let mount = VolumeMount::new(&host_path, "/data", false)?;
260
261 assert!(validator.validate(&mount).is_ok());
262
263 fs::remove_dir(&host_path)?;
264 Ok(())
265 }
266
267 #[test]
268 fn test_validator_rejects_non_whitelisted() -> Result<()> {
269 let temp_dir = std::env::temp_dir();
270 let host_path = temp_dir.join("test_validator_reject");
271 fs::create_dir_all(&host_path)?;
272
273 let validator = VolumeValidator::new(vec![PathBuf::from("/allowed")]);
274 let mount = VolumeMount::new(&host_path, "/data", false)?;
275
276 assert!(validator.validate(&mount).is_err());
277
278 fs::remove_dir(&host_path)?;
279 Ok(())
280 }
281
282 #[test]
283 fn test_validator_empty_whitelist_allows_all() -> Result<()> {
284 let temp_dir = std::env::temp_dir();
285 let host_path = temp_dir.join("test_validator_empty");
286 fs::create_dir_all(&host_path)?;
287
288 let validator = VolumeValidator::new(vec![]);
289 let mount = VolumeMount::new(&host_path, "/data", false)?;
290
291 assert!(validator.validate(&mount).is_ok());
292
293 fs::remove_dir(&host_path)?;
294 Ok(())
295 }
296
297 #[test]
298 fn test_volume_from_config() -> Result<()> {
299 let temp_dir = std::env::temp_dir();
300 let host_path = temp_dir.join("test_config");
301 fs::create_dir_all(&host_path)?;
302
303 let config = VolumeConfig {
304 host_path: host_path.to_string_lossy().to_string(),
305 container_path: "/data".to_string(),
306 read_only: Some(true),
307 };
308
309 let mount = VolumeMount::from_config(&config)?;
310 assert!(mount.is_read_only());
311 assert_eq!(mount.container_path(), Path::new("/data"));
312
313 fs::remove_dir(&host_path)?;
314 Ok(())
315 }
316}