<commit_before>// This file is part of the uutils coreutils package.
//
// (c) <NAME> <<EMAIL>>
// (c) <NAME> <<EMAIL>>
//
// For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code.
// spell-checker:ignore itotal iused iavail ipcent pcent tmpfs squashfs lofs
mod blocks;
mod columns;
mod filesystem;
mod table;

use uucore::error::{UResult, USimpleError};
use uucore::format_usage;
use uucore::fsext::{read_fs_list, MountInfo};

use clap::{crate_version, App, AppSettings, Arg, ArgMatches};

use std::fmt;
use std::path::Path;

use crate::blocks::{block_size_from_matches, BlockSize};
use crate::columns::Column;
use crate::filesystem::Filesystem;
use crate::table::{DisplayRow, Header, Row};

static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\
                      or all file systems by default.";
const USAGE: &str = "{} [OPTION]... [FILE]...";

static OPT_ALL: &str = "all";
static OPT_BLOCKSIZE: &str = "blocksize";
static OPT_DIRECT: &str = "direct";
static OPT_TOTAL: &str = "total";
static OPT_HUMAN_READABLE: &str = "human-readable";
static OPT_HUMAN_READABLE_2: &str = "human-readable-2";
static OPT_INODES: &str = "inodes";
static OPT_KILO: &str = "kilo";
static OPT_LOCAL: &str = "local";
static OPT_NO_SYNC: &str = "no-sync";
static OPT_OUTPUT: &str = "output";
static OPT_PATHS: &str = "paths";
static OPT_PORTABILITY: &str = "portability";
static OPT_SYNC: &str = "sync";
static OPT_TYPE: &str = "type";
static OPT_PRINT_TYPE: &str = "print-type";
static OPT_EXCLUDE_TYPE: &str = "exclude-type";
static OUTPUT_FIELD_LIST: [&str; 12] = [
    "source", "fstype", "itotal", "iused", "iavail", "ipcent", "size", "used", "avail", "pcent",
    "file", "target",
];

/// Parameters that control the behavior of `df`.
///
/// Most of these parameters control which rows and which columns are
/// displayed. The `block_size` determines the units to use when
/// displaying numbers of bytes or inodes.
struct Options {
    show_local_fs: bool,
    show_all_fs: bool,
    show_listed_fs: bool,
    block_size: BlockSize,

    /// Optional list of filesystem types to include in the output table.
    ///
    /// If this is not `None`, only filesystems that match one of
    /// these types will be listed.
    include: Option<Vec<String>>,

    /// Optional list of filesystem types to exclude from the output table.
    ///
    /// If this is not `None`, filesystems that match one of these
    /// types will *not* be listed.
    exclude: Option<Vec<String>>,

    /// Whether to show a final row comprising the totals for each column.
    show_total: bool,

    /// Sequence of columns to display in the output table.
    columns: Vec<Column>,
}

impl Default for Options {
    fn default() -> Self {
        Self {
            show_local_fs: Default::default(),
            show_all_fs: Default::default(),
            show_listed_fs: Default::default(),
            block_size: Default::default(),
            include: Default::default(),
            exclude: Default::default(),
            show_total: Default::default(),
            columns: vec![
                Column::Source,
                Column::Size,
                Column::Used,
                Column::Avail,
                Column::Pcent,
                Column::Target,
            ],
        }
    }
}

enum OptionsError {
    InvalidBlockSize,
}

impl fmt::Display for OptionsError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            // TODO This should include the raw string provided as the argument.
            //
            // TODO This needs to vary based on whether `--block-size`
            // or `-B` were provided.
            Self::InvalidBlockSize => write!(f, "invalid --block-size argument"),
        }
    }
}

impl Options {
    /// Convert command-line arguments into [`Options`].
    fn from(matches: &ArgMatches) -> Result<Self, OptionsError> {
        Ok(Self {
            show_local_fs: matches.is_present(OPT_LOCAL),
            show_all_fs: matches.is_present(OPT_ALL),
            show_listed_fs: false,
            block_size: block_size_from_matches(matches)
                .map_err(|_| OptionsError::InvalidBlockSize)?,
            include: matches.values_of_lossy(OPT_TYPE),
            exclude: matches.values_of_lossy(OPT_EXCLUDE_TYPE),
            show_total: matches.is_present(OPT_TOTAL),
            columns: Column::from_matches(matches),
        })
    }
}

