change-user-run 0.1.3

Run commands as other users and create users
Documentation
//! Utility functionality.

use std::{
    env::var,
    fs::copy,
    os::unix::fs::chown,
    path::{Path, PathBuf},
};

use log::debug;
use which::which;

use crate::{Error, IoError};

/// Environment variables that are passed in to a command call as a different user.
pub const COVERAGE_ENV_LIST: &[&str] = &[
    "LLVM_PROFILE_FILE",
    "CARGO_LLVM_COV",
    "CARGO_LLVM_COV_SHOW_ENV",
    "CARGO_LLVM_COV_TARGET_DIR",
    "RUSTFLAGS",
    "RUSTDOCFLAGS",
];

/// Collects all `.profraw` files from `path` and copies them to `CARGO_LLVM_COV_TARGET_DIR`.
///
/// Only copies files from `path` if the `CARGO_LLVM_COV_TARGET_DIR` environment variable is set.
/// Changes the ownership of files copied to `CARGO_LLVM_COV_TARGET_DIR` to root.
///
/// # Note
///
/// This function is useful for containerized tests, in which test payloads are run using a
/// different user than root (e.g. with the help of
/// [`run_command_as_user`][crate::run_command_as_user]).
/// In those scenarios, the generated profraw data would not be in a per-user location and not owned
/// by root.
/// This would prevent this data to be used by the user that ran the containerized test.
///
/// # Errors
///
/// Returns an error if
///
/// - the `CARGO_LLVM_COV_TARGET_DIR` environment variable is not set when calling this function
/// - `path` cannot be read
/// - an entry in `path` cannot be read
/// - copying a file from `path` to `CARGO_LLVM_COV_TARGET_DIR` fails
/// - changing the ownership of a file previously copied to `CARGO_LLVM_COV_TARGET_DIR` to root
///   fails
pub fn collect_coverage_data(path: impl AsRef<Path>) -> Result<(), crate::Error> {
    let path = path.as_ref();

    let cov_target_dir = {
        let Ok(cov_target_dir) = var("CARGO_LLVM_COV_TARGET_DIR") else {
            return Err(crate::Error::CargoLlvmCovTargetDirNotSet);
        };

        debug!("Found CARGO_LLVM_COV_TARGET_DIR={cov_target_dir}");
        PathBuf::from(cov_target_dir)
    };

    for dir_entry in path
        .read_dir()
        .map_io_path_err(path, "reading directory entries")?
    {
        let dir_entry = dir_entry.map_io_path_err(path, "accessing a child in the directory")?;
        let from = dir_entry.path();
        let Some(file_name) = &from.file_name() else {
            continue;
        };
        if from
            .extension()
            .is_some_and(|extension| extension == "profraw")
        {
            let target_file = cov_target_dir.join(file_name);
            debug!("Copying {from:?} to {target_file:?}");
            copy(&from, &target_file)
                .map_io_path_err(path, format!("copying file to {target_file:?}"))?;
            chown(&target_file, Some(0), Some(0))
                .map_io_path_err(target_file, "changing ownership to root")?;
        }
    }

    Ok(())
}

/// Returns the path to a `command`.
///
/// # Errors
///
/// Returns an error if `command` can not be found in PATH.
pub fn get_command(command: &str) -> Result<PathBuf, Error> {
    which(command).map_err(|source| Error::ExecutableNotFound {
        command: command.to_string(),
        source,
    })
}

#[cfg(test)]
#[cfg(target_os = "linux")]
mod tests {
    use rstest::rstest;
    use testresult::TestResult;

    use super::*;

    /// Ensures that the [whoami] executable can be found on a Linux system.
    ///
    /// [whoami]: https://man.archlinux.org/man/whoami.1
    #[rstest]
    #[case("whoami")]
    #[case("/usr/bin/whoami")]
    fn get_command_succeeds(#[case] cmd: &str) -> TestResult {
        get_command(cmd)?;
        Ok(())
    }

    /// Ensures that a bogus executable cannot be found on a Linux system.
    #[test]
    fn get_command_fails() -> TestResult {
        let bogus_cmd = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";

        if let Ok(path) = get_command(bogus_cmd) {
            panic!("The command {bogus_cmd} shouldn't exist, but {path:?} is found!");
        }

        Ok(())
    }
}