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..8279f0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,13 @@ license.workspace = true name = "awk" path = "src/main.rs" +[[bin]] +name = "grcat" +path = "src/bin/grcat.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/grcat.rs b/src/bin/grcat.rs new file mode 100644 index 0000000..a87e280 --- /dev/null +++ b/src/bin/grcat.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 group database in `/etc/group` 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 GROUP_DB: &str = "/etc/group"; + +fn main() { + #[cfg(unix)] + { + if let Err(err) = run() + && err.kind() != io::ErrorKind::BrokenPipe + { + let _ = writeln!(io::stderr(), "grcat: {err}"); + process::exit(1); + } + } + #[cfg(not(unix))] + { + let _ = writeln!(io::stderr(), "grcat: 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(GROUP_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/grcat.rs b/tests/grcat.rs new file mode 100644 index 0000000..9bb7fa6 --- /dev/null +++ b/tests/grcat.rs @@ -0,0 +1,75 @@ +// 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 = "grcat tests require Linux NSS via getent" +)] +#[test] +fn grcat_outputs_group_database_format() { + use std::process::Command; + + let output = Command::new(env!("CARGO_BIN_EXE_grcat")) + .output() + .expect("failed to run grcat"); + + assert!( + output.status.success(), + "grcat failed: stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + !stdout.is_empty(), + "grcat produced no output; group 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!( + fields.len() >= 4, + "expected at least 4 colon-separated fields, got {} in line: {line}", + fields.len() + ); + assert!( + fields[2].chars().all(|ch| ch.is_ascii_digit()), + "expected numeric gid in line: {line}" + ); + } +} + +// Regression test for gawk compatibility: grcat must match the group database +// format consumed by gawk library routines (see group.awk). +#[cfg_attr( + not(target_os = "linux"), + ignore = "grcat tests require Linux NSS via getent" +)] +#[test] +fn grcat_matches_getent_group() { + use std::process::Command; + + let getent = Command::new("getent") + .arg("group") + .output() + .expect("failed to run getent"); + if !getent.status.success() { + return; + } + + let grcat = Command::new(env!("CARGO_BIN_EXE_grcat")) + .output() + .expect("failed to run grcat"); + assert!( + grcat.status.success(), + "grcat failed: stderr={}", + String::from_utf8_lossy(&grcat.stderr) + ); + + assert_eq!( + getent.stdout, grcat.stdout, + "grcat output should match getent group" + ); +}