#![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 WasmTextError {
Empty,
Invalid,
UnknownMarker,
}
impl fmt::Display for WasmTextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("WebAssembly text value cannot be empty"),
Self::Invalid => formatter.write_str("invalid WebAssembly text value"),
Self::UnknownMarker => formatter.write_str("unknown WebAssembly S-expression marker"),
}
}
}
impl Error for WasmTextError {}
fn validate_text_name(value: &str) -> Result<&str, WasmTextError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(WasmTextError::Empty);
}
if trimmed
.chars()
.any(|character| character.is_control() || character.is_whitespace())
{
return Err(WasmTextError::Invalid);
}
Ok(trimmed)
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WatIdentifier(String);
impl WatIdentifier {
pub fn new(value: impl AsRef<str>) -> Result<Self, WasmTextError> {
let trimmed = validate_text_name(value.as_ref())?;
if !trimmed.starts_with('$') || trimmed.len() == 1 {
return Err(WasmTextError::Invalid);
}
Ok(Self(trimmed.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 WatIdentifier {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for WatIdentifier {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for WatIdentifier {
type Err = WasmTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for WatIdentifier {
type Error = WasmTextError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TextModuleName(String);
impl TextModuleName {
pub fn new(value: impl AsRef<str>) -> Result<Self, WasmTextError> {
validate_text_name(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 TextModuleName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for TextModuleName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for TextModuleName {
type Err = WasmTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for TextModuleName {
type Error = WasmTextError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SExpressionMarker {
#[default]
Module,
Func,
Import,
Export,
Memory,
Table,
Global,
Type,
Component,
}
impl SExpressionMarker {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Module => "module",
Self::Func => "func",
Self::Import => "import",
Self::Export => "export",
Self::Memory => "memory",
Self::Table => "table",
Self::Global => "global",
Self::Type => "type",
Self::Component => "component",
}
}
}
impl fmt::Display for SExpressionMarker {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for SExpressionMarker {
type Err = WasmTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim().trim_start_matches('(');
if trimmed.is_empty() {
return Err(WasmTextError::Empty);
}
match trimmed.to_ascii_lowercase().as_str() {
"module" => Ok(Self::Module),
"func" => Ok(Self::Func),
"import" => Ok(Self::Import),
"export" => Ok(Self::Export),
"memory" => Ok(Self::Memory),
"table" => Ok(Self::Table),
"global" => Ok(Self::Global),
"type" => Ok(Self::Type),
"component" => Ok(Self::Component),
_ => Err(WasmTextError::UnknownMarker),
}
}
}
#[must_use]
pub fn looks_like_wat_module(input: &str) -> bool {
let trimmed = input.trim_start();
trimmed.starts_with("(module") && has_balanced_parentheses_basic(trimmed)
}
#[must_use]
pub fn has_balanced_parentheses_basic(input: &str) -> bool {
let mut depth = 0_u32;
for character in input.chars() {
match character {
'(' => depth = depth.saturating_add(1),
')' => {
if depth == 0 {
return false;
}
depth -= 1;
},
_ => {},
}
}
depth == 0
}
#[cfg(test)]
mod tests {
use super::{
SExpressionMarker, TextModuleName, WasmTextError, WatIdentifier,
has_balanced_parentheses_basic, looks_like_wat_module,
};
#[test]
fn validates_text_names() {
let identifier = WatIdentifier::new("$run").expect("valid identifier");
let module = TextModuleName::new("example").expect("valid module name");
assert_eq!(identifier.as_str(), "$run");
assert_eq!(module.to_string(), "example");
assert_eq!(WatIdentifier::new("run"), Err(WasmTextError::Invalid));
}
#[test]
fn parses_markers_and_checks_text_shape() {
assert_eq!(
"(module".parse::<SExpressionMarker>(),
Ok(SExpressionMarker::Module)
);
assert!(looks_like_wat_module("(module (func))"));
assert!(has_balanced_parentheses_basic("(func (result i32))"));
assert!(!has_balanced_parentheses_basic("(func"));
}
}