#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
pub const WASM_PAGE_SIZE_BYTES: u64 = 65_536;
pub const WASM_PAGE_SIZE_KIB: u64 = 64;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum WasmMemoryError {
BytesNotPageAligned,
PageCountOverflow,
MaximumLessThanMinimum,
}
impl fmt::Display for WasmMemoryError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BytesNotPageAligned => {
formatter.write_str("byte count is not aligned to WebAssembly pages")
},
Self::PageCountOverflow => formatter.write_str("WebAssembly page count exceeds u32"),
Self::MaximumLessThanMinimum => {
formatter.write_str("maximum memory pages cannot be lower than minimum pages")
},
}
}
}
impl Error for WasmMemoryError {}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WasmPageCount(u32);
impl WasmPageCount {
#[must_use]
pub const fn new(pages: u32) -> Self {
Self(pages)
}
pub fn from_bytes(bytes: u64) -> Result<Self, WasmMemoryError> {
if !bytes.is_multiple_of(WASM_PAGE_SIZE_BYTES) {
return Err(WasmMemoryError::BytesNotPageAligned);
}
let pages = bytes / WASM_PAGE_SIZE_BYTES;
let pages = u32::try_from(pages).map_err(|_| WasmMemoryError::PageCountOverflow)?;
Ok(Self(pages))
}
#[must_use]
pub const fn pages(self) -> u32 {
self.0
}
#[must_use]
pub fn bytes(self) -> u64 {
u64::from(self.0) * WASM_PAGE_SIZE_BYTES
}
}
impl fmt::Display for WasmPageCount {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{} pages", self.pages())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MemoryMinimum(WasmPageCount);
impl MemoryMinimum {
#[must_use]
pub const fn new(pages: WasmPageCount) -> Self {
Self(pages)
}
#[must_use]
pub const fn page_count(self) -> WasmPageCount {
self.0
}
#[must_use]
pub const fn pages(self) -> u32 {
self.0.pages()
}
#[must_use]
pub fn bytes(self) -> u64 {
self.0.bytes()
}
}
impl From<WasmPageCount> for MemoryMinimum {
fn from(value: WasmPageCount) -> Self {
Self::new(value)
}
}
impl fmt::Display for MemoryMinimum {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "minimum {}", self.page_count())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MemoryMaximum(WasmPageCount);
impl MemoryMaximum {
#[must_use]
pub const fn new(pages: WasmPageCount) -> Self {
Self(pages)
}
#[must_use]
pub const fn page_count(self) -> WasmPageCount {
self.0
}
#[must_use]
pub const fn pages(self) -> u32 {
self.0.pages()
}
#[must_use]
pub fn bytes(self) -> u64 {
self.0.bytes()
}
}
impl From<WasmPageCount> for MemoryMaximum {
fn from(value: WasmPageCount) -> Self {
Self::new(value)
}
}
impl fmt::Display for MemoryMaximum {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "maximum {}", self.page_count())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SharedMemory {
#[default]
Unshared,
Shared,
}
impl SharedMemory {
#[must_use]
pub const fn from_bool(shared: bool) -> Self {
if shared { Self::Shared } else { Self::Unshared }
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Unshared => "unshared",
Self::Shared => "shared",
}
}
#[must_use]
pub const fn is_shared(self) -> bool {
matches!(self, Self::Shared)
}
}
impl From<bool> for SharedMemory {
fn from(value: bool) -> Self {
Self::from_bool(value)
}
}
impl From<SharedMemory> for bool {
fn from(value: SharedMemory) -> Self {
value.is_shared()
}
}
impl fmt::Display for SharedMemory {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct MemoryLimits {
minimum: WasmPageCount,
maximum: Option<WasmPageCount>,
shared: SharedMemory,
}
impl MemoryLimits {
pub fn new(
minimum: WasmPageCount,
maximum: Option<WasmPageCount>,
) -> Result<Self, WasmMemoryError> {
if let Some(maximum) = maximum
&& maximum < minimum
{
return Err(WasmMemoryError::MaximumLessThanMinimum);
}
Ok(Self {
minimum,
maximum,
shared: SharedMemory::Unshared,
})
}
#[must_use]
pub const fn with_shared(mut self, shared: bool) -> Self {
self.shared = SharedMemory::from_bool(shared);
self
}
#[must_use]
pub const fn with_shared_memory(mut self, shared: SharedMemory) -> Self {
self.shared = shared;
self
}
#[must_use]
pub const fn minimum(&self) -> WasmPageCount {
self.minimum
}
#[must_use]
pub const fn minimum_pages(&self) -> u32 {
self.minimum.pages()
}
#[must_use]
pub const fn maximum(&self) -> Option<WasmPageCount> {
self.maximum
}
#[must_use]
pub const fn maximum_pages(&self) -> Option<u32> {
match self.maximum {
Some(maximum) => Some(maximum.pages()),
None => None,
}
}
#[must_use]
pub const fn shared_memory(&self) -> SharedMemory {
self.shared
}
#[must_use]
pub const fn is_shared(&self) -> bool {
self.shared.is_shared()
}
}
#[cfg(test)]
mod tests {
use super::{
MemoryLimits, MemoryMaximum, MemoryMinimum, SharedMemory, WASM_PAGE_SIZE_BYTES,
WasmMemoryError, WasmPageCount,
};
#[test]
fn converts_pages_and_bytes() {
let pages = WasmPageCount::from_bytes(WASM_PAGE_SIZE_BYTES * 2).expect("aligned pages");
assert_eq!(pages.pages(), 2);
assert_eq!(pages.bytes(), WASM_PAGE_SIZE_BYTES * 2);
assert_eq!(pages.to_string(), "2 pages");
assert_eq!(MemoryMinimum::new(pages).pages(), 2);
assert_eq!(MemoryMaximum::new(pages).bytes(), WASM_PAGE_SIZE_BYTES * 2);
}
#[test]
fn validates_memory_limits() {
let limits = MemoryLimits::new(WasmPageCount::new(1), Some(WasmPageCount::new(4)))
.expect("valid limits")
.with_shared(true);
assert_eq!(limits.minimum_pages(), 1);
assert_eq!(limits.maximum_pages(), Some(4));
assert_eq!(limits.shared_memory(), SharedMemory::Shared);
assert!(limits.is_shared());
assert_eq!(SharedMemory::from_bool(false).to_string(), "unshared");
assert_eq!(
MemoryLimits::new(WasmPageCount::new(4), Some(WasmPageCount::new(1))),
Err(WasmMemoryError::MaximumLessThanMinimum)
);
}
}