use crate::container::{GidMap, UidMap};
use std::io;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubIdRange {
pub start: u32,
pub count: u32,
}
pub fn parse_subid_file(path: &Path, username: &str, uid: u32) -> io::Result<Vec<SubIdRange>> {
let contents = std::fs::read_to_string(path)?;
Ok(parse_subid_contents(&contents, username, uid))
}
fn parse_subid_contents(contents: &str, username: &str, uid: u32) -> Vec<SubIdRange> {
let uid_str = uid.to_string();
let mut ranges = Vec::new();
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.splitn(3, ':').collect();
if parts.len() != 3 {
continue;
}
let name = parts[0];
if name != username && name != uid_str {
continue;
}
if let (Ok(start), Ok(count)) = (parts[1].parse::<u32>(), parts[2].parse::<u32>()) {
ranges.push(SubIdRange { start, count });
}
}
ranges
}
pub fn has_newuidmap() -> bool {
which("newuidmap")
}
pub fn has_newgidmap() -> bool {
which("newgidmap")
}
fn which(name: &str) -> bool {
std::process::Command::new("which")
.arg(name)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
pub fn current_username() -> io::Result<String> {
let uid = unsafe { libc::getuid() };
let contents = std::fs::read_to_string("/etc/passwd")?;
for line in contents.lines() {
let parts: Vec<&str> = line.splitn(7, ':').collect();
if parts.len() >= 3 {
if let Ok(file_uid) = parts[2].parse::<u32>() {
if file_uid == uid {
return Ok(parts[0].to_string());
}
}
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no /etc/passwd entry for uid {}", uid),
))
}
pub fn apply_uid_map(pid: u32, maps: &[UidMap]) -> io::Result<()> {
let mut args: Vec<String> = vec![pid.to_string()];
for m in maps {
args.push(m.inside.to_string());
args.push(m.outside.to_string());
args.push(m.count.to_string());
}
let status = std::process::Command::new("newuidmap")
.args(&args)
.status()
.map_err(|e| io::Error::new(e.kind(), format!("newuidmap: {}", e)))?;
if !status.success() {
return Err(io::Error::other(format!(
"newuidmap exited with {}",
status
)));
}
Ok(())
}
pub fn apply_gid_map(pid: u32, maps: &[GidMap]) -> io::Result<()> {
let mut args: Vec<String> = vec![pid.to_string()];
for m in maps {
args.push(m.inside.to_string());
args.push(m.outside.to_string());
args.push(m.count.to_string());
}
let status = std::process::Command::new("newgidmap")
.args(&args)
.status()
.map_err(|e| io::Error::new(e.kind(), format!("newgidmap: {}", e)))?;
if !status.success() {
return Err(io::Error::other(format!(
"newgidmap exited with {}",
status
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_subid_single_range() {
let contents = "cb:100000:65536\n";
let ranges = parse_subid_contents(contents, "cb", 1000);
assert_eq!(
ranges,
vec![SubIdRange {
start: 100000,
count: 65536
}]
);
}
#[test]
fn test_parse_subid_multiple_ranges() {
let contents = "cb:100000:65536\ncb:200000:1000\n";
let ranges = parse_subid_contents(contents, "cb", 1000);
assert_eq!(ranges.len(), 2);
assert_eq!(
ranges[0],
SubIdRange {
start: 100000,
count: 65536
}
);
assert_eq!(
ranges[1],
SubIdRange {
start: 200000,
count: 1000
}
);
}
#[test]
fn test_parse_subid_numeric_uid() {
let contents = "1000:100000:65536\n";
let ranges = parse_subid_contents(contents, "cb", 1000);
assert_eq!(
ranges,
vec![SubIdRange {
start: 100000,
count: 65536
}]
);
}
#[test]
fn test_parse_subid_no_match() {
let contents = "alice:100000:65536\nbob:200000:65536\n";
let ranges = parse_subid_contents(contents, "cb", 1000);
assert!(ranges.is_empty());
}
#[test]
fn test_parse_subid_comments_blanks() {
let contents = "# comment\n\ncb:100000:65536\n \nbad_line\n";
let ranges = parse_subid_contents(contents, "cb", 1000);
assert_eq!(
ranges,
vec![SubIdRange {
start: 100000,
count: 65536
}]
);
}
#[test]
fn test_current_username() {
if let Ok(name) = current_username() {
assert!(!name.is_empty(), "username should not be empty");
}
}
#[test]
fn test_has_newuidmap() {
let _ = has_newuidmap();
}
}