Skip to main content

dumap_core/tree/
arena.rs

1use crate::category::FileCategory;
2use std::path::{Path, PathBuf};
3use std::time::SystemTime;
4
5/// Index into the arena-allocated node vector.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub struct NodeId(pub u32);
8
9/// A node in the arena-allocated file tree.
10#[derive(Debug, Clone)]
11pub struct TreeNode {
12    /// File or directory name (just the component, not full path)
13    pub name: String,
14    /// Kind-specific data
15    pub kind: NodeKind,
16    /// Total size of this node and all descendants (bytes)
17    pub total_size: u64,
18    /// Number of files in this subtree (1 for files, sum for dirs)
19    pub file_count: u64,
20    /// Parent node (None for root)
21    pub parent: Option<NodeId>,
22    /// Depth from root (root = 0)
23    pub depth: u16,
24}
25
26/// Whether a node is a file or directory.
27#[derive(Debug, Clone)]
28pub enum NodeKind {
29    File {
30        /// Size in bytes
31        size: u64,
32        /// File category (derived from extension)
33        category: FileCategory,
34        /// Last modified time
35        modified: Option<SystemTime>,
36    },
37    Directory {
38        /// Children NodeIds, sorted by total_size descending
39        children: Vec<NodeId>,
40    },
41}
42
43/// Arena-based file tree. All nodes stored in a single Vec for cache locality.
44#[derive(Debug)]
45pub struct FileTree {
46    /// All nodes, indexed by NodeId
47    nodes: Vec<TreeNode>,
48    /// Root node
49    root: NodeId,
50    /// The scanned path
51    scan_root: PathBuf,
52}
53
54impl FileTree {
55    /// Get a node by ID.
56    pub fn node(&self, id: NodeId) -> &TreeNode {
57        debug_assert!(
58            (id.0 as usize) < self.nodes.len(),
59            "invalid NodeId: {} (tree has {} nodes)",
60            id.0,
61            self.nodes.len()
62        );
63        &self.nodes[id.0 as usize]
64    }
65
66    /// Get the root NodeId.
67    pub fn root(&self) -> NodeId {
68        self.root
69    }
70
71    /// The scanned root path.
72    pub fn scan_root(&self) -> &Path {
73        &self.scan_root
74    }
75
76    /// Total number of nodes in the tree.
77    pub fn len(&self) -> usize {
78        self.nodes.len()
79    }
80
81    /// Whether the tree is empty.
82    pub fn is_empty(&self) -> bool {
83        self.nodes.is_empty()
84    }
85
86    /// Get direct children of a node (empty for files).
87    pub fn children(&self, id: NodeId) -> &[NodeId] {
88        match &self.node(id).kind {
89            NodeKind::Directory { children } => children,
90            NodeKind::File { .. } => &[],
91        }
92    }
93
94    /// Build the relative path for a node by walking up to (but excluding) the root.
95    ///
96    /// The root node represents `scan_root` itself, so it is excluded to avoid
97    /// duplication when callers do `scan_root.join(path(...))`.
98    pub fn path(&self, id: NodeId) -> PathBuf {
99        let mut components = Vec::new();
100        let mut current = id;
101        loop {
102            let node = self.node(current);
103            match node.parent {
104                Some(parent) => {
105                    components.push(node.name.as_str());
106                    current = parent;
107                }
108                None => break, // skip root node
109            }
110        }
111        components.reverse();
112        let mut path = PathBuf::new();
113        for c in components {
114            path.push(c);
115        }
116        path
117    }
118}
119
120/// Build a FileTree from a DirNode tree.
121///
122/// This converts the HashMap-based DirNode tree into an arena-allocated
123/// FileTree for efficient layout traversal. Children are sorted by
124/// total_size descending.
125pub fn build_file_tree(dir_node: &super::DirNode, scan_root: PathBuf) -> FileTree {
126    let mut nodes = Vec::new();
127    let root_name = scan_root
128        .file_name()
129        .map(|n| n.to_string_lossy().into_owned())
130        .unwrap_or_else(|| scan_root.to_string_lossy().into_owned());
131
132    build_recursive(dir_node, &root_name, None, 0, &mut nodes);
133
134    let root = NodeId(0);
135    FileTree {
136        nodes,
137        root,
138        scan_root,
139    }
140}
141
142fn build_recursive(
143    dir_node: &super::DirNode,
144    name: &str,
145    parent: Option<NodeId>,
146    depth: u16,
147    nodes: &mut Vec<TreeNode>,
148) -> NodeId {
149    let my_id = NodeId(nodes.len() as u32);
150
151    if dir_node.children.is_empty() {
152        // Leaf file
153        let category = FileCategory::from_path(Path::new(name));
154        nodes.push(TreeNode {
155            name: name.to_string(),
156            kind: NodeKind::File {
157                size: dir_node.file_size,
158                category,
159                modified: None,
160            },
161            total_size: dir_node.file_size,
162            file_count: 1,
163            parent,
164            depth,
165        });
166        return my_id;
167    }
168
169    // Directory — push a placeholder, then recurse into children
170    nodes.push(TreeNode {
171        name: name.to_string(),
172        kind: NodeKind::Directory {
173            children: Vec::new(),
174        },
175        total_size: 0,
176        file_count: 0,
177        parent,
178        depth,
179    });
180
181    let mut child_ids: Vec<NodeId> = Vec::with_capacity(dir_node.children.len());
182    let mut total_size = dir_node.file_size; // files directly in this dir
183    let mut file_count: u64 = dir_node.file_count as u64;
184
185    for (child_name, child_node) in &dir_node.children {
186        let child_id = build_recursive(child_node, child_name, Some(my_id), depth + 1, nodes);
187        let child = &nodes[child_id.0 as usize];
188        total_size += child.total_size;
189        file_count += child.file_count;
190        child_ids.push(child_id);
191    }
192
193    // Sort children by total_size descending (for squarified layout)
194    child_ids.sort_by(|a, b| {
195        let sa = nodes[b.0 as usize].total_size;
196        let sb = nodes[a.0 as usize].total_size;
197        sa.cmp(&sb)
198    });
199
200    // Update the placeholder
201    let node = &mut nodes[my_id.0 as usize];
202    node.kind = NodeKind::Directory {
203        children: child_ids,
204    };
205    node.total_size = total_size;
206    node.file_count = file_count;
207
208    my_id
209}
210
211#[cfg(test)]
212#[path = "arena_tests.rs"]
213mod arena_tests;