#[cfg(not(target_os = "macos"))]
#[allow(clippy::too_many_arguments)]
pub(crate) mod unsupported;
#[cfg(target_os = "macos")]
pub(crate) use crate::bridge::ffi;
#[cfg(not(target_os = "macos"))]
pub(crate) use self::unsupported as ffi;
use crate::containerization::container;
use crate::containerization::process;
#[cfg(target_os = "macos")]
use crate::containerization_error;
use crate::containerization_extras;
use crate::containerization_os::file_descriptor_ops;
use crate::error::Error;
use std::any::Any;
use std::collections::BTreeMap;
use std::os::fd::BorrowedFd;
use std::panic::AssertUnwindSafe;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::PoisonError;
use std::time::Duration;
use std::time::SystemTime;
#[derive(Clone, Default)]
pub(crate) struct Panic(Arc<Mutex<Option<Box<dyn Any + Send>>>>);
impl Panic {
pub(crate) fn catch<R>(&self, body: impl FnOnce() -> R) -> Option<R> {
std::panic::catch_unwind(AssertUnwindSafe(body))
.map_err(|payload| {
self
.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get_or_insert(payload);
})
.ok()
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) fn caught(&self) -> bool {
self
.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.is_some()
}
pub(crate) fn resume(&self) {
if let Some(payload) = self.0.lock().unwrap_or_else(PoisonError::into_inner).take() {
std::panic::resume_unwind(payload);
}
}
}
pub(crate) struct Progress {
pub(crate) handler: Option<containerization_extras::ProgressHandler>,
pub(crate) panic: Panic,
}
pub(crate) fn progressing<R>(
handler: Option<containerization_extras::ProgressHandler>,
call: impl FnOnce(Progress) -> R,
) -> R {
let panic = Panic::default();
let returned = call(Progress {
handler,
panic: panic.clone(),
});
panic.resume();
returned
}
type Configuring<T> = Box<dyn FnOnce(&mut T) -> Result<(), Error> + Send>;
pub(crate) struct Configure<T> {
configure: Mutex<Option<Configuring<T>>>,
thrown: Arc<Mutex<Option<Error>>>,
panic: Panic,
}
impl<T> Configure<T> {
pub(crate) fn new(configure: impl FnOnce(&mut T) -> Result<(), Error> + Send + 'static) -> Self {
Self {
configure: Mutex::new(Some(Box::new(configure))),
thrown: Arc::default(),
panic: Panic::default(),
}
}
pub(crate) fn call(&self, value: &mut T) -> bool {
let configure = self
.configure
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
match configure.map(|configure| self.panic.catch(|| configure(value))) {
Some(Some(Err(error))) => {
*self.thrown.lock().unwrap_or_else(PoisonError::into_inner) = Some(error);
true
}
Some(None) => true,
_ => false,
}
}
}
pub(crate) type ConfigureContainer = Configure<container::linux_container::Configuration>;
pub(crate) type ConfigureProcess = Configure<process::LinuxProcessConfiguration>;
pub(crate) type ConfigurePod = Configure<container::linux_pod::Configuration>;
pub(crate) type ConfigurePodContainer = Configure<container::linux_pod::ContainerConfiguration>;
pub(crate) fn configured<T>(
configure: impl FnOnce(&mut T) -> Result<(), Error> + Send + 'static,
call: impl FnOnce(Configure<T>) -> ffi::CzOutcome,
action: impl Into<String>,
) -> Result<ffi::CzOutcome, Error> {
let configure = Configure::new(configure);
let thrown = Arc::clone(&configure.thrown);
let panic = configure.panic.clone();
let returned = call(configure);
panic.resume();
if let Some(error) = thrown.lock().unwrap_or_else(PoisonError::into_inner).take() {
return Err(error);
}
outcome(returned, action)
}
pub(crate) fn filled<T: Default + Send + 'static, R>(call: impl FnOnce(Configure<T>) -> R) -> (R, Option<T>) {
let filled = Arc::new(Mutex::new(None));
let slot = Arc::clone(&filled);
let receive = Configure::new(move |value| {
*slot.lock().unwrap_or_else(PoisonError::into_inner) = Some(std::mem::take(value));
Ok(())
});
let returned = call(receive);
let value = filled.lock().unwrap_or_else(PoisonError::into_inner).take();
(returned, value)
}
pub(crate) fn filled_process(
call: impl FnOnce(ConfigureProcess) -> ffi::CzOutcome,
action: impl Into<String>,
) -> Result<process::LinuxProcessConfiguration, Error> {
let (returned, process) = filled(call);
outcome(returned, action)?;
Ok(process.expect("Swift hands back the configuration it fills"))
}
#[derive(Clone, Default)]
pub(crate) struct Thrown {
error: Arc<Mutex<Option<Error>>>,
panic: Panic,
}
impl Thrown {
fn threw(&self, body: impl FnOnce() -> Result<(), Error>) -> bool {
match self.panic.catch(body) {
Some(Ok(())) => false,
Some(Err(error)) => {
*self.error.lock().unwrap_or_else(PoisonError::into_inner) = Some(error);
true
}
None => true,
}
}
pub(crate) fn outcome(&self, returned: ffi::CzOutcome, action: impl Into<String>) -> Result<ffi::CzOutcome, Error> {
self.panic.resume();
if let Some(error) = self
.error
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()
{
return Err(error);
}
outcome(returned, action)
}
}
type DirectoryBody = Box<dyn FnOnce(BorrowedFd<'_>) -> Result<(), Error>>;
pub(crate) struct WithDescriptor {
body: Mutex<Option<DirectoryBody>>,
pub(crate) thrown: Thrown,
}
impl WithDescriptor {
pub(crate) fn new(body: impl FnOnce(BorrowedFd<'_>) -> Result<(), Error> + 'static) -> Self {
Self {
body: Mutex::new(Some(Box::new(body))),
thrown: Thrown::default(),
}
}
pub(crate) fn call(&self, descriptor: i32) -> bool {
let body = self
.body
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
body.is_some_and(|body| {
self
.thrown
.threw(|| body(unsafe { BorrowedFd::borrow_raw(descriptor) }))
})
}
}
type EnumerateBody = Box<dyn FnMut(&Path, file_descriptor_ops::EntryType, BorrowedFd<'_>) -> Result<(), Error>>;
pub(crate) struct Enumerate {
body: Mutex<EnumerateBody>,
pub(crate) thrown: Thrown,
}
impl Enumerate {
pub(crate) fn new(
body: impl FnMut(&Path, file_descriptor_ops::EntryType, BorrowedFd<'_>) -> Result<(), Error> + 'static,
) -> Self {
Self {
body: Mutex::new(Box::new(body)),
thrown: Thrown::default(),
}
}
pub(crate) fn call(&self, path: String, entry_type: ffi::FdOpsEntryType, parent: i32) -> bool {
let mut body = self.body.lock().unwrap_or_else(PoisonError::into_inner);
self.thrown.threw(|| {
body(Path::new(&path), entry_type.into(), unsafe {
BorrowedFd::borrow_raw(parent)
})
})
}
}
pub(crate) fn outcome(outcome: ffi::CzOutcome, action: impl Into<String>) -> Result<ffi::CzOutcome, Error> {
match outcome.error() {
Some(message) => Err(error(&outcome, action, message)),
None => Ok(outcome),
}
}
#[cfg(target_os = "macos")]
fn error(outcome: &ffi::CzOutcome, action: impl Into<String>, message: String) -> Error {
if let Some(error) = outcome.thrown_fd_ops_error() {
return Error::FileDescriptorOps {
action: action.into(),
error,
};
}
let code = outcome
.error_code()
.and_then(|code| containerization_error::Code::from_description(&code));
Error::failed_with_code(action, message, code)
}
#[cfg(not(target_os = "macos"))]
fn error(_outcome: &ffi::CzOutcome, action: impl Into<String>, message: String) -> Error {
Error::unavailable(action, message)
}
pub(crate) fn path(path: &Path) -> Result<&str, Error> {
path.to_str().ok_or_else(|| {
Error::failed(
format!("pass the path {} to Swift", path.display()),
"the path isn't UTF-8, which Swift's String requires",
)
})
}
pub(crate) fn exit_status(outcome: &ffi::CzOutcome) -> Result<process::ExitStatus, Error> {
Ok(process::ExitStatus {
exit_code: outcome.exit_code(),
exited_at: system_time(outcome.exited_at())?,
})
}
pub(crate) fn system_time(seconds: f64) -> Result<SystemTime, Error> {
let duration = |seconds: f64| Duration::try_from_secs_f64(seconds).ok();
let time = if seconds < 0.0 {
duration(-seconds).and_then(|before| SystemTime::UNIX_EPOCH.checked_sub(before))
} else {
duration(seconds).and_then(|after| SystemTime::UNIX_EPOCH.checked_add(after))
};
time.ok_or_else(|| {
Error::failed(
"read a Swift Date as a SystemTime",
format!("{seconds} seconds from 1970 is outside SystemTime's range"),
)
})
}
pub(crate) unsafe trait Plain: Copy {}
pub(crate) fn from_bytes<T: Plain>(bytes: &[u8]) -> T {
assert_eq!(
bytes.len(),
std::mem::size_of::<T>(),
"Swift's {} is a different size from Rust's",
std::any::type_name::<T>()
);
unsafe { bytes.as_ptr().cast::<T>().read_unaligned() }
}
pub(crate) fn seconds(time: SystemTime) -> f64 {
match time.duration_since(SystemTime::UNIX_EPOCH) {
Ok(after) => after.as_secs_f64(),
Err(before) => -before.duration().as_secs_f64(),
}
}
pub(crate) fn xattrs(xattrs: &BTreeMap<String, Vec<u8>>) -> (Vec<String>, Vec<u64>, Vec<u8>) {
(
xattrs.keys().cloned().collect(),
xattrs.values().map(|value| value.len() as u64).collect(),
xattrs.values().flatten().copied().collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crosses_dates_on_either_side_of_1970() {
for seconds in [-86_400.5, 0.0, 1_700_000_000.25] {
assert_eq!(super::seconds(system_time(seconds).unwrap()), seconds);
}
}
#[test]
fn fails_dates_system_time_cant_hold() {
for seconds in [9.3e18, -9.3e18, f64::INFINITY, f64::NEG_INFINITY, f64::NAN] {
assert!(matches!(system_time(seconds), Err(Error::Failed { .. })), "{seconds}");
}
}
#[test]
fn crosses_a_utf8_path_as_it_is() {
assert_eq!(super::path(Path::new("/tmp/file")).unwrap(), "/tmp/file");
}
#[test]
fn fails_a_path_that_isnt_utf8() {
use std::os::unix::ffi::OsStrExt;
let path = Path::new(std::ffi::OsStr::from_bytes(b"/tmp/\xff"));
assert!(matches!(super::path(path), Err(Error::Failed { .. })));
}
}