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