1use anyhow::{Context, Result};
2use colored::*;
3use std::path::{Path, PathBuf};
4use wasmparser::{Chunk, Encoding, Parser as WasmParser, Payload};
5use wit_component::ComponentEncoder;
6
7use wasi_preview1_component_adapter_provider::{
8 WASI_SNAPSHOT_PREVIEW1_ADAPTER_NAME, WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
9};
10
11pub fn process_wasm(input_wasm_path: &Path, debug: bool, force: bool) -> Result<Vec<u8>> {
26 let module_bytes = std::fs::read(input_wasm_path).with_context(|| {
27 format!(
28 "Failed to read raw wasm from: {}",
29 input_wasm_path.display()
30 )
31 })?;
32
33 if is_component(&module_bytes)
35 .with_context(|| "Failed to parse wasm header for component detection")?
36 {
37 println!(
38 "{} Input is already a WebAssembly component; skipping adapter injection and encoding.",
39 "[INFO]".cyan()
40 );
41
42 validate_contract_with_force(&module_bytes, debug, force)?;
43
44 return Ok(module_bytes);
45 }
46
47 let cleaned_module = strip_exports_removed_bindgen_section(&module_bytes)?;
50
51 validate_user_imports(&cleaned_module, debug);
54
55 let adapter_bytes = WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
58 if debug {
59 println!("{} Injecting WASI Reactor Adapter", "[DEBUG]".dimmed());
60 }
61
62 let component_bytes = ComponentEncoder::default()
64 .module(&cleaned_module)
65 .context("Failed to encode module into component")?
66 .adapter(WASI_SNAPSHOT_PREVIEW1_ADAPTER_NAME, adapter_bytes)
67 .context("Failed to inject WASI preview1 adapter")?
68 .validate(true)
69 .encode()
70 .map_err(|e| {
71 anyhow::anyhow!(
72 "Component encoding error: {e}\nEnsure wit-bindgen version matches adapter requirements."
73 )
74 })?;
75
76 validate_contract_with_force(&component_bytes, debug, force)?;
79
80 Ok(component_bytes)
81}
82
83pub fn write_vtx_file(
85 input_path: &Path,
86 component_bytes: &[u8],
87 metadata_json: &[u8],
88) -> Result<PathBuf> {
89 let out_path = input_path.with_extension("vtx");
90 let buf = vtx_format::encode_v2(component_bytes, metadata_json);
91
92 std::fs::write(&out_path, buf)
93 .with_context(|| format!("Failed to write vtx artifact: {}", out_path.display()))?;
94
95 Ok(out_path)
96}
97
98fn validate_user_imports(module_bytes: &[u8], debug: bool) {
106 let parser = WasmParser::new(0);
107
108 let trusted_namespaces = [
111 "wasi_snapshot_preview1", "wasi:", "vtx:", "vtx", "__wbindgen_", ];
117
118 for payload in parser.parse_all(module_bytes).flatten() {
119 if let Payload::ImportSection(reader) = payload {
120 for import in reader.into_iter().flatten() {
121 let module = import.module;
122 let field = import.name;
123
124 let is_trusted = trusted_namespaces.iter().any(|ns| module.starts_with(ns));
126
127 if !is_trusted {
128 println!(
129 "{} Unknown Import Detected: '{}::{}'\n \
130 {} This interface is not part of the standard VTX Kernel or WASI spec.\n \
131 If the kernel does not provide this host function, the plugin will crash at runtime.",
132 "[WARN]".yellow(),
133 module,
134 field,
135 "->".yellow()
136 );
137 } else if debug {
138 println!(
139 "{} Trusted import: {}::{}",
140 "[DEBUG]".dimmed(),
141 module,
142 field
143 );
144 }
145 }
146 }
147 }
148}
149
150fn validate_contract_with_force(component_bytes: &[u8], debug: bool, force: bool) -> Result<()> {
151 if let Err(e) = validate_contract(component_bytes, debug) {
152 if force {
153 println!(
154 "{} Contract validation failed but --force is enabled: {}",
155 "[WARN]".yellow(),
156 e
157 );
158 return Ok(());
159 }
160 return Err(e);
161 }
162
163 Ok(())
164}
165
166fn is_component(bytes: &[u8]) -> Result<bool> {
168 let parser = WasmParser::new(0);
169
170 for payload in parser.parse_all(bytes) {
171 let payload = payload?;
172 if let Payload::Version { encoding, .. } = payload {
173 return Ok(matches!(encoding, Encoding::Component));
174 }
175 }
176
177 Ok(false)
178}
179
180fn validate_contract(component_bytes: &[u8], debug: bool) -> Result<()> {
186 let parser = WasmParser::new(0);
187 let mut found_handle = false;
188 let mut found_manifest = false;
189 let mut found_capabilities = false;
190
191 for payload in parser.parse_all(component_bytes).flatten() {
193 if let Payload::ComponentExportSection(reader) = payload {
194 for export in reader {
195 let export = export?;
196 let name = export.name.0;
198
199 if debug {
200 println!("{} Found export: {}", "[DEBUG]".dimmed(), name);
201 }
202
203 match name {
206 "handle" | "vtx:api/plugin/handle" | "vtx:api/plugin#handle" => {
207 found_handle = true
208 }
209 "get-manifest"
210 | "vtx:api/plugin/get-manifest"
211 | "vtx:api/plugin#get-manifest" => found_manifest = true,
212 "get-capabilities"
213 | "vtx:api/plugin/get-capabilities"
214 | "vtx:api/plugin#get-capabilities" => found_capabilities = true,
215 _ => {}
216 }
217 }
218 }
219 }
220
221 if !found_handle {
222 anyhow::bail!("Contract Violation: Missing required export 'handle'.\nEnsure you have implemented the Plugin trait and used 'vtx_sdk::export_plugin!(...)' macro.");
223 }
224 if !found_manifest {
225 anyhow::bail!("Contract Violation: Missing required export 'get-manifest'.");
226 }
227 if !found_capabilities {
228 anyhow::bail!("Contract Violation: Missing required export 'get-capabilities'.");
229 }
230
231 if debug {
232 println!("{} Contract validation passed.", "[INFO]".cyan());
233 }
234
235 Ok(())
236}
237
238fn strip_exports_removed_bindgen_section(module: &[u8]) -> Result<Vec<u8>> {
240 let mut out = Vec::with_capacity(module.len());
241 let mut parser = WasmParser::new(0);
242 let mut offset = 0usize;
243
244 while offset < module.len() {
245 let chunk = parser.parse(&module[offset..], true)?;
246 let (consumed, payload) = match chunk {
247 Chunk::Parsed { consumed, payload } => (consumed, payload),
248 _ => break,
249 };
250
251 let raw = &module[offset..offset + consumed];
252 let mut keep = true;
253
254 if let Payload::CustomSection(cs) = &payload {
255 let name = cs.name();
256 if name.starts_with("component-type:wit-bindgen:")
257 && name.contains("with-all-of-its-exports-removed")
258 {
259 keep = false;
260 }
261 }
262
263 if keep {
264 out.extend_from_slice(raw);
265 }
266 offset += consumed;
267 }
268
269 Ok(out)
270}