mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
81d0e1776d
Fixes #56 — 14 man pages in docs/man/ (markdown format). Fixes #67 — 8 Copilot findings fixed: - useradd: proper date validation (Feb 31 rejected) - userdel/usermod: --root wired to SysRoot - usermod: --expiredate validates input - usermod: --login validates name + updates shadow/group - userdel: -f separated from -r behavior - useradd: home dir resolved through SysRoot - skel: preserves directory permissions Fixes #69 — benchmark script (benches/benchmark.sh) Fixes #70 — FreeBSD/NetBSD reference (docs/FREEBSD-NETBSD-REFERENCE.md) 456 tests, zero clippy warnings.
271 lines
7.9 KiB
Bash
Executable File
271 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# spell-checker:ignore timeformat tempdir ldd passwd pwck
|
|
#
|
|
# Benchmark script for shadow-rs vs GNU shadow-utils.
|
|
#
|
|
# Compares performance, binary size, and shared library dependencies
|
|
# between shadow-rs (Rust) and GNU shadow-utils (C) implementations.
|
|
#
|
|
# Usage:
|
|
# docker compose run --rm debian bash benches/benchmark.sh
|
|
#
|
|
# Requirements:
|
|
# - Must run inside Docker (needs both GNU shadow-utils and shadow-rs)
|
|
# - Must run as root (passwd -S and pwck require root or shadow access)
|
|
#
|
|
# The script builds shadow-rs in release mode, locates GNU shadow-utils
|
|
# binaries, and runs comparative benchmarks.
|
|
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
PASSWD_STATUS_ITERS=1000
|
|
PWCK_ITERS=100
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log() { printf '\033[1;34m==> %s\033[0m\n' "$*"; }
|
|
warn() { printf '\033[1;33mWARN: %s\033[0m\n' "$*"; }
|
|
err() { printf '\033[1;31mERROR: %s\033[0m\n' "$*"; exit 1; }
|
|
|
|
# Print a separator line.
|
|
separator() { printf '%.0s-' {1..72}; printf '\n'; }
|
|
|
|
# Format bytes as human-readable.
|
|
human_size() {
|
|
local bytes=$1
|
|
if [ "$bytes" -ge 1048576 ]; then
|
|
printf '%.1f MiB' "$(echo "$bytes / 1048576" | bc -l)"
|
|
elif [ "$bytes" -ge 1024 ]; then
|
|
printf '%.1f KiB' "$(echo "$bytes / 1024" | bc -l)"
|
|
else
|
|
printf '%d B' "$bytes"
|
|
fi
|
|
}
|
|
|
|
# Time a command N times, report wall-clock total in seconds.
|
|
# Usage: bench_command <label> <iterations> <command...>
|
|
bench_command() {
|
|
local label=$1
|
|
local iters=$2
|
|
shift 2
|
|
|
|
log "Benchmarking: $label ($iters iterations)"
|
|
|
|
local start end elapsed
|
|
start=$(date +%s%N)
|
|
for ((i = 0; i < iters; i++)); do
|
|
"$@" >/dev/null 2>&1 || true
|
|
done
|
|
end=$(date +%s%N)
|
|
|
|
elapsed=$(echo "scale=3; ($end - $start) / 1000000000" | bc)
|
|
local per_iter
|
|
per_iter=$(echo "scale=6; $elapsed / $iters" | bc)
|
|
|
|
printf ' %-30s total: %8ss per-iter: %ss\n' "$label" "$elapsed" "$per_iter"
|
|
echo "$elapsed"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Locate binaries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
find_gnu_binary() {
|
|
local name=$1
|
|
# GNU shadow-utils binaries are typically in /usr/sbin or /usr/bin.
|
|
for path in /usr/sbin/"$name" /usr/bin/"$name" /sbin/"$name" /bin/"$name"; do
|
|
if [ -x "$path" ]; then
|
|
# Make sure it is NOT our shadow-rs binary (check for ELF or "shadow-rs").
|
|
if ! file "$path" 2>/dev/null | grep -q 'statically linked\|shadow-rs'; then
|
|
echo "$path"
|
|
return 0
|
|
fi
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build shadow-rs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Building shadow-rs in release mode..."
|
|
cargo build --release --quiet 2>&1
|
|
|
|
SHADOW_RS_DIR="$(cargo metadata --format-version=1 --no-deps 2>/dev/null | \
|
|
python3 -c 'import sys,json; print(json.load(sys.stdin)["target_directory"])' 2>/dev/null || \
|
|
echo "target")/release"
|
|
|
|
# The multicall binary.
|
|
SHADOW_RS_BIN="$SHADOW_RS_DIR/shadow-rs"
|
|
|
|
if [ ! -x "$SHADOW_RS_BIN" ]; then
|
|
# Try individual binaries.
|
|
SHADOW_RS_BIN=""
|
|
fi
|
|
|
|
# Individual tool binaries (built by cargo as separate bins).
|
|
RS_PASSWD="$SHADOW_RS_DIR/passwd"
|
|
RS_PWCK="$SHADOW_RS_DIR/pwck"
|
|
|
|
# Fall back to the multicall binary if individual bins are not found.
|
|
if [ ! -x "$RS_PASSWD" ] && [ -x "$SHADOW_RS_DIR/shadow-rs" ]; then
|
|
RS_PASSWD="$SHADOW_RS_DIR/shadow-rs"
|
|
fi
|
|
if [ ! -x "$RS_PWCK" ] && [ -x "$SHADOW_RS_DIR/shadow-rs" ]; then
|
|
RS_PWCK="$SHADOW_RS_DIR/shadow-rs"
|
|
fi
|
|
|
|
# Locate GNU binaries.
|
|
GNU_PASSWD=$(find_gnu_binary passwd) || GNU_PASSWD=""
|
|
GNU_PWCK=$(find_gnu_binary pwck) || GNU_PWCK=""
|
|
|
|
separator
|
|
log "Binary locations:"
|
|
printf ' %-20s %s\n' "shadow-rs passwd:" "${RS_PASSWD:-NOT FOUND}"
|
|
printf ' %-20s %s\n' "shadow-rs pwck:" "${RS_PWCK:-NOT FOUND}"
|
|
printf ' %-20s %s\n' "GNU passwd:" "${GNU_PASSWD:-NOT FOUND}"
|
|
printf ' %-20s %s\n' "GNU pwck:" "${GNU_PWCK:-NOT FOUND}"
|
|
separator
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Binary size comparison
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Binary size comparison"
|
|
printf '\n'
|
|
printf ' %-35s %12s\n' "Binary" "Size"
|
|
separator
|
|
|
|
report_size() {
|
|
local label=$1
|
|
local path=$2
|
|
if [ -x "$path" ]; then
|
|
local size
|
|
size=$(stat --format='%s' "$path" 2>/dev/null || stat -f '%z' "$path" 2>/dev/null || echo 0)
|
|
printf ' %-35s %12s (%d bytes)\n' "$label" "$(human_size "$size")" "$size"
|
|
else
|
|
printf ' %-35s %12s\n' "$label" "N/A"
|
|
fi
|
|
}
|
|
|
|
report_size "shadow-rs multicall" "$SHADOW_RS_DIR/shadow-rs"
|
|
report_size "shadow-rs passwd" "$RS_PASSWD"
|
|
report_size "shadow-rs pwck" "$RS_PWCK"
|
|
report_size "GNU passwd" "$GNU_PASSWD"
|
|
report_size "GNU pwck" "$GNU_PWCK"
|
|
printf '\n'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared library dependencies
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Shared library dependencies"
|
|
printf '\n'
|
|
|
|
report_deps() {
|
|
local label=$1
|
|
local path=$2
|
|
if [ -x "$path" ] && command -v ldd >/dev/null 2>&1; then
|
|
local count
|
|
count=$(ldd "$path" 2>/dev/null | grep -c '=>' || echo 0)
|
|
printf ' %-35s %d shared libraries\n' "$label" "$count"
|
|
ldd "$path" 2>/dev/null | sed 's/^/ /'
|
|
elif [ -x "$path" ]; then
|
|
printf ' %-35s (ldd not available)\n' "$label"
|
|
else
|
|
printf ' %-35s N/A\n' "$label"
|
|
fi
|
|
printf '\n'
|
|
}
|
|
|
|
report_deps "shadow-rs passwd" "$RS_PASSWD"
|
|
report_deps "GNU passwd" "$GNU_PASSWD"
|
|
separator
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Performance benchmarks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Performance benchmarks"
|
|
printf '\n'
|
|
printf ' Test: passwd -S root (%d iterations)\n' "$PASSWD_STATUS_ITERS"
|
|
printf ' Test: pwck -r (%d iterations)\n' "$PWCK_ITERS"
|
|
printf '\n'
|
|
separator
|
|
|
|
# -- passwd -S root --
|
|
|
|
rs_passwd_time=""
|
|
gnu_passwd_time=""
|
|
|
|
if [ -x "$RS_PASSWD" ]; then
|
|
rs_passwd_time=$(bench_command "shadow-rs: passwd -S root" "$PASSWD_STATUS_ITERS" "$RS_PASSWD" -S root)
|
|
fi
|
|
|
|
if [ -x "$GNU_PASSWD" ]; then
|
|
gnu_passwd_time=$(bench_command "GNU: passwd -S root" "$PASSWD_STATUS_ITERS" "$GNU_PASSWD" -S root)
|
|
fi
|
|
|
|
printf '\n'
|
|
|
|
# -- pwck -r --
|
|
|
|
rs_pwck_time=""
|
|
gnu_pwck_time=""
|
|
|
|
if [ -x "$RS_PWCK" ]; then
|
|
rs_pwck_time=$(bench_command "shadow-rs: pwck -r" "$PWCK_ITERS" "$RS_PWCK" -r)
|
|
fi
|
|
|
|
if [ -x "$GNU_PWCK" ]; then
|
|
gnu_pwck_time=$(bench_command "GNU: pwck -r" "$PWCK_ITERS" "$GNU_PWCK" -r)
|
|
fi
|
|
|
|
printf '\n'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Summary table
|
|
# ---------------------------------------------------------------------------
|
|
|
|
separator
|
|
log "SUMMARY"
|
|
printf '\n'
|
|
printf ' %-30s %12s %12s %12s\n' "Benchmark" "shadow-rs" "GNU" "Ratio"
|
|
separator
|
|
|
|
compute_ratio() {
|
|
local rs=$1
|
|
local gnu=$2
|
|
if [ -n "$rs" ] && [ -n "$gnu" ] && [ "$(echo "$gnu > 0" | bc)" -eq 1 ]; then
|
|
printf '%.2fx' "$(echo "$rs / $gnu" | bc -l)"
|
|
else
|
|
echo "N/A"
|
|
fi
|
|
}
|
|
|
|
ratio_passwd=$(compute_ratio "${rs_passwd_time:-}" "${gnu_passwd_time:-}")
|
|
ratio_pwck=$(compute_ratio "${rs_pwck_time:-}" "${gnu_pwck_time:-}")
|
|
|
|
printf ' %-30s %11ss %11ss %12s\n' \
|
|
"passwd -S root (${PASSWD_STATUS_ITERS}x)" \
|
|
"${rs_passwd_time:-N/A}" \
|
|
"${gnu_passwd_time:-N/A}" \
|
|
"$ratio_passwd"
|
|
|
|
printf ' %-30s %11ss %11ss %12s\n' \
|
|
"pwck -r (${PWCK_ITERS}x)" \
|
|
"${rs_pwck_time:-N/A}" \
|
|
"${gnu_pwck_time:-N/A}" \
|
|
"$ratio_pwck"
|
|
|
|
printf '\n'
|
|
separator
|
|
log "Benchmark complete."
|