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