vmrunner-sysroot 0.0.5

micro-vm runner sysroot extraction helpers
use std::{
    collections::HashMap,
    ffi::{CStr, CString},
    mem,
    os::raw::{c_char, c_int, c_void},
};

use color_eyre::eyre::{Result, WrapErr, eyre};

#[allow(
    dead_code,
    non_camel_case_types,
    non_snake_case,
    non_upper_case_globals,
    improper_ctypes
)]
mod bindings {
    include!(concat!(env!("OUT_DIR"), "/guestfs_bindings.rs"));
}

use bindings::{guestfs_add_drive_opts_argv, guestfs_h};

#[derive(Debug, Default)]
pub struct AddDriveOptArgs<'a> {
    pub readonly: Option<bool>,
    pub format: Option<&'a str>,
}

pub struct Handle {
    handle: *mut guestfs_h,
}

impl Handle {
    pub fn create() -> Result<Self> {
        let handle = unsafe { bindings::guestfs_create() };
        if handle.is_null() {
            return Err(eyre!("libguestfs returned a null handle"));
        }
        unsafe {
            bindings::guestfs_set_error_handler(handle, None, std::ptr::null_mut());
        }
        Ok(Self { handle })
    }

    pub fn add_drive(&self, filename: &str, optargs: AddDriveOptArgs<'_>) -> Result<()> {
        let filename = c_string(filename, "drive filename")?;
        let format = optargs
            .format
            .map(|format| c_string(format, "drive format"))
            .transpose()?;
        let raw_optargs = raw_add_drive_opt_args(format.as_ref(), optargs.readonly);
        let status = unsafe {
            bindings::guestfs_add_drive_opts_argv(self.handle, filename.as_ptr(), &raw_optargs)
        };
        self.check_status("add_drive", status)
    }

    pub fn launch(&self) -> Result<()> {
        let status = unsafe { bindings::guestfs_launch(self.handle) };
        self.check_status("launch", status)
    }

    pub fn inspect_os(&self) -> Result<Vec<String>> {
        let list = unsafe { bindings::guestfs_inspect_os(self.handle) };
        self.check_string_list("inspect_os", list)
    }

    pub fn inspect_get_mountpoints(&self, root: &str) -> Result<HashMap<String, String>> {
        let root = c_string(root, "guest root")?;
        let list = unsafe { bindings::guestfs_inspect_get_mountpoints(self.handle, root.as_ptr()) };
        self.check_string_map("inspect_get_mountpoints", list)
    }

    pub fn mount_ro(&self, mountable: &str, mountpoint: &str) -> Result<()> {
        let mountable = c_string(mountable, "mountable")?;
        let mountpoint = c_string(mountpoint, "mountpoint")?;
        let status = unsafe {
            bindings::guestfs_mount_ro(self.handle, mountable.as_ptr(), mountpoint.as_ptr())
        };
        self.check_status("mount_ro", status)
    }

    pub fn mount_options(&self, options: &str, mountable: &str, mountpoint: &str) -> Result<()> {
        let options = c_string(options, "mount options")?;
        let mountable = c_string(mountable, "mountable")?;
        let mountpoint = c_string(mountpoint, "mountpoint")?;
        let status = unsafe {
            bindings::guestfs_mount_options(
                self.handle,
                options.as_ptr(),
                mountable.as_ptr(),
                mountpoint.as_ptr(),
            )
        };
        self.check_status("mount_options", status)
    }

    pub fn mount_vfs(
        &self,
        options: &str,
        fstype: &str,
        mountable: &str,
        mountpoint: &str,
    ) -> Result<()> {
        let options = c_string(options, "mount options")?;
        let fstype = c_string(fstype, "filesystem type")?;
        let mountable = c_string(mountable, "mountable")?;
        let mountpoint = c_string(mountpoint, "mountpoint")?;
        let status = unsafe {
            bindings::guestfs_mount_vfs(
                self.handle,
                options.as_ptr(),
                fstype.as_ptr(),
                mountable.as_ptr(),
                mountpoint.as_ptr(),
            )
        };
        self.check_status("mount_vfs", status)
    }