/// Whether to display the mount info given the inclusion settings.
fn is_included(mi: &MountInfo, opt: &Options) -> bool {
    // Don't show remote filesystems if `--local` has been given.
    if mi.remote && opt.show_local_fs {
        return false;
    }

    // Don't show pseudo filesystems unless `--all` has been given.
    if mi.dummy && !opt.show_all_fs && !opt.show_listed_fs {
        return false;
    }

    // Don't show filesystems if they have been explicitly excluded.
    if let Some(ref excludes) = opt.exclude {
        if excludes.contains(&mi.fs_type) {
            return false;
        }
    }
    if let Some(ref includes) = opt.include {
        if !includes.contains(&mi.fs_type) {
            return false;
        }
    }

    true
}

/// Whether the mount info in `m2` should be prioritized over `m1`.
///
/// The "lt" in the function name is in analogy to the
/// [`std::cmp::PartialOrd::lt`].
fn mount_info_lt(m1: &MountInfo, m2: &MountInfo) -> bool {
    // let "real" devices with '/' in the name win.
    if m1.dev_name.starts_with('/') && !m2.dev_name.starts_with('/') {
        return false;
    }

    let m1_nearer_root = m1.mount_dir.len() < m2.mount_dir.len();
    // With bind mounts, prefer items nearer the root of the source
    let m2_below_root = !m1.mount_root.is_empty()
        && !m2.mount_root.is_empty()
        && m1.mount_root.len() > m2.mount_root.len();
    // let points towards the root of the device win.
    if m1_nearer_root && !m2_below_root {
        return false;
    }

    // let an entry over-mounted on a new device win, but only when
    // matching an existing mnt point, to avoid problematic
    // replacement when given inaccurate mount lists, seen with some
    // chroot environments for example.
    if m1.dev_name != m2.dev_name && m1.mount_dir == m2.mount_dir {
        return false;
    }

    true
}

/// Whether to prioritize given mount info over all others on the same device.
///
/// This function decides whether the mount info `mi` is better than
/// all others in `previous` that mount the same device as `mi`.
fn is_best(previous: &[MountInfo], mi: &MountInfo) -> bool {
    for seen in previous {
        if seen.dev_id == mi.dev_id && mount_info_lt(mi, seen) {
            return false;
        }
    }
    true
}

/// Keep only the specified subset of [`MountInfo`] instances.
///
/// The `opt` argument specifies a variety of ways of excluding
/// [`MountInfo`] instances; see [`Options`] for more information.
///
/// Finally, if there are duplicate entries, the one with the shorter
/// path is kept.

fn filter_mount_list(vmi: Vec<MountInfo>, opt: &Options) -> Vec<MountInfo> {
    let mut result = vec![];
    for mi in vmi {
        // TODO The running time of the `is_best()` function is linear
        // in the length of `result`. That makes the running time of
        // this loop quadratic in the length of `vmi`. This could be
        // improved by a more efficient implementation of `is_best()`,
        // but `vmi` is probably not very long in practice.
        if is_included(&mi, opt) && is_best(&result, &mi) {
            result.push(mi);
        }
    }
    result
}

/// Assign 1 `MountInfo` entry to each path
/// `lofs` entries are skipped and dummy mount points are skipped
/// Only the longest matching prefix for that path is considered
/// `lofs` is for Solaris style loopback filesystem and is present in Solaris and FreeBSD.
/// It works similar to symlinks
fn get_point_list(vmi: &[MountInfo], paths: &[String]) -> Vec<MountInfo> {
    paths
        .iter()
        .map(|p| {
            vmi.iter()
                .filter(|mi| mi.fs_type.ne("lofs"))
                .filter(|mi| !mi.dummy)
                .filter(|mi| p.starts_with(&mi.mount_dir))
                .max_by_key(|mi| mi.mount_dir.len())
                .unwrap()
                .clone()
        })
        .collect::<Vec<MountInfo>>()
}

