Skip to main content

arcbox_ext4/
file_tree.rs

1// In-memory file tree used by the Formatter to track the directory structure
2// being built.  Each node holds an inode number, name, optional block ranges,
3// and pointers (indices) into the flat `nodes` vector.
4
5use std::path::{Path, PathBuf};
6
7pub type InodeNumber = u32;
8
9/// A block range [start, end) in units of filesystem blocks.
10#[derive(Debug, Clone, Copy)]
11pub struct BlockRange {
12    pub start: u32,
13    pub end: u32,
14}
15
16/// A node in the in-memory file tree.
17pub struct FileTreeNode {
18    pub inode: InodeNumber,
19    pub name: String,
20    /// Indices into `FileTree::nodes` for this node's children.
21    pub children: Vec<usize>,
22    /// Index into `FileTree::nodes` for the parent (None for root).
23    pub parent: Option<usize>,
24    /// Primary data block range allocated to this node.
25    pub blocks: Option<BlockRange>,
26    /// Additional block ranges (for files spanning multiple extents).
27    pub additional_blocks: Vec<BlockRange>,
28    /// If this entry is a hard link, the target inode number.
29    pub link: Option<InodeNumber>,
30}
31
32/// In-memory file tree tracking directory structure during formatting.
33pub struct FileTree {
34    nodes: Vec<FileTreeNode>,
35    root: usize,
36}
37
38impl FileTree {
39    /// Create a new file tree with a single root node.
40    pub fn new(root_inode: InodeNumber, name: &str) -> Self {
41        let root = FileTreeNode {
42            inode: root_inode,
43            name: name.to_string(),
44            children: Vec::new(),
45            parent: None,
46            blocks: None,
47            additional_blocks: Vec::new(),
48            link: None,
49        };
50        Self {
51            nodes: vec![root],
52            root: 0,
53        }
54    }
55
56    /// Return the index of the root node.
57    #[inline]
58    pub fn root(&self) -> usize {
59        self.root
60    }
61
62    /// Borrow a node by index.
63    #[inline]
64    pub fn node(&self, idx: usize) -> &FileTreeNode {
65        &self.nodes[idx]
66    }
67
68    /// Mutably borrow a node by index.
69    #[inline]
70    pub fn node_mut(&mut self, idx: usize) -> &mut FileTreeNode {
71        &mut self.nodes[idx]
72    }
73
74    /// Look up a node by path, walking the tree from the root.
75    ///
76    /// The path is split into components.  A leading "/" or empty first component
77    /// is skipped so that both "/foo/bar" and "foo/bar" resolve identically.
78    /// Returns `None` if any component is not found.
79    pub fn lookup(&self, path: &Path) -> Option<usize> {
80        let mut current = self.root;
81
82        for component in path.components() {
83            let name = component.as_os_str().to_str()?;
84
85            // Skip the root directory prefix.
86            if name == "/" || name.is_empty() {
87                continue;
88            }
89
90            let node = &self.nodes[current];
91            let found = node.children.iter().find(|&&child_idx| {
92                self.nodes[child_idx].name == name
93            });
94
95            match found {
96                Some(&child_idx) => current = child_idx,
97                None => return None,
98            }
99        }
100
101        Some(current)
102    }
103
104    /// Add a child node under `parent`, returning the new node's index.
105    pub fn add_child(&mut self, parent: usize, mut node: FileTreeNode) -> usize {
106        let idx = self.nodes.len();
107        node.parent = Some(parent);
108        self.nodes.push(node);
109        self.nodes[parent].children.push(idx);
110        idx
111    }
112
113    /// Remove the first child with the given `name` from `parent`'s children list.
114    ///
115    /// The node itself remains in the vector (indices are stable), but the
116    /// parent no longer references it.
117    pub fn remove_child(&mut self, parent: usize, name: &str) {
118        let pos = self.nodes[parent]
119            .children
120            .iter()
121            .position(|&child_idx| self.nodes[child_idx].name == name);
122
123        if let Some(pos) = pos {
124            self.nodes[parent].children.remove(pos);
125        }
126    }
127
128    /// Reconstruct the full path of a node by walking the parent chain.
129    pub fn node_path(&self, idx: usize) -> PathBuf {
130        let mut parts = Vec::new();
131        let mut current = idx;
132
133        loop {
134            parts.push(self.nodes[current].name.as_str());
135            match self.nodes[current].parent {
136                Some(parent) => current = parent,
137                None => break,
138            }
139        }
140
141        parts.reverse();
142
143        let mut path = PathBuf::new();
144        for part in &parts {
145            path.push(part);
146        }
147        path
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_new_tree_has_root() {
157        let tree = FileTree::new(2, "/");
158        assert_eq!(tree.root(), 0);
159        assert_eq!(tree.node(0).inode, 2);
160        assert_eq!(tree.node(0).name, "/");
161    }
162
163    #[test]
164    fn test_add_and_lookup() {
165        let mut tree = FileTree::new(2, "/");
166        let child = FileTreeNode {
167            inode: 11,
168            name: "etc".to_string(),
169            children: Vec::new(),
170            parent: None,
171            blocks: None,
172            additional_blocks: Vec::new(),
173            link: None,
174        };
175        let etc_idx = tree.add_child(tree.root(), child);
176
177        let grandchild = FileTreeNode {
178            inode: 12,
179            name: "passwd".to_string(),
180            children: Vec::new(),
181            parent: None,
182            blocks: None,
183            additional_blocks: Vec::new(),
184            link: None,
185        };
186        tree.add_child(etc_idx, grandchild);
187
188        // Lookup with leading slash.
189        assert_eq!(tree.lookup(Path::new("/etc/passwd")), Some(2));
190
191        // Lookup without leading slash.
192        assert_eq!(tree.lookup(Path::new("etc/passwd")), Some(2));
193
194        // Lookup directory itself.
195        assert_eq!(tree.lookup(Path::new("/etc")), Some(1));
196
197        // Lookup root.
198        assert_eq!(tree.lookup(Path::new("/")), Some(0));
199
200        // Missing path.
201        assert_eq!(tree.lookup(Path::new("/etc/shadow")), None);
202    }
203
204    #[test]
205    fn test_remove_child() {
206        let mut tree = FileTree::new(2, "/");
207        let child_a = FileTreeNode {
208            inode: 11,
209            name: "a".to_string(),
210            children: Vec::new(),
211            parent: None,
212            blocks: None,
213            additional_blocks: Vec::new(),
214            link: None,
215        };
216        let child_b = FileTreeNode {
217            inode: 12,
218            name: "b".to_string(),
219            children: Vec::new(),
220            parent: None,
221            blocks: None,
222            additional_blocks: Vec::new(),
223            link: None,
224        };
225        tree.add_child(tree.root(), child_a);
226        tree.add_child(tree.root(), child_b);
227
228        assert_eq!(tree.node(tree.root()).children.len(), 2);
229
230        tree.remove_child(tree.root(), "a");
231        assert_eq!(tree.node(tree.root()).children.len(), 1);
232        assert_eq!(tree.node(tree.node(tree.root()).children[0]).name, "b");
233    }
234
235    #[test]
236    fn test_node_path() {
237        let mut tree = FileTree::new(2, "/");
238        let etc = FileTreeNode {
239            inode: 11,
240            name: "etc".to_string(),
241            children: Vec::new(),
242            parent: None,
243            blocks: None,
244            additional_blocks: Vec::new(),
245            link: None,
246        };
247        let etc_idx = tree.add_child(tree.root(), etc);
248
249        let passwd = FileTreeNode {
250            inode: 12,
251            name: "passwd".to_string(),
252            children: Vec::new(),
253            parent: None,
254            blocks: None,
255            additional_blocks: Vec::new(),
256            link: None,
257        };
258        let passwd_idx = tree.add_child(etc_idx, passwd);
259
260        let path = tree.node_path(passwd_idx);
261        assert_eq!(path, PathBuf::from("/etc/passwd"));
262    }
263}