use std::collections::HashSet;
use wasmparser::{Encoding, Parser, Payload};
use super::{ChestError, invalid};
use crate::extensions::CAPABILITIES;
const SECTION: &str = "yapi:capabilities";
const MARKER: &str = "yapi-extension-api";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Use {
pub capability: &'static str,
pub source: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Capabilities {
pub metadata: bool,
pub used: Vec<Use>,
pub unsupported: Vec<String>,
pub unknown: Vec<String>,
}
impl Capabilities {
fn add(&mut self, capability: &'static str, source: String) {
if !self.used.iter().any(|used| used.capability == capability) {
self.used.push(Use { capability, source });
}
}
}
fn feature(line: &str) -> Option<&'static str> {
CAPABILITIES.into_iter().find(|name| *name == line)
}
fn import(name: &str) -> Option<&'static str> {
name.starts_with("wasi:filesystem/").then_some("fs")
}
fn unsupported(name: &str) -> bool {
name.starts_with("wasi:sockets/") || name.starts_with("wasi:http/")
}
pub fn capabilities(bytes: &[u8]) -> Result<Capabilities, ChestError> {
let mut found = Capabilities::default();
let mut features = Vec::new();
let mut imports = Vec::new();
let mut unknown = HashSet::new();
let mut depth = 0usize;
for payload in Parser::new(0).parse_all(bytes) {
let payload =
payload.map_err(|err| invalid(format!("not a WebAssembly component: {err}")))?;
match payload {
Payload::Version { encoding, .. } => {
if depth == 0 && encoding != Encoding::Component {
return Err(invalid(
"a WebAssembly module, not a component: build it for wasm32-wasip2",
));
}
depth += 1;
}
Payload::End(_) => depth = depth.saturating_sub(1),
Payload::CustomSection(section) if section.name() == SECTION => {
found.metadata = true;
let text = String::from_utf8_lossy(section.data());
for line in text
.lines()
.filter(|line| !line.is_empty() && *line != MARKER)
{
match feature(line) {
Some(capability) => features.push(capability),
None if unknown.insert(line.to_owned()) => {
found.unknown.push(line.to_owned());
}
None => {}
}
}
}
Payload::ComponentImportSection(reader) if depth == 1 => {
for entry in reader {
let entry = entry
.map_err(|err| invalid(format!("not a WebAssembly component: {err}")))?;
imports.push(entry.name.name.to_owned());
}
}
_ => {}
}
}
if features.contains(&"all") {
features = vec!["all"];
}
for capability in features {
found.add(capability, format!("SDK feature `{capability}`"));
}
for name in imports {
if let Some(capability) = import(&name) {
found.add(capability, format!("WASI import `{name}`"));
} else if unsupported(&name) {
found.unsupported.push(name);
}
}
Ok(found)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(name: &str) -> Vec<u8> {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../yapi-ext/tests/fixtures");
std::fs::read(format!("{dir}/{name}.wasm")).unwrap()
}
fn section(id: u8, contents: &[u8]) -> Vec<u8> {
let (mut bytes, mut size) = (vec![id], contents.len());
while size >= 0x80 {
bytes.push((size as u8) | 0x80);
size >>= 7;
}
bytes.push(size as u8);
[&bytes[..], contents].concat()
}
fn name(text: &str) -> Vec<u8> {
[&[text.len() as u8], text.as_bytes()].concat()
}
fn importing(interfaces: &[&str]) -> Vec<u8> {
let mut imports = vec![interfaces.len() as u8];
for interface in interfaces {
imports.extend([&[0x00][..], &name(interface), &[0x05, 0x00]].concat());
}
let types = section(0x07, &[0x01, 0x42, 0x00]);
[&EMPTY[..], &types, §ion(0x0a, &imports)].concat()
}
const EMPTY: [u8; 8] = *b"\0asm\x0d\x00\x01\x00";
#[test]
fn sdk_features_match() {
let manifest = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../guest/yapi-extension-api/Cargo.toml"
);
let manifest: toml_edit::DocumentMut =
std::fs::read_to_string(manifest).unwrap().parse().unwrap();
let features = manifest["features"].as_table().unwrap();
let mut names: Vec<&str> = features
.iter()
.map(|(name, _)| name)
.filter(|name| !["default", "widgets"].contains(name))
.collect();
let mut expected = CAPABILITIES.to_vec();
names.sort_unstable();
expected.sort_unstable();
assert_eq!(names, expected);
let all: Vec<&str> = features["all"]
.as_array()
.unwrap()
.iter()
.filter_map(|value| value.as_str())
.collect();
assert_eq!(all, CAPABILITIES[..CAPABILITIES.len() - 1]);
}
fn used(found: &Capabilities) -> Vec<(&str, &str)> {
let pairs = found.used.iter();
pairs
.map(|used| (used.capability, used.source.as_str()))
.collect()
}
#[test]
fn capabilities_from_component() {
let hello = capabilities(&fixture("hello")).unwrap();
assert!(hello.metadata && hello.unknown.is_empty());
assert_eq!(used(&hello), [("session", "SDK feature `session`")]);
let probe = capabilities(&fixture("capability-probe")).unwrap();
assert_eq!(used(&probe), [("all", "SDK feature `all`")]);
let todo = capabilities(&fixture("todo")).unwrap();
assert!(todo.metadata);
assert_eq!(used(&todo), []);
let wasi = importing(&[
"wasi:cli/environment@0.2.6",
"wasi:filesystem/types@0.2.6",
"wasi:http/outgoing-handler@0.2.6",
]);
let wasi = capabilities(&wasi).unwrap();
assert!(!wasi.metadata);
assert_eq!(
used(&wasi),
[("fs", "WASI import `wasi:filesystem/types@0.2.6`")]
);
assert_eq!(wasi.unsupported, ["wasi:http/outgoing-handler@0.2.6"]);
let lines = b"yapi-extension-api\nnet\nexec:git\nfs\nexec:git\n";
let custom = section(0x00, &[&name(SECTION)[..], lines].concat());
let declared =
capabilities(&[&importing(&["wasi:sockets/tcp@0.2.6"])[..], &custom].concat());
let declared = declared.unwrap();
assert!(declared.metadata);
assert_eq!(
used(&declared),
[("net", "SDK feature `net`"), ("fs", "SDK feature `fs`")]
);
assert_eq!(declared.unsupported, ["wasi:sockets/tcp@0.2.6"]);
assert_eq!(declared.unknown, ["exec:git"]);
let lines: String = (0..100_000).map(|n| format!("x{n}\nx{n}\n")).collect();
let custom = section(0x00, &[&name(SECTION)[..], lines.as_bytes()].concat());
let many = capabilities(&[&EMPTY[..], &custom].concat()).unwrap();
assert_eq!(many.unknown.len(), 100_000);
assert_eq!(many.unknown[..2], ["x0", "x1"]);
assert_eq!(capabilities(&EMPTY).unwrap(), Capabilities::default());
let module = capabilities(b"\0asm\x01\x00\x00\x00").unwrap_err();
assert!(module.to_string().contains("not a component"), "{module}");
assert!(capabilities(b"plain text").is_err());
}
}