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