use std::{
borrow::Cow,
collections::BTreeMap,
fmt::Debug,
fs::File,
io::{BufRead, BufReader},
path::{Path, PathBuf},
};
use anyhow::{Context, Error};
use bytes::Bytes;
use flate2::bufread::GzDecoder;
use shared_buffer::OwnedBuffer;
use tar::Archive;
use tempfile::TempDir;
use wasmer_config::package::Manifest as WasmerManifest;
use crate::{
metadata::{annotations::Wapm, Manifest as WebcManifest},
v3::{
write::{FileEntry, Writer},
ChecksumAlgorithm, Timestamps,
},
wasmer_package::{manifest::ManifestError, Strictness, Volume},
PathSegment,
};
#[derive(Debug, thiserror::Error)]
#[allow(clippy::result_large_err)]
#[non_exhaustive]
pub enum WasmerPackageError {
#[error("Unable to create a temporary directory")]
TempDir(#[source] std::io::Error),
#[error("Unable to open \"{}\"", path.display())]
FileOpen {
path: PathBuf,
#[source]
error: std::io::Error,
},
#[error("Unable to read \"{}\"", path.display())]
FileRead {
path: PathBuf,
#[source]
error: std::io::Error,
},
#[error("Unable to extract the tarball")]
Tarball(#[source] std::io::Error),
#[error("Unable to deserialize \"{}\"", path.display())]
DeserializeWasmerToml {
path: PathBuf,
#[source]
error: toml::de::Error,
},
#[error("Unable to find the \"wasmer.toml\"")]
MissingManifest,
#[error("Unable to get the absolute path for \"{}\"", path.display())]
Canonicalize {
path: PathBuf,
#[source]
error: std::io::Error,
},
#[error("Unable to load the \"wasmer.toml\" manifest")]
Manifest(#[from] ManifestError),
#[error("The manifest is invalid")]
Validation(#[from] wasmer_config::package::ValidationError),
#[error("Path: \"{}\" does not exist", path.display())]
PathNotExists {
path: PathBuf,
},
#[error("Volume creation failed: {0:?}")]
VolumeCreation(#[from] anyhow::Error),
#[error("serde error: {0:?}")]
SerdeError(#[from] serde_cbor::Error),
}
#[derive(Debug)]
pub struct Package {
#[allow(dead_code)]
base_dir: BaseDir,
manifest: WebcManifest,
atoms: BTreeMap<String, OwnedBuffer>,
strictness: Strictness,
volumes: BTreeMap<String, Volume>,
}
impl Package {
pub fn from_tarball_file(path: impl AsRef<Path>) -> Result<Self, WasmerPackageError> {
Package::from_tarball_file_with_strictness(path.as_ref(), Strictness::default())
}
pub fn from_tarball_file_with_strictness(
path: impl AsRef<Path>,
strictness: Strictness,
) -> Result<Self, WasmerPackageError> {
let path = path.as_ref();
let f = File::open(path).map_err(|error| WasmerPackageError::FileOpen {
path: path.to_path_buf(),
error,
})?;
Package::from_tarball_with_strictness(BufReader::new(f), strictness)
}
pub fn from_tarball(tarball: impl BufRead) -> Result<Self, WasmerPackageError> {
Package::from_tarball_with_strictness(tarball, Strictness::default())
}
pub fn from_tarball_with_strictness(
tarball: impl BufRead,
strictness: Strictness,
) -> Result<Self, WasmerPackageError> {
let tarball = GzDecoder::new(tarball);
let temp = tempdir().map_err(WasmerPackageError::TempDir)?;
let archive = Archive::new(tarball);
unpack_archive(archive, temp.path()).map_err(WasmerPackageError::Tarball)?;
let (manifest_path, manifest) = read_manifest(temp.path())?;
Package::load(manifest, manifest_path, temp, strictness)
}
pub fn from_manifest(wasmer_toml: impl AsRef<Path>) -> Result<Self, WasmerPackageError> {
Package::from_manifest_with_strictness(wasmer_toml, Strictness::default())
}
pub fn from_manifest_with_strictness(
wasmer_toml: impl AsRef<Path>,
strictness: Strictness,
) -> Result<Self, WasmerPackageError> {
let path = wasmer_toml.as_ref();
let path = path
.canonicalize()
.map_err(|error| WasmerPackageError::Canonicalize {
path: path.to_path_buf(),
error,
})?;
let wasmer_toml =
std::fs::read_to_string(&path).map_err(|error| WasmerPackageError::FileRead {
path: path.to_path_buf(),
error,
})?;
let wasmer_toml: WasmerManifest = toml::from_str(&wasmer_toml).map_err(|error| {
WasmerPackageError::DeserializeWasmerToml {
path: path.to_path_buf(),
error,
}
})?;
let base_dir = path
.parent()
.expect("Canonicalizing should always result in a file with a parent directory")
.to_path_buf();
for path in wasmer_toml.fs.values() {
if !base_dir.join(path).exists() {
return Err(WasmerPackageError::PathNotExists { path: path.clone() }.into());
}
}
Package::load(wasmer_toml, path.to_path_buf(), base_dir, strictness)
}
fn load(
wasmer_toml: WasmerManifest,
wasmer_toml_path: impl Into<PathBuf>,
base_dir: impl Into<BaseDir>,
strictness: Strictness,
) -> Result<Self, WasmerPackageError> {
let base_dir = base_dir.into();
let wasmer_toml_path = wasmer_toml_path.into();
if strictness.is_strict() {
wasmer_toml.validate()?;
}
let (mut manifest, atoms) = crate::wasmer_package::manifest::wasmer_manifest_to_webc(
&wasmer_toml,
base_dir.path(),
strictness,
)?;
if let Some(entry) = manifest.package.get_mut(Wapm::KEY) {
let mut wapm: Wapm = serde_cbor::value::from_value(entry.clone())?;
wapm.name.take();
wapm.version.take();
wapm.description.take();
*entry = serde_cbor::value::to_value(wapm)?;
};
let base_dir_path = base_dir.path().to_path_buf();
let metadata_volume =
Volume::new_metadata(&wasmer_toml, &wasmer_toml_path, base_dir_path.clone())?;
let mut volumes = Volume::new_assets(&wasmer_toml, base_dir_path.clone())?;
volumes.insert(metadata_volume.name().to_string(), metadata_volume);
Ok(Package {
base_dir,
manifest,
atoms,
strictness,
volumes,
})
}
pub fn webc_hash(&self) -> Option<[u8; 32]> {
None
}
pub fn manifest(&self) -> &WebcManifest {
&self.manifest
}
pub fn atoms(&self) -> &BTreeMap<String, OwnedBuffer> {
&self.atoms
}
pub fn volumes(&self) -> impl Iterator<Item = &Volume> {
self.volumes.values()
}
pub fn serialize(&self) -> Result<Bytes, Error> {
let mut w = Writer::new(ChecksumAlgorithm::Sha256)
.write_manifest(self.manifest())?
.write_atoms(self.atom_entries()?)?;
for (name, volume) in &self.volumes {
w.write_volume(name.as_str(), volume.as_directory_tree(self.strictness)?)?;
}
let serialized = w.finish(crate::v3::SignatureAlgorithm::None)?;
Ok(serialized)
}
fn atom_entries(&self) -> Result<BTreeMap<PathSegment, FileEntry<'_>>, Error> {
self.atoms()
.iter()
.map(|(key, value)| {
let filename = PathSegment::parse(key)
.with_context(|| format!("\"{key}\" isn't a valid atom name"))?;
Ok((filename, FileEntry::borrowed(value, Timestamps::default())))
})
.collect()
}
pub(crate) fn get_volume(&self, name: &str) -> Option<Volume> {
self.volumes.get(name).cloned()
}
pub(crate) fn volume_names(&self) -> Vec<Cow<'_, str>> {
self.volumes
.keys()
.map(|name| Cow::Borrowed(name.as_str()))
.collect()
}
}
const IS_WASI: bool = cfg!(all(target_family = "wasm", target_os = "wasi"));
fn tempdir() -> Result<TempDir, std::io::Error> {
if !IS_WASI {
return TempDir::new();
}
let temp_dir: PathBuf = std::env::var("TMPDIR")
.unwrap_or_else(|_| "/tmp".to_string())
.into();
if temp_dir.exists() {
TempDir::new_in(temp_dir)
} else {
if let Ok(current_exe) = std::env::current_exe() {
if let Some(parent) = current_exe.parent() {
if let Ok(temp) = TempDir::new_in(parent) {
return Ok(temp);
}
}
}
std::fs::create_dir_all(&temp_dir)?;
TempDir::new_in(temp_dir)
}
}
fn unpack_archive(
mut archive: Archive<impl std::io::Read>,
dest: &Path,
) -> Result<(), std::io::Error> {
cfg_if::cfg_if! {
if #[cfg(all(target_family = "wasm", target_os = "wasi"))]
{
for entry in archive.entries()? {
let mut entry = entry?;
let item_path = entry.path()?;
let full_path = resolve_archive_path(dest, &item_path);
match entry.header().entry_type() {
tar::EntryType::Directory => {
std::fs::create_dir_all(&full_path)?;
}
tar::EntryType::Regular => {
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut f = File::create(&full_path)?;
std::io::copy(&mut entry, &mut f)?;
let mtime = entry.header().mtime().unwrap_or_default();
if let Err(e) = set_timestamp(full_path.as_path(), mtime) {
println!("WARN: {e:?}");
}
}
_ => {}
}
}
Ok(())
} else {
return archive.unpack(dest);
}
}
}
#[cfg(all(target_family = "wasm", target_os = "wasi"))]
fn set_timestamp(path: &Path, timestamp: u64) -> Result<(), anyhow::Error> {
let fd = unsafe {
libc::open(
path.as_os_str().as_encoded_bytes().as_ptr() as _,
libc::O_RDONLY,
)
};
if fd < 0 {
anyhow::bail!(format!("failed to open: {}", path.display()));
}
let timespec = [
libc::timespec {
tv_sec: unsafe { libc::time(std::ptr::null_mut()) }, tv_nsec: 0,
},
libc::timespec {
tv_sec: timestamp as i64,
tv_nsec: 0,
},
];
let res = unsafe { libc::futimens(fd, timespec.as_ptr() as _) };
if res < 0 {
anyhow::bail!("failed to set timestamp for: {}", path.display());
}
Ok(())
}
#[cfg(all(target_family = "wasm", target_os = "wasi"))]
fn resolve_archive_path(base_dir: &Path, path: &Path) -> PathBuf {
let mut buffer = base_dir.to_path_buf();
for component in path.components() {
match component {
std::path::Component::Prefix(_)
| std::path::Component::RootDir
| std::path::Component::CurDir => continue,
std::path::Component::ParentDir => {
buffer.pop();
}
std::path::Component::Normal(segment) => {
buffer.push(segment);
}
}
}
buffer
}
fn read_manifest(base_dir: &Path) -> Result<(PathBuf, WasmerManifest), WasmerPackageError> {
for path in ["wasmer.toml", "wapm.toml"] {
let path = base_dir.join(path);
match std::fs::read_to_string(&path) {
Ok(s) => {
let toml_file = toml::from_str(&s).map_err({
let path = path.clone();
|error| WasmerPackageError::DeserializeWasmerToml { path, error }
})?;
return Ok((path, toml_file));
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => {
return Err(WasmerPackageError::FileRead { path, error });
}
}
}
Err(WasmerPackageError::MissingManifest)
}
#[derive(Debug)]
enum BaseDir {
Path(PathBuf),
Temp(TempDir),
}
impl BaseDir {
fn path(&self) -> &Path {
match self {
BaseDir::Path(p) => p.as_path(),
BaseDir::Temp(t) => t.path(),
}
}
}
impl From<TempDir> for BaseDir {
fn from(v: TempDir) -> Self {
Self::Temp(v)
}
}
impl From<PathBuf> for BaseDir {
fn from(v: PathBuf) -> Self {
Self::Path(v)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::{
metadata::{
annotations::{FileSystemMapping, VolumeSpecificPath},
Binding, BindingsExtended, WaiBindings, WitBindings,
},
Container, PathSegments,
};
use flate2::{write::GzEncoder, Compression};
use sha2::Digest;
use super::*;
#[test]
fn nonexistent_files() {
let temp = TempDir::new().unwrap();
assert!(Package::from_manifest(temp.path().join("nonexistent.toml")).is_err());
assert!(Package::from_tarball_file(temp.path().join("nonexistent.tar.gz")).is_err());
}
#[test]
fn load_a_tarball() {
let coreutils = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("wapm-targz-to-pirita")
.join("fixtures")
.join("coreutils-1.0.11.tar.gz");
assert!(coreutils.exists());
let package = Package::from_tarball_file(coreutils).unwrap();
let wapm = package.manifest().wapm().unwrap().unwrap();
assert!(wapm.name.is_none());
assert!(wapm.version.is_none());
assert!(wapm.description.is_none());
}
#[test]
fn tarball_with_no_manifest() {
let temp = TempDir::new().unwrap();
let empty_tarball = temp.path().join("empty.tar.gz");
let mut f = File::create(&empty_tarball).unwrap();
tar::Builder::new(GzEncoder::new(&mut f, Compression::fast()))
.finish()
.unwrap();
assert!(Package::from_tarball_file(&empty_tarball).is_err());
}
#[test]
fn empty_package_on_disk() {
let temp = TempDir::new().unwrap();
let manifest = temp.path().join("wasmer.toml");
std::fs::write(
&manifest,
r#"
[package]
name = "some/package"
version = "0.0.0"
description = "A dummy package"
"#,
)
.unwrap();
let package = Package::from_manifest(&manifest).unwrap();
let wapm = package.manifest().wapm().unwrap().unwrap();
assert!(wapm.name.is_none());
assert!(wapm.version.is_none());
assert!(wapm.description.is_none());
}
#[test]
fn load_old_cowsay() {
let tarball = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("wapm-to-webc")
.join("test")
.join("fixtures")
.join("cowsay-0.3.0.tar.gz");
let pkg = Package::from_tarball_file(tarball).unwrap();
insta::assert_yaml_snapshot!(pkg.manifest());
assert_eq!(
pkg.manifest.commands.keys().collect::<Vec<_>>(),
["cowsay", "cowthink"],
);
}
#[test]
fn serialize_package_with_non_existent_fs() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = "Test package"
[fs]
"/first" = "./first"
"#;
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, wasmer_toml).unwrap();
let error = Package::from_manifest(manifest).unwrap_err();
match error {
WasmerPackageError::PathNotExists { path } => {
assert_eq!(path, PathBuf::from_str("./first").unwrap());
}
e => panic!("unexpected error: {e:?}"),
}
}
#[test]
fn serialize_package_with_bundled_directories() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = "Test package"
[fs]
"/first" = "first"
second = "nested/dir"
"second/child" = "third"
empty = "empty"
"#;
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, wasmer_toml).unwrap();
let first = temp.path().join("first");
std::fs::create_dir_all(&first).unwrap();
std::fs::write(first.join("file.txt"), "File").unwrap();
let second = temp.path().join("nested").join("dir");
std::fs::create_dir_all(&second).unwrap();
std::fs::write(second.join("README.md"), "please").unwrap();
let another_dir = temp.path().join("nested").join("dir").join("another-dir");
std::fs::create_dir_all(&another_dir).unwrap();
std::fs::write(another_dir.join("empty.txt"), "").unwrap();
let third = temp.path().join("third");
std::fs::create_dir_all(&third).unwrap();
std::fs::write(third.join("file.txt"), "Hello, World!").unwrap();
let empty_dir = temp.path().join("empty");
std::fs::create_dir_all(empty_dir).unwrap();
let package = Package::from_manifest(manifest).unwrap();
let webc = package.serialize().unwrap();
let webc = Container::from_bytes(webc).unwrap();
let manifest = webc.manifest();
let wapm_metadata = manifest.wapm().unwrap().unwrap();
assert!(wapm_metadata.name.is_none());
assert!(wapm_metadata.version.is_none());
assert!(wapm_metadata.description.is_none());
let fs_table = manifest.filesystem().unwrap().unwrap();
assert_eq!(
fs_table,
[
FileSystemMapping {
from: None,
volume_name: "/first".to_string(),
host_path: None,
mount_path: "/first".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "/nested/dir".to_string(),
host_path: None,
mount_path: "/second".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "/third".to_string(),
host_path: None,
mount_path: "/second/child".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "/empty".to_string(),
host_path: None,
mount_path: "/empty".to_string(),
},
]
);
let first_file_hash: [u8; 32] = sha2::Sha256::digest(b"File").into();
let readme_hash: [u8; 32] = sha2::Sha256::digest(b"please").into();
let empty_hash: [u8; 32] = sha2::Sha256::digest(b"").into();
let third_file_hash: [u8; 32] = sha2::Sha256::digest(b"Hello, World!").into();
let first_volume = webc.get_volume("/first").unwrap();
assert_eq!(
first_volume.read_file("/file.txt").unwrap(),
(b"File".as_slice().into(), Some(first_file_hash)),
);
let nested_dir_volume = webc.get_volume("/nested/dir").unwrap();
assert_eq!(
nested_dir_volume.read_file("README.md").unwrap(),
(b"please".as_slice().into(), Some(readme_hash)),
);
assert_eq!(
nested_dir_volume
.read_file("/another-dir/empty.txt")
.unwrap(),
(b"".as_slice().into(), Some(empty_hash))
);
let third_volume = webc.get_volume("/third").unwrap();
assert_eq!(
third_volume.read_file("/file.txt").unwrap(),
(b"Hello, World!".as_slice().into(), Some(third_file_hash))
);
let empty_volume = webc.get_volume("/empty").unwrap();
assert_eq!(
empty_volume.read_dir("/").unwrap().len(),
0,
"Directories should be included, even if empty"
);
}
#[test]
fn serialize_package_with_metadata_files() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = "Test package"
readme = "README.md"
license-file = "LICENSE"
"#;
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, wasmer_toml).unwrap();
std::fs::write(temp.path().join("README.md"), "readme").unwrap();
std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
let serialized = Package::from_manifest(manifest)
.unwrap()
.serialize()
.unwrap();
let webc = Container::from_bytes(serialized).unwrap();
let metadata_volume = webc.get_volume("metadata").unwrap();
let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
assert_eq!(
metadata_volume.read_file("/README.md").unwrap(),
(b"readme".as_slice().into(), Some(readme_hash))
);
assert_eq!(
metadata_volume.read_file("/LICENSE").unwrap(),
(b"license".as_slice().into(), Some(license_hash))
);
}
#[test]
fn load_package_with_wit_bindings() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = ""
[[module]]
name = "my-lib"
source = "./my-lib.wasm"
abi = "none"
bindings = { wit-bindgen = "0.1.0", wit-exports = "./file.wit" }
"#;
std::fs::write(temp.path().join("wasmer.toml"), wasmer_toml).unwrap();
std::fs::write(temp.path().join("file.wit"), "file").unwrap();
std::fs::write(temp.path().join("my-lib.wasm"), b"\0asm...").unwrap();
let package = Package::from_manifest(temp.path().join("wasmer.toml"))
.unwrap()
.serialize()
.unwrap();
let webc = Container::from_bytes(package).unwrap();
assert_eq!(
webc.manifest().bindings,
vec![Binding {
name: "library-bindings".to_string(),
kind: "wit@0.1.0".to_string(),
annotations: serde_cbor::value::to_value(BindingsExtended::Wit(WitBindings {
exports: "metadata://file.wit".to_string(),
module: "my-lib".to_string(),
}))
.unwrap(),
}]
);
let metadata_volume = webc.get_volume("metadata").unwrap();
let file_hash: [u8; 32] = sha2::Sha256::digest(b"file").into();
assert_eq!(
metadata_volume.read_file("/file.wit").unwrap(),
(b"file".as_slice().into(), Some(file_hash))
);
insta::with_settings! {
{ description => wasmer_toml },
{ insta::assert_yaml_snapshot!(webc.manifest()); }
}
}
#[test]
fn load_package_with_wai_bindings() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = ""
[[module]]
name = "my-lib"
source = "./my-lib.wasm"
abi = "none"
bindings = { wai-version = "0.2.0", exports = "./file.wai", imports = ["a.wai", "b.wai"] }
"#;
std::fs::write(temp.path().join("wasmer.toml"), wasmer_toml).unwrap();
std::fs::write(temp.path().join("file.wai"), "file").unwrap();
std::fs::write(temp.path().join("a.wai"), "a").unwrap();
std::fs::write(temp.path().join("b.wai"), "b").unwrap();
std::fs::write(temp.path().join("my-lib.wasm"), b"\0asm...").unwrap();
let package = Package::from_manifest(temp.path().join("wasmer.toml"))
.unwrap()
.serialize()
.unwrap();
let webc = Container::from_bytes(package).unwrap();
assert_eq!(
webc.manifest().bindings,
vec![Binding {
name: "library-bindings".to_string(),
kind: "wai@0.2.0".to_string(),
annotations: serde_cbor::value::to_value(BindingsExtended::Wai(WaiBindings {
exports: Some("metadata://file.wai".to_string()),
module: "my-lib".to_string(),
imports: vec![
"metadata://a.wai".to_string(),
"metadata://b.wai".to_string(),
]
}))
.unwrap(),
}]
);
let metadata_volume = webc.get_volume("metadata").unwrap();
let file_hash: [u8; 32] = sha2::Sha256::digest(b"file").into();
let a_hash: [u8; 32] = sha2::Sha256::digest(b"a").into();
let b_hash: [u8; 32] = sha2::Sha256::digest(b"b").into();
assert_eq!(
metadata_volume.read_file("/file.wai").unwrap(),
(b"file".as_slice().into(), Some(file_hash))
);
assert_eq!(
metadata_volume.read_file("/a.wai").unwrap(),
(b"a".as_slice().into(), Some(a_hash))
);
assert_eq!(
metadata_volume.read_file("/b.wai").unwrap(),
(b"b".as_slice().into(), Some(b_hash))
);
insta::with_settings! {
{ description => wasmer_toml },
{ insta::assert_yaml_snapshot!(webc.manifest()); }
}
}
#[test]
fn absolute_paths_in_wasmer_toml_issue_105() {
let temp = TempDir::new().unwrap();
let base_dir = temp.path().canonicalize().unwrap();
let sep = std::path::MAIN_SEPARATOR;
let wasmer_toml = format!(
r#"
[package]
name = 'some/package'
version = '0.0.0'
description = 'Test package'
readme = '{BASE_DIR}{sep}README.md'
license-file = '{BASE_DIR}{sep}LICENSE'
[[module]]
name = 'first'
source = '{BASE_DIR}{sep}target{sep}debug{sep}package.wasm'
bindings = {{ wai-version = '0.2.0', exports = '{BASE_DIR}{sep}bindings{sep}file.wai', imports = ['{BASE_DIR}{sep}bindings{sep}a.wai'] }}
"#,
BASE_DIR = base_dir.display(),
);
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, &wasmer_toml).unwrap();
std::fs::write(temp.path().join("README.md"), "readme").unwrap();
std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
let bindings = temp.path().join("bindings");
std::fs::create_dir_all(&bindings).unwrap();
std::fs::write(bindings.join("file.wai"), "file.wai").unwrap();
std::fs::write(bindings.join("a.wai"), "a.wai").unwrap();
let target = temp.path().join("target").join("debug");
std::fs::create_dir_all(&target).unwrap();
std::fs::write(target.join("package.wasm"), b"\0asm...").unwrap();
let serialized = Package::from_manifest(manifest)
.unwrap()
.serialize()
.unwrap();
let webc = Container::from_bytes(serialized).unwrap();
let manifest = webc.manifest();
let wapm = manifest.wapm().unwrap().unwrap();
let lookup = |item: VolumeSpecificPath| {
let volume = webc.get_volume(&item.volume).unwrap();
let (contents, _) = volume.read_file(&item.path).unwrap();
String::from_utf8(contents.into()).unwrap()
};
assert_eq!(lookup(wapm.license_file.unwrap()), "license");
assert_eq!(lookup(wapm.readme.unwrap()), "readme");
let lookup = |item: &str| {
let (volume, path) = item.split_once(":/").unwrap();
let volume = webc.get_volume(volume).unwrap();
let (content, _) = volume.read_file(path).unwrap();
String::from_utf8(content.into()).unwrap()
};
let bindings = manifest.bindings[0].get_wai_bindings().unwrap();
assert_eq!(lookup(&bindings.imports[0]), "a.wai");
assert_eq!(lookup(bindings.exports.unwrap().as_str()), "file.wai");
let mut settings = insta::Settings::clone_current();
let base_dir = base_dir.display().to_string();
settings.set_description(wasmer_toml.replace(&base_dir, "[BASE_DIR]"));
let filter = regex::escape(&base_dir);
settings.add_filter(&filter, "[BASE_DIR]");
settings.bind(|| {
insta::assert_yaml_snapshot!(webc.manifest());
});
}
#[test]
fn serializing_will_skip_missing_metadata_by_default() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = 'some/package'
version = '0.0.0'
description = 'Test package'
readme = '/this/does/not/exist/README.md'
license-file = 'LICENSE.wtf'
"#;
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, wasmer_toml).unwrap();
let pkg = Package::from_manifest(manifest).unwrap();
let serialized = pkg.serialize().unwrap();
let webc = Container::from_bytes(serialized).unwrap();
let manifest = webc.manifest();
let wapm = manifest.wapm().unwrap().unwrap();
assert!(wapm.license_file.is_none());
assert!(wapm.readme.is_none());
let pkg = Package {
strictness: Strictness::Strict,
..pkg
};
assert!(pkg.serialize().is_err());
}
#[test]
fn serialize_package_without_local_base_fs_paths() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = "Test package"
readme = 'README.md'
license-file = 'LICENSE'
[fs]
"/path_in_wasix" = "local-dir/dir1"
"#;
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, wasmer_toml).unwrap();
std::fs::write(temp.path().join("README.md"), "readme").unwrap();
std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
let dir1 = temp.path().join("local-dir").join("dir1");
std::fs::create_dir_all(&dir1).unwrap();
let a = dir1.join("a");
let b = dir1.join("b");
std::fs::write(a, "a").unwrap();
std::fs::write(b, "b").unwrap();
let package = Package::from_manifest(manifest).unwrap();
let webc = package.serialize().unwrap();
let webc = Container::from_bytes(webc).unwrap();
let manifest = webc.manifest();
let wapm_metadata = manifest.wapm().unwrap().unwrap();
assert!(wapm_metadata.name.is_none());
assert!(wapm_metadata.version.is_none());
assert!(wapm_metadata.description.is_none());
let fs_table = manifest.filesystem().unwrap().unwrap();
assert_eq!(
fs_table,
[FileSystemMapping {
from: None,
volume_name: "/local-dir/dir1".to_string(),
host_path: None,
mount_path: "/path_in_wasix".to_string(),
},]
);
let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
let a_hash: [u8; 32] = sha2::Sha256::digest(b"a").into();
let b_hash: [u8; 32] = sha2::Sha256::digest(b"b").into();
let dir1_volume = webc.get_volume("/local-dir/dir1").unwrap();
let meta_volume = webc.get_volume("metadata").unwrap();
assert_eq!(
meta_volume.read_file("LICENSE").unwrap(),
(b"license".as_slice().into(), Some(license_hash)),
);
assert_eq!(
meta_volume.read_file("README.md").unwrap(),
(b"readme".as_slice().into(), Some(readme_hash)),
);
assert_eq!(
dir1_volume.read_file("a").unwrap(),
(b"a".as_slice().into(), Some(a_hash))
);
assert_eq!(
dir1_volume.read_file("b").unwrap(),
(b"b".as_slice().into(), Some(b_hash))
);
}
#[test]
fn serialize_package_with_nested_fs_entries_without_local_base_fs_paths() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = "Test package"
readme = 'README.md'
license-file = 'LICENSE'
[fs]
"/path_in_wasix" = "local-dir/dir1"
"#;
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, wasmer_toml).unwrap();
std::fs::write(temp.path().join("README.md"), "readme").unwrap();
std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
let local_dir = temp.path().join("local-dir");
std::fs::create_dir_all(&local_dir).unwrap();
let dir1 = local_dir.join("dir1");
std::fs::create_dir_all(&dir1).unwrap();
let dir2 = dir1.join("dir2");
std::fs::create_dir_all(&dir2).unwrap();
let a = dir2.join("a");
let b = dir1.join("b");
std::fs::write(a, "a").unwrap();
std::fs::write(b, "b").unwrap();
let package = Package::from_manifest(manifest).unwrap();
let webc = package.serialize().unwrap();
let webc = Container::from_bytes(webc).unwrap();
let manifest = webc.manifest();
let wapm_metadata = manifest.wapm().unwrap().unwrap();
assert!(wapm_metadata.name.is_none());
assert!(wapm_metadata.version.is_none());
assert!(wapm_metadata.description.is_none());
let fs_table = manifest.filesystem().unwrap().unwrap();
assert_eq!(
fs_table,
[FileSystemMapping {
from: None,
volume_name: "/local-dir/dir1".to_string(),
host_path: None,
mount_path: "/path_in_wasix".to_string(),
},]
);
let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
let a_hash: [u8; 32] = sha2::Sha256::digest(b"a").into();
let dir2_hash: [u8; 32] = sha2::Sha256::digest(a_hash).into();
let b_hash: [u8; 32] = sha2::Sha256::digest(b"b").into();
let dir1_volume = webc.get_volume("/local-dir/dir1").unwrap();
let meta_volume = webc.get_volume("metadata").unwrap();
assert_eq!(
meta_volume.read_file("LICENSE").unwrap(),
(b"license".as_slice().into(), Some(license_hash)),
);
assert_eq!(
meta_volume.read_file("README.md").unwrap(),
(b"readme".as_slice().into(), Some(readme_hash)),
);
assert_eq!(
dir1_volume
.read_dir("/")
.unwrap()
.into_iter()
.map(|(p, h, _)| (p, h))
.collect::<Vec<_>>(),
vec![
(PathSegment::parse("b").unwrap(), Some(b_hash)),
(PathSegment::parse("dir2").unwrap(), Some(dir2_hash))
]
);
assert_eq!(
dir1_volume
.read_dir("/dir2")
.unwrap()
.into_iter()
.map(|(p, h, _)| (p, h))
.collect::<Vec<_>>(),
vec![(PathSegment::parse("a").unwrap(), Some(a_hash))]
);
assert_eq!(
dir1_volume.read_file("/dir2/a").unwrap(),
(b"a".as_slice().into(), Some(a_hash))
);
assert_eq!(
dir1_volume.read_file("/b").unwrap(),
(b"b".as_slice().into(), Some(b_hash))
);
}
#[test]
fn serialize_package_mapped_to_same_dir_without_local_base_fs_paths() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = "Test package"
readme = 'README.md'
license-file = 'LICENSE'
[fs]
"/dir1" = "local-dir1/dir"
"/dir2" = "local-dir2/dir"
"#;
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, wasmer_toml).unwrap();
std::fs::write(temp.path().join("README.md"), "readme").unwrap();
std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
let dir1 = temp.path().join("local-dir1").join("dir");
std::fs::create_dir_all(&dir1).unwrap();
let dir2 = temp.path().join("local-dir2").join("dir");
std::fs::create_dir_all(&dir2).unwrap();
let package = Package::from_manifest(manifest).unwrap();
let webc = package.serialize().unwrap();
let webc = Container::from_bytes(webc).unwrap();
let manifest = webc.manifest();
let wapm_metadata = manifest.wapm().unwrap().unwrap();
assert!(wapm_metadata.name.is_none());
assert!(wapm_metadata.version.is_none());
assert!(wapm_metadata.description.is_none());
let fs_table = manifest.filesystem().unwrap().unwrap();
assert_eq!(
fs_table,
[
FileSystemMapping {
from: None,
volume_name: "/local-dir1/dir".to_string(),
host_path: None,
mount_path: "/dir1".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "/local-dir2/dir".to_string(),
host_path: None,
mount_path: "/dir2".to_string(),
},
]
);
let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
let dir1_volume = webc.get_volume("/local-dir1/dir").unwrap();
let dir2_volume = webc.get_volume("/local-dir2/dir").unwrap();
let meta_volume = webc.get_volume("metadata").unwrap();
assert_eq!(
meta_volume.read_file("LICENSE").unwrap(),
(b"license".as_slice().into(), Some(license_hash)),
);
assert_eq!(
meta_volume.read_file("README.md").unwrap(),
(b"readme".as_slice().into(), Some(readme_hash)),
);
assert!(dir1_volume.read_dir("/").unwrap().is_empty(),);
assert!(dir2_volume.read_dir("/").unwrap().is_empty(),);
}
#[test]
fn metadata_only_contains_relavent_files() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = ""
license-file = "./path/to/LICENSE"
readme = "README.md"
[[module]]
name = "asdf"
source = "asdf.wasm"
abi = "none"
bindings = { wai-version = "0.2.0", exports = "asdf.wai", imports = ["browser.wai"] }
"#;
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, wasmer_toml).unwrap();
let license_dir = temp.path().join("path").join("to");
std::fs::create_dir_all(&license_dir).unwrap();
std::fs::write(license_dir.join("LICENSE"), "license").unwrap();
std::fs::write(temp.path().join("README.md"), "readme").unwrap();
std::fs::write(temp.path().join("asdf.wasm"), b"\0asm...").unwrap();
std::fs::write(temp.path().join("asdf.wai"), "exports").unwrap();
std::fs::write(temp.path().join("browser.wai"), "imports").unwrap();
std::fs::write(temp.path().join("unwanted_file.txt"), "unwanted_file").unwrap();
let package = Package::from_manifest(manifest).unwrap();
let contents: Vec<_> = package
.get_volume("metadata")
.unwrap()
.read_dir(&PathSegments::ROOT)
.unwrap()
.into_iter()
.map(|(path, _, _)| path)
.collect();
assert_eq!(
contents,
vec![
PathSegment::parse("README.md").unwrap(),
PathSegment::parse("asdf.wai").unwrap(),
PathSegment::parse("browser.wai").unwrap(),
PathSegment::parse("path").unwrap(),
PathSegment::parse("wasmer.toml").unwrap(),
]
);
}
}