1use std::collections::HashSet;
6
7use wasmparser::{Encoding, Parser, Payload};
8
9use super::{ChestError, invalid};
10use crate::extensions::CAPABILITIES;
11
12const SECTION: &str = "yapi:capabilities";
14const MARKER: &str = "yapi-extension-api";
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct Use {
20 pub capability: &'static str,
22 pub source: String,
25}
26
27#[derive(Clone, Debug, Default, PartialEq, Eq)]
29pub struct Capabilities {
30 pub metadata: bool,
34 pub used: Vec<Use>,
38 pub unsupported: Vec<String>,
42 pub unknown: Vec<String>,
45}
46
47impl Capabilities {
48 fn add(&mut self, capability: &'static str, source: String) {
49 if !self.used.iter().any(|used| used.capability == capability) {
50 self.used.push(Use { capability, source });
51 }
52 }
53}
54
55fn feature(line: &str) -> Option<&'static str> {
57 CAPABILITIES.into_iter().find(|name| *name == line)
58}
59
60fn import(name: &str) -> Option<&'static str> {
64 name.starts_with("wasi:filesystem/").then_some("fs")
65}
66
67fn unsupported(name: &str) -> bool {
70 name.starts_with("wasi:sockets/") || name.starts_with("wasi:http/")
71}
72
73pub fn capabilities(bytes: &[u8]) -> Result<Capabilities, ChestError> {
78 let mut found = Capabilities::default();
79 let mut features = Vec::new();
80 let mut imports = Vec::new();
81 let mut unknown = HashSet::new();
82 let mut depth = 0usize;
83 for payload in Parser::new(0).parse_all(bytes) {
84 let payload =
85 payload.map_err(|err| invalid(format!("not a WebAssembly component: {err}")))?;
86 match payload {
87 Payload::Version { encoding, .. } => {
88 if depth == 0 && encoding != Encoding::Component {
89 return Err(invalid(
90 "a WebAssembly module, not a component: build it for wasm32-wasip2",
91 ));
92 }
93 depth += 1;
94 }
95 Payload::End(_) => depth = depth.saturating_sub(1),
96 Payload::CustomSection(section) if section.name() == SECTION => {
97 found.metadata = true;
98 let text = String::from_utf8_lossy(section.data());
99 for line in text
100 .lines()
101 .filter(|line| !line.is_empty() && *line != MARKER)
102 {
103 match feature(line) {
104 Some(capability) => features.push(capability),
105 None if unknown.insert(line.to_owned()) => {
106 found.unknown.push(line.to_owned());
107 }
108 None => {}
109 }
110 }
111 }
112 Payload::ComponentImportSection(reader) if depth == 1 => {
114 for entry in reader {
115 let entry = entry
116 .map_err(|err| invalid(format!("not a WebAssembly component: {err}")))?;
117 imports.push(entry.name.name.to_owned());
118 }
119 }
120 _ => {}
121 }
122 }
123 if features.contains(&"all") {
125 features = vec!["all"];
126 }
127 for capability in features {
128 found.add(capability, format!("SDK feature `{capability}`"));
129 }
130 for name in imports {
131 if let Some(capability) = import(&name) {
132 found.add(capability, format!("WASI import `{name}`"));
133 } else if unsupported(&name) {
134 found.unsupported.push(name);
135 }
136 }
137 Ok(found)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn fixture(name: &str) -> Vec<u8> {
145 let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../yapi-ext/tests/fixtures");
146 std::fs::read(format!("{dir}/{name}.wasm")).unwrap()
147 }
148
149 fn section(id: u8, contents: &[u8]) -> Vec<u8> {
151 let (mut bytes, mut size) = (vec![id], contents.len());
152 while size >= 0x80 {
153 bytes.push((size as u8) | 0x80);
154 size >>= 7;
155 }
156 bytes.push(size as u8);
157 [&bytes[..], contents].concat()
158 }
159
160 fn name(text: &str) -> Vec<u8> {
161 [&[text.len() as u8], text.as_bytes()].concat()
162 }
163
164 fn importing(interfaces: &[&str]) -> Vec<u8> {
167 let mut imports = vec![interfaces.len() as u8];
168 for interface in interfaces {
169 imports.extend([&[0x00][..], &name(interface), &[0x05, 0x00]].concat());
170 }
171 let types = section(0x07, &[0x01, 0x42, 0x00]);
172 [&EMPTY[..], &types, §ion(0x0a, &imports)].concat()
173 }
174
175 const EMPTY: [u8; 8] = *b"\0asm\x0d\x00\x01\x00";
176
177 #[test]
181 fn sdk_features_match() {
182 let manifest = concat!(
183 env!("CARGO_MANIFEST_DIR"),
184 "/../../guest/yapi-extension-api/Cargo.toml"
185 );
186 let manifest: toml_edit::DocumentMut =
187 std::fs::read_to_string(manifest).unwrap().parse().unwrap();
188 let features = manifest["features"].as_table().unwrap();
189 let mut names: Vec<&str> = features
190 .iter()
191 .map(|(name, _)| name)
192 .filter(|name| !["default", "widgets"].contains(name))
193 .collect();
194 let mut expected = CAPABILITIES.to_vec();
195 names.sort_unstable();
196 expected.sort_unstable();
197 assert_eq!(names, expected);
198 let all: Vec<&str> = features["all"]
199 .as_array()
200 .unwrap()
201 .iter()
202 .filter_map(|value| value.as_str())
203 .collect();
204 assert_eq!(all, CAPABILITIES[..CAPABILITIES.len() - 1]);
205 }
206
207 fn used(found: &Capabilities) -> Vec<(&str, &str)> {
208 let pairs = found.used.iter();
209 pairs
210 .map(|used| (used.capability, used.source.as_str()))
211 .collect()
212 }
213
214 #[test]
215 fn capabilities_from_component() {
216 let hello = capabilities(&fixture("hello")).unwrap();
217 assert!(hello.metadata && hello.unknown.is_empty());
218 assert_eq!(used(&hello), [("session", "SDK feature `session`")]);
219 let probe = capabilities(&fixture("capability-probe")).unwrap();
220 assert_eq!(used(&probe), [("all", "SDK feature `all`")]);
221 let todo = capabilities(&fixture("todo")).unwrap();
222 assert!(todo.metadata);
223 assert_eq!(used(&todo), []);
224
225 let wasi = importing(&[
226 "wasi:cli/environment@0.2.6",
227 "wasi:filesystem/types@0.2.6",
228 "wasi:http/outgoing-handler@0.2.6",
229 ]);
230 let wasi = capabilities(&wasi).unwrap();
231 assert!(!wasi.metadata);
232 assert_eq!(
233 used(&wasi),
234 [("fs", "WASI import `wasi:filesystem/types@0.2.6`")]
235 );
236 assert_eq!(wasi.unsupported, ["wasi:http/outgoing-handler@0.2.6"]);
237 let lines = b"yapi-extension-api\nnet\nexec:git\nfs\nexec:git\n";
238 let custom = section(0x00, &[&name(SECTION)[..], lines].concat());
239 let declared =
240 capabilities(&[&importing(&["wasi:sockets/tcp@0.2.6"])[..], &custom].concat());
241 let declared = declared.unwrap();
242 assert!(declared.metadata);
243 assert_eq!(
244 used(&declared),
245 [("net", "SDK feature `net`"), ("fs", "SDK feature `fs`")]
246 );
247 assert_eq!(declared.unsupported, ["wasi:sockets/tcp@0.2.6"]);
248 assert_eq!(declared.unknown, ["exec:git"]);
249 let lines: String = (0..100_000).map(|n| format!("x{n}\nx{n}\n")).collect();
252 let custom = section(0x00, &[&name(SECTION)[..], lines.as_bytes()].concat());
253 let many = capabilities(&[&EMPTY[..], &custom].concat()).unwrap();
254 assert_eq!(many.unknown.len(), 100_000);
255 assert_eq!(many.unknown[..2], ["x0", "x1"]);
256
257 assert_eq!(capabilities(&EMPTY).unwrap(), Capabilities::default());
258 let module = capabilities(b"\0asm\x01\x00\x00\x00").unwrap_err();
259 assert!(module.to_string().contains("not a component"), "{module}");
260 assert!(capabilities(b"plain text").is_err());
261 }
262}