Skip to content

Commit 7bb408d

Browse files
committed
sysusers: Import nameservice code from rpm-ostree
This imports the code from https://github.com/coreos/rpm-ostree/tree/main/rust/src/nameservice as of commit coreos/rpm-ostree@e1d43ae Signed-off-by: Colin Walters <[email protected]>
1 parent 120db64 commit 7bb408d

File tree

7 files changed

+434
-1
lines changed

7 files changed

+434
-1
lines changed

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.

sysusers/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ edition = "2021"
66
publish = false
77

88
[dependencies]
9+
anyhow = { workspace = true }
910
camino = { workspace = true }
1011
fn-error-context = { workspace = true }
1112
cap-std-ext = { version = "4" }
13+
hex = "0.4"
1214
thiserror = { workspace = true }
1315
tempfile = { workspace = true }
1416
bootc-utils = { path = "../utils" }
1517
rustix = { workspace = true }
1618
uzers = "0.12"
1719

1820
[dev-dependencies]
19-
anyhow = { workspace = true }
2021
indoc = { workspace = true }
2122
similar-asserts = { workspace = true }
2223

sysusers/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Parse and generate systemd sysusers.d entries.
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

4+
#[allow(dead_code)]
5+
mod nameservice;
6+
47
use std::path::PathBuf;
58

69
use thiserror::Error;

sysusers/src/nameservice/group.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//! Helpers for [user passwd file](https://man7.org/linux/man-pages/man5/passwd.5.html).
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
use anyhow::{anyhow, Context, Result};
5+
use std::io::{BufRead, Write};
6+
7+
// Entry from group file.
8+
#[derive(Debug, Clone, PartialEq, Eq)]
9+
pub(crate) struct GroupEntry {
10+
pub(crate) name: String,
11+
pub(crate) passwd: String,
12+
pub(crate) gid: u32,
13+
pub(crate) users: Vec<String>,
14+
}
15+
16+
impl GroupEntry {
17+
/// Parse a single group entry.
18+
pub fn parse_line(s: impl AsRef<str>) -> Option<Self> {
19+
let mut parts = s.as_ref().splitn(4, ':');
20+
let entry = Self {
21+
name: parts.next()?.to_string(),
22+
passwd: parts.next()?.to_string(),
23+
gid: parts.next().and_then(|s| s.parse().ok())?,
24+
users: {
25+
let users = parts.next()?;
26+
users.split(',').map(String::from).collect()
27+
},
28+
};
29+
Some(entry)
30+
}
31+
32+
/// Serialize entry to writer, as a group line.
33+
pub fn to_writer(&self, writer: &mut impl Write) -> Result<()> {
34+
let users: String = self.users.join(",");
35+
std::writeln!(
36+
writer,
37+
"{}:{}:{}:{}",
38+
self.name,
39+
self.passwd,
40+
self.gid,
41+
users,
42+
)
43+
.with_context(|| "failed to write passwd entry")
44+
}
45+
}
46+
47+
pub(crate) fn parse_group_content(content: impl BufRead) -> Result<Vec<GroupEntry>> {
48+
let mut groups = vec![];
49+
for (line_num, line) in content.lines().enumerate() {
50+
let input =
51+
line.with_context(|| format!("failed to read group entry at line {}", line_num))?;
52+
53+
// Skip empty and comment lines
54+
if input.is_empty() || input.starts_with('#') {
55+
continue;
56+
}
57+
// Skip NSS compat lines, see "Compatibility mode" in
58+
// https://man7.org/linux/man-pages/man5/nsswitch.conf.5.html
59+
if input.starts_with('+') || input.starts_with('-') {
60+
continue;
61+
}
62+
63+
let entry = GroupEntry::parse_line(&input).ok_or_else(|| {
64+
anyhow!(
65+
"failed to parse group entry at line {}, content: {}",
66+
line_num,
67+
&input
68+
)
69+
})?;
70+
groups.push(entry);
71+
}
72+
Ok(groups)
73+
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
use super::*;
78+
use std::io::Cursor;
79+
80+
fn mock_group_entry() -> GroupEntry {
81+
GroupEntry {
82+
name: "staff".to_string(),
83+
passwd: "x".to_string(),
84+
gid: 50,
85+
users: vec!["operator".to_string()],
86+
}
87+
}
88+
89+
#[test]
90+
fn test_parse_lines() {
91+
let content = r#"
92+
+groupA
93+
-groupB
94+
95+
root:x:0:
96+
daemon:x:1:
97+
bin:x:2:
98+
sys:x:3:
99+
adm:x:4:
100+
www-data:x:33:
101+
backup:x:34:
102+
operator:x:37:
103+
104+
# Dummy comment
105+
staff:x:50:operator
106+
107+
+
108+
"#;
109+
110+
let input = Cursor::new(content);
111+
let groups = parse_group_content(input).unwrap();
112+
assert_eq!(groups.len(), 9);
113+
assert_eq!(groups[8], mock_group_entry());
114+
}
115+
116+
#[test]
117+
fn test_write_entry() {
118+
let entry = mock_group_entry();
119+
let expected = b"staff:x:50:operator\n";
120+
let mut buf = Vec::new();
121+
entry.to_writer(&mut buf).unwrap();
122+
assert_eq!(&buf, expected);
123+
}
124+
}