#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
    let matches = uu_app().get_matches_from(args);

    // Canonicalize the input_paths and then convert to string
    let paths = matches
        .values_of(OPT_PATHS)
        .unwrap_or_default()
        .map(Path::new)
        .filter_map(|v| v.canonicalize().ok())
        .filter_map(|v| v.into_os_string().into_string().ok())
        .collect::<Vec<_>>();

    #[cfg(windows)]
    {
        if matches.is_present(OPT_INODES) {
            println!("{}: doesn't support -i option", uucore::util_name());
            return Ok(());
        }
    }

    let opt = Options::from(&matches).map_err(|e| USimpleError::new(1, format!("{}", e)))?;

    let mounts = read_fs_list();

    let op_mount_points: Vec<MountInfo> = if paths.is_empty() {
        // Get all entries
        filter_mount_list(mounts, &opt)
    } else {
        // Get Point for each input_path
        get_point_list(&mounts, &paths)
    };
    let data: Vec<Row> = op_mount_points
        .into_iter()
        .filter_map(Filesystem::new)
        .filter(|fs| fs.usage.blocks != 0 || opt.show_all_fs || opt.show_listed_fs)
        .map(Into::into)
        .collect();

    println!("{}", Header::new(&opt));
    let mut total = Row::new("total");
    for row in data {
        println!("{}", DisplayRow::new(&row, &opt));
        total += row;
    }
    if opt.show_total {
        println!("{}", DisplayRow::new(&total, &opt));
    }

    Ok(())
}

pub fn uu_app<'a>() -> App<'a> {
    App::new(uucore::util_name())
        .version(crate_version!())
        .about(ABOUT)
        .override_usage(format_usage(USAGE))
        .setting(AppSettings::InferLongArgs)<commit_msg>df: use for loop over filesystems to display rows<commit_after>// This file is part of the uutils coreutils package.
//
// (c) <NAME> <<EMAIL>>
// (c) <NAME> <<EMAIL>>
//
// For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code.
// spell-checker:ignore itotal iused iavail ipcent pcent tmpfs squashfs lofs
mod blocks;
mod columns;
mod filesystem;
mod table;

use uucore::error::{UResult, USimpleError};
use uucore::format_usage;
use uucore::fsext::{read_fs_list, MountInfo};

use clap::{crate_version, App, AppSettings, Arg, ArgMatches};

use std::fmt;
use std::path::Path;

use crate::blocks::{block_size_from_matches, BlockSize};
use crate::columns::Column;
use crate::filesystem::Filesystem;
use crate::table::{DisplayRow, Header, Row};

static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\
                      or all file systems by default.";
const USAGE: &str = "{} [OPTION]... [FILE]...";

static OPT_ALL: &str = "all";
static OPT_BLOCKSIZE: &str = "blocksize";
static OPT_DIRECT: &str = "direct";
static OPT_TOTAL: &str = "total";
static OPT_HUMAN_READABLE: &str = "human-readable";
static OPT_HUMAN_READABLE_2: &str = "human-readable-2";
static OPT_INODES: &str = "inodes";
static OPT_KILO: &str = "kilo";
static OPT_LOCAL: &str = "local";
static OPT_NO_SYNC: &str = "no-sync";
static OPT_OUTPUT: &str = "output";
static OPT_PATHS: &str = "paths";
static OPT_PORTABILITY: &str = "portability";
static OPT_SYNC: &str = "sync";
static OPT_TYPE: &str = "type";
static OPT_PRINT_TYPE: &str = "print-type";
static OPT_EXCLUDE_TYPE: &str = "exclude-type";
static OUTPUT_FIELD_LIST: [&str; 12] = [
    "source", "fstype", "itotal", "iused", "iavail", "ipcent", "size", "used", "avail", "pcent",
    "file", "target",
];

/// Parameters that control the behavior of `df`.
///
/// Most of these parameters control which rows and which columns are
/// displayed. The `block_size` determines the units to use when
/// displaying numbers of bytes or inodes.
struct Options {
    show_local_fs: bool,
    show_all_fs: bool,
    show_listed_fs: bool,
    block_size: BlockSize,

    /// Optional list of filesystem types to include in the output table.
    ///
    /// If this is not `None`, only filesystems that match one of
    /// these types will be listed.
    include: Option<Vec<String>>,

    /// Optional list of filesystem types to exclude from the output table.
    ///
    /// If this is not `None`, filesystems that match one of these
    /// types will *not* be listed.
    exclude: Option<Vec<String>>,

    /// Whether to show a final row comprising the totals for each column.
    show_total: bool,

    /// Sequence of columns to display in the output table.
    columns: Vec<Column>,
}

impl Default for Options {
    fn default() -> Self {
        Self {
            show_local_fs: Default::default(),
            show_all_fs: Default::default(),
            show_listed_fs: Default::default(),
            block_size: Default::default(),
            include: Default::default(),
            exclude: Default::default(),
            show_total: Default::default(),
            columns: vec![
                Column::Source,
                Column::Size,
                Column::Used,
                Column::Avail,
                Column::Pcent,
                Column::Target,
            ],
        }
    }
}

enum OptionsError {
    InvalidBlockSize,
}

impl fmt::Display for OptionsError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            // TODO This should include the raw string provided as the argument.
            //
            // TODO This needs to vary based on whether `--block-size`
            // or `-B` were provided.
            Self::InvalidBlockSize => write!(f, "invalid --block-size argument"),
        }
    }
}

