use std::{
fmt::{self, Display, Formatter},
path::Path,
str::FromStr,
};
use anyhow::{Context, Error};
use heck::{ToPascalCase, ToSnakeCase};
#[derive(Debug, Clone)]
pub struct Package {
metadata: Metadata,
libraries: Vec<Library>,
commands: Vec<Command>,
}
impl Package {
pub fn new(metadata: Metadata, libraries: Vec<Library>, commands: Vec<Command>) -> Self {
assert_unique_names("library", libraries.iter().map(|lib| lib.interface_name()));
assert_unique_names("command", commands.iter().map(|cmd| cmd.name.as_str()));
Package {
metadata,
libraries,
commands,
}
}
pub fn from_webc(bytes: &[u8]) -> Result<Self, Error> {
crate::pirita::load_webc_binary(bytes)
}
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
pub fn libraries(&self) -> &[Library] {
&self.libraries
}
pub fn requires_wasi(&self) -> bool {
!self.commands.is_empty() || self.libraries.iter().any(|lib| lib.requires_wasi())
}
pub fn commands(&self) -> &[Command] {
&self.commands
}
}
fn assert_unique_names<'a>(kind: &str, names: impl IntoIterator<Item = &'a str>) {
let mut already_seen: Vec<&str> = Vec::new();
for name in names {
match already_seen.binary_search(&name) {
Ok(_) => panic!("Duplicate {kind} name: {name}"),
Err(index) => already_seen.insert(index, name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageName {
namespace: Namespace,
name: String,
}
impl PackageName {
pub fn parse(raw: &str) -> Result<Self, Error> {
raw.parse()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn namespace(&self) -> &Namespace {
&self.namespace
}
pub fn javascript_package(&self) -> String {
let PackageName { namespace, name } = self;
match namespace.as_str() {
Some(ns) => format!("@{ns}/{name}").to_lowercase(),
None => name.to_string().to_lowercase(),
}
}
pub fn python_name(&self) -> String {
self.name.to_snake_case()
}
}
impl FromStr for PackageName {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.contains('/') {
let name = parse_identifier(s)
.with_context(|| format!("\"{s}\" is not a valid package name"))?;
return Ok(PackageName {
namespace: Namespace::None,
name,
});
}
let (namespace, name) = s.split_once('/').context(
"All packages must have a namespace (i.e. the \"wasmer\" in \"wasmer/wasmer-pack\")",
)?;
let namespace = if namespace == "_" {
Namespace::Underscore
} else {
let ns = parse_identifier(namespace)
.with_context(|| format!("\"{namespace}\" is not a valid namespace"))?;
Namespace::Some(ns)
};
let name = parse_identifier(name)
.with_context(|| format!("\"{name}\" is not a valid package name"))?;
Ok(PackageName { namespace, name })
}
}
impl Display for PackageName {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let PackageName { namespace, name } = self;
if let Some(ns) = namespace.as_str() {
write!(f, "{ns}/")?;
}
write!(f, "{name}")?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Namespace {
Some(String),
Underscore,
None,
}
impl Namespace {
pub fn as_str(&self) -> Option<&str> {
match self {
Namespace::Some(s) => Some(s),
Namespace::Underscore | Namespace::None => None,
}
}
}
fn parse_identifier(s: &str) -> Result<String, Error> {
anyhow::ensure!(!s.is_empty(), "Identifiers can't be empty");
anyhow::ensure!(
s.starts_with(|c: char| c.is_ascii_alphabetic()),
"Identifiers must start with an ascii letter",
);
anyhow::ensure!(
s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_')),
"Identifiers can only contain '-', '_', ascii numbers, and letters"
);
Ok(s.to_string())
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Metadata {
pub package_name: PackageName,
pub version: String,
pub description: Option<String>,
}
impl Metadata {
pub fn new(package_name: PackageName, version: impl Into<String>) -> Self {
Metadata {
package_name,
version: version.into(),
description: None,
}
}
pub fn with_description(self, description: impl Into<String>) -> Self {
Metadata {
description: Some(description.into()),
..self
}
}
}
#[derive(Debug, Clone)]
pub struct Library {
pub module: Module,
pub exports: Interface,
pub imports: Vec<Interface>,
}
impl Library {
pub fn interface_name(&self) -> &str {
self.exports.name()
}
pub fn class_name(&self) -> String {
self.interface_name().to_pascal_case()
}
pub fn module_filename(&self) -> &str {
Path::new(&self.module.name)
.file_name()
.expect("We assume module names are non-empty")
.to_str()
.expect("The original path came from a Rust string")
}
pub fn requires_wasi(&self) -> bool {
matches!(self.module.abi, Abi::Wasi)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Module {
pub name: String,
pub abi: Abi,
pub wasm: Vec<u8>,
}
impl Module {
pub fn from_path(path: impl AsRef<Path>, abi: Abi) -> Result<Self, Error> {
let path = path.as_ref();
let name = path
.file_name()
.context("Empty filename")?
.to_string_lossy()
.into_owned();
let wasm = std::fs::read(path)
.with_context(|| format!("Unable to read \"{}\"", path.display()))?;
Ok(Module { name, abi, wasm })
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Abi {
None,
Wasi,
}
impl FromStr for Abi {
type Err = Error;
fn from_str(s: &str) -> Result<Abi, Error> {
match s {
"none" => Ok(Abi::None),
"wasi" => Ok(Abi::Wasi),
_ => Err(Error::msg("Expected either \"none\" or \"wasi\"")),
}
}
}
#[derive(Debug, Clone)]
pub struct Interface(pub(crate) wai_parser::Interface);
impl Interface {
pub fn from_wit(name: &str, src: &str) -> Result<Self, Error> {
let wit =
wai_parser::Interface::parse(name, src).context("Unable to parse the WIT file")?;
Ok(Interface(wit))
}
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
let path = path.as_ref();
let wit = wai_parser::Interface::parse_file(path)
.with_context(|| format!("Unable to parse \"{}\"", path.display()))?;
Ok(Interface(wit))
}
pub fn name(&self) -> &str {
&self.0.name
}
}
#[derive(Debug, Clone)]
pub struct Command {
pub name: String,
pub wasm: Vec<u8>,
}
impl Command {
pub fn new(name: impl Into<String>, wasm: impl Into<Vec<u8>>) -> Self {
Command {
name: name.into(),
wasm: wasm.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_package_names() {
let inputs = vec![
("package", true),
("namespace/package_name", true),
("_/package_name", true),
("name-space/package-name", true),
("n9/p21", true),
("wasmer/package", true),
(
"abcdefghijklmopqrstuvwxyz_ABCDEFGHIJKLMOPQRSTUVWXYZ0123456789/abcdefghijklmopqrstuvwxyz-ABCDEFGHIJKLMOPQRSTUVWXYZ0123456789",
true,
),
("_wasmer/package", false),
("wasmer/_package", false),
("लाज/तोब", false),
("-wasmer/package", false),
("wasmer/-package", false),
("wasmer/-", false),
("wasmer/597d361e-f431-4960-9b2a-7e78ec0dbfeb", false),
("name space/name", false),
("@wasmer/package-name", false),
("", false),
];
for (original, is_okay) in inputs {
let got = PackageName::parse(original);
assert_eq!(got.is_ok(), is_okay, "{original}");
}
}
}