diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f38a45f..7d4dcb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,4 +124,4 @@ jobs: ## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} name: codecov-umbrella - fail_ci_if_error: false + fail_ci_if_error: false \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index db2fdd4..f9b1c5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,12 +67,44 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "assert_cmd" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -183,6 +215,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "displaydoc" version = "0.2.5" @@ -228,6 +266,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "fluent" version = "0.17.0" @@ -330,6 +377,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" name = "hostname" version = "0.0.1" dependencies = [ + "assert_cmd", "clap", "clap_complete", "clap_mangen", @@ -337,6 +385,7 @@ dependencies = [ "libc", "phf", "phf_codegen", + "predicates", "pretty_assertions", "procfs", "rand", @@ -494,12 +543,27 @@ dependencies = [ "libc", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -590,6 +654,36 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -858,6 +952,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "textwrap" version = "0.16.2" @@ -1053,6 +1153,15 @@ dependencies = [ "xattr", ] +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" diff --git a/Cargo.toml b/Cargo.toml index 27a181a..968be7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,8 @@ uucore = { workspace = true } # hostname = { optional = true, version = "0.0.1", package = "uu_hostname", path = "src/uu/hostname" } +predicates = "3.1.4" +assert_cmd = "2.1.2" [dev-dependencies] ctor = { workspace = true } @@ -91,6 +93,22 @@ phf_codegen = { workspace = true } name = "hostname" path = "src/bin/hostname.rs" +[[bin]] +name = "domainname" +path = "src/uu/domainname/src/main.rs" + +[[bin]] +name = "dnsdomainname" +path = "src/uu/dnsdomainname/src/main.rs" + +[[bin]] +name = "nisdomainname" +path = "src/uu/nisdomainname/src/main.rs" + +[[bin]] +name= "ypdomainname" +path = "src/uu/ypdomainname/src/main.rs" + [[bin]] name = "uudoc" path = "src/bin/uudoc.rs" diff --git a/src/uu/dnsdomainname/src/main.rs b/src/uu/dnsdomainname/src/main.rs new file mode 100644 index 0000000..4ddcfef --- /dev/null +++ b/src/uu/dnsdomainname/src/main.rs @@ -0,0 +1,12 @@ +// Wrapper binary: dnsdomainname -> hostname -d +fn main() { + uucore::panic::mute_sigpipe_panic(); + + use std::ffi::OsString; + use std::process; + + let args = std::env::args_os().skip(1); + let iter = (vec![OsString::from("hostname"), OsString::from("-d")].into_iter()).chain(args); + + process::exit(hostname::uumain(iter)); +} diff --git a/src/uu/domainname/src/main.rs b/src/uu/domainname/src/main.rs new file mode 100644 index 0000000..2d8f3ca --- /dev/null +++ b/src/uu/domainname/src/main.rs @@ -0,0 +1,12 @@ +// Wrapper binary: domainname -> hostname -y +fn main() { + uucore::panic::mute_sigpipe_panic(); + + use std::ffi::OsString; + use std::process; + + let args = std::env::args_os().skip(1); + let iter = (vec![OsString::from("hostname"), OsString::from("-y")].into_iter()).chain(args); + + process::exit(hostname::uumain(iter)); +} diff --git a/src/uu/hostname/src/change.rs b/src/uu/hostname/src/change.rs index 7497aec..6466c84 100644 --- a/src/uu/hostname/src/change.rs +++ b/src/uu/hostname/src/change.rs @@ -9,6 +9,6 @@ pub(crate) mod unix; pub(crate) mod windows; #[cfg(not(target_family = "windows"))] -pub(crate) use unix::{from_argument, from_file}; +pub(crate) use unix::{from_argument, from_argument_nis, from_file, from_file_nis}; #[cfg(target_family = "windows")] pub(crate) use windows::{from_argument, from_file}; diff --git a/src/uu/hostname/src/change/unix.rs b/src/uu/hostname/src/change/unix.rs index d12cda2..93e1b17 100644 --- a/src/uu/hostname/src/change/unix.rs +++ b/src/uu/hostname/src/change/unix.rs @@ -10,13 +10,17 @@ use std::path::Path; use uucore::error::UResult; use crate::errors::HostNameError; -use crate::net::set_host_name; +use crate::net::{set_domain_name, set_host_name}; use crate::utils::parse_host_name_file; pub(crate) fn from_file(path: &Path) -> UResult<()> { parse_host_name_file(path).map(Cow::Owned).and_then(run) } +pub(crate) fn from_file_nis(path: &Path) -> UResult<()> { + parse_host_name_file(path).map(Cow::Owned).and_then(run_nis) +} + pub(crate) fn from_argument(host_name: &OsStr) -> UResult<()> { #[cfg(target_family = "unix")] let host_name = { @@ -33,6 +37,22 @@ pub(crate) fn from_argument(host_name: &OsStr) -> UResult<()> { run(host_name) } +pub(crate) fn from_argument_nis(domain_name: &OsStr) -> UResult<()> { + #[cfg(target_family = "unix")] + let domain_name = { + use std::os::unix::ffi::OsStrExt; + Cow::Borrowed(domain_name.as_bytes()) + }; + + #[cfg(target_family = "wasm")] + let domain_name = { + use std::os::wasm::ffi::OsStrExt; + Cow::Borrowed(domain_name.as_bytes()) + }; + + run_nis(domain_name) +} + fn run(mut host_name: Cow<[u8]>) -> UResult<()> { // Trim white space. match &mut host_name { @@ -54,6 +74,32 @@ fn run(mut host_name: Cow<[u8]>) -> UResult<()> { set_host_name(&host_name) } +fn run_nis(mut domain_name: Cow<[u8]>) -> UResult<()> { + match &mut domain_name { + Cow::Borrowed(name) => *name = name.trim_ascii(), + + Cow::Owned(name) => { + while name.first().is_some_and(u8::is_ascii_whitespace) { + name.remove(0); + } + + while name.last().is_some_and(u8::is_ascii_whitespace) { + name.pop(); + } + } + }; + + let domain_name = validate_domain_name(domain_name)?; + set_domain_name(&domain_name) +} + +fn validate_domain_name(domain_name: Cow<[u8]>) -> Result { + if domain_name.is_empty() { + return Err(HostNameError::InvalidHostName); + } + CString::new(domain_name.into_owned()).map_err(|_| HostNameError::InvalidHostName) +} + fn validate_host_name(host_name: Cow<[u8]>) -> Result { // Rules: // - The only allowed prefix and suffix characters are alphanumeric. diff --git a/src/uu/hostname/src/errors.rs b/src/uu/hostname/src/errors.rs index fe85bd0..ecf3239 100644 --- a/src/uu/hostname/src/errors.rs +++ b/src/uu/hostname/src/errors.rs @@ -14,6 +14,8 @@ pub enum HostNameError { NoLocalDomainName, SetHostNameDenied, #[cfg(not(target_family = "windows"))] + SetDomainNameDenied, + #[cfg(not(target_family = "windows"))] GetNameOrAddrInfo(GetNameOrAddrInfoError), } @@ -25,6 +27,8 @@ impl fmt::Display for HostNameError { Self::NoLocalDomainName => write!(f, "local domain name not set"), Self::SetHostNameDenied => write!(f, "you must be root to change the host name"), #[cfg(not(target_family = "windows"))] + Self::SetDomainNameDenied => write!(f, "you must be root to change the domain name"), + #[cfg(not(target_family = "windows"))] Self::GetNameOrAddrInfo(r) => write!(f, "{r}"), } } diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index d34bb3c..1225700 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -38,14 +38,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let _net_lib_guard = net::LibraryGuard::load()?; - if args.contains_id("set-group") { + if args.contains_id(options::FILE) || args.contains_id(options::HOSTNAME) { + #[cfg(not(target_family = "windows"))] + let nis_mode = args.get_flag(options::NIS); if let Some(path) = args.get_one::(options::FILE) { + #[cfg(not(target_family = "windows"))] + if nis_mode { + return change::from_file_nis(path); + } change::from_file(path) } else { let host_name = args .get_one::(options::HOSTNAME) .expect("hostname must be specified"); + #[cfg(not(target_family = "windows"))] + if nis_mode { + return change::from_argument_nis(host_name); + } change::from_argument(host_name) } } else { @@ -176,11 +186,15 @@ hostname {-V|--version}"; options::IP_ADDRESS, options::ALL_IP_ADDRESSES, options::SHORT, - options::NIS, ]) .multiple(false) .conflicts_with("set-group"), ) + .group( + ArgGroup::new("nis-group") + .args([options::NIS]) + .conflicts_with("get-group"), + ) .group( ArgGroup::new("set-group") .args([options::BOOT, options::FILE, options::HOSTNAME]) diff --git a/src/uu/hostname/src/net/unix.rs b/src/uu/hostname/src/net/unix.rs index 0e72958..ba7d5c7 100644 --- a/src/uu/hostname/src/net/unix.rs +++ b/src/uu/hostname/src/net/unix.rs @@ -113,6 +113,43 @@ pub(crate) fn short_host_name() -> std::io::Result { Ok(unsafe { CString::from_vec_with_nul_unchecked(bytes) }) } +pub(crate) fn set_domain_name(domain_name: &CStr) -> UResult<()> { + use std::io::{Error, ErrorKind}; + + #[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "illumos", + target_os = "ios", + target_os = "macos", + target_os = "solaris", + ))] + let Ok(domain_name_len) = c_int::try_from(domain_name.count_bytes()) else { + return Err(Box::new(HostNameError::HostNameTooLong)); + }; + + #[cfg(not(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "illumos", + target_os = "ios", + target_os = "macos", + target_os = "solaris", + )))] + let domain_name_len = domain_name.count_bytes(); + + if unsafe { libc::setdomainname(domain_name.as_ptr(), domain_name_len) } != -1 { + return Ok(()); + } + + let err = Error::last_os_error(); + match err.kind() { + ErrorKind::PermissionDenied => Err(Box::new(HostNameError::SetDomainNameDenied)), + ErrorKind::InvalidInput => Err(Box::new(HostNameError::HostNameTooLong)), + _ => Err(err.into()), + } +} + pub(crate) fn set_host_name(host_name: &CStr) -> UResult<()> { use std::io::{Error, ErrorKind}; diff --git a/src/uu/nisdomainname/src/main.rs b/src/uu/nisdomainname/src/main.rs new file mode 100644 index 0000000..27b0974 --- /dev/null +++ b/src/uu/nisdomainname/src/main.rs @@ -0,0 +1,12 @@ +// Wrapper binary: nisdomainname -> hostname -y (visible alias) +fn main() { + uucore::panic::mute_sigpipe_panic(); + + use std::ffi::OsString; + use std::process; + + let args = std::env::args_os().skip(1); + let iter = (vec![OsString::from("hostname"), OsString::from("-y")].into_iter()).chain(args); + + process::exit(hostname::uumain(iter)); +} diff --git a/src/uu/ypdomainname/src/main.rs b/src/uu/ypdomainname/src/main.rs new file mode 100644 index 0000000..d83cbeb --- /dev/null +++ b/src/uu/ypdomainname/src/main.rs @@ -0,0 +1,12 @@ +// Wrapper binary: ypdomainname -> hostname -y +fn main() { + uucore::panic::mute_sigpipe_panic(); + + use std::ffi::OsString; + use std::process; + + let args = std::env::args_os().skip(1); + let iter = (vec![OsString::from("hostname"), OsString::from("-y")].into_iter()).chain(args); + + process::exit(hostname::uumain(iter)); +} diff --git a/tests/by-util/test_hostname.rs b/tests/by-util/test_hostname.rs deleted file mode 100644 index ed52ea7..0000000 --- a/tests/by-util/test_hostname.rs +++ /dev/null @@ -1,11 +0,0 @@ -// This file is part of the uutils hostname package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use uutests::new_ucmd; - -#[test] -fn test_invalid_arg() { - new_ucmd!().arg("--definitely-invalid").fails().code_is(1); -} diff --git a/tests/by_util/domainname.rs b/tests/by_util/domainname.rs new file mode 100644 index 0000000..93cde26 --- /dev/null +++ b/tests/by_util/domainname.rs @@ -0,0 +1,65 @@ +// Tests for the domainname utilities: dnsdomainname, domainname, nisdomainname, and ypdomainname wrapper binaries, +// These binaries are built from src/uu/*/src/main.rs and delegates to hostname. + +use assert_cmd::Command; +use predicates::prelude::*; + +/// Assert that `--help` succeeds and prints either the binary name or Usage text +fn assert_help(bin: &str) { + Command::cargo_bin(bin) + .unwrap() + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains(bin).or(predicate::str::contains("Usage"))); +} + +/// Assert that `--version` succeeds and prints the binary name +fn assert_version(bin: &str) { + Command::cargo_bin(bin) + .unwrap() + .arg("--version") + .assert() + .success() + .stdout(predicate::str::contains(bin)); +} + +#[test] +fn dnsdomainname_help() { + assert_help("dnsdomainname"); +} + +#[test] +fn domainname_help() { + assert_help("domainname"); +} + +#[test] +fn nisdomainname_help() { + assert_help("nisdomainname"); +} + +#[test] +fn ypdomainname_help() { + assert_help("ypdomainname"); +} + +#[test] +fn dnsdomainname_version() { + assert_version("dnsdomainname"); +} + +#[test] +fn domainname_version() { + assert_version("domainname"); +} + +#[test] +fn nisdomainname_version() { + assert_version("nisdomainname"); +} + +#[test] +fn ypdomainname_version() { + assert_version("ypdomainname"); +} diff --git a/tests/by_util/hostname.rs b/tests/by_util/hostname.rs new file mode 100644 index 0000000..5505a7f --- /dev/null +++ b/tests/by_util/hostname.rs @@ -0,0 +1,27 @@ +use uutests::new_ucmd; + +pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_hostname"); + +// Use the ctor attribute to run this function before any tests +#[ctor::ctor(unsafe)] +fn init() { + unsafe { + // Necessary for uutests to be able to find the binary + std::env::set_var("UUTESTS_BINARY_PATH", TESTS_BINARY); + } +} + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} + +#[test] +fn test_help_flag() { + new_ucmd!().arg("--help").succeeds(); +} + +#[test] +fn test_version_flag() { + new_ucmd!().arg("--version").succeeds(); +} diff --git a/tests/tests.rs b/tests/tests.rs index 4e3a28b..3f75a52 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -16,5 +16,8 @@ fn init() { } #[cfg(feature = "hostname")] -#[path = "by-util/test_hostname.rs"] +#[path = "by_util/hostname.rs"] mod test_hostname; + +#[path = "by_util/domainname.rs"] +mod test_domainname;