composefs_storage/
layer.rs1use crate::error::{Result, StorageError};
37use crate::storage::Storage;
38use cap_std::fs::Dir;
39
40#[derive(Debug)]
42pub struct Layer {
43 id: String,
45
46 layer_dir: Dir,
48
49 diff_dir: Dir,
51
52 link_id: String,
54
55 parent_links: Vec<String>,
57}
58
59impl Layer {
60 pub fn open(storage: &Storage, id: &str) -> Result<Self> {
66 let overlay_dir = storage.root_dir().open_dir("overlay")?;
68
69 let layer_dir = overlay_dir
71 .open_dir(id)
72 .map_err(|_| StorageError::LayerNotFound(id.to_string()))?;
73
74 let diff_dir = layer_dir.open_dir("diff")?;
76
77 let link_id = Self::read_link(&layer_dir)?;
79 let parent_links = Self::read_lower(&layer_dir)?;
80
81 Ok(Self {
82 id: id.to_string(),
83 layer_dir,
84 diff_dir,
85 link_id,
86 parent_links,
87 })
88 }
89
90 pub fn id(&self) -> &str {
92 &self.id
93 }
94
95 fn read_link(layer_dir: &Dir) -> Result<String> {
97 let content = layer_dir.read_to_string("link")?;
98 Ok(content.trim().to_string())
99 }
100
101 fn read_lower(layer_dir: &Dir) -> Result<Vec<String>> {
103 match layer_dir.read_to_string("lower") {
104 Ok(content) => {
105 let links: Vec<String> = content
107 .trim()
108 .split(':')
109 .filter_map(|s| s.strip_prefix("l/"))
110 .map(|s| s.to_string())
111 .collect();
112 Ok(links)
113 }
114 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()), Err(e) => Err(StorageError::Io(e)),
116 }
117 }
118
119 pub fn link_id(&self) -> &str {
121 &self.link_id
122 }
123
124 pub fn parent_links(&self) -> &[String] {
126 &self.parent_links
127 }
128
129 pub fn parents(&self, storage: &Storage) -> Result<Vec<String>> {
138 self.parent_links
139 .iter()
140 .map(|link_id| storage.resolve_link(link_id))
141 .collect()
142 }
143
144 pub fn layer_dir(&self) -> &Dir {
146 &self.layer_dir
147 }
148
149 pub fn diff_dir(&self) -> &Dir {
151 &self.diff_dir
152 }
153
154 pub fn layer_chain(self, storage: &Storage) -> Result<Vec<Layer>> {
162 let mut chain = vec![self];
163 let mut current_idx = 0;
164
165 const MAX_DEPTH: usize = 500;
167
168 while current_idx < chain.len() && chain.len() < MAX_DEPTH {
169 let parent_ids = chain[current_idx].parents(storage)?;
170
171 for parent_id in parent_ids {
173 chain.push(Layer::open(storage, &parent_id)?);
174 }
175
176 current_idx += 1;
177 }
178
179 if chain.len() >= MAX_DEPTH {
180 return Err(StorageError::InvalidStorage(
181 "Layer chain exceeds maximum depth of 500".to_string(),
182 ));
183 }
184
185 Ok(chain)
186 }
187
188 pub fn open_file(&self, path: impl AsRef<std::path::Path>) -> Result<cap_std::fs::File> {
194 self.diff_dir.open(path).map_err(StorageError::Io)
195 }
196
197 pub fn open_file_std(&self, path: impl AsRef<std::path::Path>) -> Result<std::fs::File> {
203 let file = self.diff_dir.open(path).map_err(StorageError::Io)?;
204 Ok(file.into_std())
205 }
206
207 pub fn metadata(&self, path: impl AsRef<std::path::Path>) -> Result<cap_std::fs::Metadata> {
213 self.diff_dir.metadata(path).map_err(StorageError::Io)
214 }
215
216 pub fn read_dir(&self, path: impl AsRef<std::path::Path>) -> Result<cap_std::fs::ReadDir> {
222 self.diff_dir.read_dir(path).map_err(StorageError::Io)
223 }
224
225 pub fn has_whiteout(&self, parent_path: &str, filename: &str) -> Result<bool> {
238 let whiteout_name = format!(".wh.{}", filename);
239
240 if parent_path.is_empty() || parent_path == "." {
242 Ok(self.diff_dir.try_exists(&whiteout_name)?)
243 } else {
244 match self.diff_dir.open_dir(parent_path) {
245 Ok(parent_dir) => Ok(parent_dir.try_exists(&whiteout_name)?),
246 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
247 Err(e) => Err(StorageError::Io(e)),
248 }
249 }
250 }
251
252 pub fn is_opaque_dir(&self, path: &str) -> Result<bool> {
260 const OPAQUE_MARKER: &str = ".wh..wh..opq";
261
262 if path.is_empty() || path == "." {
263 Ok(self.diff_dir.try_exists(OPAQUE_MARKER)?)
264 } else {
265 match self.diff_dir.open_dir(path) {
266 Ok(dir) => Ok(dir.try_exists(OPAQUE_MARKER)?),
267 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
268 Err(e) => Err(StorageError::Io(e)),
269 }
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use std::path::Path;
278
279 #[test]
280 fn test_parse_lower_format() {
281 let content = "l/ABCDEFGHIJKLMNOPQRSTUVWXY:l/BCDEFGHIJKLMNOPQRSTUVWXYZ";
283 let links: Vec<String> = content
284 .trim()
285 .split(':')
286 .filter_map(|s| s.strip_prefix("l/"))
287 .map(|s| s.to_string())
288 .collect();
289
290 assert_eq!(links.len(), 2);
291 assert_eq!(links[0], "ABCDEFGHIJKLMNOPQRSTUVWXY");
292 assert_eq!(links[1], "BCDEFGHIJKLMNOPQRSTUVWXYZ");
293 }
294
295 fn create_mock_layer(root: &Path) -> Layer {
298 for d in ["overlay", "overlay-layers", "overlay-images"] {
300 std::fs::create_dir_all(root.join(d)).unwrap();
301 }
302
303 let layer_id = "test-layer-001";
304 let layer_dir = root.join("overlay").join(layer_id);
305 std::fs::create_dir_all(layer_dir.join("diff")).unwrap();
306 std::fs::write(layer_dir.join("link"), "ABCDEFGHIJKLMNOPQRSTUVWXYZ").unwrap();
307
308 let storage = Storage::open(root).unwrap();
309 Layer::open(&storage, layer_id).unwrap()
310 }
311
312 #[test]
315 fn test_has_whiteout_in_root() {
316 let dir = tempfile::tempdir().unwrap();
317 let layer = create_mock_layer(dir.path());
318
319 assert!(!layer.has_whiteout("", "somefile").unwrap());
321 assert!(!layer.has_whiteout(".", "somefile").unwrap());
322
323 std::fs::write(
325 dir.path().join("overlay/test-layer-001/diff/.wh.somefile"),
326 "",
327 )
328 .unwrap();
329
330 assert!(layer.has_whiteout("", "somefile").unwrap());
331 assert!(layer.has_whiteout(".", "somefile").unwrap());
332 assert!(!layer.has_whiteout("", "otherfile").unwrap());
334 }
335
336 #[test]
337 fn test_has_whiteout_in_subdirectory() {
338 let dir = tempfile::tempdir().unwrap();
339 let layer = create_mock_layer(dir.path());
340 let diff = dir.path().join("overlay/test-layer-001/diff");
341
342 std::fs::create_dir_all(diff.join("etc")).unwrap();
343 std::fs::write(diff.join("etc/.wh.hosts"), "").unwrap();
344
345 assert!(layer.has_whiteout("etc", "hosts").unwrap());
346 assert!(!layer.has_whiteout("", "hosts").unwrap());
348 }
349
350 #[test]
351 fn test_has_whiteout_in_nested_subdirectory() {
352 let dir = tempfile::tempdir().unwrap();
353 let layer = create_mock_layer(dir.path());
354 let diff = dir.path().join("overlay/test-layer-001/diff");
355
356 std::fs::create_dir_all(diff.join("usr/local/bin")).unwrap();
357 std::fs::write(diff.join("usr/local/bin/.wh.myapp"), "").unwrap();
358
359 assert!(layer.has_whiteout("usr/local/bin", "myapp").unwrap());
360 assert!(!layer.has_whiteout("usr/local", "myapp").unwrap());
361 assert!(!layer.has_whiteout("usr", "myapp").unwrap());
362 }
363
364 #[test]
365 fn test_has_whiteout_nonexistent_parent() {
366 let dir = tempfile::tempdir().unwrap();
367 let layer = create_mock_layer(dir.path());
368
369 assert!(!layer.has_whiteout("no/such/dir", "file").unwrap());
371 }
372
373 #[test]
374 fn test_has_whiteout_multiple() {
375 let dir = tempfile::tempdir().unwrap();
376 let layer = create_mock_layer(dir.path());
377 let diff = dir.path().join("overlay/test-layer-001/diff");
378
379 std::fs::write(diff.join(".wh.file_a"), "").unwrap();
380 std::fs::write(diff.join(".wh.file_b"), "").unwrap();
381
382 assert!(layer.has_whiteout("", "file_a").unwrap());
383 assert!(layer.has_whiteout("", "file_b").unwrap());
384 assert!(!layer.has_whiteout("", "file_c").unwrap());
385 }
386
387 #[test]
390 fn test_is_opaque_dir_in_root() {
391 let dir = tempfile::tempdir().unwrap();
392 let layer = create_mock_layer(dir.path());
393 let diff = dir.path().join("overlay/test-layer-001/diff");
394
395 assert!(!layer.is_opaque_dir("").unwrap());
396 assert!(!layer.is_opaque_dir(".").unwrap());
397
398 std::fs::write(diff.join(".wh..wh..opq"), "").unwrap();
399
400 assert!(layer.is_opaque_dir("").unwrap());
401 assert!(layer.is_opaque_dir(".").unwrap());
402 }
403
404 #[test]
405 fn test_is_opaque_dir_in_subdirectory() {
406 let dir = tempfile::tempdir().unwrap();
407 let layer = create_mock_layer(dir.path());
408 let diff = dir.path().join("overlay/test-layer-001/diff");
409
410 std::fs::create_dir_all(diff.join("etc")).unwrap();
411 std::fs::write(diff.join("etc/.wh..wh..opq"), "").unwrap();
412
413 assert!(layer.is_opaque_dir("etc").unwrap());
414 assert!(!layer.is_opaque_dir("").unwrap());
416 }
417
418 #[test]
419 fn test_is_opaque_dir_false_for_normal_dir() {
420 let dir = tempfile::tempdir().unwrap();
421 let layer = create_mock_layer(dir.path());
422 let diff = dir.path().join("overlay/test-layer-001/diff");
423
424 std::fs::create_dir_all(diff.join("var/log")).unwrap();
426 std::fs::write(diff.join("var/log/syslog"), "log data").unwrap();
427
428 assert!(!layer.is_opaque_dir("var").unwrap());
429 assert!(!layer.is_opaque_dir("var/log").unwrap());
430 }
431
432 #[test]
433 fn test_is_opaque_dir_nonexistent_path() {
434 let dir = tempfile::tempdir().unwrap();
435 let layer = create_mock_layer(dir.path());
436
437 assert!(!layer.is_opaque_dir("no/such/path").unwrap());
439 }
440
441 #[test]
442 fn test_is_opaque_dir_nested() {
443 let dir = tempfile::tempdir().unwrap();
444 let layer = create_mock_layer(dir.path());
445 let diff = dir.path().join("overlay/test-layer-001/diff");
446
447 std::fs::create_dir_all(diff.join("a/b/c")).unwrap();
449 std::fs::write(diff.join("a/b/c/.wh..wh..opq"), "").unwrap();
450
451 assert!(!layer.is_opaque_dir("a").unwrap());
452 assert!(!layer.is_opaque_dir("a/b").unwrap());
453 assert!(layer.is_opaque_dir("a/b/c").unwrap());
454 }
455
456 #[test]
459 fn test_whiteout_and_opaque_coexist() {
460 let dir = tempfile::tempdir().unwrap();
461 let layer = create_mock_layer(dir.path());
462 let diff = dir.path().join("overlay/test-layer-001/diff");
463
464 std::fs::create_dir_all(diff.join("mydir")).unwrap();
465 std::fs::write(diff.join("mydir/.wh..wh..opq"), "").unwrap();
467 std::fs::write(diff.join("mydir/.wh.oldfile"), "").unwrap();
469
470 assert!(layer.is_opaque_dir("mydir").unwrap());
471 assert!(layer.has_whiteout("mydir", "oldfile").unwrap());
472 }
473
474 #[test]
475 fn test_whiteout_of_dotdot_prefix_name() {
476 let dir = tempfile::tempdir().unwrap();
479 let layer = create_mock_layer(dir.path());
480 let diff = dir.path().join("overlay/test-layer-001/diff");
481
482 std::fs::write(diff.join(".wh..wh."), "").unwrap();
484
485 assert!(layer.has_whiteout("", ".wh.").unwrap());
486 assert!(!layer.is_opaque_dir("").unwrap());
488 }
489}