diff --git a/Cargo.lock b/Cargo.lock index c061dcd..92b5f27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,6 +1020,7 @@ dependencies = [ "rlimit", "tempfile", "textwrap", + "uu_blockdev", "uu_ctrlaltdel", "uu_dmesg", "uu_fsfreeze", @@ -1033,6 +1034,17 @@ dependencies = [ "xattr", ] +[[package]] +name = "uu_blockdev" +version = "0.0.1" +dependencies = [ + "clap", + "linux-raw-sys 0.7.0", + "regex", + "sysinfo", + "uucore", +] + [[package]] name = "uu_ctrlaltdel" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index f681a28..e4a6862 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ default = ["feat_common_core"] uudoc = [] feat_common_core = [ + "blockdev", "ctrlaltdel", "dmesg", "fsfreeze", @@ -67,6 +68,7 @@ phf = { workspace = true } textwrap = { workspace = true } uucore = { workspace = true } +blockdev = { optional = true, version = "0.0.1", package = "uu_blockdev", path = "src/uu/blockdev" } ctrlaltdel = { optional = true, version = "0.0.1", package = "uu_ctrlaltdel", path = "src/uu/ctrlaltdel" } dmesg = { optional = true, version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg" } fsfreeze = { optional = true, version = "0.0.1", package = "uu_fsfreeze", path = "src/uu/fsfreeze" } diff --git a/src/uu/blockdev/Cargo.toml b/src/uu/blockdev/Cargo.toml new file mode 100644 index 0000000..eb81469 --- /dev/null +++ b/src/uu/blockdev/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "uu_blockdev" +version = "0.0.1" +edition = "2021" +description = "blockdev ~ Get or set various block device attributes." + +[lib] +path = "src/blockdev.rs" + +[[bin]] +name = "blockdev" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +linux-raw-sys = { workspace = true } +regex = { workspace = true } +sysinfo = { workspace = true } +uucore = { workspace = true } diff --git a/src/uu/blockdev/blockdev.md b/src/uu/blockdev/blockdev.md new file mode 100644 index 0000000..a520424 --- /dev/null +++ b/src/uu/blockdev/blockdev.md @@ -0,0 +1,8 @@ +# blockdev + +``` +blockdev +blockdev --report +``` + +Get or set various block device attributes. diff --git a/src/uu/blockdev/src/blockdev.rs b/src/uu/blockdev/src/blockdev.rs new file mode 100644 index 0000000..aabff89 --- /dev/null +++ b/src/uu/blockdev/src/blockdev.rs @@ -0,0 +1,432 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::{crate_version, value_parser, Arg, ArgAction, Command}; +use linux_raw_sys::ioctl::*; +#[cfg(target_os = "linux")] +use std::collections::BTreeMap; +#[cfg(target_os = "linux")] +use uucore::error::USimpleError; +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("blockdev.md"); +const USAGE: &str = help_usage!("blockdev.md"); + +#[derive(Copy, Clone, Debug)] +enum IoctlArgType { + Short, + Int, + Long, + U64Sectors, + U64, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +enum IoctlCommand { + GetAttribute(IoctlArgType), + SetAttribute, + Operation(u32), +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +enum BlockdevCommand { + SetVerbosity(bool), + Ioctl(&'static str, u32, IoctlCommand), +} + +const BLOCKDEV_ACTIONS: &[(&str, BlockdevCommand)] = &[ + ("verbose", BlockdevCommand::SetVerbosity(true)), + ("quiet", BlockdevCommand::SetVerbosity(false)), + ( + "flushbufs", + BlockdevCommand::Ioctl("flush buffers", BLKFLSBUF, IoctlCommand::Operation(0)), + ), + ( + "getalignoff", + BlockdevCommand::Ioctl( + "get alignment offset in bytes", + BLKALIGNOFF, + IoctlCommand::GetAttribute(IoctlArgType::Int), + ), + ), + ( + "getbsz", + BlockdevCommand::Ioctl( + "get blocksize", + BLKBSZGET, + IoctlCommand::GetAttribute(IoctlArgType::Int), + ), + ), + ( + "getdiscardzeroes", + BlockdevCommand::Ioctl( + "get discard zeroes support status", + BLKDISCARDZEROES, + IoctlCommand::GetAttribute(IoctlArgType::Int), + ), + ), + ( + "getfra", + BlockdevCommand::Ioctl( + "get filesystem readahead", + BLKFRAGET, + IoctlCommand::GetAttribute(IoctlArgType::Long), + ), + ), + ( + "getiomin", + BlockdevCommand::Ioctl( + "get minimum I/O size", + BLKIOMIN, + IoctlCommand::GetAttribute(IoctlArgType::Int), + ), + ), + ( + "getioopt", + BlockdevCommand::Ioctl( + "get optimal I/O size", + BLKIOOPT, + IoctlCommand::GetAttribute(IoctlArgType::Int), + ), + ), + ( + "getmaxsect", + BlockdevCommand::Ioctl( + "get max sectors per request", + BLKSECTGET, + IoctlCommand::GetAttribute(IoctlArgType::Short), + ), + ), + ( + "getpbsz", + BlockdevCommand::Ioctl( + "get physical block (sector) size", + BLKPBSZGET, + IoctlCommand::GetAttribute(IoctlArgType::Int), + ), + ), + ( + "getra", + BlockdevCommand::Ioctl( + "get readahead", + BLKRAGET, + IoctlCommand::GetAttribute(IoctlArgType::Long), + ), + ), + ( + "getro", + BlockdevCommand::Ioctl( + "get read-only", + BLKROGET, + IoctlCommand::GetAttribute(IoctlArgType::Int), + ), + ), + ( + "getsize64", + BlockdevCommand::Ioctl( + "get size in bytes", + BLKGETSIZE64, + IoctlCommand::GetAttribute(IoctlArgType::U64), + ), + ), + ( + "getsize", + BlockdevCommand::Ioctl( + "get 32-bit sector count (deprecated, use --getsz)", + BLKGETSIZE, + IoctlCommand::GetAttribute(IoctlArgType::Long), + ), + ), + ( + "getss", + BlockdevCommand::Ioctl( + "get logical block (sector) size", + BLKSSZGET, + IoctlCommand::GetAttribute(IoctlArgType::Int), + ), + ), + ( + "getsz", + BlockdevCommand::Ioctl( + "get size in 512-byte sectors", + BLKGETSIZE64, + IoctlCommand::GetAttribute(IoctlArgType::U64Sectors), + ), + ), + ( + "rereadpt", + BlockdevCommand::Ioctl( + "reread partition table", + BLKRRPART, + IoctlCommand::Operation(0), + ), + ), + ( + "setbsz", + BlockdevCommand::Ioctl("set blocksize", BLKBSZSET, IoctlCommand::SetAttribute), + ), + ( + "setfra", + BlockdevCommand::Ioctl( + "set filesystem readahead", + BLKFRASET, + IoctlCommand::SetAttribute, + ), + ), + ( + "setra", + BlockdevCommand::Ioctl("set readahead", BLKRASET, IoctlCommand::SetAttribute), + ), + ( + "setro", + BlockdevCommand::Ioctl("set read-only", BLKROSET, IoctlCommand::Operation(1)), + ), + ( + "setrw", + BlockdevCommand::Ioctl("set read-write", BLKROSET, IoctlCommand::Operation(0)), + ), +]; + +#[cfg(target_os = "linux")] +mod linux { + use crate::*; + use std::{fs::File, io, os::fd::AsRawFd}; + use std::{io::Read, os::unix::fs::MetadataExt, path::Path}; + use uucore::{error::UIoError, libc}; + + unsafe fn uu_ioctl(device_file: &File, ioctl_code: u32, input: T) -> UResult<()> { + if libc::ioctl(device_file.as_raw_fd(), ioctl_code.into(), input) < 0 { + Err(Box::new(UIoError::from(io::Error::last_os_error()))) + } else { + Ok(()) + } + } + + unsafe fn get_ioctl_attribute( + device_file: &File, + ioctl_code: u32, + ioctl_type: IoctlArgType, + ) -> UResult { + unsafe fn ioctl_get>( + device: &File, + ioctl_code: u32, + ) -> UResult { + let mut retval: T = Default::default(); + uu_ioctl(device, ioctl_code, &mut retval as *mut T as usize).map(|_| retval.into()) + } + + match ioctl_type { + IoctlArgType::Int => ioctl_get::(device_file, ioctl_code), + IoctlArgType::Long => ioctl_get::(device_file, ioctl_code), + IoctlArgType::Short => ioctl_get::(device_file, ioctl_code), + IoctlArgType::U64 => ioctl_get::(device_file, ioctl_code), + IoctlArgType::U64Sectors => Ok(ioctl_get::(device_file, ioctl_code)? / 512), + } + } + + fn get_partition_offset(device_file: &File) -> UResult { + let rdev = device_file.metadata()?.rdev(); + let major = unsafe { libc::major(rdev) }; + let minor = unsafe { libc::minor(rdev) }; + if Path::new(&format!("/sys/dev/block/{}:{}/partition", major, minor)).exists() { + let mut start_fd = File::open(format!("/sys/dev/block/{}:{}/start", major, minor))?; + let mut str = String::new(); + start_fd.read_to_string(&mut str)?; + return str + .trim() + .parse() + .map_err(|_| USimpleError::new(1, "Unable to parse partition start offset")); + } + Ok(0) + } + + pub fn do_report(device_path: &str) -> UResult<()> { + let device_file = File::open(device_path)?; + let partition_offset = get_partition_offset(&device_file)?; + let report_ioctls = &["getro", "getra", "getss", "getbsz", "getsize64"]; + let ioctl_values = report_ioctls + .iter() + .map(|flag| { + let Some(( + _, + BlockdevCommand::Ioctl(_, ioctl_code, IoctlCommand::GetAttribute(ioctl_type)), + )) = BLOCKDEV_ACTIONS.iter().find(|(n, _)| flag == n) + else { + unreachable!() + }; + unsafe { get_ioctl_attribute(&device_file, *ioctl_code, *ioctl_type) } + }) + .collect::, _>>()?; + println!( + "{} {:5} {:5} {:5} {:15} {:15} {}", + if ioctl_values[0] == 1 { "ro" } else { "rw" }, + ioctl_values[1], + ioctl_values[2], + ioctl_values[3], + partition_offset, + ioctl_values[4], + device_path + ); + Ok(()) + } + + pub fn do_ioctl_command( + device: &File, + name: &str, + ioctl_code: u32, + ioctl_action: &IoctlCommand, + verbose: bool, + arg: usize, + ) -> UResult<()> { + match ioctl_action { + IoctlCommand::GetAttribute(ioctl_type) => { + let ret = unsafe { get_ioctl_attribute(device, ioctl_code, *ioctl_type)? }; + if verbose { + println!("{}: {}", name, ret); + } else { + println!("{}", ret); + } + } + IoctlCommand::SetAttribute => { + unsafe { uu_ioctl(device, ioctl_code, arg)? }; + if verbose { + println!("{} succeeded.", name); + } + } + IoctlCommand::Operation(param) => { + unsafe { uu_ioctl(device, ioctl_code, param)? }; + if verbose { + println!("{} succeeded.", name); + } + } + }; + Ok(()) + } +} + +#[cfg(target_os = "linux")] +use linux::*; + +#[cfg(target_os = "linux")] +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + use std::fs::File; + + let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + let devices = matches + .get_many::("devices") + .expect("Required command-line argument"); + + if matches.get_flag("report") { + println!("RO RA SSZ BSZ StartSec Size Device"); + for device_path in devices { + uucore::show_if_err!(do_report(device_path)); + } + Ok(()) + } else { + // Recover arguments from clap in the same order they were passed + // Based on https://docs.rs/clap/latest/clap/_cookbook/find/index.html + let mut operations = BTreeMap::new(); + for (id, op) in BLOCKDEV_ACTIONS { + if matches.value_source(id) != Some(clap::parser::ValueSource::CommandLine) { + continue; + } + let indices = matches.indices_of(id).unwrap(); + let values = matches.get_many::(id).unwrap(); + for (index, value) in indices.zip(values) { + operations.insert(index, (op.clone(), *value)); + } + } + + for device_path in devices { + let mut verbose = false; + let device_file = File::open(device_path)?; + for (operation, value) in operations.values() { + match operation { + BlockdevCommand::SetVerbosity(true) => verbose = true, + BlockdevCommand::SetVerbosity(false) => verbose = false, + BlockdevCommand::Ioctl(description, ioctl_code, ioctl_action) => { + if let Err(e) = do_ioctl_command( + &device_file, + description, + *ioctl_code, + ioctl_action, + verbose, + *value, + ) { + if verbose { + println!("{} failed.", description); + } + return Err(e); + } + } + } + } + } + Ok(()) + } +} + +pub fn uu_app() -> Command { + let mut cmd = Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg( + Arg::new("report") + .long("report") + .help("print report for specified devices") + .action(ArgAction::SetTrue), + ) + .arg(Arg::new("devices").required(true).action(ArgAction::Append)); + + for (flag, action) in BLOCKDEV_ACTIONS { + let mut arg = Arg::new(flag) + .long(flag) + .conflicts_with("report") + .action(ArgAction::Append) + .value_parser(value_parser!(usize)); + + match action { + BlockdevCommand::SetVerbosity(true) => { + arg = arg.short('v').help("verbose mode"); + } + BlockdevCommand::SetVerbosity(false) => { + arg = arg.short('q').help("quiet mode"); + } + BlockdevCommand::Ioctl(name, _, _) => { + arg = arg.help(name); + } + } + + match action { + BlockdevCommand::Ioctl(_, _, IoctlCommand::SetAttribute) => { + arg = arg.num_args(1); + } + _ => { + arg = arg + .num_args(0) + .default_value("0") + .default_missing_value("0"); + } + } + cmd = cmd.arg(arg); + } + cmd +} + +#[cfg(not(target_os = "linux"))] +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let _matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + + Err(uucore::error::USimpleError::new( + 1, + "`blockdev` is available only on Linux.", + )) +} diff --git a/src/uu/blockdev/src/main.rs b/src/uu/blockdev/src/main.rs new file mode 100644 index 0000000..0452069 --- /dev/null +++ b/src/uu/blockdev/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_blockdev); diff --git a/tests/by-util/test_blockdev.rs b/tests/by-util/test_blockdev.rs new file mode 100644 index 0000000..0fed69a --- /dev/null +++ b/tests/by-util/test_blockdev.rs @@ -0,0 +1,70 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::common::util::TestScenario; + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} + +#[test] +fn test_report_mutually_exclusive_with_others() { + new_ucmd!() + .arg("--report") + .arg("--getalignoff") + .arg("/foo") + .fails() + .code_is(1) + .stderr_contains("the argument '--report' cannot be used with '--getalignoff'"); +} + +#[cfg(target_os = "linux")] +mod linux { + use crate::common::util::TestScenario; + use regex::Regex; + + #[test] + fn test_fails_on_first_error() { + new_ucmd!() + .arg("-v") + .arg("--getalignoff") + .arg("--getbsz") + .arg("/dev/null") + .fails() + .code_is(1) + .stdout_is("get alignment offset in bytes failed.\n") + .stderr_contains("Inappropriate ioctl for device"); + } + + #[test] + fn test_report_continues_on_errors() { + new_ucmd!() + .arg("--report") + .arg("/dev/null") + .arg("/non/existing") + .fails() + .code_is(1) + .stderr_matches( + &Regex::new("(?ms)Inappropriate ioctl for device.*No such file or directory") + .unwrap(), + ); + } +} + +#[cfg(not(target_os = "linux"))] +mod non_linux { + use crate::common::util::TestScenario; + + #[test] + fn test_fails_on_unsupported_platforms() { + new_ucmd!() + .arg("--report") + .arg("/dev/null") + .fails() + .code_is(1) + .stderr_is("blockdev: `blockdev` is available only on Linux.\n"); + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 824a096..23c3f90 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -17,6 +17,10 @@ mod test_lsmem; #[path = "by-util/test_mountpoint.rs"] mod test_mountpoint; +#[cfg(feature = "blockdev")] +#[path = "by-util/test_blockdev.rs"] +mod test_blockdev; + #[cfg(feature = "ctrlaltdel")] #[path = "by-util/test_ctrlaltdel.rs"] mod test_ctrlaltdel;