#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum WasmImportError {
Empty,
InvalidName,
UnknownKind,
}
impl fmt::Display for WasmImportError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("WebAssembly import value cannot be empty"),
Self::InvalidName => formatter.write_str("invalid WebAssembly import name"),
Self::UnknownKind => formatter.write_str("unknown WebAssembly import kind"),
}
}
}
impl Error for WasmImportError {}
fn validate_import_text(value: &str) -> Result<&str, WasmImportError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(WasmImportError::Empty);
}
if trimmed.chars().any(|character| {
character.is_control()
|| character.is_whitespace()
|| !(character.is_ascii_alphanumeric()
|| matches!(character, '_' | '-' | '.' | '/' | ':' | '$'))
}) {
return Err(WasmImportError::InvalidName);
}
Ok(trimmed)
}
macro_rules! import_text_newtype {
($name:ident) => {
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(String);
impl $name {
pub fn new(value: impl AsRef<str>) -> Result<Self, WasmImportError> {
validate_import_text(value.as_ref()).map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for $name {
type Err = WasmImportError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for $name {
type Error = WasmImportError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
};
}
import_text_newtype!(ImportModuleName);
import_text_newtype!(ImportName);
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ImportKind {
#[default]
Function,
Memory,
Table,
Global,
}
impl ImportKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Function => "function",
Self::Memory => "memory",
Self::Table => "table",
Self::Global => "global",
}
}
}
impl fmt::Display for ImportKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for ImportKind {
type Err = WasmImportError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(WasmImportError::Empty);
}
match trimmed.to_ascii_lowercase().as_str() {
"function" | "func" => Ok(Self::Function),
"memory" | "mem" => Ok(Self::Memory),
"table" => Ok(Self::Table),
"global" => Ok(Self::Global),
_ => Err(WasmImportError::UnknownKind),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ImportedFunction {
module: ImportModuleName,
name: ImportName,
type_index: Option<u32>,
}
impl ImportedFunction {
#[must_use]
pub const fn new(module: ImportModuleName, name: ImportName) -> Self {
Self {
module,
name,
type_index: None,
}
}
#[must_use]
pub const fn with_type_index(mut self, type_index: u32) -> Self {
self.type_index = Some(type_index);
self
}
#[must_use]
pub const fn module(&self) -> &ImportModuleName {
&self.module
}
#[must_use]
pub const fn name(&self) -> &ImportName {
&self.name
}
#[must_use]
pub const fn type_index(&self) -> Option<u32> {
self.type_index
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ImportedMemory {
module: ImportModuleName,
name: ImportName,
}
impl ImportedMemory {
#[must_use]
pub const fn new(module: ImportModuleName, name: ImportName) -> Self {
Self { module, name }
}
#[must_use]
pub const fn module(&self) -> &ImportModuleName {
&self.module
}
#[must_use]
pub const fn name(&self) -> &ImportName {
&self.name
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ImportedTable {
module: ImportModuleName,
name: ImportName,
}
impl ImportedTable {
#[must_use]
pub const fn new(module: ImportModuleName, name: ImportName) -> Self {
Self { module, name }
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ImportedGlobal {
module: ImportModuleName,
name: ImportName,
mutable: bool,
}
impl ImportedGlobal {
#[must_use]
pub const fn new(module: ImportModuleName, name: ImportName, mutable: bool) -> Self {
Self {
module,
name,
mutable,
}
}
#[must_use]
pub const fn is_mutable(&self) -> bool {
self.mutable
}
}
#[cfg(test)]
mod tests {
use super::{ImportKind, ImportModuleName, ImportName, ImportedFunction, WasmImportError};
#[test]
fn validates_import_names() {
let module = ImportModuleName::new("wasi:cli").expect("valid module");
let name = ImportName::new("run").expect("valid import name");
assert_eq!(module.as_str(), "wasi:cli");
assert_eq!(name.to_string(), "run");
assert_eq!(
ImportName::new("bad name"),
Err(WasmImportError::InvalidName)
);
}
#[test]
fn parses_import_kinds_and_metadata() {
let kind = "func".parse::<ImportKind>().expect("known import kind");
let function = ImportedFunction::new(
ImportModuleName::new("env").expect("valid module"),
ImportName::new("call").expect("valid import"),
)
.with_type_index(2);
assert_eq!(kind, ImportKind::Function);
assert_eq!(kind.to_string(), "function");
assert_eq!(function.type_index(), Some(2));
assert_eq!(function.module().as_str(), "env");
}
}