sysusers/src/nameservice/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! Linux name-service information helpers.
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
// TODO(lucab): consider moving this to its own crate.
4+
5+
pub(crate) mod group;
6+
pub(crate) mod passwd;
7+
pub(crate) mod shadow;

sysusers/src/nameservice/passwd.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//! Helpers for [password file](https://man7.org/linux/man-pages/man5/passwd.5.html).
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
use anyhow::{anyhow, Context, Result};
5+
use std::io::{BufRead, Write};
6+
7+
// Entry from passwd file.
8+
#[derive(Debug, Clone, PartialEq, Eq)]
9+
pub(crate) struct PasswdEntry {
10+
pub(crate) name: String,
11+
pub(crate) passwd: String,
12+
pub(crate) uid: u32,
13+
pub(crate) gid: u32,
14+
pub(crate) gecos: String,
15+
pub(crate) home_dir: String,
16+
pub(crate) shell: String,
17+
}
18+
19+
impl PasswdEntry {
20+
/// Parse a single passwd entry.
21+
pub fn parse_line(s: impl AsRef<str>) -> Option<Self> {
22+
let mut parts = s.as_ref().splitn(7, ':');
23+
let entry = Self {
24+
name: parts.next()?.to_string(),
25+
passwd: parts.next()?.to_string(),
26+
uid: parts.next().and_then(|s| s.parse().ok())?,
27+
gid: parts.next().and_then(|s| s.parse().ok())?,
28+
gecos: parts.next()?.to_string(),
29+
home_dir: parts.next()?.to_string(),
30+
shell: parts.next()?.to_string(),
31+
};
32+
Some(entry)
33+
}
34+
35+
/// Serialize entry to writer, as a passwd line.
36+
pub fn to_writer(&self, writer: &mut impl Write) -> Result<()> {
37+
std::writeln!(
38+
writer,
39+
"{}:{}:{}:{}:{}:{}:{}",
40+
self.name,
41+
self.passwd,
42+
self.uid,
43+
self.gid,
44+
self.gecos,
45+
self.home_dir,
46+
self.shell
47+
)
48+
.with_context(|| "failed to write passwd entry")
49+
}
50+
}
51+
52+
pub(crate) fn parse_passwd_content(content: impl BufRead) -> Result<Vec<PasswdEntry>> {
53+
let mut passwds = vec![];
54+
for (line_num, line) in content.lines().enumerate() {
55+
let input =
56+
line.with_context(|| format!("failed to read passwd entry at line {}", line_num))?;
57+
58+
// Skip empty and comment lines
59+
if input.is_empty() || input.starts_with('#') {
60+
continue;
61+
}
62+
// Skip NSS compat lines, see "Compatibility mode" in
63+
// https://man7.org/linux/man-pages/man5/nsswitch.conf.5.html
64+
if input.starts_with('+') || input.starts_with('-') {
65+
continue;
66+
}
67+
68+
let entry = PasswdEntry::parse_line(&input).ok_or_else(|| {
69+
anyhow!(
70+
"failed to parse passwd entry at line {}, content: {}",
71+
line_num,
72+
&input
73+
)
74+
})?;
75+
passwds.push(entry);
76+
}
77+
Ok(passwds)
78+
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
use super::*;
83+
use std::io::Cursor;
84+
85+
fn mock_passwd_entry() -> PasswdEntry {
86+
PasswdEntry {
87+
name: "someuser".to_string(),
88+
passwd: "x".to_string(),
89+
uid: 1000,
90+
gid: 1000,
91+
gecos: "Foo BAR,,,".to_string(),
92+
home_dir: "/home/foobar".to_string(),
93+
shell: "/bin/bash".to_string(),
94+
}
95+
}
96+
97+
#[test]
98+
fn test_parse_lines() {
99+
let content = r#"
100+
root:x:0:0:root:/root:/bin/bash
101+
102+
+userA
103+
-userB
104+
105+
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
106+
systemd-coredump:x:1:1:systemd Core Dumper:/:/usr/sbin/nologin
107+
108+
+@groupA
109+
-@groupB
110+
111+
# Dummy comment
112+
someuser:x:1000:1000:Foo BAR,,,:/home/foobar:/bin/bash
113+
114+
+
115+
"#;
116+
117+
let input = Cursor::new(content);
118+
let groups = parse_passwd_content(input).unwrap();
119+
assert_eq!(groups.len(), 4);
120+
assert_eq!(groups[3], mock_passwd_entry());
121+
}
122+
123+
#[test]
124+
fn test_write_entry() {
125+
let entry = mock_passwd_entry();
126+
let expected = b"someuser:x:1000:1000:Foo BAR,,,:/home/foobar:/bin/bash\n";
127+
let mut buf = Vec::new();
128+
entry.to_writer(&mut buf).unwrap();
129+
assert_eq!(&buf, expected);
130+
}
131+
}

0 commit comments

Comments
 (0)