uu_du/
du.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5// spell-checker:ignore fstatat openat dirfd
6
7use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue};
8use glob::Pattern;
9use std::collections::HashSet;
10use std::env;
11use std::ffi::OsStr;
12use std::ffi::OsString;
13use std::fs::Metadata;
14use std::fs::{self, DirEntry, File};
15use std::io::{BufRead, BufReader, stdout};
16#[cfg(not(windows))]
17use std::os::unix::fs::MetadataExt;
18#[cfg(windows)]
19use std::os::windows::io::AsRawHandle;
20use std::path::{Path, PathBuf};
21use std::str::FromStr;
22use std::sync::mpsc;
23use std::thread;
24use thiserror::Error;
25use uucore::display::{Quotable, print_verbatim};
26use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code};
27use uucore::fsext::{MetadataTimeField, metadata_get_time};
28use uucore::line_ending::LineEnding;
29#[cfg(target_os = "linux")]
30use uucore::safe_traversal::DirFd;
31use uucore::translate;
32
33use uucore::parser::parse_glob;
34use uucore::parser::parse_size::{ParseSizeError, parse_size_non_zero_u64, parse_size_u64};
35use uucore::parser::shortcut_value_parser::ShortcutValueParser;
36use uucore::time::{FormatSystemTimeFallback, format, format_system_time};
37use uucore::{format_usage, show, show_error, show_warning};
38#[cfg(windows)]
39use windows_sys::Win32::Foundation::HANDLE;
40#[cfg(windows)]
41use windows_sys::Win32::Storage::FileSystem::{
42    FILE_ID_128, FILE_ID_INFO, FILE_STANDARD_INFO, FileIdInfo, FileStandardInfo,
43    GetFileInformationByHandleEx,
44};
45
46mod options {
47    pub const HELP: &str = "help";
48    pub const NULL: &str = "0";
49    pub const ALL: &str = "all";
50    pub const APPARENT_SIZE: &str = "apparent-size";
51    pub const BLOCK_SIZE: &str = "block-size";
52    pub const BYTES: &str = "b";
53    pub const TOTAL: &str = "c";
54    pub const MAX_DEPTH: &str = "d";
55    pub const HUMAN_READABLE: &str = "h";
56    pub const BLOCK_SIZE_1K: &str = "k";
57    pub const COUNT_LINKS: &str = "l";
58    pub const BLOCK_SIZE_1M: &str = "m";
59    pub const SEPARATE_DIRS: &str = "S";
60    pub const SUMMARIZE: &str = "s";
61    pub const THRESHOLD: &str = "threshold";
62    pub const SI: &str = "si";
63    pub const TIME: &str = "time";
64    pub const TIME_STYLE: &str = "time-style";
65    pub const ONE_FILE_SYSTEM: &str = "one-file-system";
66    pub const DEREFERENCE: &str = "dereference";
67    pub const DEREFERENCE_ARGS: &str = "dereference-args";
68    pub const NO_DEREFERENCE: &str = "no-dereference";
69    pub const INODES: &str = "inodes";
70    pub const EXCLUDE: &str = "exclude";
71    pub const EXCLUDE_FROM: &str = "exclude-from";
72    pub const FILES0_FROM: &str = "files0-from";
73    pub const VERBOSE: &str = "verbose";
74    pub const FILE: &str = "FILE";
75}
76
77struct TraversalOptions {
78    all: bool,
79    separate_dirs: bool,
80    one_file_system: bool,
81    dereference: Deref,
82    count_links: bool,
83    verbose: bool,
84    excludes: Vec<Pattern>,
85}
86
87struct StatPrinter {
88    total: bool,
89    inodes: bool,
90    max_depth: Option<usize>,
91    threshold: Option<Threshold>,
92    apparent_size: bool,
93    size_format: SizeFormat,
94    time: Option<MetadataTimeField>,
95    time_format: String,
96    line_ending: LineEnding,
97    summarize: bool,
98    total_text: String,
99}
100
101#[derive(PartialEq, Clone)]
102enum Deref {
103    All,
104    Args(Vec<PathBuf>),
105    None,
106}
107
108#[derive(Clone)]
109enum SizeFormat {
110    HumanDecimal,
111    HumanBinary,
112    BlockSize(u64),
113}
114
115#[derive(PartialEq, Eq, Hash, Clone, Copy)]
116struct FileInfo {
117    file_id: u128,
118    dev_id: u64,
119}
120
121struct Stat {
122    path: PathBuf,
123    size: u64,
124    blocks: u64,
125    inodes: u64,
126    inode: Option<FileInfo>,
127    metadata: Metadata,
128}
129
130impl Stat {
131    fn new(
132        path: &Path,
133        dir_entry: Option<&DirEntry>,
134        options: &TraversalOptions,
135    ) -> std::io::Result<Self> {
136        // Determine whether to dereference (follow) the symbolic link
137        let should_dereference = match &options.dereference {
138            Deref::All => true,
139            Deref::Args(paths) => paths.contains(&path.to_path_buf()),
140            Deref::None => false,
141        };
142
143        let metadata = if should_dereference {
144            // Get metadata, following symbolic links if necessary
145            fs::metadata(path)
146        } else if let Some(dir_entry) = dir_entry {
147            // Get metadata directly from the DirEntry, which is faster on Windows
148            dir_entry.metadata()
149        } else {
150            // Get metadata from the filesystem without following symbolic links
151            fs::symlink_metadata(path)
152        }?;
153
154        let file_info = get_file_info(path, &metadata);
155        let blocks = get_blocks(path, &metadata);
156
157        Ok(Self {
158            path: path.to_path_buf(),
159            size: if metadata.is_dir() { 0 } else { metadata.len() },
160            blocks,
161            inodes: 1,
162            inode: file_info,
163            metadata,
164        })
165    }
166
167    /// Create a Stat using safe traversal methods with `DirFd` for the root directory
168    #[cfg(target_os = "linux")]
169    fn new_from_dirfd(dir_fd: &DirFd, full_path: &Path) -> std::io::Result<Self> {
170        // Get metadata for the directory itself using fstat
171        let safe_metadata = dir_fd.metadata()?;
172
173        // Create file info from the safe metadata
174        let file_info = safe_metadata.file_info();
175        let file_info_option = Some(FileInfo {
176            file_id: file_info.inode() as u128,
177            dev_id: file_info.device(),
178        });
179
180        let blocks = safe_metadata.blocks();
181
182        // Create a temporary std::fs::Metadata by reading the same path
183        // This is still needed for compatibility but should work since we're dealing with
184        // the root path which should be accessible
185        let std_metadata = fs::symlink_metadata(full_path)?;
186
187        Ok(Self {
188            path: full_path.to_path_buf(),
189            size: if safe_metadata.is_dir() {
190                0
191            } else {
192                safe_metadata.len()
193            },
194            blocks,
195            inodes: 1,
196            inode: file_info_option,
197            metadata: std_metadata,
198        })
199    }
200}
201
202#[cfg(not(windows))]
203fn get_blocks(_path: &Path, metadata: &Metadata) -> u64 {
204    metadata.blocks()
205}
206
207#[cfg(windows)]
208fn get_blocks(path: &Path, _metadata: &Metadata) -> u64 {
209    let mut size_on_disk = 0;
210
211    // bind file so it stays in scope until end of function
212    // if it goes out of scope the handle below becomes invalid
213    let Ok(file) = File::open(path) else {
214        return size_on_disk; // opening directories will fail
215    };
216
217    unsafe {
218        let mut file_info: FILE_STANDARD_INFO = core::mem::zeroed();
219        let file_info_ptr: *mut FILE_STANDARD_INFO = &raw mut file_info;
220
221        let success = GetFileInformationByHandleEx(
222            file.as_raw_handle() as HANDLE,
223            FileStandardInfo,
224            file_info_ptr.cast(),
225            size_of::<FILE_STANDARD_INFO>() as u32,
226        );
227
228        if success != 0 {
229            size_on_disk = file_info.AllocationSize as u64;
230        }
231    }
232
233    size_on_disk / 1024 * 2
234}
235
236#[cfg(not(windows))]
237fn get_file_info(_path: &Path, metadata: &Metadata) -> Option<FileInfo> {
238    Some(FileInfo {
239        file_id: metadata.ino() as u128,
240        dev_id: metadata.dev(),
241    })
242}
243
244#[cfg(windows)]
245fn get_file_info(path: &Path, _metadata: &Metadata) -> Option<FileInfo> {
246    let mut result = None;
247
248    let Ok(file) = File::open(path) else {
249        return result;
250    };
251
252    unsafe {
253        let mut file_info: FILE_ID_INFO = core::mem::zeroed();
254        let file_info_ptr: *mut FILE_ID_INFO = &raw mut file_info;
255
256        let success = GetFileInformationByHandleEx(
257            file.as_raw_handle() as HANDLE,
258            FileIdInfo,
259            file_info_ptr.cast(),
260            size_of::<FILE_ID_INFO>() as u32,
261        );
262
263        if success != 0 {
264            result = Some(FileInfo {
265                file_id: std::mem::transmute::<FILE_ID_128, u128>(file_info.FileId),
266                dev_id: file_info.VolumeSerialNumber,
267            });
268        }
269    }
270
271    result
272}
273
274fn block_size_from_env() -> Option<u64> {
275    for env_var in ["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] {
276        if let Ok(env_size) = env::var(env_var) {
277            return parse_size_non_zero_u64(&env_size).ok();
278        }
279    }
280
281    None
282}
283
284fn read_block_size(s: Option<&str>) -> UResult<u64> {
285    if let Some(s) = s {
286        parse_size_u64(s)
287            .map_err(|e| USimpleError::new(1, format_error_message(&e, s, options::BLOCK_SIZE)))
288    } else if let Some(bytes) = block_size_from_env() {
289        Ok(bytes)
290    } else if env::var("POSIXLY_CORRECT").is_ok() {
291        Ok(512)
292    } else {
293        Ok(1024)
294    }
295}
296
297#[cfg(target_os = "linux")]
298// For now, implement safe_du only on Linux
299// This is done for Ubuntu but should be extended to other platforms that support openat
300fn safe_du(
301    path: &Path,
302    options: &TraversalOptions,
303    depth: usize,
304    seen_inodes: &mut HashSet<FileInfo>,
305    print_tx: &mpsc::Sender<UResult<StatPrintInfo>>,
306    parent_fd: Option<&DirFd>,
307) -> Result<Stat, Box<mpsc::SendError<UResult<StatPrintInfo>>>> {
308    // Get initial stat for this path - use DirFd if available to avoid path length issues
309    let mut my_stat = if let Some(parent_fd) = parent_fd {
310        // We have a parent fd, this is a subdirectory - use openat
311        let dir_name = path.file_name().unwrap_or(path.as_os_str());
312        match parent_fd.metadata_at(dir_name, false) {
313            Ok(safe_metadata) => {
314                // Create Stat from safe metadata
315                let file_info = safe_metadata.file_info();
316                let file_info_option = Some(FileInfo {
317                    file_id: file_info.inode() as u128,
318                    dev_id: file_info.device(),
319                });
320                let blocks = safe_metadata.blocks();
321
322                // For compatibility, still try to get std::fs::Metadata
323                // but fallback to a minimal approach if it fails
324                let std_metadata = fs::symlink_metadata(path).unwrap_or_else(|_| {
325                    // If we can't get std metadata, create a minimal fake one
326                    // This should rarely happen but provides a fallback
327                    fs::symlink_metadata("/").expect("root should be accessible")
328                });
329
330                Stat {
331                    path: path.to_path_buf(),
332                    size: if safe_metadata.is_dir() {
333                        0
334                    } else {
335                        safe_metadata.len()
336                    },
337                    blocks,
338                    inodes: 1,
339                    inode: file_info_option,
340                    metadata: std_metadata,
341                }
342            }
343            Err(e) => {
344                let error = e.map_err_context(
345                    || translate!("du-error-cannot-access", "path" => path.quote()),
346                );
347                if let Err(send_error) = print_tx.send(Err(error)) {
348                    return Err(Box::new(send_error));
349                }
350                return Err(Box::new(mpsc::SendError(Err(USimpleError::new(
351                    0,
352                    "Error already handled",
353                )))));
354            }
355        }
356    } else {
357        // This is the initial directory - try regular Stat::new first, then fallback to DirFd
358        match Stat::new(path, None, options) {
359            Ok(s) => s,
360            Err(_e) => {
361                // Try using our new DirFd method for the root directory
362                match DirFd::open(path) {
363                    Ok(dir_fd) => match Stat::new_from_dirfd(&dir_fd, path) {
364                        Ok(s) => s,
365                        Err(e) => {
366                            let error = e.map_err_context(
367                                || translate!("du-error-cannot-access", "path" => path.quote()),
368                            );
369                            if let Err(send_error) = print_tx.send(Err(error)) {
370                                return Err(Box::new(send_error));
371                            }
372                            return Err(Box::new(mpsc::SendError(Err(USimpleError::new(
373                                0,
374                                "Error already handled",
375                            )))));
376                        }
377                    },
378                    Err(e) => {
379                        let error = e.map_err_context(
380                            || translate!("du-error-cannot-access", "path" => path.quote()),
381                        );
382                        if let Err(send_error) = print_tx.send(Err(error)) {
383                            return Err(Box::new(send_error));
384                        }
385                        return Err(Box::new(mpsc::SendError(Err(USimpleError::new(
386                            0,
387                            "Error already handled",
388                        )))));
389                    }
390                }
391            }
392        }
393    };
394    if !my_stat.metadata.is_dir() {
395        return Ok(my_stat);
396    }
397
398    // Open the directory using DirFd
399    let open_result = match parent_fd {
400        Some(parent) => parent.open_subdir(path.file_name().unwrap_or(path.as_os_str())),
401        None => DirFd::open(path),
402    };
403
404    let dir_fd = match open_result {
405        Ok(fd) => fd,
406        Err(e) => {
407            print_tx.send(Err(e.map_err_context(
408                || translate!("du-error-cannot-read-directory", "path" => path.quote()),
409            )))?;
410            return Ok(my_stat);
411        }
412    };
413
414    // Read directory entries
415    let entries = match dir_fd.read_dir() {
416        Ok(entries) => entries,
417        Err(e) => {
418            print_tx.send(Err(e.map_err_context(
419                || translate!("du-error-cannot-read-directory", "path" => path.quote()),
420            )))?;
421            return Ok(my_stat);
422        }
423    };
424
425    'file_loop: for entry_name in entries {
426        let entry_path = path.join(&entry_name);
427
428        // First get the lstat (without following symlinks) to check if it's a symlink
429        let lstat = match dir_fd.stat_at(&entry_name, false) {
430            Ok(stat) => stat,
431            Err(e) => {
432                print_tx.send(Err(e.map_err_context(
433                    || translate!("du-error-cannot-access", "path" => entry_path.quote()),
434                )))?;
435                continue;
436            }
437        };
438
439        // Check if it's a symlink
440        const S_IFMT: u32 = 0o170_000;
441        const S_IFDIR: u32 = 0o040_000;
442        const S_IFLNK: u32 = 0o120_000;
443        let is_symlink = (lstat.st_mode & S_IFMT) == S_IFLNK;
444
445        // Handle symlinks with -L option
446        // For safe traversal with -L, we skip symlinks to directories entirely
447        // and let the non-safe traversal handle them at the top level
448        if is_symlink && options.dereference == Deref::All {
449            // Skip symlinks to directories when using safe traversal with -L
450            // They will be handled by regular traversal
451            continue;
452        }
453
454        let is_dir = (lstat.st_mode & S_IFMT) == S_IFDIR;
455        let entry_stat = lstat;
456
457        let file_info = (entry_stat.st_ino != 0).then_some(FileInfo {
458            file_id: entry_stat.st_ino as u128,
459            dev_id: entry_stat.st_dev,
460        });
461
462        // For safe traversal, we need to handle stats differently
463        // We can't use std::fs::Metadata since that requires the full path
464        let this_stat = if is_dir {
465            // For directories, recurse using safe_du
466            Stat {
467                path: entry_path.clone(),
468                size: 0,
469                blocks: entry_stat.st_blocks as u64,
470                inodes: 1,
471                inode: file_info,
472                // We need a fake metadata - create one from symlink_metadata of parent
473                // This is a workaround since we can't get real metadata without the full path
474                metadata: my_stat.metadata.clone(),
475            }
476        } else {
477            // For files
478            Stat {
479                path: entry_path.clone(),
480                size: entry_stat.st_size as u64,
481                blocks: entry_stat.st_blocks as u64,
482                inodes: 1,
483                inode: file_info,
484                metadata: my_stat.metadata.clone(),
485            }
486        };
487
488        // Check excludes
489        for pattern in &options.excludes {
490            if pattern.matches(&this_stat.path.to_string_lossy())
491                || pattern.matches(&entry_name.to_string_lossy())
492            {
493                if options.verbose {
494                    println!(
495                        "{}",
496                        translate!("du-verbose-ignored", "path" => this_stat.path.quote())
497                    );
498                }
499                continue 'file_loop;
500            }
501        }
502
503        // Handle inodes
504        if let Some(inode) = this_stat.inode {
505            if seen_inodes.contains(&inode) && (!options.count_links || !options.all) {
506                if options.count_links && !options.all {
507                    my_stat.inodes += 1;
508                }
509                continue;
510            }
511            seen_inodes.insert(inode);
512        }
513
514        // Process directories recursively
515        if is_dir {
516            if options.one_file_system {
517                if let (Some(this_inode), Some(my_inode)) = (this_stat.inode, my_stat.inode) {
518                    if this_inode.dev_id != my_inode.dev_id {
519                        continue;
520                    }
521                }
522            }
523
524            let this_stat = safe_du(
525                &entry_path,
526                options,
527                depth + 1,
528                seen_inodes,
529                print_tx,
530                Some(&dir_fd),
531            )?;
532
533            if !options.separate_dirs {
534                my_stat.size += this_stat.size;
535                my_stat.blocks += this_stat.blocks;
536                my_stat.inodes += this_stat.inodes;
537            }
538            print_tx.send(Ok(StatPrintInfo {
539                stat: this_stat,
540                depth: depth + 1,
541            }))?;
542        } else {
543            my_stat.size += this_stat.size;
544            my_stat.blocks += this_stat.blocks;
545            my_stat.inodes += 1;
546            if options.all {
547                print_tx.send(Ok(StatPrintInfo {
548                    stat: this_stat,
549                    depth: depth + 1,
550                }))?;
551            }
552        }
553    }
554
555    Ok(my_stat)
556}
557
558// this takes `my_stat` to avoid having to stat files multiple times.
559// Only used on non-Linux platforms
560// Regular traversal using std::fs
561// Used on non-Linux platforms and as fallback for symlinks on Linux
562#[allow(clippy::cognitive_complexity)]
563fn du_regular(
564    mut my_stat: Stat,
565    options: &TraversalOptions,
566    depth: usize,
567    seen_inodes: &mut HashSet<FileInfo>,
568    print_tx: &mpsc::Sender<UResult<StatPrintInfo>>,
569    ancestors: Option<&mut HashSet<FileInfo>>,
570    symlink_depth: Option<usize>,
571) -> Result<Stat, Box<mpsc::SendError<UResult<StatPrintInfo>>>> {
572    let mut default_ancestors = HashSet::new();
573    let ancestors = ancestors.unwrap_or(&mut default_ancestors);
574    let symlink_depth = symlink_depth.unwrap_or(0);
575    // Maximum symlink depth to prevent infinite loops
576    const MAX_SYMLINK_DEPTH: usize = 40;
577
578    // Add current directory to ancestors if it's a directory
579    let my_inode = if my_stat.metadata.is_dir() {
580        my_stat.inode
581    } else {
582        None
583    };
584
585    if let Some(inode) = my_inode {
586        ancestors.insert(inode);
587    }
588    if my_stat.metadata.is_dir() {
589        let read = match fs::read_dir(&my_stat.path) {
590            Ok(read) => read,
591            Err(e) => {
592                print_tx.send(Err(e.map_err_context(
593                    || translate!("du-error-cannot-read-directory", "path" => my_stat.path.quote()),
594                )))?;
595                return Ok(my_stat);
596            }
597        };
598
599        'file_loop: for f in read {
600            match f {
601                Ok(entry) => {
602                    let entry_path = entry.path();
603
604                    // Check if this is a symlink when using -L
605                    let mut current_symlink_depth = symlink_depth;
606                    let is_symlink = match entry.file_type() {
607                        Ok(ft) => ft.is_symlink(),
608                        Err(_) => false,
609                    };
610
611                    if is_symlink && options.dereference == Deref::All {
612                        // Increment symlink depth
613                        current_symlink_depth += 1;
614
615                        // Check symlink depth limit
616                        if current_symlink_depth > MAX_SYMLINK_DEPTH {
617                            print_tx.send(Err(std::io::Error::new(
618                                std::io::ErrorKind::InvalidData,
619                                "Too many levels of symbolic links",
620                            ).map_err_context(
621                                || translate!("du-error-cannot-access", "path" => entry_path.quote()),
622                            )))?;
623                            continue 'file_loop;
624                        }
625                    }
626
627                    match Stat::new(&entry_path, Some(&entry), options) {
628                        Ok(this_stat) => {
629                            // Check if symlink with -L points to an ancestor (cycle detection)
630                            if is_symlink
631                                && options.dereference == Deref::All
632                                && this_stat.metadata.is_dir()
633                            {
634                                if let Some(inode) = this_stat.inode {
635                                    if ancestors.contains(&inode) {
636                                        // This symlink points to an ancestor directory - skip to avoid cycle
637                                        continue 'file_loop;
638                                    }
639                                }
640                            }
641
642                            // We have an exclude list
643                            for pattern in &options.excludes {
644                                // Look at all patterns with both short and long paths
645                                // if we have 'du foo' but search to exclude 'foo/bar'
646                                // we need the full path
647                                if pattern.matches(&this_stat.path.to_string_lossy())
648                                    || pattern.matches(&entry.file_name().into_string().unwrap())
649                                {
650                                    // if the directory is ignored, leave early
651                                    if options.verbose {
652                                        println!(
653                                            "{}",
654                                            translate!("du-verbose-ignored", "path" => this_stat.path.quote())
655                                        );
656                                    }
657                                    // Go to the next file
658                                    continue 'file_loop;
659                                }
660                            }
661
662                            if let Some(inode) = this_stat.inode {
663                                // Check if the inode has been seen before and if we should skip it
664                                if seen_inodes.contains(&inode)
665                                    && (!options.count_links || !options.all)
666                                {
667                                    // If `count_links` is enabled and `all` is not, increment the inode count
668                                    if options.count_links && !options.all {
669                                        my_stat.inodes += 1;
670                                    }
671                                    // Skip further processing for this inode
672                                    continue;
673                                }
674                                // Mark this inode as seen
675                                seen_inodes.insert(inode);
676                            }
677
678                            if this_stat.metadata.is_dir() {
679                                if options.one_file_system {
680                                    if let (Some(this_inode), Some(my_inode)) =
681                                        (this_stat.inode, my_stat.inode)
682                                    {
683                                        if this_inode.dev_id != my_inode.dev_id {
684                                            continue;
685                                        }
686                                    }
687                                }
688
689                                let this_stat = du_regular(
690                                    this_stat,
691                                    options,
692                                    depth + 1,
693                                    seen_inodes,
694                                    print_tx,
695                                    Some(ancestors),
696                                    Some(current_symlink_depth),
697                                )?;
698
699                                if !options.separate_dirs {
700                                    my_stat.size += this_stat.size;
701                                    my_stat.blocks += this_stat.blocks;
702                                    my_stat.inodes += this_stat.inodes;
703                                }
704                                print_tx.send(Ok(StatPrintInfo {
705                                    stat: this_stat,
706                                    depth: depth + 1,
707                                }))?;
708                            } else {
709                                my_stat.size += this_stat.size;
710                                my_stat.blocks += this_stat.blocks;
711                                my_stat.inodes += 1;
712                                if options.all {
713                                    print_tx.send(Ok(StatPrintInfo {
714                                        stat: this_stat,
715                                        depth: depth + 1,
716                                    }))?;
717                                }
718                            }
719                        }
720                        Err(e) => {
721                            print_tx.send(Err(e.map_err_context(
722                                    || translate!("du-error-cannot-access", "path" => entry_path.quote()),
723                                )))?;
724                        }
725                    }
726                }
727                Err(error) => print_tx.send(Err(error.into()))?,
728            }
729        }
730    }
731
732    // Remove current directory from ancestors before returning
733    if let Some(inode) = my_inode {
734        ancestors.remove(&inode);
735    }
736
737    Ok(my_stat)
738}
739
740#[derive(Debug, Error)]
741enum DuError {
742    #[error("{}", translate!("du-error-invalid-max-depth", "depth" => _0.quote()))]
743    InvalidMaxDepthArg(String),
744
745    #[error("{}", translate!("du-error-summarize-depth-conflict", "depth" => _0.maybe_quote()))]
746    SummarizeDepthConflict(String),
747
748    #[error("{}", translate!("du-error-invalid-time-style", "style" => _0.quote(), "help" => uucore::execution_phrase()))]
749    InvalidTimeStyleArg(String),
750
751    #[error("{}", translate!("du-error-invalid-glob", "error" => _0))]
752    InvalidGlob(String),
753}
754
755impl UError for DuError {
756    fn code(&self) -> i32 {
757        match self {
758            Self::InvalidMaxDepthArg(_)
759            | Self::SummarizeDepthConflict(_)
760            | Self::InvalidTimeStyleArg(_)
761            | Self::InvalidGlob(_) => 1,
762        }
763    }
764}
765
766/// Read a file and return each line in a vector of String
767fn file_as_vec(filename: impl AsRef<Path>) -> Vec<String> {
768    let file = File::open(filename).expect("no such file");
769    let buf = BufReader::new(file);
770
771    buf.lines()
772        .map(|l| l.expect("Could not parse line"))
773        .collect()
774}
775
776/// Given the `--exclude-from` and/or `--exclude` arguments, returns the globset lists
777/// to ignore the files
778fn build_exclude_patterns(matches: &ArgMatches) -> UResult<Vec<Pattern>> {
779    let exclude_from_iterator = matches
780        .get_many::<String>(options::EXCLUDE_FROM)
781        .unwrap_or_default()
782        .flat_map(file_as_vec);
783
784    let excludes_iterator = matches
785        .get_many::<String>(options::EXCLUDE)
786        .unwrap_or_default()
787        .cloned();
788
789    let mut exclude_patterns = Vec::new();
790    for f in excludes_iterator.chain(exclude_from_iterator) {
791        if matches.get_flag(options::VERBOSE) {
792            println!(
793                "{}",
794                translate!("du-verbose-adding-to-exclude-list", "pattern" => f.clone())
795            );
796        }
797        match parse_glob::from_str(&f) {
798            Ok(glob) => exclude_patterns.push(glob),
799            Err(err) => return Err(DuError::InvalidGlob(err.to_string()).into()),
800        }
801    }
802    Ok(exclude_patterns)
803}
804
805struct StatPrintInfo {
806    stat: Stat,
807    depth: usize,
808}
809
810impl StatPrinter {
811    fn choose_size(&self, stat: &Stat) -> u64 {
812        if self.inodes {
813            stat.inodes
814        } else if self.apparent_size {
815            stat.size
816        } else {
817            // The st_blocks field indicates the number of blocks allocated to the file, 512-byte units.
818            // See: http://linux.die.net/man/2/stat
819            stat.blocks * 512
820        }
821    }
822
823    fn print_stats(&self, rx: &mpsc::Receiver<UResult<StatPrintInfo>>) -> UResult<()> {
824        let mut grand_total = 0;
825        loop {
826            let received = rx.recv();
827
828            match received {
829                Ok(message) => match message {
830                    Ok(stat_info) => {
831                        let size = self.choose_size(&stat_info.stat);
832
833                        if stat_info.depth == 0 {
834                            grand_total += size;
835                        }
836
837                        if !self
838                            .threshold
839                            .is_some_and(|threshold| threshold.should_exclude(size))
840                            && self
841                                .max_depth
842                                .is_none_or(|max_depth| stat_info.depth <= max_depth)
843                            && (!self.summarize || stat_info.depth == 0)
844                        {
845                            self.print_stat(&stat_info.stat, size)?;
846                        }
847                    }
848                    Err(e) => show!(e),
849                },
850                Err(_) => break,
851            }
852        }
853
854        if self.total {
855            print!("{}\t{}", self.convert_size(grand_total), self.total_text);
856            print!("{}", self.line_ending);
857        }
858
859        Ok(())
860    }
861
862    fn convert_size(&self, size: u64) -> String {
863        match self.size_format {
864            SizeFormat::HumanDecimal => uucore::format::human::human_readable(
865                size,
866                uucore::format::human::SizeFormat::Decimal,
867            ),
868            SizeFormat::HumanBinary => uucore::format::human::human_readable(
869                size,
870                uucore::format::human::SizeFormat::Binary,
871            ),
872            SizeFormat::BlockSize(block_size) => {
873                if self.inodes {
874                    // we ignore block size (-B) with --inodes
875                    size.to_string()
876                } else {
877                    size.div_ceil(block_size).to_string()
878                }
879            }
880        }
881    }
882
883    fn print_stat(&self, stat: &Stat, size: u64) -> UResult<()> {
884        print!("{}\t", self.convert_size(size));
885
886        if let Some(md_time) = &self.time {
887            if let Some(time) = metadata_get_time(&stat.metadata, *md_time) {
888                format_system_time(
889                    &mut stdout(),
890                    time,
891                    &self.time_format,
892                    FormatSystemTimeFallback::IntegerError,
893                )?;
894                print!("\t");
895            } else {
896                print!("???\t");
897            }
898        }
899
900        print_verbatim(&stat.path).unwrap();
901        print!("{}", self.line_ending);
902
903        Ok(())
904    }
905}
906
907/// Read file paths from the specified file, separated by null characters
908fn read_files_from(file_name: &OsStr) -> Result<Vec<PathBuf>, std::io::Error> {
909    let reader: Box<dyn BufRead> = if file_name == "-" {
910        // Read from standard input
911        Box::new(BufReader::new(std::io::stdin()))
912    } else {
913        // First, check if the file_name is a directory
914        let path = PathBuf::from(file_name);
915        if path.is_dir() {
916            return Err(std::io::Error::other(
917                translate!("du-error-read-error-is-directory", "file" => file_name.to_string_lossy()),
918            ));
919        }
920
921        // Attempt to open the file and handle the error if it does not exist
922        match File::open(file_name) {
923            Ok(file) => Box::new(BufReader::new(file)),
924            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
925                return Err(std::io::Error::other(
926                    translate!("du-error-cannot-open-for-reading", "file" => file_name.to_string_lossy()),
927                ));
928            }
929            Err(e) => return Err(e),
930        }
931    };
932
933    let mut paths = Vec::new();
934
935    for (i, line) in reader.split(b'\0').enumerate() {
936        let path = line?;
937
938        if path.is_empty() {
939            let line_number = i + 1;
940            show_error!(
941                "{}",
942                translate!("du-error-invalid-zero-length-file-name", "file" => file_name.to_string_lossy(), "line" => line_number)
943            );
944            set_exit_code(1);
945        } else {
946            let p = PathBuf::from(&*uucore::os_str_from_bytes(&path).unwrap());
947            if !paths.contains(&p) {
948                paths.push(p);
949            }
950        }
951    }
952
953    Ok(paths)
954}
955
956#[uucore::main]
957#[allow(clippy::cognitive_complexity)]
958pub fn uumain(args: impl uucore::Args) -> UResult<()> {
959    let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
960
961    let summarize = matches.get_flag(options::SUMMARIZE);
962
963    let count_links = matches.get_flag(options::COUNT_LINKS);
964
965    let max_depth = parse_depth(
966        matches
967            .get_one::<String>(options::MAX_DEPTH)
968            .map(|s| s.as_str()),
969        summarize,
970    )?;
971
972    let files = if let Some(file_from) = matches.get_one::<OsString>(options::FILES0_FROM) {
973        if file_from == "-" && matches.get_one::<OsString>(options::FILE).is_some() {
974            return Err(std::io::Error::other(
975                translate!("du-error-extra-operand-with-files0-from",
976                    "file" => matches
977                        .get_one::<OsString>(options::FILE)
978                        .unwrap()
979                        .to_string_lossy()
980                        .quote()
981                ),
982            )
983            .into());
984        }
985
986        read_files_from(file_from)?
987    } else if let Some(files) = matches.get_many::<OsString>(options::FILE) {
988        let files = files.map(PathBuf::from);
989        if count_links {
990            files.collect()
991        } else {
992            // Deduplicate while preserving order
993            let mut seen = HashSet::new();
994            files
995                .filter(|path| seen.insert(path.clone()))
996                .collect::<Vec<_>>()
997        }
998    } else {
999        vec![PathBuf::from(".")]
1000    };
1001
1002    let time = matches.contains_id(options::TIME).then(|| {
1003        matches
1004            .get_one::<String>(options::TIME)
1005            .map_or(MetadataTimeField::Modification, |s| s.as_str().into())
1006    });
1007
1008    let size_format = if matches.get_flag(options::HUMAN_READABLE) {
1009        SizeFormat::HumanBinary
1010    } else if matches.get_flag(options::SI) {
1011        SizeFormat::HumanDecimal
1012    } else if matches.get_flag(options::BYTES) {
1013        SizeFormat::BlockSize(1)
1014    } else if matches.get_flag(options::BLOCK_SIZE_1K) {
1015        SizeFormat::BlockSize(1024)
1016    } else if matches.get_flag(options::BLOCK_SIZE_1M) {
1017        SizeFormat::BlockSize(1024 * 1024)
1018    } else {
1019        let block_size_str = matches.get_one::<String>(options::BLOCK_SIZE);
1020        let block_size = read_block_size(block_size_str.map(AsRef::as_ref))?;
1021        if block_size == 0 {
1022            return Err(std::io::Error::other(translate!("du-error-invalid-block-size-argument", "option" => options::BLOCK_SIZE, "value" => block_size_str.map_or("???BUG", |v| v).quote()))
1023            .into());
1024        }
1025        SizeFormat::BlockSize(block_size)
1026    };
1027
1028    let traversal_options = TraversalOptions {
1029        all: matches.get_flag(options::ALL),
1030        separate_dirs: matches.get_flag(options::SEPARATE_DIRS),
1031        one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM),
1032        dereference: if matches.get_flag(options::DEREFERENCE) {
1033            Deref::All
1034        } else if matches.get_flag(options::DEREFERENCE_ARGS) {
1035            // We don't care about the cost of cloning as it is rarely used
1036            Deref::Args(files.clone())
1037        } else {
1038            Deref::None
1039        },
1040        count_links,
1041        verbose: matches.get_flag(options::VERBOSE),
1042        excludes: build_exclude_patterns(&matches)?,
1043    };
1044
1045    let time_format = if time.is_some() {
1046        parse_time_style(matches.get_one::<String>("time-style"))?
1047    } else {
1048        format::LONG_ISO.to_string()
1049    };
1050
1051    let stat_printer = StatPrinter {
1052        max_depth,
1053        size_format,
1054        summarize,
1055        total: matches.get_flag(options::TOTAL),
1056        inodes: matches.get_flag(options::INODES),
1057        threshold: matches
1058            .get_one::<String>(options::THRESHOLD)
1059            .map(|s| {
1060                Threshold::from_str(s).map_err(|e| {
1061                    USimpleError::new(1, format_error_message(&e, s, options::THRESHOLD))
1062                })
1063            })
1064            .transpose()?,
1065        apparent_size: matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES),
1066        time,
1067        time_format,
1068        line_ending: LineEnding::from_zero_flag(matches.get_flag(options::NULL)),
1069        total_text: translate!("du-total"),
1070    };
1071
1072    if stat_printer.inodes
1073        && (matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES))
1074    {
1075        show_warning!(
1076            "{}",
1077            translate!("du-warning-apparent-size-ineffective-with-inodes")
1078        );
1079    }
1080
1081    // Use separate thread to print output, so we can print finished results while computation is still running
1082    let (print_tx, rx) = mpsc::channel::<UResult<StatPrintInfo>>();
1083    let printing_thread = thread::spawn(move || stat_printer.print_stats(&rx));
1084
1085    'loop_file: for path in files {
1086        // Skip if we don't want to ignore anything
1087        if !&traversal_options.excludes.is_empty() {
1088            let path_string = path.to_string_lossy();
1089            for pattern in &traversal_options.excludes {
1090                if pattern.matches(&path_string) {
1091                    // if the directory is ignored, leave early
1092                    if traversal_options.verbose {
1093                        println!(
1094                            "{}",
1095                            translate!("du-verbose-ignored", "path" => path_string.quote())
1096                        );
1097                    }
1098                    continue 'loop_file;
1099                }
1100            }
1101        }
1102
1103        // Check existence of path provided in argument
1104        let mut seen_inodes: HashSet<FileInfo> = HashSet::new();
1105
1106        // Determine which traversal method to use
1107        #[cfg(target_os = "linux")]
1108        let use_safe_traversal = traversal_options.dereference != Deref::All;
1109        #[cfg(not(target_os = "linux"))]
1110        let use_safe_traversal = false;
1111
1112        if use_safe_traversal {
1113            // Use safe traversal (Linux only, when not using -L)
1114            #[cfg(target_os = "linux")]
1115            {
1116                // Pre-populate seen_inodes with the starting directory to detect cycles
1117                if let Ok(stat) = Stat::new(&path, None, &traversal_options) {
1118                    if let Some(inode) = stat.inode {
1119                        seen_inodes.insert(inode);
1120                    }
1121                }
1122
1123                match safe_du(
1124                    &path,
1125                    &traversal_options,
1126                    0,
1127                    &mut seen_inodes,
1128                    &print_tx,
1129                    None,
1130                ) {
1131                    Ok(stat) => {
1132                        print_tx
1133                            .send(Ok(StatPrintInfo { stat, depth: 0 }))
1134                            .map_err(|e| USimpleError::new(1, e.to_string()))?;
1135                    }
1136                    Err(e) => {
1137                        // Check if this is our "already handled" error
1138                        if let mpsc::SendError(Err(simple_error)) = e.as_ref() {
1139                            if simple_error.code() == 0 {
1140                                // Error already handled, continue to next file
1141                                continue 'loop_file;
1142                            }
1143                        }
1144                        return Err(USimpleError::new(1, e.to_string()));
1145                    }
1146                }
1147            }
1148        } else {
1149            // Use regular traversal (non-Linux or when -L is used)
1150            if let Ok(stat) = Stat::new(&path, None, &traversal_options) {
1151                if let Some(inode) = stat.inode {
1152                    seen_inodes.insert(inode);
1153                }
1154                let stat = du_regular(
1155                    stat,
1156                    &traversal_options,
1157                    0,
1158                    &mut seen_inodes,
1159                    &print_tx,
1160                    None,
1161                    None,
1162                )
1163                .map_err(|e| USimpleError::new(1, e.to_string()))?;
1164
1165                print_tx
1166                    .send(Ok(StatPrintInfo { stat, depth: 0 }))
1167                    .map_err(|e| USimpleError::new(1, e.to_string()))?;
1168            } else {
1169                #[cfg(target_os = "linux")]
1170                let error_msg = translate!("du-error-cannot-access", "path" => path.quote());
1171                #[cfg(not(target_os = "linux"))]
1172                let error_msg = translate!("du-error-cannot-access-no-such-file", "path" => path.to_string_lossy().quote());
1173
1174                print_tx
1175                    .send(Err(USimpleError::new(1, error_msg)))
1176                    .map_err(|e| USimpleError::new(1, e.to_string()))?;
1177            }
1178        }
1179    }
1180
1181    drop(print_tx);
1182
1183    printing_thread
1184        .join()
1185        .map_err(|_| USimpleError::new(1, translate!("du-error-printing-thread-panicked")))??;
1186
1187    Ok(())
1188}
1189
1190// Parse --time-style argument, falling back to environment variable if necessary.
1191fn parse_time_style(s: Option<&String>) -> UResult<String> {
1192    let s = match s {
1193        Some(s) => Some(s.into()),
1194        None => {
1195            match env::var("TIME_STYLE") {
1196                // Per GNU manual, strip `posix-` if present, ignore anything after a newline if
1197                // the string starts with +, and ignore "locale".
1198                Ok(s) => {
1199                    let s = s.strip_prefix("posix-").unwrap_or(s.as_str());
1200                    let s = match s.chars().next().unwrap() {
1201                        '+' => s.split('\n').next().unwrap(),
1202                        _ => s,
1203                    };
1204                    match s {
1205                        "locale" => None,
1206                        _ => Some(s.to_string()),
1207                    }
1208                }
1209                Err(_) => None,
1210            }
1211        }
1212    };
1213    match s {
1214        Some(s) => match s.as_ref() {
1215            "full-iso" => Ok(format::FULL_ISO.to_string()),
1216            "long-iso" => Ok(format::LONG_ISO.to_string()),
1217            "iso" => Ok(format::ISO.to_string()),
1218            _ => match s.chars().next().unwrap() {
1219                '+' => Ok(s[1..].to_string()),
1220                _ => Err(DuError::InvalidTimeStyleArg(s).into()),
1221            },
1222        },
1223        None => Ok(format::LONG_ISO.to_string()),
1224    }
1225}
1226
1227fn parse_depth(max_depth_str: Option<&str>, summarize: bool) -> UResult<Option<usize>> {
1228    let max_depth = max_depth_str.as_ref().and_then(|s| s.parse::<usize>().ok());
1229    match (max_depth_str, max_depth) {
1230        (Some(s), _) if summarize => Err(DuError::SummarizeDepthConflict(s.into()).into()),
1231        (Some(s), None) => Err(DuError::InvalidMaxDepthArg(s.into()).into()),
1232        (Some(_), Some(_)) | (None, _) => Ok(max_depth),
1233    }
1234}
1235
1236pub fn uu_app() -> Command {
1237    Command::new(uucore::util_name())
1238        .version(uucore::crate_version!())
1239        .help_template(uucore::localized_help_template(uucore::util_name()))
1240        .about(translate!("du-about"))
1241        .after_help(translate!("du-after-help"))
1242        .override_usage(format_usage(&translate!("du-usage")))
1243        .infer_long_args(true)
1244        .disable_help_flag(true)
1245        .arg(
1246            Arg::new(options::HELP)
1247                .long(options::HELP)
1248                .help(translate!("du-help-print-help"))
1249                .action(ArgAction::Help),
1250        )
1251        .arg(
1252            Arg::new(options::ALL)
1253                .short('a')
1254                .long(options::ALL)
1255                .help(translate!("du-help-all"))
1256                .action(ArgAction::SetTrue),
1257        )
1258        .arg(
1259            Arg::new(options::APPARENT_SIZE)
1260                .long(options::APPARENT_SIZE)
1261                .help(translate!("du-help-apparent-size"))
1262                .action(ArgAction::SetTrue),
1263        )
1264        .arg(
1265            Arg::new(options::BLOCK_SIZE)
1266                .short('B')
1267                .long(options::BLOCK_SIZE)
1268                .value_name("SIZE")
1269                .help(translate!("du-help-block-size")),
1270        )
1271        .arg(
1272            Arg::new(options::BYTES)
1273                .short('b')
1274                .long("bytes")
1275                .help(translate!("du-help-bytes"))
1276                .action(ArgAction::SetTrue),
1277        )
1278        .arg(
1279            Arg::new(options::TOTAL)
1280                .long("total")
1281                .short('c')
1282                .help(translate!("du-help-total"))
1283                .action(ArgAction::SetTrue),
1284        )
1285        .arg(
1286            Arg::new(options::MAX_DEPTH)
1287                .short('d')
1288                .long("max-depth")
1289                .value_name("N")
1290                .help(translate!("du-help-max-depth")),
1291        )
1292        .arg(
1293            Arg::new(options::HUMAN_READABLE)
1294                .long("human-readable")
1295                .short('h')
1296                .help(translate!("du-help-human-readable"))
1297                .action(ArgAction::SetTrue),
1298        )
1299        .arg(
1300            Arg::new(options::INODES)
1301                .long(options::INODES)
1302                .help(translate!("du-help-inodes"))
1303                .action(ArgAction::SetTrue),
1304        )
1305        .arg(
1306            Arg::new(options::BLOCK_SIZE_1K)
1307                .short('k')
1308                .help(translate!("du-help-block-size-1k"))
1309                .action(ArgAction::SetTrue),
1310        )
1311        .arg(
1312            Arg::new(options::COUNT_LINKS)
1313                .short('l')
1314                .long("count-links")
1315                .help(translate!("du-help-count-links"))
1316                .action(ArgAction::SetTrue),
1317        )
1318        .arg(
1319            Arg::new(options::DEREFERENCE)
1320                .short('L')
1321                .long(options::DEREFERENCE)
1322                .help(translate!("du-help-dereference"))
1323                .action(ArgAction::SetTrue),
1324        )
1325        .arg(
1326            Arg::new(options::DEREFERENCE_ARGS)
1327                .short('D')
1328                .visible_short_alias('H')
1329                .long(options::DEREFERENCE_ARGS)
1330                .help(translate!("du-help-dereference-args"))
1331                .action(ArgAction::SetTrue),
1332        )
1333        .arg(
1334            Arg::new(options::NO_DEREFERENCE)
1335                .short('P')
1336                .long(options::NO_DEREFERENCE)
1337                .help(translate!("du-help-no-dereference"))
1338                .overrides_with(options::DEREFERENCE)
1339                .action(ArgAction::SetTrue),
1340        )
1341        .arg(
1342            Arg::new(options::BLOCK_SIZE_1M)
1343                .short('m')
1344                .help(translate!("du-help-block-size-1m"))
1345                .action(ArgAction::SetTrue),
1346        )
1347        .arg(
1348            Arg::new(options::NULL)
1349                .short('0')
1350                .long("null")
1351                .help(translate!("du-help-null"))
1352                .action(ArgAction::SetTrue),
1353        )
1354        .arg(
1355            Arg::new(options::SEPARATE_DIRS)
1356                .short('S')
1357                .long("separate-dirs")
1358                .help(translate!("du-help-separate-dirs"))
1359                .action(ArgAction::SetTrue),
1360        )
1361        .arg(
1362            Arg::new(options::SUMMARIZE)
1363                .short('s')
1364                .long("summarize")
1365                .help(translate!("du-help-summarize"))
1366                .action(ArgAction::SetTrue),
1367        )
1368        .arg(
1369            Arg::new(options::SI)
1370                .long(options::SI)
1371                .help(translate!("du-help-si"))
1372                .action(ArgAction::SetTrue),
1373        )
1374        .arg(
1375            Arg::new(options::ONE_FILE_SYSTEM)
1376                .short('x')
1377                .long(options::ONE_FILE_SYSTEM)
1378                .help(translate!("du-help-one-file-system"))
1379                .action(ArgAction::SetTrue),
1380        )
1381        .arg(
1382            Arg::new(options::THRESHOLD)
1383                .short('t')
1384                .long(options::THRESHOLD)
1385                .value_name("SIZE")
1386                .num_args(1)
1387                .allow_hyphen_values(true)
1388                .help(translate!("du-help-threshold")),
1389        )
1390        .arg(
1391            Arg::new(options::VERBOSE)
1392                .short('v')
1393                .long("verbose")
1394                .help(translate!("du-help-verbose"))
1395                .action(ArgAction::SetTrue),
1396        )
1397        .arg(
1398            Arg::new(options::EXCLUDE)
1399                .long(options::EXCLUDE)
1400                .value_name("PATTERN")
1401                .help(translate!("du-help-exclude"))
1402                .action(ArgAction::Append),
1403        )
1404        .arg(
1405            Arg::new(options::EXCLUDE_FROM)
1406                .short('X')
1407                .long("exclude-from")
1408                .value_name("FILE")
1409                .value_hint(clap::ValueHint::FilePath)
1410                .help(translate!("du-help-exclude-from"))
1411                .action(ArgAction::Append),
1412        )
1413        .arg(
1414            Arg::new(options::FILES0_FROM)
1415                .long("files0-from")
1416                .value_name("FILE")
1417                .value_hint(clap::ValueHint::FilePath)
1418                .value_parser(clap::value_parser!(OsString))
1419                .help(translate!("du-help-files0-from"))
1420                .action(ArgAction::Append),
1421        )
1422        .arg(
1423            Arg::new(options::TIME)
1424                .long(options::TIME)
1425                .value_name("WORD")
1426                .require_equals(true)
1427                .num_args(0..)
1428                .value_parser(ShortcutValueParser::new([
1429                    PossibleValue::new("atime").alias("access").alias("use"),
1430                    PossibleValue::new("ctime").alias("status"),
1431                    PossibleValue::new("creation").alias("birth"),
1432                ]))
1433                .help(translate!("du-help-time")),
1434        )
1435        .arg(
1436            Arg::new(options::TIME_STYLE)
1437                .long(options::TIME_STYLE)
1438                .value_name("STYLE")
1439                .help(translate!("du-help-time-style")),
1440        )
1441        .arg(
1442            Arg::new(options::FILE)
1443                .hide(true)
1444                .value_hint(clap::ValueHint::AnyPath)
1445                .value_parser(clap::value_parser!(OsString))
1446                .action(ArgAction::Append),
1447        )
1448}
1449
1450#[derive(Clone, Copy)]
1451enum Threshold {
1452    Lower(u64),
1453    Upper(u64),
1454}
1455
1456impl FromStr for Threshold {
1457    type Err = ParseSizeError;
1458
1459    fn from_str(s: &str) -> Result<Self, Self::Err> {
1460        let offset = usize::from(s.starts_with(&['-', '+'][..]));
1461
1462        let size = parse_size_u64(&s[offset..])?;
1463
1464        if s.starts_with('-') {
1465            // Threshold of '-0' excludes everything besides 0 sized entries
1466            // GNU's du treats '-0' as an invalid argument
1467            if size == 0 {
1468                return Err(ParseSizeError::ParseFailure(s.to_string()));
1469            }
1470            Ok(Self::Upper(size))
1471        } else {
1472            Ok(Self::Lower(size))
1473        }
1474    }
1475}
1476
1477impl Threshold {
1478    fn should_exclude(&self, size: u64) -> bool {
1479        match *self {
1480            Self::Upper(threshold) => size > threshold,
1481            Self::Lower(threshold) => size < threshold,
1482        }
1483    }
1484}
1485
1486fn format_error_message(error: &ParseSizeError, s: &str, option: &str) -> String {
1487    // NOTE:
1488    // GNU's du echos affected flag, -B or --block-size (-t or --threshold), depending user's selection
1489    match error {
1490        ParseSizeError::InvalidSuffix(_) => {
1491            translate!("du-error-invalid-suffix", "option" => option, "value" => s.quote())
1492        }
1493        ParseSizeError::ParseFailure(_) | ParseSizeError::PhysicalMem(_) => {
1494            translate!("du-error-invalid-argument", "option" => option, "value" => s.quote())
1495        }
1496        ParseSizeError::SizeTooBig(_) => {
1497            translate!("du-error-argument-too-large", "option" => option, "value" => s.quote())
1498        }
1499    }
1500}
1501
1502#[cfg(test)]
1503mod test_du {
1504    #[allow(unused_imports)]
1505    use super::*;
1506
1507    #[test]
1508    fn test_read_block_size() {
1509        let test_data = [Some("1024".to_string()), Some("K".to_string()), None];
1510        for it in &test_data {
1511            assert!(matches!(read_block_size(it.as_deref()), Ok(1024)));
1512        }
1513    }
1514}