impl Options {
    /// Convert command-line arguments into [`Options`].
    fn from(matches: &ArgMatches) -> Result<Self, OptionsError> {
        Ok(Self {
            show_local_fs: matches.is_present(OPT_LOCAL),
            show_all_fs: matches.is_present(OPT_ALL),
            show_listed_fs: false,
            block_size: block_size_from_matches(matches)
                .map_err(|_| OptionsError::InvalidBlockSize)?,
            include: matches.values_of_lossy(OPT_TYPE),
            exclude: matches.values_of_lossy(OPT_EXCLUDE_TYPE),
            show_total: matches.is_present(OPT_TOTAL),
            columns: Column::from_matches(matches),
        })
    }
}

/// Whether to display the mount info given the inclusion settings.
fn is_included(mi: &MountInfo, opt: &Options) -> bool {
    // Don't show remote filesystems if `--local` has been given.
    if mi.remote && opt.show_local_fs {
        return false;
    }

    // Don't show pseudo filesystems unless `--all` has been given.
    if mi.dummy && !opt.show_all_fs && !opt.show_listed_fs {
        return false;
    }

    // Don't show filesystems if they have been explicitly excluded.
    if let Some(ref excludes) = opt.exclude {
        if excludes.contains(&mi.fs_type) {
            return false;
        }
    }
    if let Some(ref includes) = opt.include {
        if !includes.contains(&mi.fs_type) {
            return false;
        }
    }

    true
}

/// Whether the mount info in `m2` should be prioritized over `m1`.
///
/// The "lt" in the function name is in analogy to the
/// [`std::cmp::PartialOrd::lt`].
fn mount_info_lt(m1: &MountInfo, m2: &MountInfo) -> bool {
    // let "real" devices with '/' in the name win.
    if m1.dev_name.starts_with('/') && !m2.dev_name.starts_with('/') {
        return false;
    }

    let m1_nearer_root = m1.mount_dir.len() < m2.mount_dir.len();
    // With bind mounts, prefer items nearer the root of the source
    let m2_below_root = !m1.mount_root.is_empty()
        && !m2.mount_root.is_empty()
        && m1.mount_root.len() > m2.mount_root.len();
    // let points towards the root of the device win.
    if m1_nearer_root && !m2_below_root {
        return false;
    }

    // let an entry over-mounted on a new device win, but only when
    // matching an existing mnt point, to avoid problematic
    // replacement when given inaccurate mount lists, seen with some
    // chroot environments for example.
    if m1.dev_name != m2.dev_name && m1.mount_dir == m2.mount_dir {
        return false;
    }

    true
}

/// Whether to prioritize given mount info over all others on the same device.
///
/// This function decides whether the mount info `mi` is better than
/// all others in `previous` that mount the same device as `mi`.
fn is_best(previous: &[MountInfo], mi: &MountInfo) -> bool {
    for seen in previous {
        if seen.dev_id == mi.dev_id && mount_info_lt(mi, seen) {
            return false;
        }
    }
    true
}

