use std::{error::Error, fmt};
use flagset::FlagSet;
use pyo3::{prelude::*, sync::PyOnceLock};
use crate::conversion::js_uint8_array_new;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnsupportedWasmFeatureExtensionError {
pub required: FlagSet<WasmFeatureExtension>,
pub supported: FlagSet<WasmFeatureExtension>,
}
impl UnsupportedWasmFeatureExtensionError {
pub fn check_support(py: Python, bytes: &[u8]) -> Result<Result<(), Self>, PyErr> {
let err = Self {
required: WasmFeatureExtension::required(bytes),
supported: *WasmFeatureExtension::supported(py)?,
};
if (err.required & (!err.supported)).is_empty() {
return Ok(Ok(()));
}
Ok(Err(err))
}
}
impl fmt::Display for UnsupportedWasmFeatureExtensionError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
writeln!(
fmt,
"A WASM module requires the following feature extensions, which are not supported by \
your browser:"
)?;
writeln!(fmt)?;
for missing in self.required & (!self.supported) {
writeln!(fmt, " - {missing}")?;
}
writeln!(fmt)?;
writeln!(
fmt,
"Please check out https://webassembly.org/features/ to see if a newer version of your \
browser already supports these features."
)
}
}
impl Error for UnsupportedWasmFeatureExtensionError {}
flagset::flags! {
#[non_exhaustive]
pub enum WasmFeatureExtension: u64 {
BulkMemory,
Exceptions,
ExtendedConst,
FunctionReferences,
GC,
Memory64,
MultiMemory,
MultiValue,
MutableGlobal,
ReferenceTypes,
RelaxedSimd,
SaturatingFloatToInt,
SignExtension,
Simd,
TailCall,
Threads,
}
}
impl From<WasmFeatureExtension> for wasmparser::WasmFeatures {
fn from(extension: WasmFeatureExtension) -> Self {
match extension {
WasmFeatureExtension::BulkMemory => Self::BULK_MEMORY,
WasmFeatureExtension::Exceptions => Self::EXCEPTIONS,
WasmFeatureExtension::ExtendedConst => Self::EXTENDED_CONST,
WasmFeatureExtension::FunctionReferences => Self::FUNCTION_REFERENCES,
WasmFeatureExtension::GC => Self::GC,
WasmFeatureExtension::Memory64 => Self::MEMORY64,
WasmFeatureExtension::MultiMemory => Self::MULTI_MEMORY,
WasmFeatureExtension::MultiValue => Self::MULTI_VALUE,
WasmFeatureExtension::MutableGlobal => Self::MUTABLE_GLOBAL,
WasmFeatureExtension::ReferenceTypes => Self::REFERENCE_TYPES,
WasmFeatureExtension::RelaxedSimd => Self::RELAXED_SIMD,
WasmFeatureExtension::SaturatingFloatToInt => Self::SATURATING_FLOAT_TO_INT,
WasmFeatureExtension::SignExtension => Self::SIGN_EXTENSION,
WasmFeatureExtension::Simd => Self::SIMD,
WasmFeatureExtension::TailCall => Self::TAIL_CALL,
WasmFeatureExtension::Threads => Self::THREADS,
}
}
}
impl WasmFeatureExtension {
#[allow(clippy::too_many_lines)]
#[must_use]
pub fn required(bytes: &[u8]) -> FlagSet<Self> {
let mut required: FlagSet<_> = FlagSet::default();
required.extend(
FlagSet::<Self>::full()
.into_iter()
.filter(|extension| Self::requires_features(bytes, (*extension).into())),
);
required
}
pub fn supported(py: Python) -> Result<&'static FlagSet<Self>, PyErr> {
static SUPPORTED_FEATURES: PyOnceLock<FlagSet<WasmFeatureExtension>> = PyOnceLock::new();
SUPPORTED_FEATURES.get_or_try_init(py, || {
let mut supported = FlagSet::default();
for extension in FlagSet::<Self>::full() {
if extension.check_if_supported(py)? {
supported |= extension;
}
}
Ok(supported)
})
}
pub fn check_if_supported(self, py: Python) -> Result<bool, PyErr> {
let canary = self.canary_bytes();
if matches!(self, Self::MultiMemory) {
Self::try_create_wasm_module_from_bytes(py, canary)
} else {
Self::try_validate_wasm_bytes(py, canary)
}
}
const fn canary_bytes(self) -> &'static [u8] {
match self {
Self::BulkMemory => include_bytes!("bulk-memory.wasm"),
Self::Exceptions => include_bytes!("exceptions.wasm"),
Self::ExtendedConst => include_bytes!("extended-const.wasm"),
Self::FunctionReferences => include_bytes!("function-references.wasm"),
Self::GC => include_bytes!("gc.wasm"),
Self::Memory64 => include_bytes!("memory64.wasm"),
Self::MultiMemory => include_bytes!("multi-memory.wasm"),
Self::MultiValue => include_bytes!("multi-value.wasm"),
Self::MutableGlobal => include_bytes!("mutable-global.wasm"),
Self::ReferenceTypes => include_bytes!("reference-types.wasm"),
Self::RelaxedSimd => include_bytes!("relaxed-simd.wasm"),
Self::SaturatingFloatToInt => include_bytes!("saturating-float-to-int.wasm"),
Self::SignExtension => include_bytes!("sign-extension.wasm"),
Self::Simd => include_bytes!("simd.wasm"),
Self::TailCall => include_bytes!("tail-call.wasm"),
Self::Threads => include_bytes!("threads.wasm"),
}
}
fn requires_features(bytes: &[u8], features: wasmparser::WasmFeatures) -> bool {
wasmparser::Validator::new_with_features(!features)
.validate_all(bytes)
.is_err()
}
fn try_validate_wasm_bytes(py: Python, bytes: &[u8]) -> Result<bool, PyErr> {
let buffer = js_uint8_array_new(py)?.call1((bytes,))?;
let valid = web_assembly_validate(py)?.call1((buffer,))?.extract()?;
Ok(valid)
}
fn try_create_wasm_module_from_bytes(py: Python, bytes: &[u8]) -> Result<bool, PyErr> {
let buffer = js_uint8_array_new(py)?.call1((bytes,))?;
let module = web_assembly_module_new(py)?.call1((buffer,));
Ok(module.is_ok())
}
}
impl fmt::Display for WasmFeatureExtension {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"{}",
match self {
Self::BulkMemory => "bulk-memory",
Self::Exceptions => "exceptions",
Self::ExtendedConst => "extended-const",
Self::FunctionReferences => "function-references",
Self::GC => "gc",
Self::Memory64 => "memory64",
Self::MultiMemory => "multi-memory",
Self::MultiValue => "multi-value",
Self::MutableGlobal => "mutable-global",
Self::ReferenceTypes => "reference-types",
Self::RelaxedSimd => "relaxed-simd",
Self::SaturatingFloatToInt => "saturating-float-to-int",
Self::SignExtension => "sign-extension",
Self::Simd => "simd",
Self::TailCall => "tail-call",
Self::Threads => "threads",
}
)
}
}
fn web_assembly_validate(py: Python<'_>) -> Result<&Bound<'_, PyAny>, PyErr> {
static WEB_ASSEMBLY_VALIDATE: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
WEB_ASSEMBLY_VALIDATE.import(py, "js.WebAssembly", "validate")
}
fn web_assembly_module_new(py: Python<'_>) -> Result<&Bound<'_, PyAny>, PyErr> {
static WEB_ASSEMBLY_MODULE: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
WEB_ASSEMBLY_MODULE.import(py, "js.WebAssembly.Module", "new")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn required_features() {
for feature in FlagSet::<WasmFeatureExtension>::full() {
let required = WasmFeatureExtension::required(feature.canary_bytes());
let check = match feature {
WasmFeatureExtension::FunctionReferences => {
FlagSet::from(feature)
| WasmFeatureExtension::ReferenceTypes
| WasmFeatureExtension::BulkMemory
},
WasmFeatureExtension::RelaxedSimd => {
FlagSet::from(feature) | WasmFeatureExtension::Simd
},
_ => FlagSet::from(feature),
};
assert_eq!(
required, check,
"{feature} should only require {check:?}, but needs {required:?}"
);
}
}
}