mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
passwd: implement -S, -l, -u, -d, -e, -n, -x, -w, -i, -P flags
Full drop-in replacement for GNU passwd administrative operations: - Status display (-S, -Sa) with correct L/NP/P output format - Lock (-l): prepend ! to shadow password hash - Unlock (-u): remove leading ! with safety checks - Delete (-d): set password to empty (passwordless account) - Expire (-e): set last_change to 0 (force change at next login) - Aging fields (-n/-x/-w/-i): set min/max/warn/inactive days - Prefix (-P): operate on files under a prefix directory - Correct exit codes (0-6) matching GNU passwd spec - Mutual exclusion between conflicting flags via clap shadow-core modules implemented: - lock: file locking with .lock files, stale detection, PID tracking - login_defs: /etc/login.defs KEY VALUE parser - nscd: cache invalidation (nscd + sssd) - sysroot: --prefix path resolver for testable file operations - shadow: ShadowEntry helper methods (lock/unlock/delete/expire/status) 48 tests passing on Debian/Alpine/Fedora, zero clippy warnings. Not yet implemented: PAM password change (default operation), --root (chroot), --keep-tokens, --stdin, --repository.
This commit is contained in:
@@ -36,4 +36,5 @@ pub mod selinux;
|
||||
pub mod atomic;
|
||||
pub mod lock;
|
||||
pub mod nscd;
|
||||
pub mod sysroot;
|
||||
pub mod uid_alloc;
|
||||
|
||||
@@ -2,8 +2,239 @@
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
// spell-checker:ignore lockfile
|
||||
|
||||
//! File locking for `/etc/passwd`, `/etc/shadow`, etc.
|
||||
//!
|
||||
//! Uses `.lock` files (e.g., `/etc/passwd.lock`) with timeout and stale
|
||||
//! lock detection, matching the convention used by GNU shadow-utils.
|
||||
//!
|
||||
//! Lock files are created atomically with `O_CREAT | O_EXCL` and contain
|
||||
//! the PID of the locking process for stale detection.
|
||||
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use nix::fcntl::{self, OFlag};
|
||||
use nix::sys::stat::Mode;
|
||||
use nix::unistd;
|
||||
|
||||
use crate::error::ShadowError;
|
||||
|
||||
/// Default lock timeout (matches GNU shadow-utils `LOCK_TIMEOUT`).
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
/// Retry interval when waiting for a lock.
|
||||
const RETRY_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
/// A held file lock. The lock is released when this value is dropped.
|
||||
pub struct FileLock {
|
||||
lock_path: PathBuf,
|
||||
released: bool,
|
||||
}
|
||||
|
||||
impl FileLock {
|
||||
/// Acquire a lock for the given file using the default timeout.
|
||||
///
|
||||
/// Creates `{file_path}.lock` atomically. If another process holds the lock,
|
||||
/// retries until the timeout expires. Stale locks (held by dead processes)
|
||||
/// are automatically cleaned up.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ShadowError::Lock` if the lock cannot be acquired within the timeout.
|
||||
pub fn acquire(file_path: &Path) -> Result<Self, ShadowError> {
|
||||
Self::acquire_with_timeout(file_path, DEFAULT_TIMEOUT)
|
||||
}
|
||||
|
||||
/// Acquire a lock with a custom timeout.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ShadowError::Lock` if the lock cannot be acquired within the timeout.
|
||||
pub fn acquire_with_timeout(file_path: &Path, timeout: Duration) -> Result<Self, ShadowError> {
|
||||
let lock_path = lock_path_for(file_path);
|
||||
let deadline = Instant::now() + timeout;
|
||||
|
||||
loop {
|
||||
if try_create_lock(&lock_path).is_ok() {
|
||||
return Ok(Self {
|
||||
lock_path,
|
||||
released: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Lock file exists — check if it's stale.
|
||||
if is_stale_lock(&lock_path) {
|
||||
// Remove stale lock and retry immediately.
|
||||
let _ = fs::remove_file(&lock_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if Instant::now() >= deadline {
|
||||
return Err(ShadowError::Lock(format!(
|
||||
"cannot acquire lock {}: timed out after {timeout:?}",
|
||||
lock_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
thread::sleep(RETRY_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
/// Explicitly release the lock.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ShadowError::Lock` if the lock file cannot be removed.
|
||||
pub fn release(mut self) -> Result<(), ShadowError> {
|
||||
self.released = true;
|
||||
fs::remove_file(&self.lock_path).map_err(|e| {
|
||||
ShadowError::Lock(format!(
|
||||
"cannot release lock {}: {e}",
|
||||
self.lock_path.display()
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FileLock {
|
||||
fn drop(&mut self) {
|
||||
if !self.released {
|
||||
let _ = fs::remove_file(&self.lock_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the lock file path: append `.lock` to the file path.
|
||||
fn lock_path_for(file_path: &Path) -> PathBuf {
|
||||
let mut lock = file_path.as_os_str().to_owned();
|
||||
lock.push(".lock");
|
||||
PathBuf::from(lock)
|
||||
}
|
||||
|
||||
/// Try to atomically create the lock file. Write our PID into it.
|
||||
fn try_create_lock(lock_path: &Path) -> Result<(), ShadowError> {
|
||||
// O_CREAT | O_EXCL ensures atomic creation — fails if the file exists.
|
||||
let fd = fcntl::open(
|
||||
lock_path,
|
||||
OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_WRONLY,
|
||||
Mode::from_bits_truncate(0o600),
|
||||
)
|
||||
.map_err(|e| ShadowError::Lock(format!("cannot create {}: {e}", lock_path.display())))?;
|
||||
|
||||
// Write our PID for stale detection.
|
||||
// SAFETY: fd is a valid file descriptor we just created.
|
||||
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||
let pid = unistd::getpid();
|
||||
let _ = write!(file, "{pid}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if an existing lock file is stale (held by a dead process).
|
||||
fn is_stale_lock(lock_path: &Path) -> bool {
|
||||
let Ok(contents) = fs::read_to_string(lock_path) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Ok(pid) = contents.trim().parse::<i32>() else {
|
||||
// Cannot parse PID — treat as stale.
|
||||
return true;
|
||||
};
|
||||
|
||||
if pid <= 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Signal 0 checks if the process exists without actually sending a signal.
|
||||
let pid = nix::unistd::Pid::from_raw(pid);
|
||||
nix::sys::signal::kill(pid, None).is_err()
|
||||
}
|
||||
|
||||
use std::os::unix::io::FromRawFd;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_lock_path_for() {
|
||||
assert_eq!(
|
||||
lock_path_for(Path::new("/etc/shadow")),
|
||||
PathBuf::from("/etc/shadow.lock")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acquire_and_release() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test_file");
|
||||
fs::write(&file, "data").unwrap();
|
||||
|
||||
let lock = FileLock::acquire(&file).unwrap();
|
||||
assert!(lock.lock_path.exists());
|
||||
|
||||
lock.release().unwrap();
|
||||
assert!(!dir.path().join("test_file.lock").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drop_releases_lock() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test_file");
|
||||
fs::write(&file, "data").unwrap();
|
||||
|
||||
{
|
||||
let _lock = FileLock::acquire(&file).unwrap();
|
||||
assert!(dir.path().join("test_file.lock").exists());
|
||||
}
|
||||
// Lock should be released by drop.
|
||||
assert!(!dir.path().join("test_file.lock").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_double_lock_times_out() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test_file");
|
||||
fs::write(&file, "data").unwrap();
|
||||
|
||||
let _lock1 = FileLock::acquire(&file).unwrap();
|
||||
|
||||
// Second lock should time out.
|
||||
let result = FileLock::acquire_with_timeout(&file, Duration::from_millis(200));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stale_lock_cleanup() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test_file");
|
||||
fs::write(&file, "data").unwrap();
|
||||
|
||||
// Create a lock file with a PID that doesn't exist.
|
||||
let lock_path = dir.path().join("test_file.lock");
|
||||
fs::write(&lock_path, "999999999").unwrap();
|
||||
|
||||
// Should succeed because the stale lock is cleaned up.
|
||||
let lock = FileLock::acquire(&file).unwrap();
|
||||
lock.release().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lock_file_contains_pid() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test_file");
|
||||
fs::write(&file, "data").unwrap();
|
||||
|
||||
let lock = FileLock::acquire(&file).unwrap();
|
||||
let contents = fs::read_to_string(&lock.lock_path).unwrap();
|
||||
let pid: i32 = contents.trim().parse().unwrap();
|
||||
assert_eq!(pid, i32::try_from(std::process::id()).unwrap());
|
||||
|
||||
lock.release().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,136 @@
|
||||
// spell-checker:ignore login_defs
|
||||
|
||||
//! Parser for `/etc/login.defs` configuration file.
|
||||
//!
|
||||
//! File format: `KEY VALUE` pairs, one per line. Lines starting with `#`
|
||||
//! are comments. Blank lines are ignored. Keys are case-sensitive.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufRead;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::error::ShadowError;
|
||||
|
||||
/// Parsed `/etc/login.defs` configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoginDefs {
|
||||
entries: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl LoginDefs {
|
||||
/// Load and parse `/etc/login.defs` from the given path.
|
||||
///
|
||||
/// If the file does not exist, returns an empty `LoginDefs` (this is
|
||||
/// intentional — missing `login.defs` is not an error, defaults apply).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ShadowError` on I/O errors other than file-not-found.
|
||||
pub fn load(path: &Path) -> Result<Self, ShadowError> {
|
||||
let file = match std::fs::File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Ok(Self {
|
||||
entries: HashMap::new(),
|
||||
});
|
||||
}
|
||||
Err(e) => return Err(ShadowError::IoPath(e, path.to_owned())),
|
||||
};
|
||||
|
||||
let reader = std::io::BufReader::new(file);
|
||||
let mut entries = HashMap::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let trimmed = line.trim();
|
||||
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split on first whitespace: KEY VALUE
|
||||
if let Some((key, value)) = trimmed.split_once(|c: char| c.is_whitespace()) {
|
||||
entries.insert(key.to_string(), value.trim().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { entries })
|
||||
}
|
||||
|
||||
/// Get a string value by key.
|
||||
#[must_use]
|
||||
pub fn get(&self, key: &str) -> Option<&str> {
|
||||
self.entries.get(key).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Get a numeric value by key.
|
||||
#[must_use]
|
||||
pub fn get_i64(&self, key: &str) -> Option<i64> {
|
||||
self.entries.get(key).and_then(|v| v.parse().ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn write_login_defs(dir: &Path, content: &str) -> PathBuf {
|
||||
let path = dir.join("login.defs");
|
||||
let mut f = std::fs::File::create(&path).unwrap();
|
||||
f.write_all(content.as_bytes()).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_basic() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = write_login_defs(
|
||||
dir.path(),
|
||||
"PASS_MAX_DAYS\t99999\nPASS_MIN_DAYS\t0\nPASS_WARN_AGE\t7\n",
|
||||
);
|
||||
let defs = LoginDefs::load(&path).unwrap();
|
||||
assert_eq!(defs.get_i64("PASS_MAX_DAYS"), Some(99999));
|
||||
assert_eq!(defs.get_i64("PASS_MIN_DAYS"), Some(0));
|
||||
assert_eq!(defs.get_i64("PASS_WARN_AGE"), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comments_and_blanks() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = write_login_defs(
|
||||
dir.path(),
|
||||
"# This is a comment\n\nPASS_MAX_DAYS 99999\n# Another comment\n",
|
||||
);
|
||||
let defs = LoginDefs::load(&path).unwrap();
|
||||
assert_eq!(defs.get_i64("PASS_MAX_DAYS"), Some(99999));
|
||||
assert_eq!(defs.get("# This is a comment"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_values() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = write_login_defs(
|
||||
dir.path(),
|
||||
"ENCRYPT_METHOD SHA512\nENV_PATH /bin:/usr/bin\n",
|
||||
);
|
||||
let defs = LoginDefs::load(&path).unwrap();
|
||||
assert_eq!(defs.get("ENCRYPT_METHOD"), Some("SHA512"));
|
||||
assert_eq!(defs.get("ENV_PATH"), Some("/bin:/usr/bin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_file_returns_empty() {
|
||||
let defs = LoginDefs::load(Path::new("/nonexistent/login.defs")).unwrap();
|
||||
assert_eq!(defs.get("PASS_MAX_DAYS"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_i64_invalid_returns_none() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = write_login_defs(dir.path(), "ENCRYPT_METHOD SHA512\n");
|
||||
let defs = LoginDefs::load(&path).unwrap();
|
||||
assert_eq!(defs.get_i64("ENCRYPT_METHOD"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,31 @@
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
// spell-checker:ignore nscd
|
||||
// spell-checker:ignore nscd sssd
|
||||
|
||||
//! nscd (Name Service Cache Daemon) cache invalidation.
|
||||
//! `nscd` (Name Service Cache Daemon) cache invalidation.
|
||||
//!
|
||||
//! After modifying `/etc/passwd`, `/etc/shadow`, or `/etc/group`,
|
||||
//! the nscd cache must be invalidated so lookups reflect the changes.
|
||||
//! the `nscd` cache must be invalidated so lookups reflect the changes.
|
||||
//! Also supports `sssd` cache invalidation.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
/// Invalidate the `nscd` and `sssd` caches for the given database.
|
||||
///
|
||||
/// The `database` should be one of `"passwd"`, `"shadow"`, or `"group"`.
|
||||
///
|
||||
/// Silently succeeds if `nscd`/`sssd` is not installed or not running —
|
||||
/// this matches GNU shadow-utils behavior.
|
||||
pub fn invalidate_cache(database: &str) {
|
||||
// nscd -i <database>
|
||||
let _ = Command::new("nscd").arg("-i").arg(database).status();
|
||||
|
||||
// sssd: sss_cache with the appropriate flag
|
||||
let flag = match database {
|
||||
"passwd" | "shadow" => "-U",
|
||||
"group" => "-G",
|
||||
_ => return,
|
||||
};
|
||||
let _ = Command::new("sss_cache").arg(flag).status();
|
||||
}
|
||||
|
||||
@@ -43,6 +43,64 @@ pub struct ShadowEntry {
|
||||
pub reserved: String,
|
||||
}
|
||||
|
||||
impl ShadowEntry {
|
||||
/// Whether the account password is locked (password starts with `!`).
|
||||
#[must_use]
|
||||
pub fn is_locked(&self) -> bool {
|
||||
self.passwd.starts_with('!')
|
||||
}
|
||||
|
||||
/// Whether the account has no password (empty password field).
|
||||
#[must_use]
|
||||
pub fn has_no_password(&self) -> bool {
|
||||
self.passwd.is_empty()
|
||||
}
|
||||
|
||||
/// Lock the password by prepending `!`.
|
||||
pub fn lock(&mut self) {
|
||||
self.passwd.insert(0, '!');
|
||||
}
|
||||
|
||||
/// Unlock the password by removing the leading `!`.
|
||||
///
|
||||
/// Returns `false` if the password is not locked or would become empty
|
||||
/// after unlocking (GNU passwd refuses this — use `delete` instead).
|
||||
pub fn unlock(&mut self) -> bool {
|
||||
if !self.is_locked() {
|
||||
return false;
|
||||
}
|
||||
let after = &self.passwd[1..];
|
||||
if after.is_empty() || after == "!" {
|
||||
// Would result in empty or still-locked password.
|
||||
return false;
|
||||
}
|
||||
self.passwd = after.to_string();
|
||||
true
|
||||
}
|
||||
|
||||
/// Delete the password (set to empty string, making the account passwordless).
|
||||
pub fn delete_password(&mut self) {
|
||||
self.passwd = String::new();
|
||||
}
|
||||
|
||||
/// Expire the password (set `last_change` to 0, forcing change at next login).
|
||||
pub fn expire(&mut self) {
|
||||
self.last_change = Some(0);
|
||||
}
|
||||
|
||||
/// Password status character for `passwd -S` output.
|
||||
#[must_use]
|
||||
pub fn status_char(&self) -> &'static str {
|
||||
if self.is_locked() {
|
||||
"L"
|
||||
} else if self.has_no_password() {
|
||||
"NP"
|
||||
} else {
|
||||
"P"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an optional numeric field — empty string becomes `None`.
|
||||
fn parse_optional_field(field: &str) -> Result<Option<i64>, ShadowError> {
|
||||
if field.is_empty() {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// This file is part of the shadow-rs package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
// spell-checker:ignore chroot sysroot
|
||||
|
||||
//! System root path resolver for `--root` and `--prefix` support.
|
||||
//!
|
||||
//! `--prefix DIR` prepends DIR to all file paths (no `chroot` syscall).
|
||||
//! `--root DIR` does an actual `chroot()` — paths are then relative to `/`.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Resolves file paths relative to an optional prefix directory.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SysRoot {
|
||||
prefix: PathBuf,
|
||||
}
|
||||
|
||||
impl SysRoot {
|
||||
/// Create a new `SysRoot` with the given prefix.
|
||||
///
|
||||
/// If `prefix` is `None`, paths resolve against the real root `/`.
|
||||
#[must_use]
|
||||
pub fn new(prefix: Option<&Path>) -> Self {
|
||||
Self {
|
||||
prefix: prefix.unwrap_or_else(|| Path::new("/")).to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a path relative to the prefix.
|
||||
///
|
||||
/// Strips leading `/` from `relative` before joining with the prefix.
|
||||
#[must_use]
|
||||
pub fn resolve(&self, relative: &str) -> PathBuf {
|
||||
let stripped = relative.strip_prefix('/').unwrap_or(relative);
|
||||
self.prefix.join(stripped)
|
||||
}
|
||||
|
||||
/// Path to `/etc/passwd`.
|
||||
#[must_use]
|
||||
pub fn passwd_path(&self) -> PathBuf {
|
||||
self.resolve("/etc/passwd")
|
||||
}
|
||||
|
||||
/// Path to `/etc/shadow`.
|
||||
#[must_use]
|
||||
pub fn shadow_path(&self) -> PathBuf {
|
||||
self.resolve("/etc/shadow")
|
||||
}
|
||||
|
||||
/// Path to `/etc/group`.
|
||||
#[must_use]
|
||||
pub fn group_path(&self) -> PathBuf {
|
||||
self.resolve("/etc/group")
|
||||
}
|
||||
|
||||
/// Path to `/etc/login.defs`.
|
||||
#[must_use]
|
||||
pub fn login_defs_path(&self) -> PathBuf {
|
||||
self.resolve("/etc/login.defs")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SysRoot {
|
||||
fn default() -> Self {
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_root() {
|
||||
let root = SysRoot::default();
|
||||
assert_eq!(root.shadow_path(), PathBuf::from("/etc/shadow"));
|
||||
assert_eq!(root.passwd_path(), PathBuf::from("/etc/passwd"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_prefix() {
|
||||
let root = SysRoot::new(Some(Path::new("/tmp/test")));
|
||||
assert_eq!(root.shadow_path(), PathBuf::from("/tmp/test/etc/shadow"));
|
||||
assert_eq!(root.passwd_path(), PathBuf::from("/tmp/test/etc/passwd"));
|
||||
assert_eq!(
|
||||
root.login_defs_path(),
|
||||
PathBuf::from("/tmp/test/etc/login.defs")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_strips_leading_slash() {
|
||||
let root = SysRoot::new(Some(Path::new("/mnt")));
|
||||
assert_eq!(
|
||||
root.resolve("/etc/shadow"),
|
||||
PathBuf::from("/mnt/etc/shadow")
|
||||
);
|
||||
assert_eq!(root.resolve("etc/shadow"), PathBuf::from("/mnt/etc/shadow"));
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,13 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
shadow-core = { workspace = true, features = ["shadow", "pam"] }
|
||||
libc = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
shadow-core = { workspace = true, features = ["shadow", "login-defs"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+699
-28
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user