wasmer_package/package/
manifest.rs

1use std::{
2    collections::BTreeMap,
3    path::{Path, PathBuf},
4};
5
6use ciborium::Value;
7use semver::VersionReq;
8use sha2::Digest;
9use shared_buffer::{MmapError, OwnedBuffer};
10use url::Url;
11#[allow(deprecated)]
12use wasmer_config::package::{CommandV1, CommandV2, Manifest as WasmerManifest, Package};
13use webc::{
14    indexmap::{self, IndexMap},
15    metadata::AtomSignature,
16    sanitize_path,
17};
18
19use crate::utils::features_to_wasm_annotations;
20
21use webc::metadata::{
22    Atom, Binding, Command, Manifest as WebcManifest, UrlOrManifest, WaiBindings, WitBindings,
23    annotations::{
24        Atom as AtomAnnotation, FileSystemMapping, FileSystemMappings, VolumeSpecificPath, Wapm,
25        Wasi,
26    },
27};
28
29use super::{FsVolume, Strictness};
30
31const METADATA_VOLUME: &str = FsVolume::METADATA;
32
33/// Errors that may occur when converting from a `wasmer_config::package::Manifest`
34/// into a WebC manifest.
35#[derive(Debug, thiserror::Error)]
36#[non_exhaustive]
37pub enum ManifestError {
38    /// A dependency specification had a syntax error.
39    #[error("The dependency, \"{_0}\", isn't in the \"namespace/name\" format")]
40    InvalidDependency(String),
41    /// Unable to serialize an annotation.
42    #[error("Unable to serialize the \"{key}\" annotation")]
43    SerializeCborAnnotation {
44        /// Which annotation was being serialized?
45        key: String,
46        /// The underlying error.
47        #[source]
48        error: ciborium::value::Error,
49    },
50    /// Specified an unknown atom kind.
51    #[error("Unknown atom kind, \"{_0}\"")]
52    UnknownAtomKind(String),
53    /// A module was specified more than once.
54    #[error("Duplicate module, \"{_0}\"")]
55    DuplicateModule(String),
56    /// Unable to read a module's `source`.
57    #[error("Unable to read the \"{module}\" module's file from \"{}\"", path.display())]
58    ReadAtomFile {
59        /// The name of the module.
60        module: String,
61        /// The path that was read.
62        path: PathBuf,
63        /// The underlying error.
64        #[source]
65        error: std::io::Error,
66    },
67    /// A command was specified more than once.
68    #[error("Duplicate command, \"{_0}\"")]
69    DuplicateCommand(String),
70    /// An unknown runner kind was specified.
71    #[error("Unknown runner kind, \"{_0}\"")]
72    UnknownRunnerKind(String),
73    /// An error occurred while merging user-defined annotations in with
74    /// automatically generated ones.
75    #[error("Unable to merge in user-defined \"{key}\" annotations for the \"{command}\" command")]
76    #[non_exhaustive]
77    MergeAnnotations {
78        /// The command annotations were being merged for.
79        command: String,
80        /// The annotation that was being merged.
81        key: String,
82    },
83    /// A command uses a non-existent module.
84    #[error("The \"{command}\" command uses a non-existent module, \"{module}\"")]
85    InvalidModuleReference {
86        /// The command name.
87        command: String,
88        /// The module name.
89        module: String,
90    },
91    /// A command references a module from an undeclared dependency.
92    #[error("The \"{command}\" command references the undeclared dependency \"{dependency}\"")]
93    UndeclaredCommandDependency {
94        /// The command name.
95        command: String,
96        /// The dependency name.
97        dependency: String,
98    },
99    /// Unable to deserialize custom annotations from the `wasmer.toml`
100    /// manifest.
101    #[error("Unable to deserialize custom annotations from the wasmer.toml manifest")]
102    WasmerTomlAnnotations {
103        /// The underlying error.
104        #[source]
105        error: Box<dyn std::error::Error + Send + Sync>,
106    },
107    /// The `wasmer.toml` file references a file outside of its base directory.
108    #[error("\"{}\" is outside of \"{}\"", path.display(), base_dir.display())]
109    OutsideBaseDirectory {
110        /// The file that was referenced.
111        path: PathBuf,
112        /// The base directory.
113        base_dir: PathBuf,
114    },
115    /// The manifest references a file that doesn't exist.
116    #[error("The \"{}\" doesn't exist (base dir: {})", path.display(), base_dir.display())]
117    MissingFile {
118        /// The file that was referenced.
119        path: PathBuf,
120        /// The base directory.
121        base_dir: PathBuf,
122    },
123    /// File based commands are not supported for in-memory package creation
124    #[error("File based commands are not supported for in-memory package creation")]
125    FileNotSupported,
126}
127
128/// take a `wasmer.toml` manifest and convert it to the `*.webc` equivalent.
129pub(crate) fn wasmer_manifest_to_webc(
130    manifest: &WasmerManifest,
131    base_dir: &Path,
132    strictness: Strictness,
133) -> Result<(WebcManifest, BTreeMap<String, OwnedBuffer>), ManifestError> {
134    let use_map = transform_dependencies(&manifest.dependencies)?;
135
136    // Note: We need to clone the [fs] table because the wasmer-toml crate has
137    // already upgraded to indexmap v2.0, but the webc crate needs to stay at
138    // 1.9.2 for backwards compatibility reasons.
139    let fs: IndexMap<String, PathBuf> = manifest.fs.clone().into_iter().collect();
140
141    let package =
142        transform_package_annotations(manifest.package.as_ref(), &fs, base_dir, strictness)?;
143    let (atoms, atom_files) = transform_atoms(manifest, base_dir)?;
144    let commands = transform_commands(manifest, base_dir)?;
145    let bindings = transform_bindings(manifest, base_dir)?;
146
147    let manifest = WebcManifest {
148        origin: None,
149        use_map,
150        package,
151        atoms,
152        commands,
153        bindings,
154        entrypoint: entrypoint(manifest),
155    };
156
157    Ok((manifest, atom_files))
158}
159
160/// take a `wasmer.toml` manifest and convert it to the `*.webc` equivalent.
161pub(crate) fn in_memory_wasmer_manifest_to_webc(
162    manifest: &WasmerManifest,
163    atoms: &BTreeMap<String, (Option<String>, OwnedBuffer)>,
164) -> Result<(WebcManifest, BTreeMap<String, OwnedBuffer>), ManifestError> {
165    let use_map = transform_dependencies(&manifest.dependencies)?;
166
167    // Note: We need to clone the [fs] table because the wasmer-toml crate has
168    // already upgraded to indexmap v2.0, but the webc crate needs to stay at
169    // 1.9.2 for backwards compatibility reasons.
170    let fs: IndexMap<String, PathBuf> = manifest.fs.clone().into_iter().collect();
171
172    let package = transform_in_memory_package_annotations(manifest.package.as_ref(), &fs)?;
173    let (atoms, atom_files) = transform_in_memory_atoms(atoms)?;
174    let commands = transform_in_memory_commands(manifest)?;
175    let bindings = transform_in_memory_bindings(manifest)?;
176
177    let manifest = WebcManifest {
178        origin: None,
179        use_map,
180        package,
181        atoms,
182        commands,
183        bindings,
184        entrypoint: entrypoint(manifest),
185    };
186
187    Ok((manifest, atom_files))
188}
189
190fn transform_package_annotations(
191    package: Option<&wasmer_config::package::Package>,
192    fs: &IndexMap<String, PathBuf>,
193    base_dir: &Path,
194    strictness: Strictness,
195) -> Result<IndexMap<String, Value>, ManifestError> {
196    transform_package_annotations_shared(package, fs, |package| {
197        transform_package_meta_to_annotations(package, base_dir, strictness)
198    })
199}
200
201fn transform_in_memory_package_annotations(
202    package: Option<&wasmer_config::package::Package>,
203    fs: &IndexMap<String, PathBuf>,
204) -> Result<IndexMap<String, Value>, ManifestError> {
205    transform_package_annotations_shared(package, fs, |package| {
206        transform_in_memory_package_meta_to_annotations(package)
207    })
208}
209
210fn transform_package_annotations_shared(
211    package: Option<&wasmer_config::package::Package>,
212    fs: &IndexMap<String, PathBuf>,
213    transform_package_meta_to_annotations: impl Fn(&Package) -> Result<Wapm, ManifestError>,
214) -> Result<IndexMap<String, Value>, ManifestError> {
215    let mut annotations = IndexMap::new();
216
217    if let Some(wasmer_package) = package {
218        let wapm = transform_package_meta_to_annotations(wasmer_package)?;
219        insert_annotation(&mut annotations, Wapm::KEY, wapm)?;
220    }
221
222    let fs = get_fs_table(fs);
223
224    if !fs.is_empty() {
225        insert_annotation(&mut annotations, FileSystemMappings::KEY, fs)?;
226    }
227
228    Ok(annotations)
229}
230
231fn transform_dependencies(
232    original_dependencies: &IndexMap<String, VersionReq>,
233) -> Result<IndexMap<String, UrlOrManifest>, ManifestError> {
234    let mut dependencies = IndexMap::new();
235
236    for (dep, version) in original_dependencies {
237        let (namespace, package_name) = extract_dependency_parts(dep)
238            .ok_or_else(|| ManifestError::InvalidDependency(dep.clone()))?;
239
240        // Note: the wasmer.toml format forces you to go through a registry for
241        // all dependencies. There's no way to specify a URL-based dependency.
242        let dependency_specifier =
243            UrlOrManifest::RegistryDependentUrl(format!("{namespace}/{package_name}@{version}"));
244
245        dependencies.insert(dep.clone(), dependency_specifier);
246    }
247
248    Ok(dependencies)
249}
250
251fn extract_dependency_parts(dep: &str) -> Option<(&str, &str)> {
252    let (namespace, package_name) = dep.split_once('/')?;
253
254    fn invalid_char(c: char) -> bool {
255        !matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '-' | '0'..='9')
256    }
257
258    if namespace.contains(invalid_char) || package_name.contains(invalid_char) {
259        None
260    } else {
261        Some((namespace, package_name))
262    }
263}
264
265type Atoms = BTreeMap<String, OwnedBuffer>;
266
267fn transform_atoms(
268    manifest: &WasmerManifest,
269    base_dir: &Path,
270) -> Result<(IndexMap<String, Atom>, Atoms), ManifestError> {
271    let mut atom_entries = BTreeMap::new();
272
273    for module in &manifest.modules {
274        let name = &module.name;
275        let path = base_dir.join(&module.source);
276        let file = open_file(&path).map_err(|error| ManifestError::ReadAtomFile {
277            module: name.clone(),
278            path,
279            error,
280        })?;
281
282        atom_entries.insert(name.clone(), (module.kind.clone(), file));
283    }
284
285    transform_atoms_shared(&atom_entries)
286}
287
288fn transform_in_memory_atoms(
289    atoms: &BTreeMap<String, (Option<String>, OwnedBuffer)>,
290) -> Result<(IndexMap<String, Atom>, Atoms), ManifestError> {
291    transform_atoms_shared(atoms)
292}
293
294fn transform_atoms_shared(
295    atoms: &BTreeMap<String, (Option<String>, OwnedBuffer)>,
296) -> Result<(IndexMap<String, Atom>, Atoms), ManifestError> {
297    let mut atom_files = BTreeMap::new();
298    let mut metadata = IndexMap::new();
299
300    for (name, (kind, content)) in atoms.iter() {
301        // Create atom with annotations including Wasm features if available
302        let mut annotations = IndexMap::new();
303
304        // Detect required WebAssembly features by analyzing the module binary
305        let features_result = wasmer_types::Features::detect_from_wasm(content);
306
307        if let Ok(features) = features_result {
308            // Convert wasmer_types::Features to webc::metadata::annotations::Wasm
309            let feature_strings = features_to_wasm_annotations(&features);
310
311            // Only create annotation if we detected features
312            if !feature_strings.is_empty() {
313                let wasm = webc::metadata::annotations::Wasm::new(feature_strings);
314                match ciborium::value::Value::serialized(&wasm) {
315                    Ok(wasm_value) => {
316                        annotations.insert(
317                            webc::metadata::annotations::Wasm::KEY.to_string(),
318                            wasm_value,
319                        );
320                    }
321                    Err(e) => {
322                        eprintln!("Failed to serialize wasm features: {e}");
323                    }
324                }
325            }
326        }
327
328        let atom = Atom {
329            kind: atom_kind(kind.as_ref().map(|s| s.as_str()))?,
330            signature: atom_signature(content),
331            annotations,
332        };
333
334        if metadata.contains_key(name) {
335            return Err(ManifestError::DuplicateModule(name.clone()));
336        }
337
338        metadata.insert(name.clone(), atom);
339        atom_files.insert(name.clone(), content.clone());
340    }
341
342    Ok((metadata, atom_files))
343}
344
345fn atom_signature(atom: &[u8]) -> String {
346    let hash: [u8; 32] = sha2::Sha256::digest(atom).into();
347    AtomSignature::Sha256(hash).to_string()
348}
349
350/// Map the "kind" field in a `[module]` to the corresponding URI.
351fn atom_kind(kind: Option<&str>) -> Result<Url, ManifestError> {
352    const WASM_ATOM_KIND: &str = "https://webc.org/kind/wasm";
353    const TENSORFLOW_SAVED_MODEL_KIND: &str = "https://webc.org/kind/tensorflow-SavedModel";
354
355    let url = match kind {
356        Some("wasm") | None => WASM_ATOM_KIND.parse().expect("Should never fail"),
357        Some("tensorflow-SavedModel") => TENSORFLOW_SAVED_MODEL_KIND
358            .parse()
359            .expect("Should never fail"),
360        Some(other) => {
361            if let Ok(url) = Url::parse(other) {
362                // if it is a valid URL, pass that through as-is
363                url
364            } else {
365                return Err(ManifestError::UnknownAtomKind(other.to_string()));
366            }
367        }
368    };
369
370    Ok(url)
371}
372
373/// Try to open a file, preferring mmap and falling back to [`std::fs::read()`]
374/// if mapping fails.
375fn open_file(path: &Path) -> Result<OwnedBuffer, std::io::Error> {
376    match OwnedBuffer::mmap(path) {
377        Ok(b) => return Ok(b),
378        Err(MmapError::Map(_)) => {
379            // Unable to mmap the atom file. Falling back to std::fs::read()
380        }
381        Err(MmapError::FileOpen { error, .. }) => {
382            return Err(error);
383        }
384    }
385
386    let bytes = std::fs::read(path)?;
387
388    Ok(OwnedBuffer::from_bytes(bytes))
389}
390
391fn insert_annotation(
392    annotations: &mut IndexMap<String, ciborium::Value>,
393    key: impl Into<String>,
394    value: impl serde::Serialize,
395) -> Result<(), ManifestError> {
396    let key = key.into();
397
398    match ciborium::value::Value::serialized(&value) {
399        Ok(value) => {
400            annotations.insert(key, value);
401            Ok(())
402        }
403        Err(error) => Err(ManifestError::SerializeCborAnnotation { key, error }),
404    }
405}
406
407fn get_fs_table(fs: &IndexMap<String, PathBuf>) -> FileSystemMappings {
408    if fs.is_empty() {
409        return FileSystemMappings::default();
410    }
411
412    // When wapm-targz-to-pirita creates the final webc all files will be
413    // merged into one "atom" volume, but we want to map each directory
414    // separately.
415    let mut entries = Vec::new();
416    for (guest, host) in fs {
417        let volume_name = host
418            .to_str()
419            .expect("failed to convert path to string")
420            .to_string();
421
422        let volume_name = sanitize_path(volume_name);
423
424        let mapping = FileSystemMapping {
425            from: None,
426            volume_name,
427            host_path: None,
428            mount_path: sanitize_path(guest),
429        };
430        entries.push(mapping);
431    }
432
433    FileSystemMappings(entries)
434}
435
436fn transform_package_meta_to_annotations(
437    package: &wasmer_config::package::Package,
438    base_dir: &Path,
439    strictness: Strictness,
440) -> Result<Wapm, ManifestError> {
441    fn metadata_file(
442        path: Option<&PathBuf>,
443        base_dir: &Path,
444        strictness: Strictness,
445    ) -> Result<Option<VolumeSpecificPath>, ManifestError> {
446        let path = match path {
447            Some(p) => p,
448            None => return Ok(None),
449        };
450
451        let absolute_path = base_dir.join(path);
452
453        // Touch the file to make sure it actually exists
454        if !absolute_path.exists() {
455            match strictness.missing_file(path, base_dir) {
456                Ok(_) => return Ok(None),
457                Err(e) => {
458                    return Err(e);
459                }
460            }
461        }
462
463        match base_dir.join(path).strip_prefix(base_dir) {
464            Ok(without_prefix) => Ok(Some(VolumeSpecificPath {
465                volume: METADATA_VOLUME.to_string(),
466                path: sanitize_path(without_prefix),
467            })),
468            Err(_) => match strictness.outside_base_directory(path, base_dir) {
469                Ok(_) => Ok(None),
470                Err(e) => Err(e),
471            },
472        }
473    }
474
475    transform_package_meta_to_annotations_shared(package, |path| {
476        metadata_file(path, base_dir, strictness)
477    })
478}
479
480fn transform_in_memory_package_meta_to_annotations(
481    package: &wasmer_config::package::Package,
482) -> Result<Wapm, ManifestError> {
483    transform_package_meta_to_annotations_shared(package, |path| {
484        Ok(path.map(|readme_file| VolumeSpecificPath {
485            volume: METADATA_VOLUME.to_string(),
486            path: sanitize_path(readme_file),
487        }))
488    })
489}
490
491fn transform_package_meta_to_annotations_shared(
492    package: &wasmer_config::package::Package,
493    volume_specific_path: impl Fn(Option<&PathBuf>) -> Result<Option<VolumeSpecificPath>, ManifestError>,
494) -> Result<Wapm, ManifestError> {
495    let mut wapm = Wapm::new(
496        package.name.clone(),
497        package.version.clone().map(|v| v.to_string()),
498        package.description.clone(),
499    );
500
501    wapm.license = package.license.clone();
502    wapm.license_file = volume_specific_path(package.license_file.as_ref())?;
503    wapm.readme = volume_specific_path(package.readme.as_ref())?;
504    wapm.repository = package.repository.clone();
505    wapm.homepage = package.homepage.clone();
506    wapm.private = package.private;
507
508    Ok(wapm)
509}
510
511fn transform_commands(
512    manifest: &WasmerManifest,
513    base_dir: &Path,
514) -> Result<IndexMap<String, Command>, ManifestError> {
515    trasform_commands_shared(
516        manifest,
517        |cmd| transform_command_v1(cmd, manifest),
518        |cmd| transform_command_v2(cmd, base_dir),
519    )
520}
521
522fn transform_in_memory_commands(
523    manifest: &WasmerManifest,
524) -> Result<IndexMap<String, Command>, ManifestError> {
525    trasform_commands_shared(
526        manifest,
527        |cmd| transform_command_v1(cmd, manifest),
528        transform_in_memory_command_v2,
529    )
530}
531
532#[allow(deprecated)]
533fn trasform_commands_shared(
534    manifest: &WasmerManifest,
535    transform_command_v1: impl Fn(&CommandV1) -> Result<Command, ManifestError>,
536    transform_command_v2: impl Fn(&CommandV2) -> Result<Command, ManifestError>,
537) -> Result<IndexMap<String, Command>, ManifestError> {
538    let mut commands = IndexMap::new();
539
540    for command in &manifest.commands {
541        let cmd = match command {
542            wasmer_config::package::Command::V1(cmd) => transform_command_v1(cmd)?,
543            wasmer_config::package::Command::V2(cmd) => transform_command_v2(cmd)?,
544        };
545
546        // If a command uses a module from a dependency, then ensure that
547        // the dependency is declared.
548        match command.get_module() {
549            wasmer_config::package::ModuleReference::CurrentPackage { .. } => {}
550            wasmer_config::package::ModuleReference::Dependency { dependency, .. } => {
551                if !manifest.dependencies.contains_key(dependency) {
552                    return Err(ManifestError::UndeclaredCommandDependency {
553                        command: command.get_name().to_string(),
554                        dependency: dependency.to_string(),
555                    });
556                }
557            }
558        }
559
560        match commands.entry(command.get_name().to_string()) {
561            indexmap::map::Entry::Occupied(_) => {
562                return Err(ManifestError::DuplicateCommand(
563                    command.get_name().to_string(),
564                ));
565            }
566            indexmap::map::Entry::Vacant(entry) => {
567                entry.insert(cmd);
568            }
569        }
570    }
571
572    Ok(commands)
573}
574
575#[allow(deprecated)]
576fn transform_command_v1(
577    cmd: &wasmer_config::package::CommandV1,
578    manifest: &WasmerManifest,
579) -> Result<Command, ManifestError> {
580    // Note: a key difference between CommandV1 and CommandV2 is that v1 uses
581    // a module's "abi" field to figure out which runner to use, whereas v2 has
582    // a dedicated "runner" field and ignores module.abi.
583    let runner = match &cmd.module {
584        wasmer_config::package::ModuleReference::CurrentPackage { module } => {
585            let module = manifest
586                .modules
587                .iter()
588                .find(|m| m.name == module.as_str())
589                .ok_or_else(|| ManifestError::InvalidModuleReference {
590                    command: cmd.name.clone(),
591                    module: cmd.module.to_string(),
592                })?;
593
594            RunnerKind::from_name(module.abi.to_str())?
595        }
596        wasmer_config::package::ModuleReference::Dependency { .. } => {
597            // Note: We don't have any visibility into dependencies (this code
598            // doesn't do resolution), so we blindly assume it's a WASI command.
599            // That should be fine because people shouldn't use the CommandV1
600            // syntax any more.
601            RunnerKind::Wasi
602        }
603    };
604
605    let mut annotations = IndexMap::new();
606    // Splitting by whitespace isn't really correct, but proper shell splitting
607    // would require a dependency and CommandV1 won't be used any more, anyway.
608    let main_args = cmd
609        .main_args
610        .as_deref()
611        .map(|args| args.split_whitespace().map(String::from).collect());
612    runner.runner_specific_annotations(
613        &mut annotations,
614        &cmd.module,
615        cmd.package.clone(),
616        main_args,
617    )?;
618
619    Ok(Command {
620        runner: runner.uri().to_string(),
621        annotations,
622    })
623}
624
625fn transform_command_v2(
626    cmd: &wasmer_config::package::CommandV2,
627    base_dir: &Path,
628) -> Result<Command, ManifestError> {
629    transform_command_v2_shared(cmd, || {
630        cmd.get_annotations(base_dir)
631            .map_err(|error| ManifestError::WasmerTomlAnnotations {
632                error: error.into(),
633            })
634    })
635}
636
637fn transform_in_memory_command_v2(
638    cmd: &wasmer_config::package::CommandV2,
639) -> Result<Command, ManifestError> {
640    transform_command_v2_shared(cmd, || {
641        cmd.annotations
642            .as_ref()
643            .map(|a| match a {
644                wasmer_config::package::CommandAnnotations::File(_) => {
645                    Err(ManifestError::FileNotSupported)
646                }
647                wasmer_config::package::CommandAnnotations::Raw(v) => Ok(toml_to_cbor_value(v)),
648            })
649            .transpose()
650    })
651}
652
653fn transform_command_v2_shared(
654    cmd: &wasmer_config::package::CommandV2,
655    custom_annotations: impl Fn() -> Result<Option<Value>, ManifestError>,
656) -> Result<Command, ManifestError> {
657    let runner = RunnerKind::from_name(&cmd.runner)?;
658    let mut annotations = IndexMap::new();
659
660    runner.runner_specific_annotations(&mut annotations, &cmd.module, None, None)?;
661
662    let custom_annotations = custom_annotations()?;
663
664    if let Some(ciborium::Value::Map(custom_annotations)) = custom_annotations {
665        for (key, value) in custom_annotations {
666            if let ciborium::Value::Text(key) = key {
667                match annotations.entry(key) {
668                    indexmap::map::Entry::Occupied(mut entry) => {
669                        merge_cbor(entry.get_mut(), value).map_err(|_| {
670                            ManifestError::MergeAnnotations {
671                                command: cmd.name.clone(),
672                                key: entry.key().clone(),
673                            }
674                        })?;
675                    }
676                    indexmap::map::Entry::Vacant(entry) => {
677                        entry.insert(value);
678                    }
679                }
680            }
681        }
682    }
683
684    Ok(Command {
685        runner: runner.uri().to_string(),
686        annotations,
687    })
688}
689
690fn toml_to_cbor_value(val: &toml::value::Value) -> ciborium::Value {
691    match val {
692        toml::Value::String(s) => ciborium::Value::Text(s.clone()),
693        toml::Value::Integer(i) => ciborium::Value::Integer(ciborium::value::Integer::from(*i)),
694        toml::Value::Float(f) => ciborium::Value::Float(*f),
695        toml::Value::Boolean(b) => ciborium::Value::Bool(*b),
696        toml::Value::Datetime(d) => ciborium::Value::Text(format!("{d}")),
697        toml::Value::Array(sq) => {
698            ciborium::Value::Array(sq.iter().map(toml_to_cbor_value).collect())
699        }
700        toml::Value::Table(m) => ciborium::Value::Map(
701            m.iter()
702                .map(|(k, v)| (ciborium::Value::Text(k.clone()), toml_to_cbor_value(v)))
703                .collect(),
704        ),
705    }
706}
707
708fn merge_cbor(original: &mut Value, addition: Value) -> Result<(), ()> {
709    match (original, addition) {
710        (Value::Map(left), Value::Map(right)) => {
711            for (k, v) in right {
712                if let Some(entry) = left.iter_mut().find(|lk| lk.0 == k) {
713                    merge_cbor(&mut entry.1, v)?;
714                } else {
715                    left.push((k, v));
716                }
717            }
718        }
719        (Value::Array(left), Value::Array(right)) => {
720            left.extend(right);
721        }
722        // Primitives that have the same values are fine
723        (Value::Bool(left), Value::Bool(right)) if *left == right => {}
724        (Value::Bytes(left), Value::Bytes(right)) if *left == right => {}
725        (Value::Float(left), Value::Float(right)) if *left == right => {}
726        (Value::Integer(left), Value::Integer(right)) if *left == right => {}
727        (Value::Text(left), Value::Text(right)) if *left == right => {}
728        // null can be overwritten
729        (original @ Value::Null, value) => {
730            *original = value;
731        }
732        (_original, Value::Null) => {}
733        // Oh well, we tried...
734        (_left, _right) => {
735            return Err(());
736        }
737    }
738
739    Ok(())
740}
741
742#[derive(Debug, Clone, PartialEq)]
743enum RunnerKind {
744    Wasi,
745    Wcgi,
746    Wasm4,
747    Other(Url),
748}
749
750impl RunnerKind {
751    fn from_name(name: &str) -> Result<Self, ManifestError> {
752        match name {
753            "wasi" | "wasi@unstable_" | webc::metadata::annotations::WASI_RUNNER_URI => {
754                Ok(RunnerKind::Wasi)
755            }
756            "generic" => {
757                // This is what you get with a CommandV1 and abi = "none"
758                Ok(RunnerKind::Wasi)
759            }
760            "wcgi" | webc::metadata::annotations::WCGI_RUNNER_URI => Ok(RunnerKind::Wcgi),
761            "wasm4" | webc::metadata::annotations::WASM4_RUNNER_URI => Ok(RunnerKind::Wasm4),
762            other => {
763                if let Ok(other) = Url::parse(other) {
764                    Ok(RunnerKind::Other(other))
765                } else if let Ok(other) = format!("https://webc.org/runner/{other}").parse() {
766                    // fall back to something under webc.org
767                    Ok(RunnerKind::Other(other))
768                } else {
769                    Err(ManifestError::UnknownRunnerKind(other.to_string()))
770                }
771            }
772        }
773    }
774
775    fn uri(&self) -> &str {
776        match self {
777            RunnerKind::Wasi => webc::metadata::annotations::WASI_RUNNER_URI,
778            RunnerKind::Wcgi => webc::metadata::annotations::WCGI_RUNNER_URI,
779            RunnerKind::Wasm4 => webc::metadata::annotations::WASM4_RUNNER_URI,
780            RunnerKind::Other(other) => other.as_str(),
781        }
782    }
783
784    #[allow(deprecated)]
785    fn runner_specific_annotations(
786        &self,
787        annotations: &mut IndexMap<String, Value>,
788        module: &wasmer_config::package::ModuleReference,
789        package: Option<String>,
790        main_args: Option<Vec<String>>,
791    ) -> Result<(), ManifestError> {
792        let atom_annotation = match module {
793            wasmer_config::package::ModuleReference::CurrentPackage { module } => {
794                AtomAnnotation::new(module, None)
795            }
796            wasmer_config::package::ModuleReference::Dependency { dependency, module } => {
797                AtomAnnotation::new(module, dependency.to_string())
798            }
799        };
800        insert_annotation(annotations, AtomAnnotation::KEY, atom_annotation)?;
801
802        match self {
803            RunnerKind::Wasi | RunnerKind::Wcgi => {
804                let mut wasi = Wasi::new(module.to_string());
805                wasi.main_args = main_args;
806                wasi.package = package;
807                insert_annotation(annotations, Wasi::KEY, wasi)?;
808            }
809            RunnerKind::Wasm4 | RunnerKind::Other(_) => {
810                // No extra annotations to add
811            }
812        }
813
814        Ok(())
815    }
816}
817
818/// Infer the package's entrypoint.
819fn entrypoint(manifest: &WasmerManifest) -> Option<String> {
820    // check if manifest.package is none
821    if let Some(package) = &manifest.package
822        && let Some(entrypoint) = &package.entrypoint
823    {
824        return Some(entrypoint.clone());
825    }
826
827    if let [only_command] = manifest.commands.as_slice() {
828        // For convenience (and to stay compatible with old docs), if there is
829        // only one command we'll use that as the entrypoint
830        return Some(only_command.get_name().to_string());
831    }
832
833    None
834}
835
836fn transform_bindings(
837    manifest: &WasmerManifest,
838    base_dir: &Path,
839) -> Result<Vec<Binding>, ManifestError> {
840    transform_bindings_shared(
841        manifest,
842        |wit, module| transform_wit_bindings(wit, module, base_dir),
843        |wit, module| transform_wai_bindings(wit, module, base_dir),
844    )
845}
846
847fn transform_in_memory_bindings(manifest: &WasmerManifest) -> Result<Vec<Binding>, ManifestError> {
848    transform_bindings_shared(
849        manifest,
850        transform_in_memory_wit_bindings,
851        transform_in_memory_wai_bindings,
852    )
853}
854
855fn transform_bindings_shared(
856    manifest: &WasmerManifest,
857    wit_binding: impl Fn(
858        &wasmer_config::package::WitBindings,
859        &wasmer_config::package::Module,
860    ) -> Result<Binding, ManifestError>,
861    wai_binding: impl Fn(
862        &wasmer_config::package::WaiBindings,
863        &wasmer_config::package::Module,
864    ) -> Result<Binding, ManifestError>,
865) -> Result<Vec<Binding>, ManifestError> {
866    let mut bindings = Vec::new();
867
868    for module in &manifest.modules {
869        let b = match &module.bindings {
870            Some(wasmer_config::package::Bindings::Wit(wit)) => wit_binding(wit, module)?,
871            Some(wasmer_config::package::Bindings::Wai(wai)) => wai_binding(wai, module)?,
872            None => continue,
873        };
874        bindings.push(b);
875    }
876
877    Ok(bindings)
878}
879
880fn transform_wai_bindings(
881    wai: &wasmer_config::package::WaiBindings,
882    module: &wasmer_config::package::Module,
883    base_dir: &Path,
884) -> Result<Binding, ManifestError> {
885    transform_wai_bindings_shared(wai, module, |path| metadata_volume_uri(path, base_dir))
886}
887
888fn transform_in_memory_wai_bindings(
889    wai: &wasmer_config::package::WaiBindings,
890    module: &wasmer_config::package::Module,
891) -> Result<Binding, ManifestError> {
892    transform_wai_bindings_shared(wai, module, |path| {
893        Ok(format!("{METADATA_VOLUME}:/{}", sanitize_path(path)))
894    })
895}
896
897fn transform_wai_bindings_shared(
898    wai: &wasmer_config::package::WaiBindings,
899    module: &wasmer_config::package::Module,
900    metadata_volume_path: impl Fn(&PathBuf) -> Result<String, ManifestError>,
901) -> Result<Binding, ManifestError> {
902    let wasmer_config::package::WaiBindings {
903        wai_version,
904        exports,
905        imports,
906    } = wai;
907
908    let bindings = WaiBindings {
909        exports: exports.as_ref().map(&metadata_volume_path).transpose()?,
910        module: module.name.clone(),
911        imports: imports
912            .iter()
913            .map(metadata_volume_path)
914            .collect::<Result<Vec<_>, ManifestError>>()?,
915    };
916    let mut annotations = IndexMap::new();
917    insert_annotation(&mut annotations, "wai", bindings)?;
918
919    Ok(Binding {
920        name: "library-bindings".to_string(),
921        kind: format!("wai@{wai_version}"),
922        annotations: Value::Map(
923            annotations
924                .into_iter()
925                .map(|(k, v)| (Value::Text(k), v))
926                .collect(),
927        ),
928    })
929}
930
931fn metadata_volume_uri(path: &Path, base_dir: &Path) -> Result<String, ManifestError> {
932    make_relative_path(path, base_dir)
933        .map(sanitize_path)
934        .map(|p| format!("{METADATA_VOLUME}:/{p}"))
935}
936
937fn transform_wit_bindings(
938    wit: &wasmer_config::package::WitBindings,
939    module: &wasmer_config::package::Module,
940    base_dir: &Path,
941) -> Result<Binding, ManifestError> {
942    transform_wit_bindings_shared(wit, module, |path| metadata_volume_uri(path, base_dir))
943}
944
945fn transform_in_memory_wit_bindings(
946    wit: &wasmer_config::package::WitBindings,
947    module: &wasmer_config::package::Module,
948) -> Result<Binding, ManifestError> {
949    transform_wit_bindings_shared(wit, module, |path| {
950        Ok(format!("{METADATA_VOLUME}:/{}", sanitize_path(path)))
951    })
952}
953
954fn transform_wit_bindings_shared(
955    wit: &wasmer_config::package::WitBindings,
956    module: &wasmer_config::package::Module,
957    metadata_volume_path: impl Fn(&PathBuf) -> Result<String, ManifestError>,
958) -> Result<Binding, ManifestError> {
959    let wasmer_config::package::WitBindings {
960        wit_bindgen,
961        wit_exports,
962    } = wit;
963
964    let bindings = WitBindings {
965        exports: metadata_volume_path(wit_exports)?,
966        module: module.name.clone(),
967    };
968    let mut annotations = IndexMap::new();
969    insert_annotation(&mut annotations, "wit", bindings)?;
970
971    Ok(Binding {
972        name: "library-bindings".to_string(),
973        kind: format!("wit@{wit_bindgen}"),
974        annotations: Value::Map(
975            annotations
976                .into_iter()
977                .map(|(k, v)| (Value::Text(k), v))
978                .collect(),
979        ),
980    })
981}
982
983/// Resolve an item relative to the base directory, returning an error if the
984/// file lies outside of it.
985fn make_relative_path(path: &Path, base_dir: &Path) -> Result<PathBuf, ManifestError> {
986    let absolute_path = base_dir.join(path);
987
988    match absolute_path.strip_prefix(base_dir) {
989        Ok(p) => Ok(p.into()),
990        Err(_) => Err(ManifestError::OutsideBaseDirectory {
991            path: absolute_path,
992            base_dir: base_dir.to_path_buf(),
993        }),
994    }
995}
996
997#[cfg(test)]
998mod tests {
999    use tempfile::TempDir;
1000    use webc::metadata::annotations::Wasi;
1001
1002    use super::*;
1003
1004    #[test]
1005    fn custom_annotations_are_copied_across_verbatim() {
1006        let temp = TempDir::new().unwrap();
1007        let wasmer_toml = r#"
1008        [package]
1009        name = "test"
1010        version = "0.0.0"
1011        description = "asdf"
1012
1013        [[module]]
1014        name = "module"
1015        source = "file.wasm"
1016        abi = "wasi"
1017
1018        [[command]]
1019        name = "command"
1020        module = "module"
1021        runner = "asdf"
1022        annotations = { first = 42, second = ["a", "b"] }
1023        "#;
1024        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1025        std::fs::write(temp.path().join("file.wasm"), b"\0asm...").unwrap();
1026
1027        let (transformed, _) =
1028            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1029
1030        let command = &transformed.commands["command"];
1031        assert_eq!(command.annotation::<u32>("first").unwrap(), Some(42));
1032        assert_eq!(command.annotation::<String>("non-existent").unwrap(), None);
1033        insta::with_settings! {
1034            { description => wasmer_toml },
1035            { insta::assert_yaml_snapshot!(&transformed); }
1036        }
1037    }
1038
1039    #[test]
1040    fn transform_empty_manifest() {
1041        let temp = TempDir::new().unwrap();
1042        let wasmer_toml = r#"
1043            [package]
1044            name = "some/package"
1045            version = "0.0.0"
1046            description = "My awesome package"
1047        "#;
1048        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1049
1050        let (transformed, atoms) =
1051            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1052
1053        assert!(atoms.is_empty());
1054        insta::with_settings! {
1055            { description => wasmer_toml },
1056            { insta::assert_yaml_snapshot!(&transformed); }
1057        }
1058    }
1059
1060    #[test]
1061    fn transform_manifest_with_single_atom() {
1062        let temp = TempDir::new().unwrap();
1063        let wasmer_toml = r#"
1064            [package]
1065            name = "some/package"
1066            version = "0.0.0"
1067            description = "My awesome package"
1068
1069            [[module]]
1070            name = "first"
1071            source = "./path/to/file.wasm"
1072            abi = "wasi"
1073        "#;
1074        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1075        let dir = temp.path().join("path").join("to");
1076        std::fs::create_dir_all(&dir).unwrap();
1077        std::fs::write(dir.join("file.wasm"), b"\0asm...").unwrap();
1078
1079        let (transformed, atoms) =
1080            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1081
1082        assert_eq!(atoms.len(), 1);
1083        assert_eq!(atoms["first"].as_slice(), b"\0asm...");
1084        insta::with_settings! {
1085            { description => wasmer_toml },
1086            { insta::assert_yaml_snapshot!(&transformed); }
1087        }
1088    }
1089
1090    #[test]
1091    fn transform_manifest_with_atom_and_command() {
1092        let temp = TempDir::new().unwrap();
1093        let wasmer_toml = r#"
1094            [package]
1095            name = "some/package"
1096            version = "0.0.0"
1097            description = "My awesome package"
1098
1099            [[module]]
1100            name = "cpython"
1101            source = "python.wasm"
1102            abi = "wasi"
1103
1104            [[command]]
1105            name = "python"
1106            module = "cpython"
1107            runner = "wasi"
1108        "#;
1109        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1110        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1111
1112        let (transformed, _) =
1113            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1114
1115        assert_eq!(transformed.commands.len(), 1);
1116        let python = &transformed.commands["python"];
1117        assert_eq!(&python.runner, webc::metadata::annotations::WASI_RUNNER_URI);
1118        assert_eq!(python.wasi().unwrap().unwrap(), Wasi::new("cpython"));
1119        insta::with_settings! {
1120            { description => wasmer_toml },
1121            { insta::assert_yaml_snapshot!(&transformed); }
1122        }
1123    }
1124
1125    #[test]
1126    fn transform_manifest_with_multiple_commands() {
1127        let temp = TempDir::new().unwrap();
1128        let wasmer_toml = r#"
1129            [package]
1130            name = "some/package"
1131            version = "0.0.0"
1132            description = "My awesome package"
1133
1134            [[module]]
1135            name = "cpython"
1136            source = "python.wasm"
1137            abi = "wasi"
1138
1139            [[command]]
1140            name = "first"
1141            module = "cpython"
1142            runner = "wasi"
1143
1144            [[command]]
1145            name = "second"
1146            module = "cpython"
1147            runner = "wasi"
1148
1149            [[command]]
1150            name = "third"
1151            module = "cpython"
1152            runner = "wasi"
1153        "#;
1154        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1155        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1156
1157        let (transformed, _) =
1158            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1159
1160        assert_eq!(transformed.commands.len(), 3);
1161        assert!(transformed.commands.contains_key("first"));
1162        assert!(transformed.commands.contains_key("second"));
1163        assert!(transformed.commands.contains_key("third"));
1164        insta::with_settings! {
1165            { description => wasmer_toml },
1166            { insta::assert_yaml_snapshot!(&transformed); }
1167        }
1168    }
1169
1170    #[test]
1171    fn merge_custom_attributes_with_builtin_ones() {
1172        let temp = TempDir::new().unwrap();
1173        let wasmer_toml = r#"
1174            [package]
1175            name = "some/package"
1176            version = "0.0.0"
1177            description = "My awesome package"
1178
1179            [[module]]
1180            name = "cpython"
1181            source = "python.wasm"
1182            abi = "wasi"
1183
1184            [[command]]
1185            name = "python"
1186            module = "cpython"
1187            runner = "wasi"
1188            annotations = { wasi = { env = ["KEY=val"]} }
1189        "#;
1190        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1191        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1192
1193        let (transformed, _) =
1194            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1195
1196        assert_eq!(transformed.commands.len(), 1);
1197        let cmd = &transformed.commands["python"];
1198        assert_eq!(
1199            &cmd.wasi().unwrap().unwrap(),
1200            Wasi::new("cpython").with_env("KEY", "val")
1201        );
1202        insta::with_settings! {
1203            { description => wasmer_toml },
1204            { insta::assert_yaml_snapshot!(&transformed); }
1205        }
1206    }
1207
1208    #[test]
1209    fn transform_bash_manifest() {
1210        let temp = TempDir::new().unwrap();
1211        let wasmer_toml = r#"
1212            [package]
1213            name = "sharrattj/bash"
1214            version = "1.0.17"
1215            description = "Bash is a modern POSIX-compliant implementation of /bin/sh."
1216            license = "GNU"
1217            wasmer-extra-flags = "--enable-threads --enable-bulk-memory"
1218
1219            [dependencies]
1220            "wasmer/coreutils" = "1.0.19"
1221
1222            [[module]]
1223            name = "bash"
1224            source = "bash.wasm"
1225            abi = "wasi"
1226
1227            [[command]]
1228            name = "bash"
1229            module = "bash"
1230            runner = "wasi@unstable_"
1231        "#;
1232        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1233        std::fs::write(temp.path().join("bash.wasm"), b"\0asm...").unwrap();
1234
1235        let (transformed, _) =
1236            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1237
1238        insta::with_settings! {
1239            { description => wasmer_toml },
1240            { insta::assert_yaml_snapshot!(&transformed); }
1241        }
1242    }
1243
1244    #[test]
1245    fn transform_wasmer_pack_manifest() {
1246        let temp = TempDir::new().unwrap();
1247        let wasmer_toml = r#"
1248            [package]
1249            name = "wasmer/wasmer-pack"
1250            version = "0.7.0"
1251            description = "The WebAssembly interface to wasmer-pack."
1252            license = "MIT"
1253            readme = "README.md"
1254            repository = "https://github.com/wasmerio/wasmer-pack"
1255            homepage = "https://wasmer.io/"
1256
1257            [[module]]
1258            name = "wasmer-pack-wasm"
1259            source = "wasmer_pack_wasm.wasm"
1260
1261            [module.bindings]
1262            wai-version = "0.2.0"
1263            exports = "wasmer-pack.exports.wai"
1264            imports = []
1265        "#;
1266        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1267        std::fs::write(temp.path().join("wasmer_pack_wasm.wasm"), b"\0asm...").unwrap();
1268        std::fs::write(temp.path().join("wasmer-pack.exports.wai"), b"").unwrap();
1269        std::fs::write(temp.path().join("README.md"), b"").unwrap();
1270
1271        let (transformed, _) =
1272            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1273
1274        insta::with_settings! {
1275            { description => wasmer_toml },
1276            { insta::assert_yaml_snapshot!(&transformed); }
1277        }
1278    }
1279
1280    #[test]
1281    fn transform_python_manifest() {
1282        let temp = TempDir::new().unwrap();
1283        let wasmer_toml = r#"
1284            [package]
1285            name = "python"
1286            version = "0.1.0"
1287            description = "Python is an interpreted, high-level, general-purpose programming language"
1288            license = "ISC"
1289            repository = "https://github.com/wapm-packages/python"
1290
1291            [[module]]
1292            name = "python"
1293            source = "bin/python.wasm"
1294            abi = "wasi"
1295
1296            [module.interfaces]
1297            wasi = "0.0.0-unstable"
1298
1299            [[command]]
1300            name = "python"
1301            module = "python"
1302
1303            [fs]
1304            lib = "lib"
1305        "#;
1306        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1307        let bin = temp.path().join("bin");
1308        std::fs::create_dir_all(&bin).unwrap();
1309        std::fs::write(bin.join("python.wasm"), b"\0asm...").unwrap();
1310
1311        let (transformed, _) =
1312            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1313
1314        insta::with_settings! {
1315            { description => wasmer_toml },
1316            { insta::assert_yaml_snapshot!(&transformed); }
1317        }
1318    }
1319
1320    #[test]
1321    fn transform_manifest_with_fs_table() {
1322        let temp = TempDir::new().unwrap();
1323        let wasmer_toml = r#"
1324            [package]
1325            name = "some/package"
1326            version = "0.0.0"
1327            description = "This is a package"
1328
1329            [fs]
1330            lib = "lib"
1331            "/public" = "out"
1332        "#;
1333        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1334        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1335
1336        let (transformed, _) =
1337            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1338
1339        let fs = transformed.filesystem().unwrap().unwrap();
1340        assert_eq!(
1341            fs,
1342            [
1343                FileSystemMapping {
1344                    from: None,
1345                    volume_name: "/lib".to_string(),
1346                    host_path: None,
1347                    mount_path: "/lib".to_string(),
1348                },
1349                FileSystemMapping {
1350                    from: None,
1351                    volume_name: "/out".to_string(),
1352                    host_path: None,
1353                    mount_path: "/public".to_string(),
1354                }
1355            ]
1356        );
1357        insta::with_settings! {
1358            { description => wasmer_toml },
1359            { insta::assert_yaml_snapshot!(&transformed); }
1360        }
1361    }
1362
1363    #[test]
1364    fn missing_command_dependency() {
1365        let temp = TempDir::new().unwrap();
1366        let wasmer_toml = r#"
1367            [[command]]
1368            name = "python"
1369            module = "test/python:python"
1370        "#;
1371        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1372        let bin = temp.path().join("bin");
1373        std::fs::create_dir_all(&bin).unwrap();
1374        let res = wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict);
1375
1376        assert!(matches!(
1377            res,
1378            Err(ManifestError::UndeclaredCommandDependency { .. })
1379        ));
1380    }
1381
1382    #[test]
1383    fn issue_124_command_runner_is_swallowed() {
1384        let temp = TempDir::new().unwrap();
1385        let wasmer_toml = r#"
1386            [package]
1387            name = "wasmer-tests/wcgi-always-panic"
1388            version = "0.1.0"
1389            description = "wasmer-tests/wcgi-always-panic website"
1390
1391            [[module]]
1392            name = "wcgi-always-panic"
1393            source = "./wcgi-always-panic.wasm"
1394            abi = "wasi"
1395
1396            [[command]]
1397            name = "wcgi"
1398            module = "wcgi-always-panic"
1399            runner = "https://webc.org/runner/wcgi"
1400        "#;
1401        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1402        std::fs::write(temp.path().join("wcgi-always-panic.wasm"), b"\0asm...").unwrap();
1403
1404        let (transformed, _) =
1405            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1406
1407        let cmd = &transformed.commands["wcgi"];
1408        assert_eq!(cmd.runner, webc::metadata::annotations::WCGI_RUNNER_URI);
1409        assert_eq!(cmd.wasi().unwrap().unwrap(), Wasi::new("wcgi-always-panic"));
1410        insta::with_settings! {
1411            { description => wasmer_toml },
1412            { insta::assert_yaml_snapshot!(&transformed); }
1413        }
1414    }
1415}