iocore 3.1.0

IOCore is a safe library for unix CLI tools and Systems programming. IOCore provides the [`iocore::Path`] abstraction of file-system paths designed to replace most [`std::path`] and [`std::fs`] operations with practical methods, other abstractions include: - handling file-system permissions via [`iocore::PathPermissions`] powered by the crate [`trilobyte`]. - handling file-system timestamps via [`iocore::PathTimestamps`] granularly via [`iocore::PathDateTime`]. IOCore provides the [`iocore::walk_dir`] function and its companion trait [`iocore::WalkProgressHandler`] which traverses file-systems quickly via threads. IOcore provides [`iocore::User`] which provides unix user information such as uid, path to home etc. The module [`iocore::env`] provides [`iocore::env:args`] returns a [`Vec<String>`] from [`std::env:args`], and [`iocore::env:var`] that returns environment variables as string.
Documentation
use std::process::{Command, Stdio};
use std::str::FromStr;

use regex::Regex;
use sanitation::{from_hex, SBoolean, SString};

use crate::{env_var, traceback, Error};

pub const DEFAULT_UID: u32 = if cfg!(target_os = "macos") { 501 } else { 1001 };

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Group {
    pub gid: u32,
    pub name: String,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct User {
    pub uid: u32,
    pub gid: Option<u32>,
    pub name: String,
    pub group: Option<String>,
    pub groups: Vec<Group>,
}

impl User {
    pub fn id() -> Result<User, Error> {
        let stdout = get_stdout_string("/usr/bin/id")?;
        Ok(User::from_id_cmd_string(stdout)?)
    }

    pub fn from_env() -> User {
        let uid = env_var_uid().unwrap_or_else(|_| DEFAULT_UID);
        let name = env_var_user();
        User {
            uid,
            name,
            group: None,
            gid: None,
            groups: Vec::new(),
        }
    }

    pub fn from_id_cmd_string(id_stdout: impl std::fmt::Display) -> Result<User, Error> {
        let stdout = id_stdout.to_string();
        let uexpr = Regex::new(
            r"uid=(?<uid>\d+)([(](?<name>[^)]+)[)])\s*gid=(?<gid>\d+)[(](?<group>[^)]+)[)]",
        )
        .unwrap();
        let gexpr = Regex::new(r"(\d+)([(]([^)]+)[)])").unwrap();
        if let Some(captures) = uexpr.captures(&stdout) {
            let gid = parse_u32(captures.name("gid").map(|s| s.as_str()).unwrap(), "gid").unwrap();
            let uid = parse_u32(captures.name("uid").map(|s| s.as_str()).unwrap(), "uid").unwrap();
            let name = captures.name("name").map(|s| s.as_str().to_string()).unwrap();
            let group = captures.name("group").map(|s| s.as_str().to_string()).unwrap();
            let groups = gexpr
                .captures_iter(&stdout)
                .map(|c| c.extract::<3>())
                .map(|(_n_, m)| {
                    let g = m.iter().map(|n| n.to_string()).collect::<Vec<String>>();
                    let gid = parse_u32(&g[0], "gid").unwrap();
                    let name = g[2].to_string();
                    Group { gid, name }
                })
                .collect::<Vec<Group>>();
            Ok(User {
                uid,
                gid: Some(gid),
                name,
                group: Some(group),
                groups,
            })
        } else {
            Err(traceback!(SystemError, "could not secure user information from /usr/bin/id"))
        }
    }

    pub fn uid(&self) -> u32 {
        self.uid
    }

    pub fn gid(&self) -> Option<u32> {
        self.gid.clone()
    }

    pub fn name(&self) -> String {
        self.name.to_string()
    }

    pub fn home(&self) -> Result<String, Error> {
        let user = self.name();
        let uid = self.uid();
        Ok(unix_user_info_home("/etc/passwd", &user, uid)
            .map_err(|e| traceback!(SystemError, e))
            .or_else(|_| env_var_home(&user, uid, None).map_err(|e| traceback!(SystemError, e)))
            .or_else(|_| best_guess_home(&user).map_err(|e| traceback!(SystemError, e)))?
            .to_string())
    }
}

pub fn parse_u32(s: impl Into<String>, short_description: &str) -> Result<u32, Error> {
    let s = s.into();
    Ok(u32::from_str(&s).map_err(|e| {
        traceback!(SafetyError, "{} in converting {:#?} {:#?} to u32", e, s, short_description)
    })?)
}

pub fn get_subprocess_output(name: &str) -> Result<std::process::Output, Error> {
    Ok(Command::new(name)
        .stdin(Stdio::null())
        .stderr(Stdio::null())
        .output()
        .map_err(|e| traceback!(SubprocessError, "failed to execute {:#?}: {}", name, e))?)
}
pub fn get_stdout_string(executable: &str) -> Result<String, Error> {
    let (exit_code, stdout, stderr) =
        crate::sh::shell_command_string_output(executable, crate::fs::Path::cwd())?;
    if exit_code != 0 {
        return Err(traceback!(
            ShellCommandError,
            "{:#?} failed with exit code {}{}",
            executable,
            exit_code,
            if stderr.len() > 0 { format!(": {:#?}", stderr) } else { String::new() }
        ));
    }
    safe_string(stdout.as_bytes(), &format!("stdout of {:#?}", executable))
}

pub fn safe_string(bytes: &[u8], short_description: &str) -> Result<String, Error> {
    Ok(SString::new(bytes)
        .safe()
        .map_err(|e| traceback!(SafetyError, "{} in converting {:#?}", e, short_description))?)
}

pub fn unix_user_info_home(path: &str, name: &str, uid: u32) -> Result<String, Error> {
    for (n, line) in crate::Path::from(path).read_lines()?.iter().enumerate() {
        let location = format!("{}:{}", path, n + 1);
        if !line.starts_with(&format!("{}:", &name)) {
            continue;
        }

        let fields = line.split(':').into_iter().collect::<Vec<_>>();
        if fields[2] != uid.to_string() {
            return Err(traceback!(
                SystemError,
                "unexpected uid in {:#?}: {} != {}",
                location,
                fields[2],
                uid
            ));
        }

        return Ok(path_owned_expectedly(
            crate::Path::raw(match fields.len() {
                7 => fields[5],
                10 => fields[7],
                e =>
                    return Err(traceback!(
                        SystemError,
                        "unexpected number of fields in {:#?} {}",
                        location,
                        e
                    )),
            }),
            name,
            uid,
        )?
        .to_string());
    }
    Err(traceback!(
        SystemError,
        "home not found in {} for uid {} ({})",
        path,
        uid,
        name
    ))
}

fn env_var_home(user: &str, uid: u32, key: Option<String>) -> Result<String, Error> {
    let key = key.unwrap_or("HOME".to_string());
    Ok(path_owned_expectedly(
        crate::Path::directory(crate::env::var(&key)?).map_err(|e| {
            traceback!(
                SystemError,
                "fetching home directory from environment variable {:#?}: {}",
                key,
                e
            )
        })?,
        user,
        uid,
    )?
    .to_string())
}
fn env_var_uid() -> Result<u32, Error> {
    Ok(parse_u32(env_var!("UID"), "UID environment variable")?)
}
fn env_var_user() -> String {
    env_var!("USER")
}
fn path_owned_expectedly(path: crate::Path, user: &str, uid: u32) -> Result<crate::Path, Error> {
    if path.uid() == uid {
        Ok(path)
    } else {
        Err(traceback!(SystemError, "{:#?} ain't owned by uid {} ({:#?})", path, uid, user))
    }
}

pub fn guess_unix_home(user: impl Into<String>) -> Result<String, Error> {
    let user = user.into();
    use crate::fs::Path;

    let path = if cfg!(target_os = "macos") {
        format!("/Users/{}", &user)
    } else if cfg!(unix) {
        format!("/home/{}", &user)
    } else {
        return Err(traceback!(
            SystemError,
            "windows, wasm and other non-unix platforms not supported"
        ));
    };

    if Path::raw(&path).is_dir() {
        Ok(path)
    } else {
        Err(traceback!(
            HomePathError,
            "guessed unix user home {:#?} is not a folder",
            &path
        ))
    }
}

pub fn best_guess_home(user: impl Into<String>) -> Result<String, Error> {
    let user = user.into();
    use crate::fs::Path;
    Ok(if let Ok(home) = crate::env::var("HOME") {
        Path::directory(home.trim().to_string()).map(|p| p.to_string()).map_err(|e| {
            traceback!(
                SafetyError,
                "environment variable HOME points to a non-accessible path {:#?}: {}",
                home,
                e
            )
        })?
    } else {
        guess_unix_home(&user)?
    })
}
impl Default for User {
    fn default() -> User {
        User::from_env()
    }
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct XPC {
    pub null_bootstrap: Option<bool>,
    pub flags: Option<Vec<u8>>,
    pub service_name: Option<String>,
}
impl XPC {
    pub fn from_env() -> XPC {
        let null_bootstrap = match crate::env::var("XPC_NULL_BOOTSTRAP") {
            Ok(nb) => match u8::from_str_radix(&nb, 10) {
                Ok(nb) => {
                    eprintln!("[warning] XPC_NULL_BOOTSTRAP environment variable is set");
                    Some(SBoolean::new(nb).value())
                },
                Err(_) => None,
            },
            Err(_) => None,
        };
        let flags = match crate::env::var("XPC_FLAGS") {
            Ok(flags) => match from_hex(&flags) {
                Ok(flags) => {
                    eprintln!("[warning] XPC_FLAGS environment variable is set");
                    Some(flags)
                },
                Err(_) => None,
            },
            Err(_) => None,
        };
        let service_name = match crate::env::var("XPC_SERVICE_NAME") {
            Ok(service_name) => Some(service_name),
            Err(_) => None,
        };
        XPC {
            null_bootstrap,
            flags,
            service_name,
        }
    }
}