1use std::collections::HashSet;
2use std::io::{self, BufRead, Write};
3use std::os::unix::fs::MetadataExt;
4use std::path::{Path, PathBuf};
5use std::time::SystemTime;
6
7pub struct DuConfig {
9 pub all: bool,
11 pub apparent_size: bool,
13 pub block_size: u64,
15 pub human_readable: bool,
17 pub si: bool,
19 pub total: bool,
21 pub max_depth: Option<usize>,
23 pub summarize: bool,
25 pub one_file_system: bool,
27 pub dereference: bool,
29 pub separate_dirs: bool,
31 pub count_links: bool,
33 pub null_terminator: bool,
35 pub threshold: Option<i64>,
37 pub show_time: bool,
39 pub time_style: String,
41 pub exclude_patterns: Vec<String>,
43 pub inodes: bool,
45}
46
47impl Default for DuConfig {
48 fn default() -> Self {
49 DuConfig {
50 all: false,
51 apparent_size: false,
52 block_size: 1024,
53 human_readable: false,
54 si: false,
55 total: false,
56 max_depth: None,
57 summarize: false,
58 one_file_system: false,
59 dereference: false,
60 separate_dirs: false,
61 count_links: false,
62 null_terminator: false,
63 threshold: None,
64 show_time: false,
65 time_style: "long-iso".to_string(),
66 exclude_patterns: Vec::new(),
67 inodes: false,
68 }
69 }
70}
71
72pub struct DuEntry {
74 pub size: u64,
76 pub path: PathBuf,
78 pub mtime: Option<i64>,
80}
81
82pub fn du_path(path: &Path, config: &DuConfig) -> io::Result<Vec<DuEntry>> {
84 let mut seen_inodes: HashSet<(u64, u64)> = HashSet::new();
85 let mut entries = Vec::new();
86 du_recursive(path, config, &mut seen_inodes, &mut entries, 0, None)?;
87 Ok(entries)
88}
89
90fn du_recursive(
92 path: &Path,
93 config: &DuConfig,
94 seen: &mut HashSet<(u64, u64)>,
95 entries: &mut Vec<DuEntry>,
96 depth: usize,
97 root_dev: Option<u64>,
98) -> io::Result<u64> {
99 let meta = if config.dereference {
100 std::fs::metadata(path)?
101 } else {
102 std::fs::symlink_metadata(path)?
103 };
104
105 if let Some(dev) = root_dev {
107 if meta.dev() != dev && config.one_file_system {
108 return Ok(0);
109 }
110 }
111
112 let ino_key = (meta.dev(), meta.ino());
114 if meta.nlink() > 1 && !config.count_links {
115 if !seen.insert(ino_key) {
116 return Ok(0);
117 }
118 }
119
120 let size = if config.inodes {
121 1
122 } else if config.apparent_size {
123 meta.len()
124 } else {
125 meta.blocks() * 512
126 };
127
128 let mtime = meta.mtime();
129
130 if meta.is_dir() {
131 let mut subtree_size: u64 = size;
134 let mut display_size: u64 = size;
136
137 let read_dir = match std::fs::read_dir(path) {
138 Ok(rd) => rd,
139 Err(e) => {
140 eprintln!(
141 "du: cannot read directory '{}': {}",
142 path.display(),
143 format_io_error(&e)
144 );
145 if should_report_dir(config, depth) {
147 entries.push(DuEntry {
148 size,
149 path: path.to_path_buf(),
150 mtime: if config.show_time { Some(mtime) } else { None },
151 });
152 }
153 return Ok(size);
154 }
155 };
156
157 for entry in read_dir {
158 let entry = match entry {
159 Ok(e) => e,
160 Err(e) => {
161 eprintln!(
162 "du: cannot access entry in '{}': {}",
163 path.display(),
164 format_io_error(&e)
165 );
166 continue;
167 }
168 };
169 let child_path = entry.path();
170
171 if let Some(name) = child_path.file_name() {
173 let name_str = name.to_string_lossy();
174 if config
175 .exclude_patterns
176 .iter()
177 .any(|pat| glob_match(pat, &name_str))
178 {
179 continue;
180 }
181 }
182
183 let child_is_dir = child_path.symlink_metadata().map_or(false, |m| m.is_dir());
185
186 let child_size = du_recursive(
187 &child_path,
188 config,
189 seen,
190 entries,
191 depth + 1,
192 Some(root_dev.unwrap_or(meta.dev())),
193 )?;
194 subtree_size += child_size;
195 if config.separate_dirs && child_is_dir {
196 } else {
198 display_size += child_size;
199 }
200 }
201
202 if should_report_dir(config, depth) {
204 entries.push(DuEntry {
205 size: display_size,
206 path: path.to_path_buf(),
207 mtime: if config.show_time { Some(mtime) } else { None },
208 });
209 }
210
211 Ok(subtree_size)
212 } else {
213 if config.all && within_depth(config, depth) {
215 entries.push(DuEntry {
216 size,
217 path: path.to_path_buf(),
218 mtime: if config.show_time { Some(mtime) } else { None },
219 });
220 }
221 Ok(size)
222 }
223}
224
225fn should_report_dir(config: &DuConfig, depth: usize) -> bool {
227 if config.summarize {
228 return depth == 0;
229 }
230 within_depth(config, depth)
231}
232
233fn within_depth(config: &DuConfig, depth: usize) -> bool {
235 match config.max_depth {
236 Some(max) => depth <= max,
237 None => true,
238 }
239}
240
241pub fn glob_match(pattern: &str, text: &str) -> bool {
243 let pat: Vec<char> = pattern.chars().collect();
244 let txt: Vec<char> = text.chars().collect();
245 glob_match_inner(&pat, &txt)
246}
247
248fn glob_match_inner(pat: &[char], txt: &[char]) -> bool {
249 let mut pi = 0;
250 let mut ti = 0;
251 let mut star_pi = usize::MAX;
252 let mut star_ti = 0;
253
254 while ti < txt.len() {
255 if pi < pat.len() && (pat[pi] == '?' || pat[pi] == txt[ti]) {
256 pi += 1;
257 ti += 1;
258 } else if pi < pat.len() && pat[pi] == '*' {
259 star_pi = pi;
260 star_ti = ti;
261 pi += 1;
262 } else if star_pi != usize::MAX {
263 pi = star_pi + 1;
264 star_ti += 1;
265 ti = star_ti;
266 } else {
267 return false;
268 }
269 }
270
271 while pi < pat.len() && pat[pi] == '*' {
272 pi += 1;
273 }
274 pi == pat.len()
275}
276
277pub fn format_size(raw_bytes: u64, config: &DuConfig) -> String {
279 if config.human_readable {
280 human_readable(raw_bytes, 1024)
281 } else if config.si {
282 human_readable(raw_bytes, 1000)
283 } else if config.inodes {
284 raw_bytes.to_string()
285 } else {
286 let scaled = (raw_bytes + config.block_size - 1) / config.block_size;
288 scaled.to_string()
289 }
290}
291
292fn human_readable(bytes: u64, base: u64) -> String {
294 let suffixes = if base == 1024 {
295 &["", "K", "M", "G", "T", "P", "E"]
296 } else {
297 &["", "k", "M", "G", "T", "P", "E"]
298 };
299
300 if bytes < base {
301 return format!("{}", bytes);
302 }
303
304 let mut value = bytes as f64;
305 let mut idx = 0;
306 while value >= base as f64 && idx + 1 < suffixes.len() {
307 value /= base as f64;
308 idx += 1;
309 }
310
311 if value >= 10.0 {
312 format!("{:.0}{}", value, suffixes[idx])
313 } else {
314 let formatted = format!("{:.1}{}", value, suffixes[idx]);
316 formatted.replace(".0", "").replacen(".0", "", 1) }
319}
320
321pub fn format_time(epoch_secs: i64, style: &str) -> String {
323 let secs = epoch_secs;
325 let st = match SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(secs as u64)) {
326 Some(t) => t,
327 None => return String::from("?"),
328 };
329
330 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
332 let time_t = secs as libc::time_t;
333 unsafe {
334 libc::localtime_r(&time_t, &mut tm);
335 }
336 let _ = st;
338
339 let year = tm.tm_year + 1900;
340 let mon = tm.tm_mon + 1;
341 let day = tm.tm_mday;
342 let hour = tm.tm_hour;
343 let min = tm.tm_min;
344 let sec = tm.tm_sec;
345
346 match style {
347 "full-iso" => format!(
348 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.000000000 +0000",
349 year, mon, day, hour, min, sec
350 ),
351 "iso" => format!("{:04}-{:02}-{:02}", year, mon, day),
352 _ => {
353 format!("{:04}-{:02}-{:02} {:02}:{:02}", year, mon, day, hour, min)
355 }
356 }
357}
358
359pub fn print_entry<W: Write>(out: &mut W, entry: &DuEntry, config: &DuConfig) -> io::Result<()> {
361 if let Some(thresh) = config.threshold {
363 let size_signed = entry.size as i64;
364 if thresh >= 0 && size_signed < thresh {
365 return Ok(());
366 }
367 if thresh < 0 && size_signed > thresh.unsigned_abs() as i64 {
368 return Ok(());
369 }
370 }
371
372 let size_str = format_size(entry.size, config);
373
374 if config.show_time {
375 if let Some(mtime) = entry.mtime {
376 let time_str = format_time(mtime, &config.time_style);
377 write!(out, "{}\t{}\t{}", size_str, time_str, entry.path.display())?;
378 } else {
379 write!(out, "{}\t{}", size_str, entry.path.display())?;
380 }
381 } else {
382 write!(out, "{}\t{}", size_str, entry.path.display())?;
383 }
384
385 if config.null_terminator {
386 out.write_all(b"\0")?;
387 } else {
388 out.write_all(b"\n")?;
389 }
390
391 Ok(())
392}
393
394pub fn parse_block_size(s: &str) -> Result<u64, String> {
397 let s = s.trim();
398 if s.is_empty() {
399 return Err("empty block size".to_string());
400 }
401
402 let mut num_end = 0;
403 for (i, c) in s.char_indices() {
404 if c.is_ascii_digit() {
405 num_end = i + 1;
406 } else {
407 break;
408 }
409 }
410
411 let (num_str, suffix) = s.split_at(num_end);
412 let base_val: u64 = if num_str.is_empty() {
413 1
414 } else {
415 num_str
416 .parse()
417 .map_err(|_| format!("invalid block size: '{}'", s))?
418 };
419
420 let multiplier = match suffix.to_uppercase().as_str() {
421 "" => 1u64,
422 "B" => 1,
423 "K" | "KB" => 1024,
424 "M" | "MB" => 1024 * 1024,
425 "G" | "GB" => 1024 * 1024 * 1024,
426 "T" | "TB" => 1024u64 * 1024 * 1024 * 1024,
427 "P" | "PB" => 1024u64 * 1024 * 1024 * 1024 * 1024,
428 "KB_SI" => 1000,
429 _ => return Err(format!("invalid suffix in block size: '{}'", s)),
430 };
431
432 Ok(base_val * multiplier)
433}
434
435pub fn parse_threshold(s: &str) -> Result<i64, String> {
438 let s = s.trim();
439 let (negative, rest) = if let Some(stripped) = s.strip_prefix('-') {
440 (true, stripped)
441 } else {
442 (false, s)
443 };
444
445 let val = parse_block_size(rest)? as i64;
446 if negative { Ok(-val) } else { Ok(val) }
447}
448
449pub fn read_exclude_file(path: &str) -> io::Result<Vec<String>> {
451 let file = std::fs::File::open(path)?;
452 let reader = io::BufReader::new(file);
453 let mut patterns = Vec::new();
454 for line in reader.lines() {
455 let line = line?;
456 let trimmed = line.trim();
457 if !trimmed.is_empty() {
458 patterns.push(trimmed.to_string());
459 }
460 }
461 Ok(patterns)
462}
463
464fn format_io_error(e: &io::Error) -> String {
466 if let Some(raw) = e.raw_os_error() {
467 let os_err = io::Error::from_raw_os_error(raw);
468 let msg = format!("{}", os_err);
469 msg.replace(&format!(" (os error {})", raw), "")
470 } else {
471 format!("{}", e)
472 }
473}