// SPDX-License-Identifier: AGPL-3.0-only
//! The OpenZL frame decoder (feature `openzl`). Heimdall is a defensive sandbox.
//!
//! On wasm32 this is the vendored OpenZL 0.3.0 C decoder (`openzl/`, compiled
//! into the module by `build.rs`): a C bug on hostile bytes is a wasm trap or,
//! at worst, corrupts this one guest instance's own memory — never the host.
//! Natively it is `openzl-sys-rs` 0.4.1, the library Holger itself links.
//!
//! The caller ([`crate::znippy`]) bounds everything BEFORE a byte is allocated:
//! the frame header's size must equal the size the index declares for the chunk,
//! and the member's declared size already passed its cap and the archive's
//! expansion ratio. The decoder then writes into exactly that many bytes.
/// Why a frame was not decoded: OpenZL's error name and number.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ZlError(pub String);
/// The decompressed size a frame's header declares.
pub fn frame_size(frame: &[u8]) -> Result<u64, ZlError> {
imp::frame_size(frame)
}
/// Decode `frame` into `out` (exactly the declared size); returns bytes written.
pub fn decode(frame: &[u8], out: &mut [u8]) -> Result<usize, ZlError> {
imp::decode(frame, out)
}
/// Decode `frame` into exactly `dst`: the frame header must declare
/// `dst.len()` bytes BEFORE the decoder runs (a frame that claims more is a
/// size lie or a bomb, refused without decoding), and the decoder must fill
/// all of it. The `Err` text names what failed; the archive reader puts it
/// into Holger's container refusal.
pub fn decode_exact(frame: &[u8], dst: &mut [u8]) -> Result<(), String> {
let declared = frame_size(frame).map_err(|e| format!("not an OpenZL frame: {}", e.0))?;
if declared != dst.len() as u64 {
return Err(format!(
"the OpenZL frame declares {declared} bytes but the index declares {}",
dst.len()
));
}
let n = decode(frame, dst).map_err(|e| format!("the OpenZL frame does not decode: {}", e.0))?;
if n != dst.len() {
return Err(format!(
"the OpenZL frame decoded to {n} bytes, not the declared {}",
dst.len()
));
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
mod imp {
use super::ZlError;
use openzl_sys_rs as z;
fn err(r: z::ZL_Report) -> ZlError {
// SAFETY: an error report's `_code` is the error arm of the union, and
// ZL_ErrorCode_toString returns a static NUL-terminated string.
#[allow(unsafe_code)]
unsafe {
let code = r._code;
let name = std::ffi::CStr::from_ptr(z::ZL_ErrorCode_toString(code));
ZlError(format!("{} ({code})", name.to_string_lossy()))
}
}
pub fn frame_size(frame: &[u8]) -> Result<u64, ZlError> {
// SAFETY: pointer and length of one live slice.
#[allow(unsafe_code)]
let r = unsafe { z::ZL_getDecompressedSize(frame.as_ptr().cast(), frame.len()) };
if z::zl_is_error(r) {
return Err(err(r));
}
Ok(z::zl_report_value(r) as u64)
}
pub fn decode(frame: &[u8], out: &mut [u8]) -> Result<usize, ZlError> {
// SAFETY: pointers and lengths of two live, disjoint slices.
#[allow(unsafe_code)]
let r = unsafe {
z::ZL_decompress(
out.as_mut_ptr().cast(),
out.len(),
frame.as_ptr().cast(),
frame.len(),
)
};
if z::zl_is_error(r) {
return Err(err(r));
}
Ok(z::zl_report_value(r))
}
}
#[cfg(target_arch = "wasm32")]
#[allow(unsafe_code)]
mod imp {
//! The scalar-only ABI of openzl/shim/hzl.c, and the C heap = Rust's heap.
use super::ZlError;
use std::alloc::{alloc, alloc_zeroed, dealloc, realloc as re, Layout};
use std::ffi::{c_char, c_int, c_void, CStr};
extern "C" {
fn hzl_frame_size(src: *const u8, len: usize, out: *mut u64) -> c_int;
fn hzl_decode(
dst: *mut u8,
cap: usize,
src: *const u8,
len: usize,
written: *mut usize,
) -> c_int;
fn hzl_error_name(code: c_int) -> *const c_char;
}
fn err(code: c_int) -> ZlError {
// SAFETY: OpenZL returns a static NUL-terminated string for any code.
let name = unsafe { CStr::from_ptr(hzl_error_name(code)) };
ZlError(format!("{} ({code})", name.to_string_lossy()))
}
pub fn frame_size(frame: &[u8]) -> Result<u64, ZlError> {
let mut n = 0u64;
// SAFETY: a live slice and a live out-pointer.
match unsafe { hzl_frame_size(frame.as_ptr(), frame.len(), &mut n) } {
0 => Ok(n),
c => Err(err(c)),
}
}
pub fn decode(frame: &[u8], out: &mut [u8]) -> Result<usize, ZlError> {
let mut w = 0usize;
// SAFETY: two live, disjoint slices and a live out-pointer.
match unsafe {
hzl_decode(
out.as_mut_ptr(),
out.len(),
frame.as_ptr(),
frame.len(),
&mut w,
)
} {
0 if w <= out.len() => Ok(w),
0 => Err(ZlError(
"decoder reported more bytes than the buffer".into(),
)),
c => Err(err(c)),
}
}
// ---- malloc/calloc/realloc/free for the C decoder, on Rust's allocator.
// Every block carries a 16-byte header holding its size; 16-byte aligned
// like a C malloc. Allocation failure returns NULL (OpenZL then reports an
// error); it never aborts the guest by itself.
const HDR: usize = 16;
fn layout(size: usize) -> Option<Layout> {
Layout::from_size_align(size.checked_add(HDR)?, HDR).ok()
}
unsafe fn finish(base: *mut u8, size: usize) -> *mut c_void {
if base.is_null() {
return std::ptr::null_mut();
}
base.cast::<usize>().write(size);
base.add(HDR).cast()
}
#[no_mangle]
pub unsafe extern "C" fn malloc(size: usize) -> *mut c_void {
match layout(size) {
Some(l) => finish(alloc(l), size),
None => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn calloc(n: usize, size: usize) -> *mut c_void {
match n.checked_mul(size).and_then(|t| layout(t).map(|l| (t, l))) {
Some((t, l)) => finish(alloc_zeroed(l), t),
None => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn free(p: *mut c_void) {
if p.is_null() {
return;
}
let base = p.cast::<u8>().sub(HDR);
let size = base.cast::<usize>().read();
dealloc(base, Layout::from_size_align_unchecked(size + HDR, HDR));
}
#[no_mangle]
pub unsafe extern "C" fn realloc(p: *mut c_void, size: usize) -> *mut c_void {
if p.is_null() {
return malloc(size);
}
let Some(new) = layout(size) else {
return std::ptr::null_mut();
};
let base = p.cast::<u8>().sub(HDR);
let old = base.cast::<usize>().read();
let nb = re(
base,
Layout::from_size_align_unchecked(old + HDR, HDR),
new.size(),
);
finish(nb, size)
}
}