    pub fn copy_out(&self, remote_path: &str, local_dir: &str) -> Result<()> {
        let remote_path = c_string(remote_path, "remote guest path")?;
        let local_dir = c_string(local_dir, "local destination directory")?;
        let status = unsafe {
            bindings::guestfs_copy_out(self.handle, remote_path.as_ptr(), local_dir.as_ptr())
        };
        self.check_status("copy_out", status)
    }

    pub fn exists(&self, path: &str) -> Result<bool> {
        let path = c_string(path, "guest path")?;
        let status = unsafe { bindings::guestfs_exists(self.handle, path.as_ptr()) };
        if status == -1 {
            return Err(self.api_error("exists"));
        }
        Ok(status == 1)
    }

    pub fn is_symlink(&self, path: &str) -> Result<bool> {
        let path = c_string(path, "guest path")?;
        let status = unsafe { bindings::guestfs_is_symlink(self.handle, path.as_ptr()) };
        if status == -1 {
            return Err(self.api_error("is_symlink"));
        }
        Ok(status == 1)
    }

    pub fn umount_all(&self) -> Result<()> {
        let status = unsafe { bindings::guestfs_umount_all(self.handle) };
        self.check_status("umount_all", status)
    }

    pub fn shutdown(&self) -> Result<()> {
        let status = unsafe { bindings::guestfs_shutdown(self.handle) };
        self.check_status("shutdown", status)
    }

    fn check_status(&self, operation: &str, status: c_int) -> Result<()> {
        if status == -1 {
            return Err(self.api_error(operation));
        }
        Ok(())
    }

    fn check_string_list(&self, operation: &str, list: *mut *mut c_char) -> Result<Vec<String>> {
        if list.is_null() {
            return Err(self.api_error(operation));
        }
        unsafe { take_string_list(list) }
    }

    fn check_string_map(
        &self,
        operation: &str,
        list: *mut *mut c_char,
    ) -> Result<HashMap<String, String>> {
        if list.is_null() {
            return Err(self.api_error(operation));
        }
        let list = unsafe { take_string_list(list) }?;
        if list.len() % 2 != 0 {
            return Err(eyre!(
                "libguestfs {operation} returned an odd number of mountpoint entries"
            ));
        }

        Ok(HashMap::from_iter(
            list.chunks_exact(2)
                .map(|entry| (entry[0].clone(), entry[1].clone())),
        ))
    }

    fn api_error(&self, operation: &str) -> color_eyre::Report {
        let message = unsafe {
            let message = bindings::guestfs_last_error(self.handle);
            if message.is_null() {
                "unknown libguestfs error".to_owned()
            } else {
                CStr::from_ptr(message).to_string_lossy().into_owned()
            }
        };
        let errno = unsafe { bindings::guestfs_last_errno(self.handle) };
        eyre!("libguestfs {operation} failed: {message} (errno {errno})")
    }
}

impl Drop for Handle {
    fn drop(&mut self) {
        if !self.handle.is_null() {
            unsafe { bindings::guestfs_close(self.handle) };
        }
    }
}

fn raw_add_drive_opt_args(
    format: Option<&CString>,
    readonly: Option<bool>,
) -> guestfs_add_drive_opts_argv {
    let mut raw = unsafe { mem::zeroed::<guestfs_add_drive_opts_argv>() };
    if let Some(readonly) = readonly {
        raw.bitmask |= 1_u64 << bindings::GUESTFS_ADD_DRIVE_OPTS_READONLY;
        raw.readonly = c_int::from(readonly);
    }
    if let Some(format) = format {
        raw.bitmask |= 1_u64 << bindings::GUESTFS_ADD_DRIVE_OPTS_FORMAT;
        raw.format = format.as_ptr();
    }
    raw
}

fn c_string(value: &str, description: &str) -> Result<CString> {
    CString::new(value).wrap_err_with(|| format!("{description} contains an interior NUL byte"))
}

unsafe fn take_string_list(list: *mut *mut c_char) -> Result<Vec<String>> {
    let mut values = Vec::new();
    let mut index = 0;
    loop {
        let item = unsafe { *list.add(index) };
        if item.is_null() {
            break;
        }
        values.push(unsafe { CStr::from_ptr(item) }.to_str()?.to_owned());
        unsafe { libc::free(item.cast::<c_void>()) };
        index += 1;
    }
    unsafe { libc::free(list.cast::<c_void>()) };
    Ok(values)
}