fox-core 0.1.0

Modern Ansible replacement
#[doc(hidden)]
pub struct RemoteFn {
    pub uuid: &'static str,
    pub function: fn(args: &[u8]) -> Vec<u8>,
}
inventory::collect!(RemoteFn);

#[doc(hidden)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct RpcPayload {
    pub uuid: String,
    pub data: Vec<u8>,
}

pub fn remote_rpc() -> anyhow_serde::Result<()> {
    std::fs::remove_file(std::env::current_exe()?)?;

    let m = inventory::iter::<RemoteFn>
        .into_iter()
        .map(|rfn| (rfn.uuid, rfn.function))
        .collect::<std::collections::HashMap<_, _>>();

    if m.is_empty() {
        return Ok(());
    }

    let mut stdin = std::io::stdin().lock();
    let mut stdout = std::io::stdout().lock();
    let mut buffer = Vec::with_capacity(4096);

    loop {
        buffer.clear();
        use std::io::BufRead as _;
        match stdin.read_until(0x00, &mut buffer) {
            Ok(0) => break,
            Ok(_) => {
                let parsed_payload = match postcard::from_bytes_cobs::<RpcPayload>(&mut buffer) {
                    Ok(payload) => payload,
                    Err(err) => panic!("Corrupted packet received: {err}"),
                };

                let result = m
                    .get(parsed_payload.uuid.as_str())
                    .expect("Called an unregistered remote function")(
                    &parsed_payload.data
                );

                use std::io::Write as _;
                stdout.write_all(&result).expect("Couldn't write to stdout");
                stdout.flush().expect("Couldn't flush stdout");
            }
            Err(err) => panic!("Error reading stdin: {err}"),
        }
    }

    Ok(())
}

pub use anyhow_serde::Result;
pub use anyhow_serde::bail;
pub use inventory;
#[doc(hidden)]
pub use postcard;
#[doc(hidden)]
pub use serde;

pub use fox_macros::{main, remote, rpc};

pub mod builtin {
    use std::path::PathBuf;

    use super as fox;
    use fox_macros::remote;
    use serde::{Deserialize, Serialize};

    #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
    pub struct Mode(pub u32);
    impl From<Mode> for std::fs::Permissions {
        fn from(Mode(value): Mode) -> Self {
            use std::os::unix::fs::PermissionsExt as _;
            std::fs::Permissions::from_mode(value)
        }
    }
    impl From<u32> for Mode {
        fn from(value: u32) -> Self {
            Mode(value)
        }
    }

    #[derive(Clone, Debug, Deserialize, Serialize)]
    pub struct CopyArgs {
        pub source: Vec<u8>,
        pub destination: PathBuf,
        pub owner: String,
        pub group: String,
        pub mode: Mode,
    }

    #[derive(Debug, thiserror::Error)]
    #[fox::rpc]
    pub enum CopyError {
        #[error("asdf")]
        InvalidUser,
        #[error("asdf")]
        InvalidGroup,
        #[error("asdf")]
        ParentDirUnavailable,
        #[error("asdf")]
        CannotCreateTempFile,
        #[error("asdf")]
        CannotWriteToTempFile,
        #[error("asdf")]
        Chown,
        #[error("asdf")]
        Chmod,
        #[error("asdf")]
        TempFilePersist,
    }

    /// Copy content into a file on the remote machine
    ///
    /// ```no_run
    /// fox::builtin::copy(fox::builtin::CopyArgs {
    ///     source: "hello".into(),
    ///     destination: "/var/tmp/hello.txt".into(),
    /// }).unwrap()
    /// ```
    #[remote]
    pub fn copy(args: CopyArgs) -> Result<(), CopyError> {
        use std::io::Write as _;

        let user = nix::unistd::User::from_name(&args.owner)
            .ok()
            .flatten()
            .ok_or(CopyError::InvalidUser)?;
        let group = nix::unistd::Group::from_name(&args.group)
            .ok()
            .flatten()
            .ok_or(CopyError::InvalidGroup)?;

        let mut tmp_file = tempfile::Builder::new()
            .prefix(".fox.")
            .tempfile_in(
                args.destination
                    .parent()
                    .ok_or(CopyError::ParentDirUnavailable)?,
            )
            .map_err(|_| CopyError::CannotCreateTempFile)?;

        tmp_file
            .write_all(&args.source)
            .map_err(|_| CopyError::CannotWriteToTempFile)?;

        nix::unistd::chown(tmp_file.path(), Some(user.uid), Some(group.gid))
            .map_err(|_| CopyError::Chown)?;
        std::fs::set_permissions(tmp_file.path(), args.mode.into())
            .map_err(|_| CopyError::Chmod)?;

        tmp_file
            .persist(&args.destination)
            .map_err(|_| CopyError::TempFilePersist)?;

        Ok(())
    }
}