1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::ffi::OsString;
3use std::fmt;
4use std::io::{Read, Seek, SeekFrom};
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9use crate::path_bytes::os_string_from_bytes;
10
11const DEFAULT_MAX_TOTAL_SIZE: u64 = 10 * 1024 * 1024 * 1024; const DEFAULT_MAX_FILE_SIZE: u64 = 5 * 1024 * 1024 * 1024; const DEFAULT_MAX_ENTRY_COUNT: u64 = 1_000_000;
18const DEFAULT_MAX_PATH_LENGTH: usize = 4096;
19const DEFAULT_MAX_PATH_DEPTH: usize = 128;
20const DEFAULT_MAX_SYMLINK_TARGET: usize = 4096;
21
22const DEFAULT_DIR_MODE: u16 = 0o755;
23
24pub(crate) const WHITEOUT_MAJOR: u32 = 0;
26pub(crate) const WHITEOUT_MINOR: u32 = 0;
27
28pub(crate) const OPAQUE_XATTR_NAME: &[u8] = b"trusted.overlay.opaque";
30pub(crate) const OPAQUE_XATTR_VALUE: &[u8] = b"y";
31
32#[derive(Clone)]
39pub enum FileData {
40 Memory(Vec<u8>),
42 SharedMemory(Arc<[u8]>),
44 Spool {
48 spool: Arc<std::sync::Mutex<std::fs::File>>,
49 offset: u64,
50 len: u64,
51 },
52}
53
54impl std::fmt::Debug for FileData {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 FileData::Memory(data) => f.debug_tuple("Memory").field(&data.len()).finish(),
58 FileData::SharedMemory(data) => {
59 f.debug_tuple("SharedMemory").field(&data.len()).finish()
60 }
61 FileData::Spool { offset, len, .. } => f
62 .debug_struct("Spool")
63 .field("offset", offset)
64 .field("len", len)
65 .finish(),
66 }
67 }
68}
69
70impl PartialEq for FileData {
71 fn eq(&self, other: &Self) -> bool {
72 match (self, other) {
73 (FileData::Memory(a), FileData::Memory(b)) => a == b,
74 (FileData::Memory(a), FileData::SharedMemory(b))
75 | (FileData::SharedMemory(b), FileData::Memory(a)) => a.as_slice() == b.as_ref(),
76 (FileData::SharedMemory(a), FileData::SharedMemory(b)) => a.as_ref() == b.as_ref(),
77 _ => false,
78 }
79 }
80}
81
82pub const SPOOL_THRESHOLD: u64 = 64 * 1024;
85
86pub struct DataSpool {
88 file: std::fs::File,
89 shared: Arc<std::sync::Mutex<std::fs::File>>,
90 offset: u64,
91}
92
93pub struct ResourceLimits {
94 pub max_total_size: u64,
95 pub max_file_size: u64,
96 pub max_entry_count: u64,
97 pub max_path_length: usize,
98 pub max_path_depth: usize,
99 pub max_symlink_target: usize,
100}
101
102#[derive(Clone)]
103pub struct InodeMetadata {
104 pub uid: u32,
105 pub gid: u32,
106 pub mode: u16,
107 pub mtime: u64,
108 pub mtime_nsec: u32,
109}
110
111#[derive(Clone)]
112pub struct Xattr {
113 pub name: Vec<u8>,
114 pub value: Vec<u8>,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub struct RegularFileId(u64);
119
120#[derive(Clone)]
121pub enum TreeNode {
122 RegularFile(RegularFileNode),
123 Directory(DirectoryNode),
124 Symlink(SymlinkNode),
125 CharDevice(DeviceNode),
126 BlockDevice(DeviceNode),
127 Fifo(InodeMetadata),
128 Socket(InodeMetadata),
129}
130
131#[derive(Clone)]
132pub struct RegularFileNode {
133 pub id: RegularFileId,
134 pub metadata: InodeMetadata,
135 pub xattrs: Vec<Xattr>,
136 pub data: FileData,
137 pub nlink: u32,
138}
139
140#[derive(Clone)]
141pub struct DirectoryNode {
142 pub metadata: InodeMetadata,
143 pub xattrs: Vec<Xattr>,
144 pub entries: BTreeMap<OsString, TreeNode>,
145}
146
147#[derive(Clone)]
148pub struct SymlinkNode {
149 pub metadata: InodeMetadata,
150 pub target: Vec<u8>,
151}
152
153#[derive(Clone)]
154pub struct DeviceNode {
155 pub metadata: InodeMetadata,
156 pub major: u32,
157 pub minor: u32,
158}
159
160#[derive(Clone)]
161pub struct FileTree {
162 pub root: DirectoryNode,
163}
164
165#[derive(Debug)]
166pub enum FileTreeError {
167 PathEmpty,
168 PathTraversal(String),
169 InvalidPathEncoding(String),
170 NotADirectory(String),
171 EntryExists(String),
172}
173
174impl FileData {
179 pub fn len(&self) -> usize {
181 match self {
182 FileData::Memory(v) => v.len(),
183 FileData::SharedMemory(v) => v.len(),
184 FileData::Spool { len, .. } => *len as usize,
185 }
186 }
187
188 pub fn is_empty(&self) -> bool {
189 self.len() == 0
190 }
191
192 pub fn read_all(&self) -> std::io::Result<Vec<u8>> {
195 match self {
196 FileData::Memory(v) => Ok(v.clone()),
197 FileData::SharedMemory(v) => Ok(v.to_vec()),
198 FileData::Spool { spool, offset, len } => {
199 let mut buf = vec![0u8; *len as usize];
200 let mut file = spool
201 .lock()
202 .map_err(|_| std::io::Error::other("spool lock poisoned"))?;
203 file.seek(SeekFrom::Start(*offset))?;
204 file.read_exact(&mut buf)?;
205 Ok(buf)
206 }
207 }
208 }
209
210 pub fn as_bytes(&self) -> Option<&[u8]> {
212 match self {
213 FileData::Memory(v) => Some(v),
214 FileData::SharedMemory(v) => Some(v),
215 FileData::Spool { .. } => None,
216 }
217 }
218
219 pub fn write_to(&self, out: &mut impl std::io::Write) -> std::io::Result<()> {
222 self.write_range(0, self.len(), out)
223 }
224
225 pub fn write_range(
227 &self,
228 start: usize,
229 len: usize,
230 out: &mut impl std::io::Write,
231 ) -> std::io::Result<()> {
232 match self {
233 FileData::Memory(v) => out.write_all(&v[start..start + len]),
234 FileData::SharedMemory(v) => out.write_all(&v[start..start + len]),
235 FileData::Spool { spool, offset, .. } => {
236 let mut file = spool
237 .lock()
238 .map_err(|_| std::io::Error::other("spool lock poisoned"))?;
239 file.seek(SeekFrom::Start(*offset + start as u64))?;
240 let mut remaining = len;
241 let mut buf = [0u8; 65536];
242 while remaining > 0 {
243 let to_read = remaining.min(buf.len());
244 file.read_exact(&mut buf[..to_read])?;
245 out.write_all(&buf[..to_read])?;
246 remaining -= to_read;
247 }
248 Ok(())
249 }
250 }
251 }
252
253 pub fn clone_ref(&mut self) -> FileData {
255 match self {
256 FileData::Memory(data) => {
257 let shared: Arc<[u8]> = std::mem::take(data).into();
258 *self = FileData::SharedMemory(Arc::clone(&shared));
259 FileData::SharedMemory(shared)
260 }
261 FileData::SharedMemory(data) => FileData::SharedMemory(Arc::clone(data)),
262 FileData::Spool { spool, offset, len } => FileData::Spool {
263 spool: Arc::clone(spool),
264 offset: *offset,
265 len: *len,
266 },
267 }
268 }
269}
270
271impl RegularFileId {
272 pub fn new() -> Self {
273 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
274 Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
275 }
276}
277
278impl Default for RegularFileId {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284impl DataSpool {
285 pub fn new(path: &std::path::Path) -> std::io::Result<Self> {
287 let file = std::fs::OpenOptions::new()
288 .create(true)
289 .truncate(true)
290 .read(true)
291 .write(true)
292 .open(path)?;
293 let shared = Arc::new(std::sync::Mutex::new(file.try_clone()?));
294 Ok(Self {
295 file,
296 shared,
297 offset: 0,
298 })
299 }
300
301 pub fn write_data(&mut self, data: &[u8]) -> std::io::Result<FileData> {
303 use std::io::Write;
304 let offset = self.offset;
305 self.file.write_all(data)?;
306 self.offset += data.len() as u64;
307 Ok(FileData::Spool {
308 spool: Arc::clone(&self.shared),
309 offset,
310 len: data.len() as u64,
311 })
312 }
313
314 pub fn current_offset(&self) -> u64 {
315 self.offset
316 }
317
318 pub fn write_chunk(&mut self, data: &[u8]) -> std::io::Result<()> {
319 use std::io::Write;
320 self.file.write_all(data)?;
321 self.offset += data.len() as u64;
322 Ok(())
323 }
324
325 pub fn data_ref(&self, offset: u64, len: u64) -> FileData {
326 FileData::Spool {
327 spool: Arc::clone(&self.shared),
328 offset,
329 len,
330 }
331 }
332}
333
334impl DirectoryNode {
335 pub fn new(metadata: InodeMetadata) -> Self {
336 Self {
337 metadata,
338 xattrs: Vec::new(),
339 entries: BTreeMap::new(),
340 }
341 }
342
343 pub fn entry_count(&self) -> usize {
344 self.entries.len()
345 }
346}
347
348impl Default for FileTree {
349 fn default() -> Self {
350 Self::new()
351 }
352}
353
354impl FileTree {
355 pub fn new() -> Self {
356 Self {
357 root: DirectoryNode::new(InodeMetadata::default()),
358 }
359 }
360
361 pub fn insert(&mut self, path: &[u8], node: TreeNode) -> Result<(), FileTreeError> {
362 use std::collections::btree_map::Entry;
363
364 let components = split_path(path)?;
365 if components.is_empty() {
366 return Err(FileTreeError::PathEmpty);
367 }
368
369 let (parent_components, file_name) = components.split_at(components.len() - 1);
370
371 let mut current = &mut self.root;
375 for component in parent_components {
376 let key = tree_key(component)?;
377 current = match current.entries.entry(key) {
378 Entry::Vacant(e) => {
379 let dir = TreeNode::Directory(DirectoryNode::new(InodeMetadata::default()));
380 match e.insert(dir) {
381 TreeNode::Directory(d) => d,
382 _ => unreachable!(),
383 }
384 }
385 Entry::Occupied(e) => match e.into_mut() {
386 TreeNode::Directory(d) => d,
387 _ => {
388 let path_str = String::from_utf8_lossy(component).into_owned();
389 return Err(FileTreeError::NotADirectory(path_str));
390 }
391 },
392 };
393 }
394
395 let key = tree_key(file_name[0])?;
398 match current.entries.entry(key) {
399 Entry::Vacant(e) => {
400 e.insert(node);
401 }
402 Entry::Occupied(mut e) => match (e.get(), &node) {
403 (TreeNode::Directory(_), TreeNode::Directory(_)) => {
404 if let TreeNode::Directory(existing) = e.get_mut()
405 && let TreeNode::Directory(new_dir) = node
406 {
407 existing.metadata = new_dir.metadata;
408 existing.xattrs = new_dir.xattrs;
409 }
410 }
411 (TreeNode::Directory(_), _) => {
412 let path_str = String::from_utf8_lossy(file_name[0]).into_owned();
413 return Err(FileTreeError::EntryExists(path_str));
414 }
415 _ => {
416 e.insert(node);
417 }
418 },
419 }
420
421 Ok(())
422 }
423
424 pub fn get(&self, path: &[u8]) -> Option<&TreeNode> {
425 let components = split_path(path).ok()?;
426 if components.is_empty() {
427 return None;
428 }
429
430 let (parent_components, file_name) = components.split_at(components.len() - 1);
431
432 let mut current = &self.root;
433 for component in parent_components {
434 let key = tree_key(component).ok()?;
435 match current.entries.get(&key) {
436 Some(TreeNode::Directory(dir)) => {
437 current = dir;
438 }
439 _ => return None,
440 }
441 }
442
443 let key = tree_key(file_name[0]).ok()?;
444 current.entries.get(&key)
445 }
446
447 pub fn get_mut(&mut self, path: &[u8]) -> Option<&mut TreeNode> {
448 let components = split_path(path).ok()?;
449 if components.is_empty() {
450 return None;
451 }
452
453 let (parent_components, file_name) = components.split_at(components.len() - 1);
454
455 let mut current = &mut self.root;
456 for component in parent_components {
457 let key = tree_key(component).ok()?;
458 match current.entries.get_mut(&key) {
459 Some(TreeNode::Directory(dir)) => {
460 current = dir;
461 }
462 _ => return None,
463 }
464 }
465
466 let key = tree_key(file_name[0]).ok()?;
467 current.entries.get_mut(&key)
468 }
469
470 pub fn remove(&mut self, path: &[u8]) -> Option<TreeNode> {
471 let components = split_path(path).ok()?;
472 if components.is_empty() {
473 return None;
474 }
475
476 let (parent_components, file_name) = components.split_at(components.len() - 1);
477
478 let mut current = &mut self.root;
479 for component in parent_components {
480 let key = tree_key(component).ok()?;
481 match current.entries.get_mut(&key) {
482 Some(TreeNode::Directory(dir)) => {
483 current = dir;
484 }
485 _ => return None,
486 }
487 }
488
489 let key = tree_key(file_name[0]).ok()?;
490 current.entries.remove(&key)
491 }
492
493 pub fn node_count(&self) -> u64 {
494 count_nodes_in_dir(&self.root)
495 }
496
497 pub fn total_data_size(&self) -> u64 {
498 data_size_in_dir(&self.root)
499 }
500
501 pub(crate) fn regular_file_link_counts(&self) -> HashMap<RegularFileId, u32> {
502 let mut counts = HashMap::new();
503 count_regular_links_in_dir(&self.root, &mut counts);
504 counts
505 }
506
507 pub(crate) fn refresh_regular_nlinks(&mut self) {
508 let counts = self.regular_file_link_counts();
509 refresh_regular_nlinks_in_dir(&mut self.root, &counts);
510 }
511
512 pub fn merge_layer(&mut self, layer: FileTree) {
513 merge_directory(&mut self.root, layer.root);
514 self.refresh_regular_nlinks();
515 }
516
517 pub fn strip_file_data(&mut self) {
523 strip_data_in_dir(&mut self.root);
524 }
525}
526
527impl Default for ResourceLimits {
532 fn default() -> Self {
533 Self {
534 max_total_size: DEFAULT_MAX_TOTAL_SIZE,
535 max_file_size: DEFAULT_MAX_FILE_SIZE,
536 max_entry_count: DEFAULT_MAX_ENTRY_COUNT,
537 max_path_length: DEFAULT_MAX_PATH_LENGTH,
538 max_path_depth: DEFAULT_MAX_PATH_DEPTH,
539 max_symlink_target: DEFAULT_MAX_SYMLINK_TARGET,
540 }
541 }
542}
543
544impl Default for InodeMetadata {
545 fn default() -> Self {
546 Self {
547 uid: 0,
548 gid: 0,
549 mode: DEFAULT_DIR_MODE,
550 mtime: 0,
551 mtime_nsec: 0,
552 }
553 }
554}
555
556impl fmt::Display for FileTreeError {
557 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558 match self {
559 FileTreeError::PathEmpty => write!(f, "path is empty"),
560 FileTreeError::PathTraversal(p) => {
561 write!(f, "path traversal attempt: \"..\" in path \"{p}\"")
562 }
563 FileTreeError::InvalidPathEncoding(p) => {
564 write!(f, "path is not valid host encoding: \"{p}\"")
565 }
566 FileTreeError::NotADirectory(p) => {
567 write!(f, "not a directory: \"{p}\"")
568 }
569 FileTreeError::EntryExists(p) => {
570 write!(f, "entry already exists: \"{p}\"")
571 }
572 }
573 }
574}
575
576impl std::error::Error for FileTreeError {}
577
578fn split_path(path: &[u8]) -> Result<Vec<&[u8]>, FileTreeError> {
583 let components: Vec<&[u8]> = path
584 .split(|&b| b == b'/')
585 .filter(|c| !c.is_empty())
586 .collect();
587
588 if components.is_empty() {
589 return Err(FileTreeError::PathEmpty);
590 }
591
592 for component in &components {
593 if *component == b".." {
594 let path_str = String::from_utf8_lossy(path).into_owned();
595 return Err(FileTreeError::PathTraversal(path_str));
596 }
597 }
598
599 Ok(components)
600}
601
602fn tree_key(component: &[u8]) -> Result<OsString, FileTreeError> {
603 os_string_from_bytes(component)
604 .map_err(|_| FileTreeError::InvalidPathEncoding(String::from_utf8_lossy(component).into()))
605}
606
607fn count_nodes_in_dir(dir: &DirectoryNode) -> u64 {
608 let mut count = 0u64;
609 for node in dir.entries.values() {
610 count += 1;
611 if let TreeNode::Directory(child_dir) = node {
612 count += count_nodes_in_dir(child_dir);
613 }
614 }
615 count
616}
617
618fn data_size_in_dir(dir: &DirectoryNode) -> u64 {
619 let mut seen = HashSet::new();
620 data_size_in_dir_once(dir, &mut seen)
621}
622
623fn data_size_in_dir_once(dir: &DirectoryNode, seen: &mut HashSet<RegularFileId>) -> u64 {
624 let mut size = 0u64;
625 for node in dir.entries.values() {
626 match node {
627 TreeNode::RegularFile(file) if seen.insert(file.id) => {
628 size += file.data.len() as u64;
629 }
630 TreeNode::Directory(child_dir) => {
631 size += data_size_in_dir_once(child_dir, seen);
632 }
633 _ => {}
634 }
635 }
636 size
637}
638
639fn count_regular_links_in_dir(dir: &DirectoryNode, counts: &mut HashMap<RegularFileId, u32>) {
640 for node in dir.entries.values() {
641 match node {
642 TreeNode::RegularFile(file) => {
643 *counts.entry(file.id).or_insert(0) += 1;
644 }
645 TreeNode::Directory(child_dir) => {
646 count_regular_links_in_dir(child_dir, counts);
647 }
648 _ => {}
649 }
650 }
651}
652
653fn refresh_regular_nlinks_in_dir(dir: &mut DirectoryNode, counts: &HashMap<RegularFileId, u32>) {
654 for node in dir.entries.values_mut() {
655 match node {
656 TreeNode::RegularFile(file) => {
657 file.nlink = counts.get(&file.id).copied().unwrap_or(1);
658 }
659 TreeNode::Directory(child_dir) => {
660 refresh_regular_nlinks_in_dir(child_dir, counts);
661 }
662 _ => {}
663 }
664 }
665}
666
667fn strip_data_in_dir(dir: &mut DirectoryNode) {
668 for node in dir.entries.values_mut() {
669 match node {
670 TreeNode::RegularFile(f) => {
671 f.data = FileData::Memory(Vec::new());
672 }
673 TreeNode::Directory(d) => {
674 strip_data_in_dir(d);
675 }
676 _ => {}
677 }
678 }
679}
680
681pub fn merge_layers_with_provenance(layers: Vec<FileTree>) -> (FileTree, HashMap<PathBuf, usize>) {
690 let mut merged = FileTree::new();
691 let mut provenance: HashMap<PathBuf, usize> = HashMap::new();
692
693 for (layer_idx, layer) in layers.into_iter().enumerate() {
694 let path = PathBuf::new();
695 merge_directory_with_provenance(
696 &mut merged.root,
697 layer.root,
698 layer_idx,
699 &path,
700 &mut provenance,
701 );
702 }
703
704 strip_opaque_xattrs(&mut merged.root);
707 merged.refresh_regular_nlinks();
708
709 (merged, provenance)
710}
711
712fn merge_directory_with_provenance(
713 base: &mut DirectoryNode,
714 layer: DirectoryNode,
715 layer_idx: usize,
716 current_path: &Path,
717 provenance: &mut HashMap<PathBuf, usize>,
718) {
719 for (name, layer_node) in layer.entries {
720 let child_path = current_path.join(&name);
721
722 if is_whiteout_device(&layer_node) {
724 base.entries.remove(&name);
726 provenance.retain(|k, _| !k.starts_with(&child_path));
727 continue;
728 }
729
730 match layer_node {
731 TreeNode::Directory(layer_dir) => {
732 let opaque = has_opaque_xattr(&layer_dir);
733
734 match base.entries.get_mut(&name) {
735 Some(TreeNode::Directory(base_dir)) => {
736 if opaque {
737 provenance.retain(|k, _| !k.starts_with(&child_path));
739 base_dir.entries.clear();
740 }
741 base_dir.metadata = layer_dir.metadata;
742 base_dir.xattrs = layer_dir.xattrs;
743 merge_directory_with_provenance(
744 base_dir,
745 DirectoryNode {
746 metadata: InodeMetadata::default(),
747 xattrs: Vec::new(),
748 entries: layer_dir.entries,
749 },
750 layer_idx,
751 &child_path,
752 provenance,
753 );
754 }
755 _ => {
756 provenance.retain(|k, _| !k.starts_with(&child_path));
758 record_provenance_recursive(&layer_dir, layer_idx, &child_path, provenance);
760 base.entries.insert(name, TreeNode::Directory(layer_dir));
761 }
762 }
763 }
764 other => {
765 provenance.insert(child_path, layer_idx);
767 base.entries.insert(name, other);
768 }
769 }
770 }
771}
772
773fn record_provenance_recursive(
774 dir: &DirectoryNode,
775 layer_idx: usize,
776 current_path: &Path,
777 provenance: &mut HashMap<PathBuf, usize>,
778) {
779 for (name, child) in &dir.entries {
780 let child_path = current_path.join(name);
781 match child {
782 TreeNode::Directory(child_dir) => {
783 record_provenance_recursive(child_dir, layer_idx, &child_path, provenance);
784 }
785 _ => {
786 provenance.insert(child_path, layer_idx);
787 }
788 }
789 }
790}
791
792fn strip_opaque_xattrs(dir: &mut DirectoryNode) {
793 dir.xattrs
794 .retain(|x| !(x.name == OPAQUE_XATTR_NAME && x.value == OPAQUE_XATTR_VALUE));
795 for node in dir.entries.values_mut() {
796 if let TreeNode::Directory(child_dir) = node {
797 strip_opaque_xattrs(child_dir);
798 }
799 }
800}
801
802fn is_whiteout_device(node: &TreeNode) -> bool {
803 matches!(node, TreeNode::CharDevice(dev) if dev.major == WHITEOUT_MAJOR && dev.minor == WHITEOUT_MINOR)
804}
805
806fn has_opaque_xattr(dir: &DirectoryNode) -> bool {
807 dir.xattrs
808 .iter()
809 .any(|x| x.name == OPAQUE_XATTR_NAME && x.value == OPAQUE_XATTR_VALUE)
810}
811
812fn merge_directory(base: &mut DirectoryNode, layer: DirectoryNode) {
813 for (name, layer_node) in layer.entries {
814 if is_whiteout_device(&layer_node) {
815 base.entries.remove(&name);
816 continue;
817 }
818
819 match layer_node {
820 TreeNode::Directory(layer_dir) => {
821 let opaque = has_opaque_xattr(&layer_dir);
822
823 match base.entries.get_mut(&name) {
824 Some(TreeNode::Directory(base_dir)) => {
825 if opaque {
826 base_dir.entries.clear();
827 }
828 base_dir.metadata = layer_dir.metadata;
829 base_dir.xattrs = layer_dir.xattrs;
830 merge_directory(
831 base_dir,
832 DirectoryNode {
833 metadata: InodeMetadata::default(),
834 xattrs: Vec::new(),
835 entries: layer_dir.entries,
836 },
837 );
838 }
839 _ => {
840 base.entries.insert(name, TreeNode::Directory(layer_dir));
841 }
842 }
843 }
844 other => {
845 base.entries.insert(name, other);
846 }
847 }
848 }
849}
850
851#[cfg(test)]
856mod tests {
857 use super::*;
858
859 fn make_regular_file(data: &[u8]) -> TreeNode {
860 make_regular_file_with_id(data, RegularFileId::new())
861 }
862
863 fn make_regular_file_with_id(data: &[u8], id: RegularFileId) -> TreeNode {
864 TreeNode::RegularFile(RegularFileNode {
865 id,
866 metadata: InodeMetadata::default(),
867 xattrs: Vec::new(),
868 data: FileData::Memory(data.to_vec()),
869 nlink: 1,
870 })
871 }
872
873 fn make_directory() -> TreeNode {
874 TreeNode::Directory(DirectoryNode::new(InodeMetadata::default()))
875 }
876
877 fn make_whiteout() -> TreeNode {
878 TreeNode::CharDevice(DeviceNode {
879 metadata: InodeMetadata::default(),
880 major: 0,
881 minor: 0,
882 })
883 }
884
885 fn make_opaque_directory() -> DirectoryNode {
886 DirectoryNode {
887 metadata: InodeMetadata::default(),
888 xattrs: vec![Xattr {
889 name: OPAQUE_XATTR_NAME.to_vec(),
890 value: OPAQUE_XATTR_VALUE.to_vec(),
891 }],
892 entries: BTreeMap::new(),
893 }
894 }
895
896 #[test]
897 fn insert_and_get_file() {
898 let mut tree = FileTree::new();
899 tree.insert(b"hello.txt", make_regular_file(b"hello world"))
900 .unwrap();
901
902 let node = tree.get(b"hello.txt").unwrap();
903 match node {
904 TreeNode::RegularFile(f) => {
905 assert_eq!(f.data, FileData::Memory(b"hello world".to_vec()))
906 }
907 _ => panic!("expected regular file"),
908 }
909 }
910
911 #[test]
912 fn insert_with_missing_parents_creates_them() {
913 let mut tree = FileTree::new();
914 tree.insert(b"a/b/c/file.txt", make_regular_file(b"deep"))
915 .unwrap();
916
917 let node = tree.get(b"a").unwrap();
919 assert!(matches!(node, TreeNode::Directory(_)));
920
921 let node = tree.get(b"a/b").unwrap();
922 assert!(matches!(node, TreeNode::Directory(_)));
923
924 let node = tree.get(b"a/b/c").unwrap();
925 assert!(matches!(node, TreeNode::Directory(_)));
926
927 let node = tree.get(b"a/b/c/file.txt").unwrap();
928 assert!(matches!(node, TreeNode::RegularFile(_)));
929 }
930
931 #[test]
932 fn reject_dotdot_in_path() {
933 let mut tree = FileTree::new();
934 let result = tree.insert(b"a/../etc/passwd", make_regular_file(b"bad"));
935 assert!(matches!(result, Err(FileTreeError::PathTraversal(_))));
936 }
937
938 #[test]
939 fn merge_layer_replaces_file() {
940 let mut base = FileTree::new();
941 base.insert(b"config.txt", make_regular_file(b"old"))
942 .unwrap();
943
944 let mut layer = FileTree::new();
945 layer
946 .insert(b"config.txt", make_regular_file(b"new"))
947 .unwrap();
948
949 base.merge_layer(layer);
950
951 match base.get(b"config.txt").unwrap() {
952 TreeNode::RegularFile(f) => assert_eq!(f.data, FileData::Memory(b"new".to_vec())),
953 _ => panic!("expected regular file"),
954 }
955 }
956
957 #[test]
958 fn merge_layer_whiteout_removes_file() {
959 let mut base = FileTree::new();
960 base.insert(b"dir/secret.txt", make_regular_file(b"sensitive"))
961 .unwrap();
962
963 let mut layer = FileTree::new();
964 layer.insert(b"dir", make_directory()).unwrap();
965 layer.insert(b"dir/secret.txt", make_whiteout()).unwrap();
966
967 base.merge_layer(layer);
968
969 assert!(base.get(b"dir/secret.txt").is_none());
970 assert!(base.get(b"dir").is_some());
972 }
973
974 #[test]
975 fn merge_layer_opaque_dir_clears_existing_entries() {
976 let mut base = FileTree::new();
977 base.insert(b"dir/a.txt", make_regular_file(b"a")).unwrap();
978 base.insert(b"dir/b.txt", make_regular_file(b"b")).unwrap();
979
980 let mut layer = FileTree::new();
981 let mut opaque_dir = make_opaque_directory();
982 opaque_dir
983 .entries
984 .insert(OsString::from("c.txt"), make_regular_file(b"c"));
985 layer
986 .root
987 .entries
988 .insert(OsString::from("dir"), TreeNode::Directory(opaque_dir));
989
990 base.merge_layer(layer);
991
992 assert!(base.get(b"dir/a.txt").is_none());
994 assert!(base.get(b"dir/b.txt").is_none());
995 match base.get(b"dir/c.txt").unwrap() {
997 TreeNode::RegularFile(f) => assert_eq!(f.data, FileData::Memory(b"c".to_vec())),
998 _ => panic!("expected regular file"),
999 }
1000 }
1001
1002 #[test]
1003 fn node_count_and_data_size() {
1004 let mut tree = FileTree::new();
1005 tree.insert(b"a/file1.txt", make_regular_file(b"hello"))
1006 .unwrap();
1007 tree.insert(b"a/file2.txt", make_regular_file(b"world!"))
1008 .unwrap();
1009 tree.insert(b"b/nested/file3.txt", make_regular_file(b"!"))
1010 .unwrap();
1011
1012 assert_eq!(tree.node_count(), 6);
1014 assert_eq!(tree.total_data_size(), 12);
1016 }
1017
1018 #[test]
1019 fn data_size_counts_hardlinked_regular_file_once() {
1020 let mut tree = FileTree::new();
1021 let file_id = RegularFileId::new();
1022
1023 tree.insert(b"a.txt", make_regular_file_with_id(b"shared", file_id))
1024 .unwrap();
1025 tree.insert(b"b.txt", make_regular_file_with_id(b"shared", file_id))
1026 .unwrap();
1027
1028 tree.refresh_regular_nlinks();
1029
1030 assert_eq!(tree.total_data_size(), b"shared".len() as u64);
1031 for path in [b"a.txt".as_slice(), b"b.txt".as_slice()] {
1032 match tree.get(path).unwrap() {
1033 TreeNode::RegularFile(file) => assert_eq!(file.nlink, 2),
1034 _ => panic!("expected regular file"),
1035 }
1036 }
1037 }
1038
1039 #[test]
1040 fn remove_node() {
1041 let mut tree = FileTree::new();
1042 tree.insert(b"a/b.txt", make_regular_file(b"data")).unwrap();
1043 assert!(tree.get(b"a/b.txt").is_some());
1044
1045 let removed = tree.remove(b"a/b.txt");
1046 assert!(removed.is_some());
1047 assert!(tree.get(b"a/b.txt").is_none());
1048 }
1049
1050 #[test]
1051 fn empty_path_is_rejected() {
1052 let mut tree = FileTree::new();
1053 let result = tree.insert(b"", make_regular_file(b"data"));
1054 assert!(matches!(result, Err(FileTreeError::PathEmpty)));
1055 }
1056
1057 #[test]
1058 fn not_a_directory_error() {
1059 let mut tree = FileTree::new();
1060 tree.insert(b"a", make_regular_file(b"file")).unwrap();
1061
1062 let result = tree.insert(b"a/b", make_regular_file(b"nested"));
1063 assert!(matches!(result, Err(FileTreeError::NotADirectory(_))));
1064 }
1065
1066 #[test]
1067 fn resource_limits_default() {
1068 let limits = ResourceLimits::default();
1069 assert_eq!(limits.max_total_size, 10 * 1024 * 1024 * 1024);
1070 assert_eq!(limits.max_file_size, 5 * 1024 * 1024 * 1024);
1071 assert_eq!(limits.max_entry_count, 1_000_000);
1072 assert_eq!(limits.max_path_length, 4096);
1073 assert_eq!(limits.max_path_depth, 128);
1074 assert_eq!(limits.max_symlink_target, 4096);
1075 }
1076}