mod blocks;
mod columns;
mod filesystem;
mod table;
use blocks::HumanReadable;
use clap::builder::ValueParser;
use table::HeaderMode;
use uucore::display::Quotable;
use uucore::error::{UError, UResult, USimpleError};
use uucore::fsext::{read_fs_list, MountInfo};
use uucore::parse_size::ParseSizeError;
use uucore::{format_usage, help_about, help_section, help_usage, show};
use clap::{crate_version, parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
use std::error::Error;
use std::ffi::OsString;
use std::fmt;
use std::path::Path;
use crate::blocks::{read_block_size, BlockSize};
use crate::columns::{Column, ColumnError};
use crate::filesystem::Filesystem;
use crate::filesystem::FsError;
use crate::table::Table;
const ABOUT: &str = help_about!("df.md");
const USAGE: &str = help_usage!("df.md");
const AFTER_HELP: &str = help_section!("after help", "df.md");
static OPT_HELP: &str = "help";
static OPT_ALL: &str = "all";
static OPT_BLOCKSIZE: &str = "blocksize";
static OPT_TOTAL: &str = "total";
static OPT_HUMAN_READABLE_BINARY: &str = "human-readable-binary";
static OPT_HUMAN_READABLE_DECIMAL: &str = "human-readable-decimal";
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",
];
struct Options {
show_local_fs: bool,
show_all_fs: bool,
human_readable: Option<HumanReadable>,
block_size: BlockSize,
header_mode: HeaderMode,
include: Option<Vec<String>>,
exclude: Option<Vec<String>>,
sync: bool,
show_total: bool,
columns: Vec<Column>,
}
impl Default for Options {
fn default() -> Self {
Self {
show_local_fs: Default::default(),
show_all_fs: Default::default(),
block_size: BlockSize::default(),
human_readable: Option::default(),
header_mode: HeaderMode::default(),
include: Option::default(),
exclude: Option::default(),
sync: Default::default(),
show_total: Default::default(),
columns: vec![
Column::Source,
Column::Size,
Column::Used,
Column::Avail,
Column::Pcent,
Column::Target,
],
}
}
}
#[derive(Debug)]
enum OptionsError {
BlockSizeTooLarge(String),
InvalidBlockSize(String),
InvalidSuffix(String),
ColumnError(ColumnError),
FilesystemTypeBothSelectedAndExcluded(Vec<String>),
}
impl fmt::Display for OptionsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::BlockSizeTooLarge(s) => {
write!(f, "--block-size argument {} too large", s.quote())
}
Self::InvalidBlockSize(s) => write!(f, "invalid --block-size argument {s}"),
Self::InvalidSuffix(s) => write!(f, "invalid suffix in --block-size argument {s}"),
Self::ColumnError(ColumnError::MultipleColumns(s)) => write!(
f,
"option --output: field {} used more than once",
s.quote()
),
#[allow(clippy::print_in_format_impl)]
Self::FilesystemTypeBothSelectedAndExcluded(types) => {
for t in types {
eprintln!(
"{}: file system type {} both selected and excluded",
uucore::util_name(),
t.quote()
);
}
Ok(())
}
}
}
}
impl Options {
fn from(matches: &ArgMatches) -> Result<Self, OptionsError> {
let include: Option<Vec<_>> = matches
.get_many::<OsString>(OPT_TYPE)
.map(|v| v.map(|s| s.to_string_lossy().to_string()).collect());
let exclude: Option<Vec<_>> = matches
.get_many::<OsString>(OPT_EXCLUDE_TYPE)
.map(|v| v.map(|s| s.to_string_lossy().to_string()).collect());
if let (Some(include), Some(exclude)) = (&include, &exclude) {
if let Some(types) = Self::get_intersected_types(include, exclude) {
return Err(OptionsError::FilesystemTypeBothSelectedAndExcluded(types));
}
}
Ok(Self {
show_local_fs: matches.get_flag(OPT_LOCAL),
show_all_fs: matches.get_flag(OPT_ALL),
sync: matches.get_flag(OPT_SYNC),
block_size: read_block_size(matches).map_err(|e| match e {
ParseSizeError::InvalidSuffix(s) => OptionsError::InvalidSuffix(s),
ParseSizeError::SizeTooBig(_) => OptionsError::BlockSizeTooLarge(
matches
.get_one::<String>(OPT_BLOCKSIZE)
.unwrap()
.to_string(),
),
ParseSizeError::ParseFailure(s) => OptionsError::InvalidBlockSize(s),
ParseSizeError::PhysicalMem(s) => OptionsError::InvalidBlockSize(s),
})?,
header_mode: {
if matches.get_flag(OPT_HUMAN_READABLE_BINARY)
|| matches.get_flag(OPT_HUMAN_READABLE_DECIMAL)
{
HeaderMode::HumanReadable
} else if matches.get_flag(OPT_PORTABILITY) {
HeaderMode::PosixPortability
} else if matches.value_source(OPT_OUTPUT) == Some(ValueSource::CommandLine) {
HeaderMode::Output
} else {
HeaderMode::Default
}
},
human_readable: {
if matches.get_flag(OPT_HUMAN_READABLE_BINARY) {
Some(HumanReadable::Binary)
} else if matches.get_flag(OPT_HUMAN_READABLE_DECIMAL) {
Some(HumanReadable::Decimal)
} else {
None
}
},
include,
exclude,
show_total: matches.get_flag(OPT_TOTAL),
columns: Column::from_matches(matches).map_err(OptionsError::ColumnError)?,
})
}
fn get_intersected_types(include: &[String], exclude: &[String]) -> Option<Vec<String>> {
let mut intersected_types = Vec::new();
for t in include {
if exclude.contains(t) {
intersected_types.push(t.clone());
}
}
(!intersected_types.is_empty()).then_some(intersected_types)
}
}
fn is_included(mi: &MountInfo, opt: &Options) -> bool {
if mi.remote && opt.show_local_fs {
return false;
}
if mi.dummy && !opt.show_all_fs {
return false;
}
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
}
fn mount_info_lt(m1: &MountInfo, m2: &MountInfo) -> bool {
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();
let m2_below_root = !m1.mount_root.is_empty()
&& !m2.mount_root.is_empty()
&& m1.mount_root.len() > m2.mount_root.len();
if m1_nearer_root && !m2_below_root {
return false;
}
if m1.dev_name != m2.dev_name && m1.mount_dir == m2.mount_dir {
return false;
}
true
}
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
}
fn filter_mount_list(vmi: Vec<MountInfo>, opt: &Options) -> Vec<MountInfo> {
let mut result = vec![];
for mi in vmi {
if is_included(&mi, opt) && is_best(&result, &mi) {
result.push(mi);
}
}
result
}
fn get_all_filesystems(opt: &Options) -> UResult<Vec<Filesystem>> {
if opt.sync {
#[cfg(not(any(windows, target_os = "redox")))]
unsafe {
#[cfg(not(target_os = "android"))]
uucore::libc::sync();
#[cfg(target_os = "android")]
uucore::libc::syscall(uucore::libc::SYS_sync);
}
}
let mounts: Vec<MountInfo> = filter_mount_list(read_fs_list()?, opt);
#[cfg(not(windows))]
{
let maybe_mount = |m| Filesystem::from_mount(&mounts, &m, None).ok();
Ok(mounts
.clone()
.into_iter()
.filter_map(maybe_mount)
.filter(|fs| opt.show_all_fs || fs.usage.blocks > 0)
.collect())
}
#[cfg(windows)]
{
let maybe_mount = |m| Filesystem::from_mount(&m, None).ok();
Ok(mounts
.into_iter()
.filter_map(maybe_mount)
.filter(|fs| opt.show_all_fs || fs.usage.blocks > 0)
.collect())
}
}
fn get_named_filesystems<P>(paths: &[P], opt: &Options) -> UResult<Vec<Filesystem>>
where
P: AsRef<Path>,
{
let mounts: Vec<MountInfo> = filter_mount_list(read_fs_list()?, opt)
.into_iter()
.filter(|mi| mi.fs_type != "lofs" && !mi.dummy)
.collect();
let mut result = vec![];
if mounts.is_empty() {
show!(USimpleError::new(1, "no file systems processed"));
return Ok(result);
}
for path in paths {
match Filesystem::from_path(&mounts, path) {
Ok(fs) => result.push(fs),
Err(FsError::InvalidPath) => {
show!(USimpleError::new(
1,
format!("{}: No such file or directory", path.as_ref().display())
));
}
Err(FsError::MountMissing) => {
show!(USimpleError::new(1, "no file systems processed"));
}
#[cfg(not(windows))]
Err(FsError::OverMounted) => {
show!(USimpleError::new(
1,
format!(
"cannot access {}: over-mounted by another device",
path.as_ref().quote()
)
));
}
}
}
Ok(result)
}
#[derive(Debug)]
enum DfError {
OptionsError(OptionsError),
}
impl Error for DfError {}
impl UError for DfError {
fn usage(&self) -> bool {
matches!(self, Self::OptionsError(OptionsError::ColumnError(_)))
}
}
impl fmt::Display for DfError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::OptionsError(e) => e.fmt(f),
}
}
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;
#[cfg(windows)]
{
if matches.get_flag(OPT_INODES) {
println!("{}: doesn't support -i option", uucore::util_name());
return Ok(());
}
}
let opt = Options::from(&matches).map_err(DfError::OptionsError)?;
let filesystems: Vec<Filesystem> = match matches.get_many::<String>(OPT_PATHS) {
None => {
let filesystems = get_all_filesystems(&opt).map_err(|e| {
let context = "cannot read table of mounted file systems";
USimpleError::new(e.code(), format!("{context}: {e}"))
})?;
if filesystems.is_empty() {
return Err(USimpleError::new(1, "no file systems processed"));
}
filesystems
}
Some(paths) => {
let paths: Vec<_> = paths.collect();
let filesystems = get_named_filesystems(&paths, &opt).map_err(|e| {
let context = "cannot read table of mounted file systems";
USimpleError::new(e.code(), format!("{context}: {e}"))
})?;
if filesystems.is_empty() {
return Ok(());
}
filesystems
}
};
println!("{}", Table::new(&opt, filesystems));
Ok(())
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.after_help(AFTER_HELP)
.infer_long_args(true)
.disable_help_flag(true)
.arg(
Arg::new(OPT_HELP)
.long(OPT_HELP)
.help("Print help information.")
.action(ArgAction::Help),
)
.arg(
Arg::new(OPT_ALL)
.short('a')
.long("all")
.overrides_with(OPT_ALL)
.help("include dummy file systems")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_BLOCKSIZE)
.short('B')
.long("block-size")
.value_name("SIZE")
.overrides_with_all([OPT_KILO, OPT_BLOCKSIZE])
.help(
"scale sizes by SIZE before printing them; e.g.\
'-BM' prints sizes in units of 1,048,576 bytes",
),
)
.arg(
Arg::new(OPT_TOTAL)
.long("total")
.overrides_with(OPT_TOTAL)
.help("produce a grand total")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_HUMAN_READABLE_BINARY)
.short('h')
.long("human-readable")
.overrides_with_all([OPT_HUMAN_READABLE_DECIMAL, OPT_HUMAN_READABLE_BINARY])
.help("print sizes in human readable format (e.g., 1K 234M 2G)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_HUMAN_READABLE_DECIMAL)
.short('H')
.long("si")
.overrides_with_all([OPT_HUMAN_READABLE_BINARY, OPT_HUMAN_READABLE_DECIMAL])
.help("likewise, but use powers of 1000 not 1024")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_INODES)
.short('i')
.long("inodes")
.overrides_with(OPT_INODES)
.help("list inode information instead of block usage")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_KILO)
.short('k')
.help("like --block-size=1K")
.overrides_with_all([OPT_BLOCKSIZE, OPT_KILO])
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_LOCAL)
.short('l')
.long("local")
.overrides_with(OPT_LOCAL)
.help("limit listing to local file systems")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_NO_SYNC)
.long("no-sync")
.overrides_with_all([OPT_SYNC, OPT_NO_SYNC])
.help("do not invoke sync before getting usage info (default)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_OUTPUT)
.long("output")
.value_name("FIELD_LIST")
.action(ArgAction::Append)
.num_args(0..)
.require_equals(true)
.use_value_delimiter(true)
.value_parser(OUTPUT_FIELD_LIST)
.default_missing_values(OUTPUT_FIELD_LIST)
.default_values(["source", "size", "used", "avail", "pcent", "target"])
.conflicts_with_all([OPT_INODES, OPT_PORTABILITY, OPT_PRINT_TYPE])
.help(
"use the output format defined by FIELD_LIST, \
or print all fields if FIELD_LIST is omitted.",
),
)
.arg(
Arg::new(OPT_PORTABILITY)
.short('P')
.long("portability")
.overrides_with(OPT_PORTABILITY)
.help("use the POSIX output format")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_SYNC)
.long("sync")
.overrides_with_all([OPT_NO_SYNC, OPT_SYNC])
.help("invoke sync before getting usage info (non-windows only)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_TYPE)
.short('t')
.long("type")
.value_parser(ValueParser::os_string())
.value_name("TYPE")
.action(ArgAction::Append)
.help("limit listing to file systems of type TYPE"),
)
.arg(
Arg::new(OPT_PRINT_TYPE)
.short('T')
.long("print-type")
.overrides_with(OPT_PRINT_TYPE)
.help("print file system type")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_EXCLUDE_TYPE)
.short('x')
.long("exclude-type")
.action(ArgAction::Append)
.value_parser(ValueParser::os_string())
.value_name("TYPE")
.use_value_delimiter(true)
.help("limit listing to file systems not of type TYPE"),
)
.arg(
Arg::new(OPT_PATHS)
.action(ArgAction::Append)
.value_hint(clap::ValueHint::AnyPath),
)
}
#[cfg(test)]
mod tests {
mod mount_info_lt {
use crate::mount_info_lt;
use uucore::fsext::MountInfo;
fn mount_info(dev_name: &str, mount_root: &str, mount_dir: &str) -> MountInfo {
MountInfo {
dev_id: String::new(),
dev_name: String::from(dev_name),
fs_type: String::new(),
mount_dir: String::from(mount_dir),
mount_option: String::new(),
mount_root: String::from(mount_root),
remote: false,
dummy: false,
}
}
#[test]
fn test_absolute() {
let m1 = mount_info("/dev/foo", "/", "/mnt/bar");
let m2 = mount_info("dev_foo", "/", "/mnt/bar");
assert!(!mount_info_lt(&m1, &m2));
}
#[test]
fn test_shorter() {
let m1 = mount_info("/dev/foo", "/", "/mnt/bar");
let m2 = mount_info("/dev/foo", "/", "/mnt/bar/baz");
assert!(!mount_info_lt(&m1, &m2));
let m1 = mount_info("/dev/foo", "/root", "/mnt/bar");
let m2 = mount_info("/dev/foo", "/", "/mnt/bar/baz");
assert!(mount_info_lt(&m1, &m2));
}
#[test]
fn test_over_mounted() {
let m1 = mount_info("/dev/foo", "/", "/mnt/baz");
let m2 = mount_info("/dev/bar", "/", "/mnt/baz");
assert!(!mount_info_lt(&m1, &m2));
}
}
mod is_best {
use crate::is_best;
use uucore::fsext::MountInfo;
fn mount_info(dev_id: &str, mount_dir: &str) -> MountInfo {
MountInfo {
dev_id: String::from(dev_id),
dev_name: String::new(),
fs_type: String::new(),
mount_dir: String::from(mount_dir),
mount_option: String::new(),
mount_root: String::new(),
remote: false,
dummy: false,
}
}
#[test]
fn test_empty() {
let m = mount_info("0", "/mnt/bar");
assert!(is_best(&[], &m));
}
#[test]
fn test_different_dev_id() {
let m1 = mount_info("0", "/mnt/bar");
let m2 = mount_info("1", "/mnt/bar");
assert!(is_best(&[m1.clone()], &m2));
assert!(is_best(&[m2], &m1));
}
#[test]
fn test_same_dev_id() {
let m1 = mount_info("0", "/mnt/bar");
let m2 = mount_info("0", "/mnt/bar/baz");
assert!(!is_best(&[m1.clone()], &m2));
assert!(is_best(&[m2], &m1));
}
}
mod is_included {
use crate::{is_included, Options};
use uucore::fsext::MountInfo;
fn mount_info(fs_type: &str, mount_dir: &str, remote: bool, dummy: bool) -> MountInfo {
MountInfo {
dev_id: String::new(),
dev_name: String::new(),
fs_type: String::from(fs_type),
mount_dir: String::from(mount_dir),
mount_option: String::new(),
mount_root: String::new(),
remote,
dummy,
}
}
#[test]
fn test_remote_included() {
let opt = Options::default();
let m = mount_info("ext4", "/mnt/foo", true, false);
assert!(is_included(&m, &opt));
}
#[test]
fn test_remote_excluded() {
let opt = Options {
show_local_fs: true,
..Default::default()
};
let m = mount_info("ext4", "/mnt/foo", true, false);
assert!(!is_included(&m, &opt));
}
#[test]
fn test_dummy_included() {
let opt = Options {
show_all_fs: true,
..Default::default()
};
let m = mount_info("ext4", "/mnt/foo", false, true);
assert!(is_included(&m, &opt));
}
#[test]
fn test_dummy_excluded() {
let opt = Options::default();
let m = mount_info("ext4", "/mnt/foo", false, true);
assert!(!is_included(&m, &opt));
}
#[test]
fn test_exclude_match() {
let exclude = Some(vec![String::from("ext4")]);
let opt = Options {
exclude,
..Default::default()
};
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(!is_included(&m, &opt));
}
#[test]
fn test_exclude_no_match() {
let exclude = Some(vec![String::from("tmpfs")]);
let opt = Options {
exclude,
..Default::default()
};
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(is_included(&m, &opt));
}
#[test]
fn test_include_match() {
let include = Some(vec![String::from("ext4")]);
let opt = Options {
include,
..Default::default()
};
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(is_included(&m, &opt));
}
#[test]
fn test_include_no_match() {
let include = Some(vec![String::from("tmpfs")]);
let opt = Options {
include,
..Default::default()
};
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(!is_included(&m, &opt));
}
#[test]
fn test_include_and_exclude_match_neither() {
let include = Some(vec![String::from("tmpfs")]);
let exclude = Some(vec![String::from("squashfs")]);
let opt = Options {
include,
exclude,
..Default::default()
};
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(!is_included(&m, &opt));
}
#[test]
fn test_include_and_exclude_match_exclude() {
let include = Some(vec![String::from("tmpfs")]);
let exclude = Some(vec![String::from("ext4")]);
let opt = Options {
include,
exclude,
..Default::default()
};
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(!is_included(&m, &opt));
}
#[test]
fn test_include_and_exclude_match_include() {
let include = Some(vec![String::from("ext4")]);
let exclude = Some(vec![String::from("squashfs")]);
let opt = Options {
include,
exclude,
..Default::default()
};
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(is_included(&m, &opt));
}
}
mod filter_mount_list {
use crate::{filter_mount_list, Options};
#[test]
fn test_empty() {
let opt = Options::default();
let mount_infos = vec![];
assert!(filter_mount_list(mount_infos, &opt).is_empty());
}
}
}