diff --git a/Cargo.lock b/Cargo.lock index 4da953b..5d023ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1283,6 +1283,7 @@ dependencies = [ "interpreter", "memchr", "parser", + "rustix", "thiserror", "tracing", "tracing-error", diff --git a/Cargo.toml b/Cargo.toml index 5ee4afa..949b9a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,13 @@ license.workspace = true name = "awk" path = "src/main.rs" +[[bin]] +name = "pwcat" +path = "src/bin/pwcat.rs" + +[target.'cfg(unix)'.dependencies] +rustix = { version = "1.1.4", features = ["fs"] } + [workspace.package] version = "0.1.0" license = "MIT OR Apache-2.0" diff --git a/src/bin/pwcat.rs b/src/bin/pwcat.rs new file mode 100644 index 0000000..432b303 --- /dev/null +++ b/src/bin/pwcat.rs @@ -0,0 +1,47 @@ +// This file is part of the uutils awk package. +// +// For the full copyright and license information, please view the LICENSE +// files that was distributed with this source code. + +//! Dump the password database in `/etc/passwd` format for gawk library routines. +//! +//! Based on the program from the GNU Awk User's Guide (public domain). +//! + +use std::{ + io::{self, Write}, + process, +}; + +#[cfg(unix)] +const PASSWD_DB: &str = "/etc/passwd"; + +fn main() { + #[cfg(unix)] + { + if let Err(err) = run() + && err.kind() != io::ErrorKind::BrokenPipe + { + let _ = writeln!(io::stderr(), "pwcat: {err}"); + process::exit(1); + } + } + #[cfg(not(unix))] + { + let _ = writeln!(io::stderr(), "pwcat: not supported on this platform"); + process::exit(1); + } +} + +#[cfg(unix)] +fn run() -> io::Result<()> { + use rustix::fs::{Mode, OFlags, open}; + use std::fs::File; + + let fd = open(PASSWD_DB, OFlags::RDONLY, Mode::empty()) + .map_err(|err| io::Error::from_raw_os_error(err.raw_os_error()))?; + let mut input = File::from(fd); + let mut out = io::stdout().lock(); + io::copy(&mut input, &mut out)?; + Ok(()) +} diff --git a/tests/pwcat.rs b/tests/pwcat.rs new file mode 100644 index 0000000..3f69ced --- /dev/null +++ b/tests/pwcat.rs @@ -0,0 +1,79 @@ +// This file is part of the uutils awk package. +// +// For the full copyright and license information, please view the LICENSE +// files that was distributed with this source code. + +#[cfg_attr( + not(target_os = "linux"), + ignore = "pwcat tests require Linux NSS via getent" +)] +#[test] +fn pwcat_outputs_passwd_database_format() { + use std::process::Command; + + let output = Command::new(env!("CARGO_BIN_EXE_pwcat")) + .output() + .expect("failed to run pwcat"); + + assert!( + output.status.success(), + "pwcat failed: stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + !stdout.is_empty(), + "pwcat produced no output; password database may be unavailable in this environment" + ); + + for line in stdout.lines().filter(|line| !line.is_empty()) { + let fields: Vec<&str> = line.split(':').collect(); + assert_eq!( + fields.len(), + 7, + "expected 7 colon-separated fields in line: {line}" + ); + assert!( + fields[2].chars().all(|ch| ch.is_ascii_digit()), + "expected numeric uid in line: {line}" + ); + assert!( + fields[3].chars().all(|ch| ch.is_ascii_digit()), + "expected numeric gid in line: {line}" + ); + } +} + +// Regression test for gawk compatibility: pwcat must match the password database +// format consumed by gawk library routines (see passwd.awk). +#[cfg_attr( + not(target_os = "linux"), + ignore = "pwcat tests require Linux NSS via getent" +)] +#[test] +fn pwcat_matches_getent_passwd() { + use std::process::Command; + + let getent = Command::new("getent") + .arg("passwd") + .output() + .expect("failed to run getent"); + if !getent.status.success() { + return; + } + + let pwcat = Command::new(env!("CARGO_BIN_EXE_pwcat")) + .output() + .expect("failed to run pwcat"); + assert!( + pwcat.status.success(), + "pwcat failed: stderr={}", + String::from_utf8_lossy(&pwcat.stderr) + ); + + assert_eq!( + getent.stdout, pwcat.stdout, + "pwcat output should match getent passwd" + ); +}