/// Keep only the specified subset of [`MountInfo`] instances.
///
/// The `opt` argument specifies a variety of ways of excluding
/// [`MountInfo`] instances; see [`Options`] for more information.
///
/// Finally, if there are duplicate entries, the one with the shorter
/// path is kept.

fn filter_mount_list(vmi: Vec<MountInfo>, opt: &Options) -> Vec<MountInfo> {
    let mut result = vec![];
    for mi in vmi {
        // TODO The running time of the `is_best()` function is linear
        // in the length of `result`. That makes the running time of
        // this loop quadratic in the length of `vmi`. This could be
        // improved by a more efficient implementation of `is_best()`,
        // but `vmi` is probably not very long in practice.
        if is_included(&mi, opt) && is_best(&result, &mi) {
            result.push(mi);
        }
    }
    result
}

/// Assign 1 `MountInfo` entry to each path
/// `lofs` entries are skipped and dummy mount points are skipped
/// Only the longest matching prefix for that path is considered
/// `lofs` is for Solaris style loopback filesystem and is present in Solaris and FreeBSD.
/// It works similar to symlinks
fn get_point_list(vmi: &[MountInfo], paths: &[String]) -> Vec<MountInfo> {
    paths
        .iter()
        .map(|p| {
            vmi.iter()
                .filter(|mi| mi.fs_type.ne("lofs"))
                .filter(|mi| !mi.dummy)
                .filter(|mi| p.starts_with(&mi.mount_dir))
                .max_by_key(|mi| mi.mount_dir.len())
                .unwrap()
                .clone()
        })
        .collect::<Vec<MountInfo>>()
}

#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
    let matches = uu_app().get_matches_from(args);

    // Canonicalize the input_paths and then convert to string
    let paths = matches
        .values_of(OPT_PATHS)
        .unwrap_or_default()
        .map(Path::new)
        .filter_map(|v| v.canonicalize().ok())
        .filter_map(|v| v.into_os_string().into_string().ok())
        .collect::<Vec<_>>();

    #[cfg(windows)]
    {
        if matches.is_present(OPT_INODES) {
            println!("{}: doesn't support -i option", uucore::util_name());
            return Ok(());
        }
    }

    let opt = Options::from(&matches).map_err(|e| USimpleError::new(1, format!("{}", e)))?;

    let mounts = read_fs_list();

    let op_mount_points: Vec<MountInfo> = if paths.is_empty() {
        // Get all entries
        filter_mount_list(mounts, &opt)
    } else {
        // Get Point for each input_path
        get_point_list(&mounts, &paths)
    };

    // Get the list of filesystems to display in the output table.
    let filesystems: Vec<Filesystem> = op_mount_points
        .into_iter()
        .filter_map(Filesystem::new)
        .collect();

    // The running total of filesystem sizes and usage.
    //
    // This accumulator is computed in case we need to display the
    // total counts in the last row of the table.
    let mut total = Row::new("total");

    println!("{}", Header::new(&opt));
    for filesystem in filesystems {
        // If the filesystem is not empty, or if the options require
        // showing all filesystems, then print the data as a row in
        // the output table.
        if opt.show_all_fs || opt.show_listed_fs || filesystem.usage.blocks > 0 {
            let row = Row::from(filesystem);
            println!("{}", DisplayRow::new(&row, &opt));
            total += row;
        }
    }
    if opt.show_total {
        println!("{}", DisplayRow::new(&total, &opt));
    }

    Ok(())
}

pub fn uu_app<'a>() -> App<'a> {
    App::new(uucore::util_name())
        .version(crate_version!())
        .about(ABOUT)
        .override_usage(format_usage(USAGE))
        .setting(AppSettings::InferLongArgs)
{'fraction_non_alphanumeric': 0.13215774593895252, 'fraction_numerical': 0.002587570080023001, 'mean_word_length': 2.528914440311126, 'pattern_counts': {'":': 0, '<': 53, '<?xml version=': 0, '>': 76, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1958821', 'n_tokens_mistral': 6498, 'n_tokens_neox': 6343, 'n_words': 1975}