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