1use std::time::SystemTime;
4
5use compact_str::CompactString;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct NodeId(pub u64);
11
12impl NodeId {
13 pub fn new(id: u64) -> Self {
15 Self(id)
16 }
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub struct ContentHash(pub [u8; 32]);
22
23impl ContentHash {
24 pub fn new(bytes: [u8; 32]) -> Self {
26 Self(bytes)
27 }
28
29 pub fn to_hex(&self) -> String {
31 self.0.iter().map(|b| format!("{b:02x}")).collect()
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub struct InodeInfo {
38 pub inode: u64,
40 pub device: u64,
42}
43
44impl InodeInfo {
45 pub fn new(inode: u64, device: u64) -> Self {
47 Self { inode, device }
48 }
49}
50
51#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
53pub struct Timestamps {
54 pub modified: SystemTime,
56 pub accessed: Option<SystemTime>,
58 pub created: Option<SystemTime>,
60}
61
62impl Timestamps {
63 pub fn with_modified(modified: SystemTime) -> Self {
65 Self {
66 modified,
67 accessed: None,
68 created: None,
69 }
70 }
71
72 pub fn new(
74 modified: SystemTime,
75 accessed: Option<SystemTime>,
76 created: Option<SystemTime>,
77 ) -> Self {
78 Self {
79 modified,
80 accessed,
81 created,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub enum NodeKind {
89 File {
91 executable: bool,
93 },
94 Directory {
96 file_count: u64,
98 dir_count: u64,
100 },
101 Symlink {
103 target: CompactString,
105 broken: bool,
107 },
108 Other,
110}
111
112impl NodeKind {
113 pub fn is_dir(&self) -> bool {
115 matches!(self, NodeKind::Directory { .. })
116 }
117
118 pub fn is_file(&self) -> bool {
120 matches!(self, NodeKind::File { .. })
121 }
122
123 pub fn is_symlink(&self) -> bool {
125 matches!(self, NodeKind::Symlink { .. })
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct FileNode {
132 pub id: NodeId,
134
135 pub name: CompactString,
137
138 pub kind: NodeKind,
140
141 pub size: u64,
143
144 pub blocks: u64,
146
147 pub timestamps: Timestamps,
149
150 pub inode: Option<InodeInfo>,
152
153 pub content_hash: Option<ContentHash>,
155
156 pub children: Vec<FileNode>,
158}
159
160impl FileNode {
161 pub fn new_file(
163 id: NodeId,
164 name: impl Into<CompactString>,
165 size: u64,
166 blocks: u64,
167 timestamps: Timestamps,
168 executable: bool,
169 ) -> Self {
170 Self {
171 id,
172 name: name.into(),
173 kind: NodeKind::File { executable },
174 size,
175 blocks,
176 timestamps,
177 inode: None,
178 content_hash: None,
179 children: Vec::new(),
180 }
181 }
182
183 pub fn new_directory(
185 id: NodeId,
186 name: impl Into<CompactString>,
187 timestamps: Timestamps,
188 ) -> Self {
189 Self {
190 id,
191 name: name.into(),
192 kind: NodeKind::Directory {
193 file_count: 0,
194 dir_count: 0,
195 },
196 size: 0,
197 blocks: 0,
198 timestamps,
199 inode: None,
200 content_hash: None,
201 children: Vec::new(),
202 }
203 }
204
205 pub fn is_dir(&self) -> bool {
207 self.kind.is_dir()
208 }
209
210 pub fn is_file(&self) -> bool {
212 self.kind.is_file()
213 }
214
215 pub fn child_count(&self) -> usize {
217 self.children.len()
218 }
219
220 pub fn file_count(&self) -> u64 {
222 match &self.kind {
223 NodeKind::Directory { file_count, .. } => *file_count,
224 NodeKind::File { .. } => 1,
225 _ => 0,
226 }
227 }
228
229 pub fn dir_count(&self) -> u64 {
231 match &self.kind {
232 NodeKind::Directory { dir_count, .. } => *dir_count,
233 _ => 0,
234 }
235 }
236
237 pub fn sort_children_by_size(&mut self) {
239 self.children.sort_by(|a, b| b.size.cmp(&a.size));
240 for child in &mut self.children {
241 child.sort_children_by_size();
242 }
243 }
244
245 pub fn update_counts(&mut self) {
247 if let NodeKind::Directory {
248 ref mut file_count,
249 ref mut dir_count,
250 } = self.kind
251 {
252 *file_count = 0;
253 *dir_count = 0;
254
255 for child in &self.children {
256 match &child.kind {
257 NodeKind::File { .. } => *file_count += 1,
258 NodeKind::Directory {
259 file_count: fc,
260 dir_count: dc,
261 } => {
262 *file_count += fc;
263 *dir_count += dc + 1;
264 }
265 _ => {}
266 }
267 }
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_node_id() {
278 let id = NodeId::new(42);
279 assert_eq!(id.0, 42);
280 }
281
282 #[test]
283 fn test_content_hash_hex() {
284 let hash = ContentHash::new([0xab; 32]);
285 assert_eq!(hash.to_hex().len(), 64);
286 assert!(hash.to_hex().starts_with("abab"));
287 }
288
289 #[test]
290 fn test_file_node_creation() {
291 let node = FileNode::new_file(
292 NodeId::new(1),
293 "test.txt",
294 1024,
295 2,
296 Timestamps::with_modified(SystemTime::now()),
297 false,
298 );
299 assert!(node.is_file());
300 assert!(!node.is_dir());
301 assert_eq!(node.size, 1024);
302 }
303
304 #[test]
305 fn test_directory_node_creation() {
306 let node = FileNode::new_directory(
307 NodeId::new(1),
308 "test_dir",
309 Timestamps::with_modified(SystemTime::now()),
310 );
311 assert!(node.is_dir());
312 assert!(!node.is_file());
313 }
314}