From be9f4ad82411b867553825ec1c9a8456165a83e3 Mon Sep 17 00:00:00 2001 From: Franklin-Qi Date: Wed, 30 Sep 2026 18:00:20 +0800 Subject: [PATCH 1/2] clippy: fix mismatched lifetime syntaxes in parser tests Annotate elided lifetimes on prop-test helpers so clippy -D warnings passes under the current toolchain. --- parser/src/tests/ast_gen.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parser/src/tests/ast_gen.rs b/parser/src/tests/ast_gen.rs index 39c5db0..16fbdd1 100644 --- a/parser/src/tests/ast_gen.rs +++ b/parser/src/tests/ast_gen.rs @@ -306,11 +306,11 @@ fn materialize_atom<'a>(atom: &GenAtom, arena: &'a Bump) -> Atom<'a> { } } -fn materialize_var(index: u8, arena: &Bump) -> Variable { +fn materialize_var(index: u8, arena: &Bump) -> Variable<'_> { Variable::User(ident(arena, index)) } -fn ident(arena: &Bump, index: u8) -> Identifier { +fn ident(arena: &Bump, index: u8) -> Identifier<'_> { let literal = match index % 4 { 0 => "a", 1 => "b", @@ -327,6 +327,6 @@ fn text_slice<'a>(arena: &'a Bump, content: &str) -> lexer::Slice<'a> { arena.alloc_str(content).as_bytes().into() } -fn regex_slice(arena: &Bump, index: u8) -> lexer::Slice { +fn regex_slice(arena: &Bump, index: u8) -> lexer::Slice<'_> { text_slice(arena, &format!("p{index}")) } From d6279c401cfa38b60295d2d3a9641530a6a5ccad Mon Sep 17 00:00:00 2001 From: Franklin-Qi Date: Wed, 30 Sep 2026 18:00:20 +0800 Subject: [PATCH 2/2] grcat: dump NSS group database via getgrent Add the grcat helper used by gawk library routines such as group.awk. Print /etc/group-format lines from getgrent/endgrent, return ExitCode from main, and treat BrokenPipe as success. libc is needed because rustix/nix do not expose the group-database iterators. Closes #60 --- Cargo.lock | 1 + Cargo.toml | 7 +++ src/bin/grcat.rs | 111 ++++++++++++++++++++++++++++++++++++ tests/by-util/test_grcat.rs | 85 +++++++++++++++++++++++++++ tests/grcat.rs | 27 +++++++++ 5 files changed, 231 insertions(+) create mode 100644 src/bin/grcat.rs create mode 100644 tests/by-util/test_grcat.rs create mode 100644 tests/grcat.rs diff --git a/Cargo.lock b/Cargo.lock index 5a558db..52f5ff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1481,6 +1481,7 @@ dependencies = [ "either", "interpreter", "lexer", + "libc", "memchr", "parser", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 82eb224..75d6170 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] +libc = "0.2" + [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..25fa0d5 --- /dev/null +++ b/src/bin/grcat.rs @@ -0,0 +1,111 @@ +// 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. +//! +//! Behavior matches the `grcat` helper described in the GNU Awk User's Guide: +//! +//! +//! Uses `getgrent()`/`endgrent()` so NSS sources beyond local files (LDAP, SSSD, +//! systemd-sysusers, etc.) are included, as required by routines such as +//! `group.awk`. + +use std::{ + io::{self, Write}, + process::ExitCode, +}; + +fn main() -> ExitCode { + #[cfg(unix)] + { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(err) if err.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS, + Err(err) => { + let _ = writeln!(io::stderr(), "grcat: {err}"); + ExitCode::FAILURE + } + } + } + #[cfg(not(unix))] + { + let _ = writeln!(io::stderr(), "grcat: not supported on this platform"); + ExitCode::FAILURE + } +} + +#[cfg(unix)] +fn run() -> io::Result<()> { + use std::ffi::CStr; + + struct EndGrent; + + impl Drop for EndGrent { + fn drop(&mut self) { + // SAFETY: pairs with `setgrent` below; `grcat` is single-threaded. + unsafe { + libc::endgrent(); + } + } + } + + let mut out = io::stdout().lock(); + + // SAFETY: `getgrent`/`setgrent`/`endgrent` share process-global state and are + // not thread-safe. `grcat` is a single-threaded helper, so exclusive use is OK. + unsafe { + libc::setgrent(); + } + let _end = EndGrent; + + loop { + // SAFETY: see note above; pointer is only used while non-null and before + // the next `getgrent`/`endgrent` call. + let group = unsafe { libc::getgrent() }; + if group.is_null() { + break; + } + // SAFETY: `getgrent` returned a non-null pointer to a valid `group`. + let group = unsafe { &*group }; + + // SAFETY: `gr_name` is a NUL-terminated C string from the C library. + let name = unsafe { CStr::from_ptr(group.gr_name) }; + let passwd = if group.gr_passwd.is_null() { + None + } else { + // SAFETY: when non-null, `gr_passwd` is a NUL-terminated C string. + Some(unsafe { CStr::from_ptr(group.gr_passwd) }) + }; + + out.write_all(name.to_bytes())?; + out.write_all(b":")?; + match passwd { + Some(pw) => out.write_all(pw.to_bytes())?, + None => out.write_all(b"*")?, + } + write!(out, ":{}:", group.gr_gid)?; + + if !group.gr_mem.is_null() { + let mut i = 0usize; + loop { + // SAFETY: `gr_mem` is a NULL-terminated array of C string pointers. + let member = unsafe { *group.gr_mem.add(i) }; + if member.is_null() { + break; + } + if i > 0 { + out.write_all(b",")?; + } + // SAFETY: non-null entries are NUL-terminated C strings. + let member = unsafe { CStr::from_ptr(member) }; + out.write_all(member.to_bytes())?; + i += 1; + } + } + out.write_all(b"\n")?; + } + + Ok(()) +} diff --git a/tests/by-util/test_grcat.rs b/tests/by-util/test_grcat.rs new file mode 100644 index 0000000..0f38aca --- /dev/null +++ b/tests/by-util/test_grcat.rs @@ -0,0 +1,85 @@ +// 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. + +use std::process::{Command, Stdio}; + +use crate::{TESTS_BINARY, ucmd}; + +#[cfg_attr( + not(target_os = "linux"), + ignore = "grcat tests require Linux NSS via getent" +)] +#[test] +fn grcat_outputs_group_database_format() { + let result = ucmd().succeeds(); + let stdout = result.stdout_str(); + + 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() { + let getent = Command::new("getent") + .arg("group") + .output() + .expect("failed to spawn getent; install it or skip this host explicitly"); + assert!( + getent.status.success(), + "getent group is required for this test (exit {:?}): {}", + getent.status.code(), + String::from_utf8_lossy(&getent.stderr) + ); + + let grcat = ucmd().succeeds(); + assert_eq!( + getent.stdout.as_slice(), + grcat.stdout(), + "grcat output should match getent group" + ); +} + +#[cfg_attr( + not(target_os = "linux"), + ignore = "grcat tests require Linux NSS via getent" +)] +#[test] +fn grcat_ignores_broken_pipe() { + let mut child = Command::new(TESTS_BINARY) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn grcat"); + + // Close the read end immediately so the next write hits EPIPE/BrokenPipe. + drop(child.stdout.take()); + + let status = child.wait().expect("failed to wait for grcat"); + assert!( + status.success(), + "grcat should treat broken pipe as success, got {status}" + ); +} diff --git a/tests/grcat.rs b/tests/grcat.rs new file mode 100644 index 0000000..c65c2b0 --- /dev/null +++ b/tests/grcat.rs @@ -0,0 +1,27 @@ +// 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. + +use std::env; + +use uutests::util::TestScenario; + +pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_grcat"); + +#[ctor::ctor(unsafe)] +fn init() { + unsafe { + env::set_var("UUTESTS_BINARY_PATH", TESTS_BINARY); + env::remove_var("UUTESTS_UTIL_NAME"); + env::set_var("UUTESTS_UTIL_NAME", ""); + env::set_var("UUTILS_MULTICALL", "0"); + } +} + +fn ucmd() -> uutests::util::UCommand { + TestScenario::new("grcat").cmd(TESTS_BINARY) +} + +#[path = "by-util/test_grcat.rs"] +mod test_grcat;