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 if 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
319 let features_result = wasmer_types::Features::detect_from_wasm(content);
321
322 if let Ok(features) = features_result {
323 let feature_strings = features_to_wasm_annotations(&features);
325
326 if !feature_strings.is_empty() {
328 let wasm = webc::metadata::annotations::Wasm::new(feature_strings);
329 match ciborium::value::Value::serialized(&wasm) {
330 Ok(wasm_value) => {
331 annotations.insert(
332 webc::metadata::annotations::Wasm::KEY.to_string(),
333 wasm_value,
334 );
335 }
336 Err(e) => {
337 eprintln!("Failed to serialize wasm features: {e}");
338 }
339 }
340 }
341 }
342
343 let atom = Atom {
344 kind: atom_kind(kind.as_ref().map(|s| s.as_str()))?,
345 signature: atom_signature(content),
346 annotations,
347 };
348
349 if metadata.contains_key(name) {
350 return Err(ManifestError::DuplicateModule(name.clone()));
351 }
352
353 metadata.insert(name.clone(), atom);
354 atom_files.insert(name.clone(), content.clone());
355 }
356
357 Ok((metadata, atom_files))
358}
359
360fn atom_signature(atom: &[u8]) -> String {
361 let hash: [u8; 32] = sha2::Sha256::digest(atom).into();
362 AtomSignature::Sha256(hash).to_string()
363}
364
365fn atom_kind(kind: Option<&str>) -> Result<Url, ManifestError> {
367 const WASM_ATOM_KIND: &str = "https://webc.org/kind/wasm";
368 const TENSORFLOW_SAVED_MODEL_KIND: &str = "https://webc.org/kind/tensorflow-SavedModel";
369
370 let url = match kind {
371 Some("wasm") | None => WASM_ATOM_KIND.parse().expect("Should never fail"),
372 Some("tensorflow-SavedModel") => TENSORFLOW_SAVED_MODEL_KIND
373 .parse()
374 .expect("Should never fail"),
375 Some(other) => {
376 if let Ok(url) = Url::parse(other) {
377 url
379 } else {
380 return Err(ManifestError::UnknownAtomKind(other.to_string()));
381 }
382 }
383 };
384
385 Ok(url)
386}
387
388fn open_file(path: &Path) -> Result<OwnedBuffer, std::io::Error> {
391 match OwnedBuffer::mmap(path) {
392 Ok(b) => return Ok(b),
393 Err(MmapError::Map(_)) => {
394 }
396 Err(MmapError::FileOpen { error, .. }) => {
397 return Err(error);
398 }
399 }
400
401 let bytes = std::fs::read(path)?;
402
403 Ok(OwnedBuffer::from_bytes(bytes))
404}
405
406fn insert_annotation(
407 annotations: &mut IndexMap<String, ciborium::Value>,
408 key: impl Into<String>,
409 value: impl serde::Serialize,
410) -> Result<(), ManifestError> {
411 let key = key.into();
412
413 match ciborium::value::Value::serialized(&value) {
414 Ok(value) => {
415 annotations.insert(key, value);
416 Ok(())
417 }
418 Err(error) => Err(ManifestError::SerializeCborAnnotation { key, error }),
419 }
420}
421
422fn get_fs_table(fs: &IndexMap<String, PathBuf>) -> FileSystemMappings {
423 if fs.is_empty() {
424 return FileSystemMappings::default();
425 }
426
427 let mut entries = Vec::new();
431 for (guest, host) in fs {
432 let volume_name = host
433 .to_str()
434 .expect("failed to convert path to string")
435 .to_string();
436
437 let volume_name = sanitize_path(volume_name);
438
439 let mapping = FileSystemMapping {
440 from: None,
441 volume_name,
442 host_path: None,
443 mount_path: sanitize_path(guest),
444 };
445 entries.push(mapping);
446 }
447
448 FileSystemMappings(entries)
449}
450
451fn transform_package_meta_to_annotations(
452 package: &wasmer_config::package::Package,
453 base_dir: &Path,
454 strictness: Strictness,
455) -> Result<Wapm, ManifestError> {
456 fn metadata_file(
457 path: Option<&PathBuf>,
458 base_dir: &Path,
459 strictness: Strictness,
460 ) -> Result<Option<VolumeSpecificPath>, ManifestError> {
461 let path = match path {
462 Some(p) => p,
463 None => return Ok(None),
464 };
465
466 let absolute_path = base_dir.join(path);
467
468 if !absolute_path.exists() {
470 match strictness.missing_file(path, base_dir) {
471 Ok(_) => return Ok(None),
472 Err(e) => {
473 return Err(e);
474 }
475 }
476 }
477
478 match base_dir.join(path).strip_prefix(base_dir) {
479 Ok(without_prefix) => Ok(Some(VolumeSpecificPath {
480 volume: METADATA_VOLUME.to_string(),
481 path: sanitize_path(without_prefix),
482 })),
483 Err(_) => match strictness.outside_base_directory(path, base_dir) {
484 Ok(_) => Ok(None),
485 Err(e) => Err(e),
486 },
487 }
488 }
489
490 transform_package_meta_to_annotations_shared(package, |path| {
491 metadata_file(path, base_dir, strictness)
492 })
493}
494
495fn transform_in_memory_package_meta_to_annotations(
496 package: &wasmer_config::package::Package,
497) -> Result<Wapm, ManifestError> {
498 transform_package_meta_to_annotations_shared(package, |path| {
499 Ok(path.map(|readme_file| VolumeSpecificPath {
500 volume: METADATA_VOLUME.to_string(),
501 path: sanitize_path(readme_file),
502 }))
503 })
504}
505
506fn transform_package_meta_to_annotations_shared(
507 package: &wasmer_config::package::Package,
508 volume_specific_path: impl Fn(Option<&PathBuf>) -> Result<Option<VolumeSpecificPath>, ManifestError>,
509) -> Result<Wapm, ManifestError> {
510 let mut wapm = Wapm::new(
511 package.name.clone(),
512 package.version.clone().map(|v| v.to_string()),
513 package.description.clone(),
514 );
515
516 wapm.license = package.license.clone();
517 wapm.license_file = volume_specific_path(package.license_file.as_ref())?;
518 wapm.readme = volume_specific_path(package.readme.as_ref())?;
519 wapm.repository = package.repository.clone();
520 wapm.homepage = package.homepage.clone();
521 wapm.private = package.private;
522
523 Ok(wapm)
524}
525
526fn transform_commands(
527 manifest: &WasmerManifest,
528 base_dir: &Path,
529) -> Result<IndexMap<String, Command>, ManifestError> {
530 trasform_commands_shared(
531 manifest,
532 |cmd| transform_command_v1(cmd, manifest),
533 |cmd| transform_command_v2(cmd, base_dir),
534 )
535}
536
537fn transform_in_memory_commands(
538 manifest: &WasmerManifest,
539) -> Result<IndexMap<String, Command>, ManifestError> {
540 trasform_commands_shared(
541 manifest,
542 |cmd| transform_command_v1(cmd, manifest),
543 transform_in_memory_command_v2,
544 )
545}
546
547#[allow(deprecated)]
548fn trasform_commands_shared(
549 manifest: &WasmerManifest,
550 transform_command_v1: impl Fn(&CommandV1) -> Result<Command, ManifestError>,
551 transform_command_v2: impl Fn(&CommandV2) -> Result<Command, ManifestError>,
552) -> Result<IndexMap<String, Command>, ManifestError> {
553 let mut commands = IndexMap::new();
554
555 for command in &manifest.commands {
556 let cmd = match command {
557 wasmer_config::package::Command::V1(cmd) => transform_command_v1(cmd)?,
558 wasmer_config::package::Command::V2(cmd) => transform_command_v2(cmd)?,
559 };
560
561 match command.get_module() {
564 wasmer_config::package::ModuleReference::CurrentPackage { .. } => {}
565 wasmer_config::package::ModuleReference::Dependency { dependency, .. } => {
566 if !manifest.dependencies.contains_key(dependency) {
567 return Err(ManifestError::UndeclaredCommandDependency {
568 command: command.get_name().to_string(),
569 dependency: dependency.to_string(),
570 });
571 }
572 }
573 }
574
575 match commands.entry(command.get_name().to_string()) {
576 indexmap::map::Entry::Occupied(_) => {
577 return Err(ManifestError::DuplicateCommand(
578 command.get_name().to_string(),
579 ));
580 }
581 indexmap::map::Entry::Vacant(entry) => {
582 entry.insert(cmd);
583 }
584 }
585 }
586
587 Ok(commands)
588}
589
590#[allow(deprecated)]
591fn transform_command_v1(
592 cmd: &wasmer_config::package::CommandV1,
593 manifest: &WasmerManifest,
594) -> Result<Command, ManifestError> {
595 let runner = match &cmd.module {
599 wasmer_config::package::ModuleReference::CurrentPackage { module } => {
600 let module = manifest
601 .modules
602 .iter()
603 .find(|m| m.name == module.as_str())
604 .ok_or_else(|| ManifestError::InvalidModuleReference {
605 command: cmd.name.clone(),
606 module: cmd.module.to_string(),
607 })?;
608
609 RunnerKind::from_name(module.abi.to_str())?
610 }
611 wasmer_config::package::ModuleReference::Dependency { .. } => {
612 RunnerKind::Wasi
617 }
618 };
619
620 let mut annotations = IndexMap::new();
621 let main_args = cmd
624 .main_args
625 .as_deref()
626 .map(|args| args.split_whitespace().map(String::from).collect());
627 runner.runner_specific_annotations(
628 &mut annotations,
629 &cmd.module,
630 cmd.package.clone(),
631 main_args,
632 )?;
633
634 Ok(Command {
635 runner: runner.uri().to_string(),
636 annotations,
637 })
638}
639
640fn transform_command_v2(
641 cmd: &wasmer_config::package::CommandV2,
642 base_dir: &Path,
643) -> Result<Command, ManifestError> {
644 transform_command_v2_shared(cmd, || {
645 cmd.get_annotations(base_dir)
646 .map_err(|error| ManifestError::WasmerTomlAnnotations {
647 error: error.into(),
648 })
649 })
650}
651
652fn transform_in_memory_command_v2(
653 cmd: &wasmer_config::package::CommandV2,
654) -> Result<Command, ManifestError> {
655 transform_command_v2_shared(cmd, || {
656 cmd.annotations
657 .as_ref()
658 .map(|a| match a {
659 wasmer_config::package::CommandAnnotations::File(_) => {
660 Err(ManifestError::FileNotSupported)
661 }
662 wasmer_config::package::CommandAnnotations::Raw(v) => Ok(toml_to_cbor_value(v)),
663 })
664 .transpose()
665 })
666}
667
668fn transform_command_v2_shared(
669 cmd: &wasmer_config::package::CommandV2,
670 custom_annotations: impl Fn() -> Result<Option<Value>, ManifestError>,
671) -> Result<Command, ManifestError> {
672 let runner = RunnerKind::from_name(&cmd.runner)?;
673 let mut annotations = IndexMap::new();
674
675 runner.runner_specific_annotations(&mut annotations, &cmd.module, None, None)?;
676
677 let custom_annotations = custom_annotations()?;
678
679 if let Some(ciborium::Value::Map(custom_annotations)) = custom_annotations {
680 for (key, value) in custom_annotations {
681 if let ciborium::Value::Text(key) = key {
682 match annotations.entry(key) {
683 indexmap::map::Entry::Occupied(mut entry) => {
684 merge_cbor(entry.get_mut(), value).map_err(|_| {
685 ManifestError::MergeAnnotations {
686 command: cmd.name.clone(),
687 key: entry.key().clone(),
688 }
689 })?;
690 }
691 indexmap::map::Entry::Vacant(entry) => {
692 entry.insert(value);
693 }
694 }
695 }
696 }
697 }
698
699 Ok(Command {
700 runner: runner.uri().to_string(),
701 annotations,
702 })
703}
704
705fn toml_to_cbor_value(val: &toml::value::Value) -> ciborium::Value {
706 match val {
707 toml::Value::String(s) => ciborium::Value::Text(s.clone()),
708 toml::Value::Integer(i) => ciborium::Value::Integer(ciborium::value::Integer::from(*i)),
709 toml::Value::Float(f) => ciborium::Value::Float(*f),
710 toml::Value::Boolean(b) => ciborium::Value::Bool(*b),
711 toml::Value::Datetime(d) => ciborium::Value::Text(format!("{d}")),
712 toml::Value::Array(sq) => {
713 ciborium::Value::Array(sq.iter().map(toml_to_cbor_value).collect())
714 }
715 toml::Value::Table(m) => ciborium::Value::Map(
716 m.iter()
717 .map(|(k, v)| (ciborium::Value::Text(k.clone()), toml_to_cbor_value(v)))
718 .collect(),
719 ),
720 }
721}
722
723fn merge_cbor(original: &mut Value, addition: Value) -> Result<(), ()> {
724 match (original, addition) {
725 (Value::Map(left), Value::Map(right)) => {
726 for (k, v) in right {
727 if let Some(entry) = left.iter_mut().find(|lk| lk.0 == k) {
728 merge_cbor(&mut entry.1, v)?;
729 } else {
730 left.push((k, v));
731 }
732 }
733 }
734 (Value::Array(left), Value::Array(right)) => {
735 left.extend(right);
736 }
737 (Value::Bool(left), Value::Bool(right)) if *left == right => {}
739 (Value::Bytes(left), Value::Bytes(right)) if *left == right => {}
740 (Value::Float(left), Value::Float(right)) if *left == right => {}
741 (Value::Integer(left), Value::Integer(right)) if *left == right => {}
742 (Value::Text(left), Value::Text(right)) if *left == right => {}
743 (original @ Value::Null, value) => {
745 *original = value;
746 }
747 (_original, Value::Null) => {}
748 (_left, _right) => {
750 return Err(());
751 }
752 }
753
754 Ok(())
755}
756
757#[derive(Debug, Clone, PartialEq)]
758enum RunnerKind {
759 Wasi,
760 Wcgi,
761 Wasm4,
762 Other(Url),
763}
764
765impl RunnerKind {
766 fn from_name(name: &str) -> Result<Self, ManifestError> {
767 match name {
768 "wasi" | "wasi@unstable_" | webc::metadata::annotations::WASI_RUNNER_URI => {
769 Ok(RunnerKind::Wasi)
770 }
771 "generic" => {
772 Ok(RunnerKind::Wasi)
774 }
775 "wcgi" | webc::metadata::annotations::WCGI_RUNNER_URI => Ok(RunnerKind::Wcgi),
776 "wasm4" | webc::metadata::annotations::WASM4_RUNNER_URI => Ok(RunnerKind::Wasm4),
777 other => {
778 if let Ok(other) = Url::parse(other) {
779 Ok(RunnerKind::Other(other))
780 } else if let Ok(other) = format!("https://webc.org/runner/{other}").parse() {
781 Ok(RunnerKind::Other(other))
783 } else {
784 Err(ManifestError::UnknownRunnerKind(other.to_string()))
785 }
786 }
787 }
788 }
789
790 fn uri(&self) -> &str {
791 match self {
792 RunnerKind::Wasi => webc::metadata::annotations::WASI_RUNNER_URI,
793 RunnerKind::Wcgi => webc::metadata::annotations::WCGI_RUNNER_URI,
794 RunnerKind::Wasm4 => webc::metadata::annotations::WASM4_RUNNER_URI,
795 RunnerKind::Other(other) => other.as_str(),
796 }
797 }
798
799 #[allow(deprecated)]
800 fn runner_specific_annotations(
801 &self,
802 annotations: &mut IndexMap<String, Value>,
803 module: &wasmer_config::package::ModuleReference,
804 package: Option<String>,
805 main_args: Option<Vec<String>>,
806 ) -> Result<(), ManifestError> {
807 let atom_annotation = match module {
808 wasmer_config::package::ModuleReference::CurrentPackage { module } => {
809 AtomAnnotation::new(module, None)
810 }
811 wasmer_config::package::ModuleReference::Dependency { dependency, module } => {
812 AtomAnnotation::new(module, dependency.to_string())
813 }
814 };
815 insert_annotation(annotations, AtomAnnotation::KEY, atom_annotation)?;
816
817 match self {
818 RunnerKind::Wasi | RunnerKind::Wcgi => {
819 let mut wasi = Wasi::new(module.to_string());
820 wasi.main_args = main_args;
821 wasi.package = package;
822 insert_annotation(annotations, Wasi::KEY, wasi)?;
823 }
824 RunnerKind::Wasm4 | RunnerKind::Other(_) => {
825 }
827 }
828
829 Ok(())
830 }
831}
832
833fn entrypoint(manifest: &WasmerManifest) -> Option<String> {
835 if let Some(package) = &manifest.package {
837 if let Some(entrypoint) = &package.entrypoint {
838 return Some(entrypoint.clone());
839 }
840 }
841
842 if let [only_command] = manifest.commands.as_slice() {
843 return Some(only_command.get_name().to_string());
846 }
847
848 None
849}
850
851fn transform_bindings(
852 manifest: &WasmerManifest,
853 base_dir: &Path,
854) -> Result<Vec<Binding>, ManifestError> {
855 transform_bindings_shared(
856 manifest,
857 |wit, module| transform_wit_bindings(wit, module, base_dir),
858 |wit, module| transform_wai_bindings(wit, module, base_dir),
859 )
860}
861
862fn transform_in_memory_bindings(manifest: &WasmerManifest) -> Result<Vec<Binding>, ManifestError> {
863 transform_bindings_shared(
864 manifest,
865 transform_in_memory_wit_bindings,
866 transform_in_memory_wai_bindings,
867 )
868}
869
870fn transform_bindings_shared(
871 manifest: &WasmerManifest,
872 wit_binding: impl Fn(
873 &wasmer_config::package::WitBindings,
874 &wasmer_config::package::Module,
875 ) -> Result<Binding, ManifestError>,
876 wai_binding: impl Fn(
877 &wasmer_config::package::WaiBindings,
878 &wasmer_config::package::Module,
879 ) -> Result<Binding, ManifestError>,
880) -> Result<Vec<Binding>, ManifestError> {
881 let mut bindings = Vec::new();
882
883 for module in &manifest.modules {
884 let b = match &module.bindings {
885 Some(wasmer_config::package::Bindings::Wit(wit)) => wit_binding(wit, module)?,
886 Some(wasmer_config::package::Bindings::Wai(wai)) => wai_binding(wai, module)?,
887 None => continue,
888 };
889 bindings.push(b);
890 }
891
892 Ok(bindings)
893}
894
895fn transform_wai_bindings(
896 wai: &wasmer_config::package::WaiBindings,
897 module: &wasmer_config::package::Module,
898 base_dir: &Path,
899) -> Result<Binding, ManifestError> {
900 transform_wai_bindings_shared(wai, module, |path| metadata_volume_uri(path, base_dir))
901}
902
903fn transform_in_memory_wai_bindings(
904 wai: &wasmer_config::package::WaiBindings,
905 module: &wasmer_config::package::Module,
906) -> Result<Binding, ManifestError> {
907 transform_wai_bindings_shared(wai, module, |path| {
908 Ok(format!("{METADATA_VOLUME}:/{}", sanitize_path(path)))
909 })
910}
911
912fn transform_wai_bindings_shared(
913 wai: &wasmer_config::package::WaiBindings,
914 module: &wasmer_config::package::Module,
915 metadata_volume_path: impl Fn(&PathBuf) -> Result<String, ManifestError>,
916) -> Result<Binding, ManifestError> {
917 let wasmer_config::package::WaiBindings {
918 wai_version,
919 exports,
920 imports,
921 } = wai;
922
923 let bindings = WaiBindings {
924 exports: exports.as_ref().map(&metadata_volume_path).transpose()?,
925 module: module.name.clone(),
926 imports: imports
927 .iter()
928 .map(metadata_volume_path)
929 .collect::<Result<Vec<_>, ManifestError>>()?,
930 };
931 let mut annotations = IndexMap::new();
932 insert_annotation(&mut annotations, "wai", bindings)?;
933
934 Ok(Binding {
935 name: "library-bindings".to_string(),
936 kind: format!("wai@{wai_version}"),
937 annotations: Value::Map(
938 annotations
939 .into_iter()
940 .map(|(k, v)| (Value::Text(k), v))
941 .collect(),
942 ),
943 })
944}
945
946fn metadata_volume_uri(path: &Path, base_dir: &Path) -> Result<String, ManifestError> {
947 make_relative_path(path, base_dir)
948 .map(sanitize_path)
949 .map(|p| format!("{METADATA_VOLUME}:/{p}"))
950}
951
952fn transform_wit_bindings(
953 wit: &wasmer_config::package::WitBindings,
954 module: &wasmer_config::package::Module,
955 base_dir: &Path,
956) -> Result<Binding, ManifestError> {
957 transform_wit_bindings_shared(wit, module, |path| metadata_volume_uri(path, base_dir))
958}
959
960fn transform_in_memory_wit_bindings(
961 wit: &wasmer_config::package::WitBindings,
962 module: &wasmer_config::package::Module,
963) -> Result<Binding, ManifestError> {
964 transform_wit_bindings_shared(wit, module, |path| {
965 Ok(format!("{METADATA_VOLUME}:/{}", sanitize_path(path)))
966 })
967}
968
969fn transform_wit_bindings_shared(
970 wit: &wasmer_config::package::WitBindings,
971 module: &wasmer_config::package::Module,
972 metadata_volume_path: impl Fn(&PathBuf) -> Result<String, ManifestError>,
973) -> Result<Binding, ManifestError> {
974 let wasmer_config::package::WitBindings {
975 wit_bindgen,
976 wit_exports,
977 } = wit;
978
979 let bindings = WitBindings {
980 exports: metadata_volume_path(wit_exports)?,
981 module: module.name.clone(),
982 };
983 let mut annotations = IndexMap::new();
984 insert_annotation(&mut annotations, "wit", bindings)?;
985
986 Ok(Binding {
987 name: "library-bindings".to_string(),
988 kind: format!("wit@{wit_bindgen}"),
989 annotations: Value::Map(
990 annotations
991 .into_iter()
992 .map(|(k, v)| (Value::Text(k), v))
993 .collect(),
994 ),
995 })
996}
997
998fn make_relative_path(path: &Path, base_dir: &Path) -> Result<PathBuf, ManifestError> {
1001 let absolute_path = base_dir.join(path);
1002
1003 match absolute_path.strip_prefix(base_dir) {
1004 Ok(p) => Ok(p.into()),
1005 Err(_) => Err(ManifestError::OutsideBaseDirectory {
1006 path: absolute_path,
1007 base_dir: base_dir.to_path_buf(),
1008 }),
1009 }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014 use tempfile::TempDir;
1015 use webc::metadata::annotations::Wasi;
1016
1017 use super::*;
1018
1019 #[test]
1020 fn custom_annotations_are_copied_across_verbatim() {
1021 let temp = TempDir::new().unwrap();
1022 let wasmer_toml = r#"
1023 [package]
1024 name = "test"
1025 version = "0.0.0"
1026 description = "asdf"
1027
1028 [[module]]
1029 name = "module"
1030 source = "file.wasm"
1031 abi = "wasi"
1032
1033 [[command]]
1034 name = "command"
1035 module = "module"
1036 runner = "asdf"
1037 annotations = { first = 42, second = ["a", "b"] }
1038 "#;
1039 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1040 std::fs::write(temp.path().join("file.wasm"), b"\0asm...").unwrap();
1041
1042 let (transformed, _) =
1043 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1044
1045 let command = &transformed.commands["command"];
1046 assert_eq!(command.annotation::<u32>("first").unwrap(), Some(42));
1047 assert_eq!(command.annotation::<String>("non-existent").unwrap(), None);
1048 insta::with_settings! {
1049 { description => wasmer_toml },
1050 { insta::assert_yaml_snapshot!(&transformed); }
1051 }
1052 }
1053
1054 #[test]
1055 fn transform_empty_manifest() {
1056 let temp = TempDir::new().unwrap();
1057 let wasmer_toml = r#"
1058 [package]
1059 name = "some/package"
1060 version = "0.0.0"
1061 description = "My awesome package"
1062 "#;
1063 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1064
1065 let (transformed, atoms) =
1066 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1067
1068 assert!(atoms.is_empty());
1069 insta::with_settings! {
1070 { description => wasmer_toml },
1071 { insta::assert_yaml_snapshot!(&transformed); }
1072 }
1073 }
1074
1075 #[test]
1076 fn transform_manifest_with_single_atom() {
1077 let temp = TempDir::new().unwrap();
1078 let wasmer_toml = r#"
1079 [package]
1080 name = "some/package"
1081 version = "0.0.0"
1082 description = "My awesome package"
1083
1084 [[module]]
1085 name = "first"
1086 source = "./path/to/file.wasm"
1087 abi = "wasi"
1088 "#;
1089 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1090 let dir = temp.path().join("path").join("to");
1091 std::fs::create_dir_all(&dir).unwrap();
1092 std::fs::write(dir.join("file.wasm"), b"\0asm...").unwrap();
1093
1094 let (transformed, atoms) =
1095 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1096
1097 assert_eq!(atoms.len(), 1);
1098 assert_eq!(atoms["first"].as_slice(), b"\0asm...");
1099 insta::with_settings! {
1100 { description => wasmer_toml },
1101 { insta::assert_yaml_snapshot!(&transformed); }
1102 }
1103 }
1104
1105 #[test]
1106 fn transform_manifest_with_atom_and_command() {
1107 let temp = TempDir::new().unwrap();
1108 let wasmer_toml = r#"
1109 [package]
1110 name = "some/package"
1111 version = "0.0.0"
1112 description = "My awesome package"
1113
1114 [[module]]
1115 name = "cpython"
1116 source = "python.wasm"
1117 abi = "wasi"
1118
1119 [[command]]
1120 name = "python"
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(), 1);
1131 let python = &transformed.commands["python"];
1132 assert_eq!(&python.runner, webc::metadata::annotations::WASI_RUNNER_URI);
1133 assert_eq!(python.wasi().unwrap().unwrap(), Wasi::new("cpython"));
1134 insta::with_settings! {
1135 { description => wasmer_toml },
1136 { insta::assert_yaml_snapshot!(&transformed); }
1137 }
1138 }
1139
1140 #[test]
1141 fn transform_manifest_with_multiple_commands() {
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 = "first"
1156 module = "cpython"
1157 runner = "wasi"
1158
1159 [[command]]
1160 name = "second"
1161 module = "cpython"
1162 runner = "wasi"
1163
1164 [[command]]
1165 name = "third"
1166 module = "cpython"
1167 runner = "wasi"
1168 "#;
1169 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1170 std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1171
1172 let (transformed, _) =
1173 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1174
1175 assert_eq!(transformed.commands.len(), 3);
1176 assert!(transformed.commands.contains_key("first"));
1177 assert!(transformed.commands.contains_key("second"));
1178 assert!(transformed.commands.contains_key("third"));
1179 insta::with_settings! {
1180 { description => wasmer_toml },
1181 { insta::assert_yaml_snapshot!(&transformed); }
1182 }
1183 }
1184
1185 #[test]
1186 fn merge_custom_attributes_with_builtin_ones() {
1187 let temp = TempDir::new().unwrap();
1188 let wasmer_toml = r#"
1189 [package]
1190 name = "some/package"
1191 version = "0.0.0"
1192 description = "My awesome package"
1193
1194 [[module]]
1195 name = "cpython"
1196 source = "python.wasm"
1197 abi = "wasi"
1198
1199 [[command]]
1200 name = "python"
1201 module = "cpython"
1202 runner = "wasi"
1203 annotations = { wasi = { env = ["KEY=val"]} }
1204 "#;
1205 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1206 std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1207
1208 let (transformed, _) =
1209 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1210
1211 assert_eq!(transformed.commands.len(), 1);
1212 let cmd = &transformed.commands["python"];
1213 assert_eq!(
1214 &cmd.wasi().unwrap().unwrap(),
1215 Wasi::new("cpython").with_env("KEY", "val")
1216 );
1217 insta::with_settings! {
1218 { description => wasmer_toml },
1219 { insta::assert_yaml_snapshot!(&transformed); }
1220 }
1221 }
1222
1223 #[test]
1224 fn transform_bash_manifest() {
1225 let temp = TempDir::new().unwrap();
1226 let wasmer_toml = r#"
1227 [package]
1228 name = "sharrattj/bash"
1229 version = "1.0.17"
1230 description = "Bash is a modern POSIX-compliant implementation of /bin/sh."
1231 license = "GNU"
1232 wasmer-extra-flags = "--enable-threads --enable-bulk-memory"
1233
1234 [dependencies]
1235 "sharrattj/coreutils" = "1.0.16"
1236
1237 [[module]]
1238 name = "bash"
1239 source = "bash.wasm"
1240 abi = "wasi"
1241
1242 [[command]]
1243 name = "bash"
1244 module = "bash"
1245 runner = "wasi@unstable_"
1246 "#;
1247 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1248 std::fs::write(temp.path().join("bash.wasm"), b"\0asm...").unwrap();
1249
1250 let (transformed, _) =
1251 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1252
1253 insta::with_settings! {
1254 { description => wasmer_toml },
1255 { insta::assert_yaml_snapshot!(&transformed); }
1256 }
1257 }
1258
1259 #[test]
1260 fn transform_wasmer_pack_manifest() {
1261 let temp = TempDir::new().unwrap();
1262 let wasmer_toml = r#"
1263 [package]
1264 name = "wasmer/wasmer-pack"
1265 version = "0.7.0"
1266 description = "The WebAssembly interface to wasmer-pack."
1267 license = "MIT"
1268 readme = "README.md"
1269 repository = "https://github.com/wasmerio/wasmer-pack"
1270 homepage = "https://wasmer.io/"
1271
1272 [[module]]
1273 name = "wasmer-pack-wasm"
1274 source = "wasmer_pack_wasm.wasm"
1275
1276 [module.bindings]
1277 wai-version = "0.2.0"
1278 exports = "wasmer-pack.exports.wai"
1279 imports = []
1280 "#;
1281 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1282 std::fs::write(temp.path().join("wasmer_pack_wasm.wasm"), b"\0asm...").unwrap();
1283 std::fs::write(temp.path().join("wasmer-pack.exports.wai"), b"").unwrap();
1284 std::fs::write(temp.path().join("README.md"), b"").unwrap();
1285
1286 let (transformed, _) =
1287 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1288
1289 insta::with_settings! {
1290 { description => wasmer_toml },
1291 { insta::assert_yaml_snapshot!(&transformed); }
1292 }
1293 }
1294
1295 #[test]
1296 fn transform_python_manifest() {
1297 let temp = TempDir::new().unwrap();
1298 let wasmer_toml = r#"
1299 [package]
1300 name = "python"
1301 version = "0.1.0"
1302 description = "Python is an interpreted, high-level, general-purpose programming language"
1303 license = "ISC"
1304 repository = "https://github.com/wapm-packages/python"
1305
1306 [[module]]
1307 name = "python"
1308 source = "bin/python.wasm"
1309 abi = "wasi"
1310
1311 [module.interfaces]
1312 wasi = "0.0.0-unstable"
1313
1314 [[command]]
1315 name = "python"
1316 module = "python"
1317
1318 [fs]
1319 lib = "lib"
1320 "#;
1321 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1322 let bin = temp.path().join("bin");
1323 std::fs::create_dir_all(&bin).unwrap();
1324 std::fs::write(bin.join("python.wasm"), b"\0asm...").unwrap();
1325
1326 let (transformed, _) =
1327 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1328
1329 insta::with_settings! {
1330 { description => wasmer_toml },
1331 { insta::assert_yaml_snapshot!(&transformed); }
1332 }
1333 }
1334
1335 #[test]
1336 fn transform_manifest_with_fs_table() {
1337 let temp = TempDir::new().unwrap();
1338 let wasmer_toml = r#"
1339 [package]
1340 name = "some/package"
1341 version = "0.0.0"
1342 description = "This is a package"
1343
1344 [fs]
1345 lib = "lib"
1346 "/public" = "out"
1347 "#;
1348 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1349 std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1350
1351 let (transformed, _) =
1352 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1353
1354 let fs = transformed.filesystem().unwrap().unwrap();
1355 assert_eq!(
1356 fs,
1357 [
1358 FileSystemMapping {
1359 from: None,
1360 volume_name: "/lib".to_string(),
1361 host_path: None,
1362 mount_path: "/lib".to_string(),
1363 },
1364 FileSystemMapping {
1365 from: None,
1366 volume_name: "/out".to_string(),
1367 host_path: None,
1368 mount_path: "/public".to_string(),
1369 }
1370 ]
1371 );
1372 insta::with_settings! {
1373 { description => wasmer_toml },
1374 { insta::assert_yaml_snapshot!(&transformed); }
1375 }
1376 }
1377
1378 #[test]
1379 fn missing_command_dependency() {
1380 let temp = TempDir::new().unwrap();
1381 let wasmer_toml = r#"
1382 [[command]]
1383 name = "python"
1384 module = "test/python:python"
1385 "#;
1386 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1387 let bin = temp.path().join("bin");
1388 std::fs::create_dir_all(&bin).unwrap();
1389 let res = wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict);
1390
1391 assert!(matches!(
1392 res,
1393 Err(ManifestError::UndeclaredCommandDependency { .. })
1394 ));
1395 }
1396
1397 #[test]
1398 fn issue_124_command_runner_is_swallowed() {
1399 let temp = TempDir::new().unwrap();
1400 let wasmer_toml = r#"
1401 [package]
1402 name = "wasmer-tests/wcgi-always-panic"
1403 version = "0.1.0"
1404 description = "wasmer-tests/wcgi-always-panic website"
1405
1406 [[module]]
1407 name = "wcgi-always-panic"
1408 source = "./wcgi-always-panic.wasm"
1409 abi = "wasi"
1410
1411 [[command]]
1412 name = "wcgi"
1413 module = "wcgi-always-panic"
1414 runner = "https://webc.org/runner/wcgi"
1415 "#;
1416 let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1417 std::fs::write(temp.path().join("wcgi-always-panic.wasm"), b"\0asm...").unwrap();
1418
1419 let (transformed, _) =
1420 wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1421
1422 let cmd = &transformed.commands["wcgi"];
1423 assert_eq!(cmd.runner, webc::metadata::annotations::WCGI_RUNNER_URI);
1424 assert_eq!(cmd.wasi().unwrap().unwrap(), Wasi::new("wcgi-always-panic"));
1425 insta::with_settings! {
1426 { description => wasmer_toml },
1427 { insta::assert_yaml_snapshot!(&transformed); }
1428 }
1429 }
1430}