Skip to content

Commit c2117de

Browse files
committed
pwcat: add password database dump utility
Add a `pwcat` binary that prints the password database in `/etc/passwd` format for gawk library routines, matching the helper shipped with GNU awk. Includes integration tests that validate the seven-field output format and compare against `getent passwd`. Closes #61
1 parent 5a8d1dc commit c2117de

4 files changed

Lines changed: 134 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ license.workspace = true
88
name = "awk"
99
path = "src/main.rs"
1010

11+
[[bin]]
12+
name = "pwcat"
13+
path = "src/bin/pwcat.rs"
14+
15+
[target.'cfg(unix)'.dependencies]
16+
rustix = { version = "1.1.4", features = ["fs"] }
17+
1118
[workspace.package]
1219
version = "0.1.0"
1320
license = "MIT OR Apache-2.0"

src/bin/pwcat.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// This file is part of the uutils awk package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// files that was distributed with this source code.
5+
6+
//! Dump the password database in `/etc/passwd` format for gawk library routines.
7+
//!
8+
//! Based on the program from the GNU Awk User's Guide (public domain).
9+
//! <https://www.gnu.org/software/gawk/manual/html_node/Passwd-Functions.html>
10+
11+
use std::{
12+
io::{self, Write},
13+
process,
14+
};
15+
16+
#[cfg(unix)]
17+
const PASSWD_DB: &str = "/etc/passwd";
18+
19+
fn main() {
20+
#[cfg(unix)]
21+
{
22+
if let Err(err) = run()
23+
&& err.kind() != io::ErrorKind::BrokenPipe
24+
{
25+
let _ = writeln!(io::stderr(), "pwcat: {err}");
26+
process::exit(1);
27+
}
28+
}
29+
#[cfg(not(unix))]
30+
{
31+
let _ = writeln!(io::stderr(), "pwcat: not supported on this platform");
32+
process::exit(1);
33+
}
34+
}
35+
36+
#[cfg(unix)]
37+
fn run() -> io::Result<()> {
38+
use rustix::fs::{Mode, OFlags, open};
39+
use std::fs::File;
40+
41+
let fd = open(PASSWD_DB, OFlags::RDONLY, Mode::empty())
42+
.map_err(|err| io::Error::from_raw_os_error(err.raw_os_error()))?;
43+
let mut input = File::from(fd);
44+
let mut out = io::stdout().lock();
45+
io::copy(&mut input, &mut out)?;
46+
Ok(())
47+
}

tests/pwcat.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// This file is part of the uutils awk package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// files that was distributed with this source code.
5+
6+
#[cfg_attr(
7+
not(target_os = "linux"),
8+
ignore = "pwcat tests require Linux NSS via getent"
9+
)]
10+
#[test]
11+
fn pwcat_outputs_passwd_database_format() {
12+
use std::process::Command;
13+
14+
let output = Command::new(env!("CARGO_BIN_EXE_pwcat"))
15+
.output()
16+
.expect("failed to run pwcat");
17+
18+
assert!(
19+
output.status.success(),
20+
"pwcat failed: stderr={}",
21+
String::from_utf8_lossy(&output.stderr)
22+
);
23+
24+
let stdout = String::from_utf8_lossy(&output.stdout);
25+
assert!(
26+
!stdout.is_empty(),
27+
"pwcat produced no output; password database may be unavailable in this environment"
28+
);
29+
30+
for line in stdout.lines().filter(|line| !line.is_empty()) {
31+
let fields: Vec<&str> = line.split(':').collect();
32+
assert_eq!(
33+
fields.len(),
34+
7,
35+
"expected 7 colon-separated fields in line: {line}"
36+
);
37+
assert!(
38+
fields[2].chars().all(|ch| ch.is_ascii_digit()),
39+
"expected numeric uid in line: {line}"
40+
);
41+
assert!(
42+
fields[3].chars().all(|ch| ch.is_ascii_digit()),
43+
"expected numeric gid in line: {line}"
44+
);
45+
}
46+
}
47+
48+
// Regression test for gawk compatibility: pwcat must match the password database
49+
// format consumed by gawk library routines (see passwd.awk).
50+
#[cfg_attr(
51+
not(target_os = "linux"),
52+
ignore = "pwcat tests require Linux NSS via getent"
53+
)]
54+
#[test]
55+
fn pwcat_matches_getent_passwd() {
56+
use std::process::Command;
57+
58+
let getent = Command::new("getent")
59+
.arg("passwd")
60+
.output()
61+
.expect("failed to run getent");
62+
if !getent.status.success() {
63+
return;
64+
}
65+
66+
let pwcat = Command::new(env!("CARGO_BIN_EXE_pwcat"))
67+
.output()
68+
.expect("failed to run pwcat");
69+
assert!(
70+
pwcat.status.success(),
71+
"pwcat failed: stderr={}",
72+
String::from_utf8_lossy(&pwcat.stderr)
73+
);
74+
75+
assert_eq!(
76+
getent.stdout, pwcat.stdout,
77+
"pwcat output should match getent passwd"
78+
);
79+
}

0 commit comments

Comments
 (0)