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)
}