uu_df/
df.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 itotal iused iavail ipcent pcent tmpfs squashfs lofs
6mod blocks;
7mod columns;
8mod filesystem;
9mod table;
10
11use blocks::HumanReadable;
12use clap::builder::ValueParser;
13use table::HeaderMode;
14use uucore::display::Quotable;
15use uucore::error::{UError, UResult, USimpleError};
16use uucore::fsext::{read_fs_list, MountInfo};
17use uucore::parse_size::ParseSizeError;
18use uucore::{format_usage, help_about, help_section, help_usage, show};
19
20use clap::{crate_version, parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
21
22use std::error::Error;
23use std::ffi::OsString;
24use std::fmt;
25use std::path::Path;
26
27use crate::blocks::{read_block_size, BlockSize};
28use crate::columns::{Column, ColumnError};
29use crate::filesystem::Filesystem;
30use crate::filesystem::FsError;
31use crate::table::Table;
32
33const ABOUT: &str = help_about!("df.md");
34const USAGE: &str = help_usage!("df.md");
35const AFTER_HELP: &str = help_section!("after help", "df.md");
36
37static OPT_HELP: &str = "help";
38static OPT_ALL: &str = "all";
39static OPT_BLOCKSIZE: &str = "blocksize";
40static OPT_TOTAL: &str = "total";
41static OPT_HUMAN_READABLE_BINARY: &str = "human-readable-binary";
42static OPT_HUMAN_READABLE_DECIMAL: &str = "human-readable-decimal";
43static OPT_INODES: &str = "inodes";
44static OPT_KILO: &str = "kilo";
45static OPT_LOCAL: &str = "local";
46static OPT_NO_SYNC: &str = "no-sync";
47static OPT_OUTPUT: &str = "output";
48static OPT_PATHS: &str = "paths";
49static OPT_PORTABILITY: &str = "portability";
50static OPT_SYNC: &str = "sync";
51static OPT_TYPE: &str = "type";
52static OPT_PRINT_TYPE: &str = "print-type";
53static OPT_EXCLUDE_TYPE: &str = "exclude-type";
54static OUTPUT_FIELD_LIST: [&str; 12] = [
55    "source", "fstype", "itotal", "iused", "iavail", "ipcent", "size", "used", "avail", "pcent",
56    "file", "target",
57];
58
59/// Parameters that control the behavior of `df`.
60///
61/// Most of these parameters control which rows and which columns are
62/// displayed. The `block_size` determines the units to use when
63/// displaying numbers of bytes or inodes.
64struct Options {
65    show_local_fs: bool,
66    show_all_fs: bool,
67    human_readable: Option<HumanReadable>,
68    block_size: BlockSize,
69    header_mode: HeaderMode,
70
71    /// Optional list of filesystem types to include in the output table.
72    ///
73    /// If this is not `None`, only filesystems that match one of
74    /// these types will be listed.
75    include: Option<Vec<String>>,
76
77    /// Optional list of filesystem types to exclude from the output table.
78    ///
79    /// If this is not `None`, filesystems that match one of these
80    /// types will *not* be listed.
81    exclude: Option<Vec<String>>,
82
83    /// Whether to sync before operating.
84    sync: bool,
85
86    /// Whether to show a final row comprising the totals for each column.
87    show_total: bool,
88
89    /// Sequence of columns to display in the output table.
90    columns: Vec<Column>,
91}
92
93impl Default for Options {
94    fn default() -> Self {
95        Self {
96            show_local_fs: Default::default(),
97            show_all_fs: Default::default(),
98            block_size: BlockSize::default(),
99            human_readable: Option::default(),
100            header_mode: HeaderMode::default(),
101            include: Option::default(),
102            exclude: Option::default(),
103            sync: Default::default(),
104            show_total: Default::default(),
105            columns: vec![
106                Column::Source,
107                Column::Size,
108                Column::Used,
109                Column::Avail,
110                Column::Pcent,
111                Column::Target,
112            ],
113        }
114    }
115}
116
117#[derive(Debug)]
118enum OptionsError {
119    BlockSizeTooLarge(String),
120    InvalidBlockSize(String),
121    InvalidSuffix(String),
122
123    /// An error getting the columns to display in the output table.
124    ColumnError(ColumnError),
125
126    FilesystemTypeBothSelectedAndExcluded(Vec<String>),
127}
128
129impl fmt::Display for OptionsError {
130    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
131        match self {
132            // TODO This needs to vary based on whether `--block-size`
133            // or `-B` were provided.
134            Self::BlockSizeTooLarge(s) => {
135                write!(f, "--block-size argument {} too large", s.quote())
136            }
137            // TODO This needs to vary based on whether `--block-size`
138            // or `-B` were provided.
139            Self::InvalidBlockSize(s) => write!(f, "invalid --block-size argument {s}"),
140            // TODO This needs to vary based on whether `--block-size`
141            // or `-B` were provided.
142            Self::InvalidSuffix(s) => write!(f, "invalid suffix in --block-size argument {s}"),
143            Self::ColumnError(ColumnError::MultipleColumns(s)) => write!(
144                f,
145                "option --output: field {} used more than once",
146                s.quote()
147            ),
148            #[allow(clippy::print_in_format_impl)]
149            Self::FilesystemTypeBothSelectedAndExcluded(types) => {
150                for t in types {
151                    eprintln!(
152                        "{}: file system type {} both selected and excluded",
153                        uucore::util_name(),
154                        t.quote()
155                    );
156                }
157                Ok(())
158            }
159        }
160    }
161}
162
163impl Options {
164    /// Convert command-line arguments into [`Options`].
165    fn from(matches: &ArgMatches) -> Result<Self, OptionsError> {
166        let include: Option<Vec<_>> = matches
167            .get_many::<OsString>(OPT_TYPE)
168            .map(|v| v.map(|s| s.to_string_lossy().to_string()).collect());
169        let exclude: Option<Vec<_>> = matches
170            .get_many::<OsString>(OPT_EXCLUDE_TYPE)
171            .map(|v| v.map(|s| s.to_string_lossy().to_string()).collect());
172
173        if let (Some(include), Some(exclude)) = (&include, &exclude) {
174            if let Some(types) = Self::get_intersected_types(include, exclude) {
175                return Err(OptionsError::FilesystemTypeBothSelectedAndExcluded(types));
176            }
177        }
178
179        Ok(Self {
180            show_local_fs: matches.get_flag(OPT_LOCAL),
181            show_all_fs: matches.get_flag(OPT_ALL),
182            sync: matches.get_flag(OPT_SYNC),
183            block_size: read_block_size(matches).map_err(|e| match e {
184                ParseSizeError::InvalidSuffix(s) => OptionsError::InvalidSuffix(s),
185                ParseSizeError::SizeTooBig(_) => OptionsError::BlockSizeTooLarge(
186                    matches
187                        .get_one::<String>(OPT_BLOCKSIZE)
188                        .unwrap()
189                        .to_string(),
190                ),
191                ParseSizeError::ParseFailure(s) => OptionsError::InvalidBlockSize(s),
192            })?,
193            header_mode: {
194                if matches.get_flag(OPT_HUMAN_READABLE_BINARY)
195                    || matches.get_flag(OPT_HUMAN_READABLE_DECIMAL)
196                {
197                    HeaderMode::HumanReadable
198                } else if matches.get_flag(OPT_PORTABILITY) {
199                    HeaderMode::PosixPortability
200                // get_flag() doesn't work here, it always returns true because OPT_OUTPUT has
201                // default values and hence is always present
202                } else if matches.value_source(OPT_OUTPUT) == Some(ValueSource::CommandLine) {
203                    HeaderMode::Output
204                } else {
205                    HeaderMode::Default
206                }
207            },
208            human_readable: {
209                if matches.get_flag(OPT_HUMAN_READABLE_BINARY) {
210                    Some(HumanReadable::Binary)
211                } else if matches.get_flag(OPT_HUMAN_READABLE_DECIMAL) {
212                    Some(HumanReadable::Decimal)
213                } else {
214                    None
215                }
216            },
217            include,
218            exclude,
219            show_total: matches.get_flag(OPT_TOTAL),
220            columns: Column::from_matches(matches).map_err(OptionsError::ColumnError)?,
221        })
222    }
223
224    fn get_intersected_types(include: &[String], exclude: &[String]) -> Option<Vec<String>> {
225        let mut intersected_types = Vec::new();
226
227        for t in include {
228            if exclude.contains(t) {
229                intersected_types.push(t.clone());
230            }
231        }
232
233        (!intersected_types.is_empty()).then_some(intersected_types)
234    }
235}
236
237/// Whether to display the mount info given the inclusion settings.
238fn is_included(mi: &MountInfo, opt: &Options) -> bool {
239    // Don't show remote filesystems if `--local` has been given.
240    if mi.remote && opt.show_local_fs {
241        return false;
242    }
243
244    // Don't show pseudo filesystems unless `--all` has been given.
245    if mi.dummy && !opt.show_all_fs {
246        return false;
247    }
248
249    // Don't show filesystems if they have been explicitly excluded.
250    if let Some(ref excludes) = opt.exclude {
251        if excludes.contains(&mi.fs_type) {
252            return false;
253        }
254    }
255    if let Some(ref includes) = opt.include {
256        if !includes.contains(&mi.fs_type) {
257            return false;
258        }
259    }
260
261    true
262}
263
264/// Whether the mount info in `m2` should be prioritized over `m1`.
265///
266/// The "lt" in the function name is in analogy to the
267/// [`std::cmp::PartialOrd::lt`].
268fn mount_info_lt(m1: &MountInfo, m2: &MountInfo) -> bool {
269    // let "real" devices with '/' in the name win.
270    if m1.dev_name.starts_with('/') && !m2.dev_name.starts_with('/') {
271        return false;
272    }
273
274    let m1_nearer_root = m1.mount_dir.len() < m2.mount_dir.len();
275    // With bind mounts, prefer items nearer the root of the source
276    let m2_below_root = !m1.mount_root.is_empty()
277        && !m2.mount_root.is_empty()
278        && m1.mount_root.len() > m2.mount_root.len();
279    // let points towards the root of the device win.
280    if m1_nearer_root && !m2_below_root {
281        return false;
282    }
283
284    // let an entry over-mounted on a new device win, but only when
285    // matching an existing mnt point, to avoid problematic
286    // replacement when given inaccurate mount lists, seen with some
287    // chroot environments for example.
288    if m1.dev_name != m2.dev_name && m1.mount_dir == m2.mount_dir {
289        return false;
290    }
291
292    true
293}
294
295/// Whether to prioritize given mount info over all others on the same device.
296///
297/// This function decides whether the mount info `mi` is better than
298/// all others in `previous` that mount the same device as `mi`.
299fn is_best(previous: &[MountInfo], mi: &MountInfo) -> bool {
300    for seen in previous {
301        if seen.dev_id == mi.dev_id && mount_info_lt(mi, seen) {
302            return false;
303        }
304    }
305    true
306}
307
308/// Keep only the specified subset of [`MountInfo`] instances.
309///
310/// The `opt` argument specifies a variety of ways of excluding
311/// [`MountInfo`] instances; see [`Options`] for more information.
312///
313/// Finally, if there are duplicate entries, the one with the shorter
314/// path is kept.
315fn filter_mount_list(vmi: Vec<MountInfo>, opt: &Options) -> Vec<MountInfo> {
316    let mut result = vec![];
317    for mi in vmi {
318        // TODO The running time of the `is_best()` function is linear
319        // in the length of `result`. That makes the running time of
320        // this loop quadratic in the length of `vmi`. This could be
321        // improved by a more efficient implementation of `is_best()`,
322        // but `vmi` is probably not very long in practice.
323        if is_included(&mi, opt) && is_best(&result, &mi) {
324            result.push(mi);
325        }
326    }
327    result
328}
329
330/// Get all currently mounted filesystems.
331///
332/// `opt` excludes certain filesystems from consideration and allows for the synchronization of filesystems before running; see
333/// [`Options`] for more information.
334fn get_all_filesystems(opt: &Options) -> UResult<Vec<Filesystem>> {
335    // Run a sync call before any operation if so instructed.
336    if opt.sync {
337        #[cfg(not(any(windows, target_os = "redox")))]
338        unsafe {
339            #[cfg(not(target_os = "android"))]
340            uucore::libc::sync();
341            #[cfg(target_os = "android")]
342            uucore::libc::syscall(uucore::libc::SYS_sync);
343        }
344    }
345
346    // The list of all mounted filesystems.
347    //
348    // Filesystems excluded by the command-line options are
349    // not considered.
350    let mounts: Vec<MountInfo> = filter_mount_list(read_fs_list()?, opt);
351
352    // Convert each `MountInfo` into a `Filesystem`, which contains
353    // both the mount information and usage information.
354    #[cfg(not(windows))]
355    {
356        let maybe_mount = |m| Filesystem::from_mount(&mounts, &m, None).ok();
357        Ok(mounts
358            .clone()
359            .into_iter()
360            .filter_map(maybe_mount)
361            .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0)
362            .collect())
363    }
364    #[cfg(windows)]
365    {
366        let maybe_mount = |m| Filesystem::from_mount(&m, None).ok();
367        Ok(mounts
368            .into_iter()
369            .filter_map(maybe_mount)
370            .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0)
371            .collect())
372    }
373}
374
375/// For each path, get the filesystem that contains that path.
376fn get_named_filesystems<P>(paths: &[P], opt: &Options) -> UResult<Vec<Filesystem>>
377where
378    P: AsRef<Path>,
379{
380    // The list of all mounted filesystems.
381    //
382    // Filesystems marked as `dummy` or of type "lofs" are not
383    // considered. The "lofs" filesystem is a loopback
384    // filesystem present on Solaris and FreeBSD systems. It
385    // is similar to a symbolic link.
386    let mounts: Vec<MountInfo> = filter_mount_list(read_fs_list()?, opt)
387        .into_iter()
388        .filter(|mi| mi.fs_type != "lofs" && !mi.dummy)
389        .collect();
390
391    let mut result = vec![];
392
393    // this happens if the file system type doesn't exist
394    if mounts.is_empty() {
395        show!(USimpleError::new(1, "no file systems processed"));
396        return Ok(result);
397    }
398
399    // Convert each path into a `Filesystem`, which contains
400    // both the mount information and usage information.
401    for path in paths {
402        match Filesystem::from_path(&mounts, path) {
403            Ok(fs) => result.push(fs),
404            Err(FsError::InvalidPath) => {
405                show!(USimpleError::new(
406                    1,
407                    format!("{}: No such file or directory", path.as_ref().display())
408                ));
409            }
410            Err(FsError::MountMissing) => {
411                show!(USimpleError::new(1, "no file systems processed"));
412            }
413            #[cfg(not(windows))]
414            Err(FsError::OverMounted) => {
415                show!(USimpleError::new(
416                    1,
417                    format!(
418                        "cannot access {}: over-mounted by another device",
419                        path.as_ref().quote()
420                    )
421                ));
422            }
423        }
424    }
425    Ok(result)
426}
427
428#[derive(Debug)]
429enum DfError {
430    /// A problem while parsing command-line options.
431    OptionsError(OptionsError),
432}
433
434impl Error for DfError {}
435
436impl UError for DfError {
437    fn usage(&self) -> bool {
438        matches!(self, Self::OptionsError(OptionsError::ColumnError(_)))
439    }
440}
441
442impl fmt::Display for DfError {
443    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
444        match self {
445            Self::OptionsError(e) => e.fmt(f),
446        }
447    }
448}
449
450#[uucore::main]
451pub fn uumain(args: impl uucore::Args) -> UResult<()> {
452    let matches = uu_app().try_get_matches_from(args)?;
453
454    #[cfg(windows)]
455    {
456        if matches.get_flag(OPT_INODES) {
457            println!("{}: doesn't support -i option", uucore::util_name());
458            return Ok(());
459        }
460    }
461
462    let opt = Options::from(&matches).map_err(DfError::OptionsError)?;
463    // Get the list of filesystems to display in the output table.
464    let filesystems: Vec<Filesystem> = match matches.get_many::<String>(OPT_PATHS) {
465        None => {
466            let filesystems = get_all_filesystems(&opt).map_err(|e| {
467                let context = "cannot read table of mounted file systems";
468                USimpleError::new(e.code(), format!("{context}: {e}"))
469            })?;
470
471            if filesystems.is_empty() {
472                return Err(USimpleError::new(1, "no file systems processed"));
473            }
474
475            filesystems
476        }
477        Some(paths) => {
478            let paths: Vec<_> = paths.collect();
479            let filesystems = get_named_filesystems(&paths, &opt).map_err(|e| {
480                let context = "cannot read table of mounted file systems";
481                USimpleError::new(e.code(), format!("{context}: {e}"))
482            })?;
483
484            // This can happen if paths are given as command-line arguments
485            // but none of the paths exist.
486            if filesystems.is_empty() {
487                return Ok(());
488            }
489
490            filesystems
491        }
492    };
493
494    println!("{}", Table::new(&opt, filesystems));
495
496    Ok(())
497}
498
499pub fn uu_app() -> Command {
500    Command::new(uucore::util_name())
501        .version(crate_version!())
502        .about(ABOUT)
503        .override_usage(format_usage(USAGE))
504        .after_help(AFTER_HELP)
505        .infer_long_args(true)
506        .disable_help_flag(true)
507        .arg(
508            Arg::new(OPT_HELP)
509                .long(OPT_HELP)
510                .help("Print help information.")
511                .action(ArgAction::Help),
512        )
513        .arg(
514            Arg::new(OPT_ALL)
515                .short('a')
516                .long("all")
517                .overrides_with(OPT_ALL)
518                .help("include dummy file systems")
519                .action(ArgAction::SetTrue),
520        )
521        .arg(
522            Arg::new(OPT_BLOCKSIZE)
523                .short('B')
524                .long("block-size")
525                .value_name("SIZE")
526                .overrides_with_all([OPT_KILO, OPT_BLOCKSIZE])
527                .help(
528                    "scale sizes by SIZE before printing them; e.g.\
529                    '-BM' prints sizes in units of 1,048,576 bytes",
530                ),
531        )
532        .arg(
533            Arg::new(OPT_TOTAL)
534                .long("total")
535                .overrides_with(OPT_TOTAL)
536                .help("produce a grand total")
537                .action(ArgAction::SetTrue),
538        )
539        .arg(
540            Arg::new(OPT_HUMAN_READABLE_BINARY)
541                .short('h')
542                .long("human-readable")
543                .overrides_with_all([OPT_HUMAN_READABLE_DECIMAL, OPT_HUMAN_READABLE_BINARY])
544                .help("print sizes in human readable format (e.g., 1K 234M 2G)")
545                .action(ArgAction::SetTrue),
546        )
547        .arg(
548            Arg::new(OPT_HUMAN_READABLE_DECIMAL)
549                .short('H')
550                .long("si")
551                .overrides_with_all([OPT_HUMAN_READABLE_BINARY, OPT_HUMAN_READABLE_DECIMAL])
552                .help("likewise, but use powers of 1000 not 1024")
553                .action(ArgAction::SetTrue),
554        )
555        .arg(
556            Arg::new(OPT_INODES)
557                .short('i')
558                .long("inodes")
559                .overrides_with(OPT_INODES)
560                .help("list inode information instead of block usage")
561                .action(ArgAction::SetTrue),
562        )
563        .arg(
564            Arg::new(OPT_KILO)
565                .short('k')
566                .help("like --block-size=1K")
567                .overrides_with_all([OPT_BLOCKSIZE, OPT_KILO])
568                .action(ArgAction::SetTrue),
569        )
570        .arg(
571            Arg::new(OPT_LOCAL)
572                .short('l')
573                .long("local")
574                .overrides_with(OPT_LOCAL)
575                .help("limit listing to local file systems")
576                .action(ArgAction::SetTrue),
577        )
578        .arg(
579            Arg::new(OPT_NO_SYNC)
580                .long("no-sync")
581                .overrides_with_all([OPT_SYNC, OPT_NO_SYNC])
582                .help("do not invoke sync before getting usage info (default)")
583                .action(ArgAction::SetTrue),
584        )
585        .arg(
586            Arg::new(OPT_OUTPUT)
587                .long("output")
588                .value_name("FIELD_LIST")
589                .action(ArgAction::Append)
590                .num_args(0..)
591                .require_equals(true)
592                .use_value_delimiter(true)
593                .value_parser(OUTPUT_FIELD_LIST)
594                .default_missing_values(OUTPUT_FIELD_LIST)
595                .default_values(["source", "size", "used", "avail", "pcent", "target"])
596                .conflicts_with_all([OPT_INODES, OPT_PORTABILITY, OPT_PRINT_TYPE])
597                .help(
598                    "use the output format defined by FIELD_LIST, \
599                     or print all fields if FIELD_LIST is omitted.",
600                ),
601        )
602        .arg(
603            Arg::new(OPT_PORTABILITY)
604                .short('P')
605                .long("portability")
606                .overrides_with(OPT_PORTABILITY)
607                .help("use the POSIX output format")
608                .action(ArgAction::SetTrue),
609        )
610        .arg(
611            Arg::new(OPT_SYNC)
612                .long("sync")
613                .overrides_with_all([OPT_NO_SYNC, OPT_SYNC])
614                .help("invoke sync before getting usage info (non-windows only)")
615                .action(ArgAction::SetTrue),
616        )
617        .arg(
618            Arg::new(OPT_TYPE)
619                .short('t')
620                .long("type")
621                .value_parser(ValueParser::os_string())
622                .value_name("TYPE")
623                .action(ArgAction::Append)
624                .help("limit listing to file systems of type TYPE"),
625        )
626        .arg(
627            Arg::new(OPT_PRINT_TYPE)
628                .short('T')
629                .long("print-type")
630                .overrides_with(OPT_PRINT_TYPE)
631                .help("print file system type")
632                .action(ArgAction::SetTrue),
633        )
634        .arg(
635            Arg::new(OPT_EXCLUDE_TYPE)
636                .short('x')
637                .long("exclude-type")
638                .action(ArgAction::Append)
639                .value_parser(ValueParser::os_string())
640                .value_name("TYPE")
641                .use_value_delimiter(true)
642                .help("limit listing to file systems not of type TYPE"),
643        )
644        .arg(
645            Arg::new(OPT_PATHS)
646                .action(ArgAction::Append)
647                .value_hint(clap::ValueHint::AnyPath),
648        )
649}
650
651#[cfg(test)]
652mod tests {
653
654    mod mount_info_lt {
655
656        use crate::mount_info_lt;
657        use uucore::fsext::MountInfo;
658
659        /// Instantiate a [`MountInfo`] with the given fields.
660        fn mount_info(dev_name: &str, mount_root: &str, mount_dir: &str) -> MountInfo {
661            MountInfo {
662                dev_id: String::new(),
663                dev_name: String::from(dev_name),
664                fs_type: String::new(),
665                mount_dir: String::from(mount_dir),
666                mount_option: String::new(),
667                mount_root: String::from(mount_root),
668                remote: false,
669                dummy: false,
670            }
671        }
672
673        #[test]
674        fn test_absolute() {
675            // Prefer device name "/dev/foo" over "dev_foo".
676            let m1 = mount_info("/dev/foo", "/", "/mnt/bar");
677            let m2 = mount_info("dev_foo", "/", "/mnt/bar");
678            assert!(!mount_info_lt(&m1, &m2));
679        }
680
681        #[test]
682        fn test_shorter() {
683            // Prefer mount directory "/mnt/bar" over "/mnt/bar/baz"...
684            let m1 = mount_info("/dev/foo", "/", "/mnt/bar");
685            let m2 = mount_info("/dev/foo", "/", "/mnt/bar/baz");
686            assert!(!mount_info_lt(&m1, &m2));
687
688            // ..but prefer mount root "/root" over "/".
689            let m1 = mount_info("/dev/foo", "/root", "/mnt/bar");
690            let m2 = mount_info("/dev/foo", "/", "/mnt/bar/baz");
691            assert!(mount_info_lt(&m1, &m2));
692        }
693
694        #[test]
695        fn test_over_mounted() {
696            // Prefer the earlier entry if the devices are different but
697            // the mount directory is the same.
698            let m1 = mount_info("/dev/foo", "/", "/mnt/baz");
699            let m2 = mount_info("/dev/bar", "/", "/mnt/baz");
700            assert!(!mount_info_lt(&m1, &m2));
701        }
702    }
703
704    mod is_best {
705
706        use crate::is_best;
707        use uucore::fsext::MountInfo;
708
709        /// Instantiate a [`MountInfo`] with the given fields.
710        fn mount_info(dev_id: &str, mount_dir: &str) -> MountInfo {
711            MountInfo {
712                dev_id: String::from(dev_id),
713                dev_name: String::new(),
714                fs_type: String::new(),
715                mount_dir: String::from(mount_dir),
716                mount_option: String::new(),
717                mount_root: String::new(),
718                remote: false,
719                dummy: false,
720            }
721        }
722
723        #[test]
724        fn test_empty() {
725            let m = mount_info("0", "/mnt/bar");
726            assert!(is_best(&[], &m));
727        }
728
729        #[test]
730        fn test_different_dev_id() {
731            let m1 = mount_info("0", "/mnt/bar");
732            let m2 = mount_info("1", "/mnt/bar");
733            assert!(is_best(&[m1.clone()], &m2));
734            assert!(is_best(&[m2], &m1));
735        }
736
737        #[test]
738        fn test_same_dev_id() {
739            // There are several conditions under which a `MountInfo` is
740            // considered "better" than the others, we're just checking
741            // one condition in this test.
742            let m1 = mount_info("0", "/mnt/bar");
743            let m2 = mount_info("0", "/mnt/bar/baz");
744            assert!(!is_best(&[m1.clone()], &m2));
745            assert!(is_best(&[m2], &m1));
746        }
747    }
748
749    mod is_included {
750
751        use crate::{is_included, Options};
752        use uucore::fsext::MountInfo;
753
754        /// Instantiate a [`MountInfo`] with the given fields.
755        fn mount_info(fs_type: &str, mount_dir: &str, remote: bool, dummy: bool) -> MountInfo {
756            MountInfo {
757                dev_id: String::new(),
758                dev_name: String::new(),
759                fs_type: String::from(fs_type),
760                mount_dir: String::from(mount_dir),
761                mount_option: String::new(),
762                mount_root: String::new(),
763                remote,
764                dummy,
765            }
766        }
767
768        #[test]
769        fn test_remote_included() {
770            let opt = Options::default();
771            let m = mount_info("ext4", "/mnt/foo", true, false);
772            assert!(is_included(&m, &opt));
773        }
774
775        #[test]
776        fn test_remote_excluded() {
777            let opt = Options {
778                show_local_fs: true,
779                ..Default::default()
780            };
781            let m = mount_info("ext4", "/mnt/foo", true, false);
782            assert!(!is_included(&m, &opt));
783        }
784
785        #[test]
786        fn test_dummy_included() {
787            let opt = Options {
788                show_all_fs: true,
789                ..Default::default()
790            };
791            let m = mount_info("ext4", "/mnt/foo", false, true);
792            assert!(is_included(&m, &opt));
793        }
794
795        #[test]
796        fn test_dummy_excluded() {
797            let opt = Options::default();
798            let m = mount_info("ext4", "/mnt/foo", false, true);
799            assert!(!is_included(&m, &opt));
800        }
801
802        #[test]
803        fn test_exclude_match() {
804            let exclude = Some(vec![String::from("ext4")]);
805            let opt = Options {
806                exclude,
807                ..Default::default()
808            };
809            let m = mount_info("ext4", "/mnt/foo", false, false);
810            assert!(!is_included(&m, &opt));
811        }
812
813        #[test]
814        fn test_exclude_no_match() {
815            let exclude = Some(vec![String::from("tmpfs")]);
816            let opt = Options {
817                exclude,
818                ..Default::default()
819            };
820            let m = mount_info("ext4", "/mnt/foo", false, false);
821            assert!(is_included(&m, &opt));
822        }
823
824        #[test]
825        fn test_include_match() {
826            let include = Some(vec![String::from("ext4")]);
827            let opt = Options {
828                include,
829                ..Default::default()
830            };
831            let m = mount_info("ext4", "/mnt/foo", false, false);
832            assert!(is_included(&m, &opt));
833        }
834
835        #[test]
836        fn test_include_no_match() {
837            let include = Some(vec![String::from("tmpfs")]);
838            let opt = Options {
839                include,
840                ..Default::default()
841            };
842            let m = mount_info("ext4", "/mnt/foo", false, false);
843            assert!(!is_included(&m, &opt));
844        }
845
846        #[test]
847        fn test_include_and_exclude_match_neither() {
848            let include = Some(vec![String::from("tmpfs")]);
849            let exclude = Some(vec![String::from("squashfs")]);
850            let opt = Options {
851                include,
852                exclude,
853                ..Default::default()
854            };
855            let m = mount_info("ext4", "/mnt/foo", false, false);
856            assert!(!is_included(&m, &opt));
857        }
858
859        #[test]
860        fn test_include_and_exclude_match_exclude() {
861            let include = Some(vec![String::from("tmpfs")]);
862            let exclude = Some(vec![String::from("ext4")]);
863            let opt = Options {
864                include,
865                exclude,
866                ..Default::default()
867            };
868            let m = mount_info("ext4", "/mnt/foo", false, false);
869            assert!(!is_included(&m, &opt));
870        }
871
872        #[test]
873        fn test_include_and_exclude_match_include() {
874            let include = Some(vec![String::from("ext4")]);
875            let exclude = Some(vec![String::from("squashfs")]);
876            let opt = Options {
877                include,
878                exclude,
879                ..Default::default()
880            };
881            let m = mount_info("ext4", "/mnt/foo", false, false);
882            assert!(is_included(&m, &opt));
883        }
884    }
885
886    mod filter_mount_list {
887
888        use crate::{filter_mount_list, Options};
889
890        #[test]
891        fn test_empty() {
892            let opt = Options::default();
893            let mount_infos = vec![];
894            assert!(filter_mount_list(mount_infos, &opt).is_empty());
895        }
896    }
897}