diff --git a/src/uu/hostname/src/hostname.rs b/src/uu/hostname/src/hostname.rs index 1c06c944335..15ec1418ff3 100644 --- a/src/uu/hostname/src/hostname.rs +++ b/src/uu/hostname/src/hostname.rs @@ -19,7 +19,7 @@ use uucore::translate; use uucore::{ error::{FromIo, UResult}, - format_usage, + format_usage, os_str_as_bytes_lossy, }; static OPT_DOMAIN: &str = "domain"; @@ -87,13 +87,10 @@ pub fn uu_app() -> Command { } fn display_hostname(matches: &ArgMatches) -> UResult<()> { - let hostname = hostname::get() - .map_err_context(|| "failed to get hostname".to_owned())? - .to_string_lossy() - .into_owned(); + let hostname = hostname::get().map_err_context(|| "failed to get hostname".to_owned())?; if matches.get_flag(OPT_IP_ADDRESS) { - let addresses = (hostname, 1) + let addresses = (hostname.to_string_lossy().as_ref(), 1) .to_socket_addrs() .map_err_context(|| "failed to resolve socket addresses".to_owned())?; @@ -118,22 +115,27 @@ fn display_hostname(matches: &ArgMatches) -> UResult<()> { Ok(()) } else { - if matches.get_flag(OPT_SHORT) || matches.get_flag(OPT_DOMAIN) { - let mut it = hostname.char_indices().filter(|&ci| ci.1 == '.'); - if let Some(ci) = it.next() { + let hostname_bytes = os_str_as_bytes_lossy(&hostname); + let hostname_bytes = hostname_bytes.as_ref(); + let name = if matches.get_flag(OPT_SHORT) || matches.get_flag(OPT_DOMAIN) { + if let Some(dot) = hostname_bytes.iter().position(|&byte| byte == b'.') { if matches.get_flag(OPT_SHORT) { - writeln!(stdout(), "{}", &hostname[0..ci.0])?; + &hostname_bytes[..dot] } else { - writeln!(stdout(), "{}", &hostname[ci.0 + 1..])?; + &hostname_bytes[dot + 1..] } } else if matches.get_flag(OPT_SHORT) { - writeln!(stdout(), "{hostname}")?; + hostname_bytes + } else { + return Ok(()); } - return Ok(()); - } - - writeln!(stdout(), "{hostname}")?; + } else { + hostname_bytes + }; + let mut output = stdout().lock(); + output.write_all(name)?; + writeln!(output)?; Ok(()) } } diff --git a/tests/by-util/test_hostname.rs b/tests/by-util/test_hostname.rs index dc0b7510192..343b0dd2c22 100644 --- a/tests/by-util/test_hostname.rs +++ b/tests/by-util/test_hostname.rs @@ -49,3 +49,40 @@ fn test_hostname_domain_empty() { assert!(domain_short.stdout().is_empty()); } } + +#[cfg(all(target_os = "linux", not(wasi_runner)))] +#[rstest::rstest] +#[case(b"node\xff.example\xfe", &[], b"node\xff.example\xfe\n")] +#[case(b"node\xff.example\xfe", &["-f"], b"node\xff.example\xfe\n")] +#[case(b"node\xff.example\xfe", &["-s"], b"node\xff\n")] +#[case(b"node\xff.example\xfe", &["-d"], b"example\xfe\n")] +#[case(b"node\xff", &[], b"node\xff\n")] +#[case(b"node\xff", &["-s"], b"node\xff\n")] +#[case(b"node\xff", &["-d"], b"")] +fn test_hostname_non_utf8(#[case] hostname: &[u8], #[case] args: &[&str], #[case] expected: &[u8]) { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + use std::process::Command; + use uutests::util::TestScenario; + + if !Command::new("unshare") + .args(["-Ur", "-u", "true"]) + .output() + .is_ok_and(|output| output.status.success()) + { + eprintln!("Skipping test: user and UTS namespaces are unavailable"); + return; + } + + let ts = TestScenario::new("hostname"); + ts.cmd("unshare") + .args(&["-Ur", "-u", "sh", "-c"]) + // Set the hostname inside the isolated namespace, then query it with the requested flags. + .arg(r#"bin=$1; "$bin" hostname "$2" || exit; shift 2; exec "$bin" hostname "$@""#) + .arg("sh") + .arg(&ts.bin_path) + .arg(OsStr::from_bytes(hostname)) + .args(args) + .succeeds() + .stdout_only_bytes(expected); +}