//! High-level interactions with the WAPM backend. //! //! The GraphQL schema can be updated by running `make` in the Wasmer repo's //! root directory. //! //! ```console //! $ make update-graphql-schema //! curl -sSfL https://registry.wapm.io/graphql/schema.graphql > lib/registry/graphql/schema.graphql //! ``` use anyhow::Context; use core::ops::Range; use reqwest::header::{ACCEPT, RANGE}; use std::fmt; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::time::Duration; use url::Url; pub mod config; pub mod graphql; pub mod interface; pub mod login; pub mod package; pub mod publish; pub mod queries; pub mod utils; pub use crate::{ config::{format_graphql, WasmerConfig}, package::Package, queries::get_bindings_query::ProgrammingLanguage, }; pub static PACKAGE_TOML_FILE_NAME: &str = "wasmer.toml"; pub static PACKAGE_TOML_FALLBACK_NAME: &str = "wapm.toml"; pub static GLOBAL_CONFIG_FILE_NAME: &str = "wasmer.toml"; #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] pub struct PackageDownloadInfo { pub registry: String, pub package: String, pub version: String, pub is_latest_version: bool, pub commands: String, pub manifest: String, pub url: String, pub pirita_url: Option, } pub fn get_package_local_dir(wasmer_dir: &Path, url: &str, version: &str) -> Option { let checkouts_dir = get_checkouts_dir(wasmer_dir); let url_hash = Package::hash_url(url); let dir = checkouts_dir.join(format!("{url_hash}@{version}")); Some(dir) } pub fn try_finding_local_command(wasmer_dir: &Path, cmd: &str) -> Option { let local_packages = get_all_local_packages(wasmer_dir); for p in local_packages { let commands = p.get_commands(); if commands.unwrap_or_default().iter().any(|c| c == cmd) { return Some(p); } } None } #[derive(Debug, Clone)] pub struct LocalPackage { pub registry: String, pub name: String, pub version: String, pub path: PathBuf, } impl fmt::Display for LocalPackage { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}@{} (registry {}, located at {})", self.name, self.version, self.registry, self.path.display() ) } } impl LocalPackage { pub fn get_path(&self) -> Result { Ok(self.path.clone()) } /// Returns the wasmer.toml path if it exists pub fn get_wasmer_toml_path(base_path: &Path) -> Result { let path = base_path.join(PACKAGE_TOML_FILE_NAME); let fallback_path = base_path.join(PACKAGE_TOML_FALLBACK_NAME); if path.exists() { Ok(path) } else if fallback_path.exists() { Ok(fallback_path) } else { Err(anyhow::anyhow!( "neither {} nor {} exists", path.display(), fallback_path.display() )) } } /// Reads the wasmer.toml fron $PATH with wapm.toml as a fallback pub fn read_toml(base_path: &Path) -> Result { let wasmer_toml = std::fs::read_to_string(base_path.join(PACKAGE_TOML_FILE_NAME)) .or_else(|_| std::fs::read_to_string(base_path.join(PACKAGE_TOML_FALLBACK_NAME))) .map_err(|_| { format!( "Path {} has no {PACKAGE_TOML_FILE_NAME} or {PACKAGE_TOML_FALLBACK_NAME}", base_path.display() ) })?; let wasmer_toml = toml::from_str::(&wasmer_toml) .map_err(|e| format!("Could not parse toml for {:?}: {e}", base_path.display()))?; Ok(wasmer_toml) } pub fn get_commands(&self) -> Result, String> { let path = self.get_path()?; let toml_parsed = Self::read_toml(&path)?; Ok(toml_parsed .command .unwrap_or_default() .iter() .map(|c| c.get_name()) .collect()) } } /// Returns the (manifest, .wasm file name), given a package dir pub fn get_executable_file_from_path( package_dir: &Path, command: Option<&str>, ) -> Result<(wasmer_toml::Manifest, PathBuf), anyhow::Error> { let wasmer_toml = LocalPackage::read_toml(package_dir).map_err(|e| anyhow::anyhow!("{e}"))?; let name = wasmer_toml.package.name.clone(); let version = wasmer_toml.package.version.clone(); let commands = wasmer_toml.command.clone().unwrap_or_default(); let entrypoint_module = match command { Some(s) => commands.iter().find(|c| c.get_name() == s).ok_or_else(|| { anyhow::anyhow!("Cannot run {name}@{version}: package has no command {s:?}") })?, None => { if commands.is_empty() { Err(anyhow::anyhow!( "Cannot run {name}@{version}: package has no commands" )) } else if commands.len() == 1 { Ok(&commands[0]) } else { Err(anyhow::anyhow!(" -> wasmer run {name}@{version} --command-name={0}", commands.first().map(|f| f.get_name()).unwrap())) .context(anyhow::anyhow!("{}", commands.iter().map(|c| format!("`{}`", c.get_name())).collect::>().join(", "))) .context(anyhow::anyhow!("You can run any of those by using the --command-name=COMMAND flag")) .context(anyhow::anyhow!("The `{name}@{version}` package doesn't have a default entrypoint, but has multiple available commands:")) }? } }; let module_name = entrypoint_module.get_module(); let modules = wasmer_toml.module.clone().unwrap_or_default(); let entrypoint_module = modules .iter() .find(|m| m.name == module_name) .ok_or_else(|| { anyhow::anyhow!( "Cannot run {name}@{version}: module {module_name} not found in {GLOBAL_CONFIG_FILE_NAME}" ) })?; let entrypoint_source = package_dir.join(&entrypoint_module.source); Ok((wasmer_toml, entrypoint_source)) } fn get_all_names_in_dir(dir: &PathBuf) -> Vec<(PathBuf, String)> { if !dir.is_dir() { return Vec::new(); } let read_dir = match std::fs::read_dir(dir) { Ok(o) => o, Err(_) => return Vec::new(), }; let entries = read_dir .map(|res| res.map(|e| e.path())) .collect::, std::io::Error>>(); let registry_entries = match entries { Ok(o) => o, Err(_) => return Vec::new(), }; registry_entries .into_iter() .filter_map(|re| Some((re.clone(), re.file_name()?.to_str()?.to_string()))) .collect() } /// Returns a list of all locally installed packages pub fn get_all_local_packages(wasmer_dir: &Path) -> Vec { let mut packages = Vec::new(); let checkouts_dir = get_checkouts_dir(wasmer_dir); for (path, url_hash_with_version) in get_all_names_in_dir(&checkouts_dir) { let manifest = match LocalPackage::read_toml(&path) { Ok(o) => o, Err(_) => continue, }; let url_hash = match url_hash_with_version.split('@').next() { Some(s) => s, None => continue, }; let package = Url::parse(&Package::unhash_url(url_hash)).map(|s| s.origin().ascii_serialization()); let host = match package { Ok(s) => s, Err(_) => continue, }; packages.push(LocalPackage { registry: host, name: manifest.package.name, version: manifest.package.version.to_string(), path, }); } packages } pub fn get_local_package( wasmer_dir: &Path, name: &str, version: Option<&str>, ) -> Option { get_all_local_packages(wasmer_dir) .iter() .find(|p| { if p.name != name { return false; } if let Some(v) = version { if p.version != v { return false; } } true }) .cloned() } pub fn query_command_from_registry( registry_url: &str, command_name: &str, ) -> Result { use crate::{ graphql::execute_query, queries::{get_package_by_command_query, GetPackageByCommandQuery}, }; use graphql_client::GraphQLQuery; let q = GetPackageByCommandQuery::build_query(get_package_by_command_query::Variables { command_name: command_name.to_string(), }); let response: get_package_by_command_query::ResponseData = execute_query(registry_url, "", &q) .map_err(|e| format!("Error sending GetPackageByCommandQuery: {e}"))?; let command = response .get_command .ok_or_else(|| "GetPackageByCommandQuery: no get_command".to_string())?; let package = command.package_version.package.display_name; let version = command.package_version.version; let url = command.package_version.distribution.download_url; let pirita_url = command.package_version.distribution.pirita_download_url; Ok(PackageDownloadInfo { registry: registry_url.to_string(), package, version, is_latest_version: command.package_version.is_last_version, manifest: command.package_version.manifest, commands: command_name.to_string(), url, pirita_url, }) } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] pub enum QueryPackageError { ErrorSendingQuery(String), NoPackageFound { name: String, version: Option, }, } impl fmt::Display for QueryPackageError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { QueryPackageError::ErrorSendingQuery(q) => write!(f, "error sending query: {q}"), QueryPackageError::NoPackageFound { name, version } => { write!(f, "no package found for {name:?} (version = {version:?})") } } } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] pub enum GetIfPackageHasNewVersionResult { // if version = Some(...) and the ~/.wasmer/checkouts/.../{version} exists, the package is already installed UseLocalAlreadyInstalled { registry_host: String, namespace: String, name: String, version: String, path: PathBuf, }, // if version = None, check for the latest version LocalVersionMayBeOutdated { registry_host: String, namespace: String, name: String, /// Versions that are already installed + whether they are /// older (true) or younger (false) than the timeout installed_versions: Vec<(String, bool)>, }, // registry / namespace / name / version doesn't exist yet PackageNotInstalledYet { registry_url: String, namespace: String, name: String, version: Option, }, } /// Returns the download info of the packages, on error returns all the available packages /// i.e. (("foo/python", "wapm.io"), ("bar/python" "wapm.io"))) pub fn query_package_from_registry( registry_url: &str, name: &str, version: Option<&str>, ) -> Result { use crate::{ graphql::execute_query, queries::{get_package_version_query, GetPackageVersionQuery}, }; use graphql_client::GraphQLQuery; let q = GetPackageVersionQuery::build_query(get_package_version_query::Variables { name: name.to_string(), version: version.map(|s| s.to_string()), }); let response: get_package_version_query::ResponseData = execute_query(registry_url, "", &q) .map_err(|e| { QueryPackageError::ErrorSendingQuery(format!("Error sending GetPackagesQuery: {e}")) })?; let v = response.package_version.as_ref().ok_or_else(|| { QueryPackageError::ErrorSendingQuery(format!("no package version for {name:?}")) })?; let manifest = toml::from_str::(&v.manifest).map_err(|e| { QueryPackageError::ErrorSendingQuery(format!("Invalid manifest for crate {name:?}: {e}")) })?; Ok(PackageDownloadInfo { registry: registry_url.to_string(), package: v.package.name.clone(), version: v.version.clone(), is_latest_version: v.is_last_version, manifest: v.manifest.clone(), commands: manifest .command .unwrap_or_default() .iter() .map(|s| s.get_name()) .collect::>() .join(", "), url: v.distribution.download_url.clone(), pirita_url: v.distribution.pirita_download_url.clone(), }) } pub fn get_checkouts_dir(wasmer_dir: &Path) -> PathBuf { wasmer_dir.join("checkouts") } pub fn get_webc_dir(wasmer_dir: &Path) -> PathBuf { wasmer_dir.join("webc") } /// Convenience function that will unpack .tar.gz files and .tar.bz /// files to a target directory (does NOT remove the original .tar.gz) pub fn try_unpack_targz>( target_targz_path: P, target_path: P, strip_toplevel: bool, ) -> Result { let target_targz_path = target_targz_path.as_ref(); let target_path = target_path.as_ref(); let open_file = || { std::fs::File::open(&target_targz_path) .map_err(|e| anyhow::anyhow!("failed to open {}: {e}", target_targz_path.display())) }; let try_decode_gz = || { let file = open_file()?; let gz_decoded = flate2::read::GzDecoder::new(&file); let mut ar = tar::Archive::new(gz_decoded); if strip_toplevel { unpack_sans_parent(ar, target_path).map_err(|e| { anyhow::anyhow!("failed to unpack {}: {e}", target_targz_path.display()) }) } else { ar.unpack(target_path).map_err(|e| { anyhow::anyhow!("failed to unpack {}: {e}", target_targz_path.display()) }) } }; let try_decode_xz = || { let file = open_file()?; let mut decomp: Vec = Vec::new(); let mut bufread = std::io::BufReader::new(&file); lzma_rs::xz_decompress(&mut bufread, &mut decomp).map_err(|e| { anyhow::anyhow!("failed to unpack {}: {e}", target_targz_path.display()) })?; let cursor = std::io::Cursor::new(decomp); let mut ar = tar::Archive::new(cursor); if strip_toplevel { unpack_sans_parent(ar, target_path).map_err(|e| { anyhow::anyhow!("failed to unpack {}: {e}", target_targz_path.display()) }) } else { ar.unpack(target_path).map_err(|e| { anyhow::anyhow!("failed to unpack {}: {e}", target_targz_path.display()) }) } }; try_decode_gz().or_else(|_| try_decode_xz())?; Ok(target_targz_path.to_path_buf()) } /// Whether the top-level directory should be stripped pub fn download_and_unpack_targz( url: &str, target_path: &Path, strip_toplevel: bool, ) -> Result { let tempdir = tempdir::TempDir::new("wasmer-download-targz")?; let target_targz_path = tempdir.path().join("package.tar.gz"); let mut resp = reqwest::blocking::get(url) .map_err(|e| anyhow::anyhow!("failed to download {url}: {e}"))?; { let mut file = std::fs::File::create(&target_targz_path).map_err(|e| { anyhow::anyhow!( "failed to download {url} into {}: {e}", target_targz_path.display() ) })?; resp.copy_to(&mut file) .map_err(|e| anyhow::anyhow!("{e}"))?; } try_unpack_targz(target_targz_path.as_path(), target_path, strip_toplevel) .with_context(|| anyhow::anyhow!("Could not download {url}"))?; Ok(target_path.to_path_buf()) } pub fn unpack_sans_parent(mut archive: tar::Archive, dst: &Path) -> std::io::Result<()> where R: std::io::Read, { use std::path::Component::Normal; for entry in archive.entries()? { let mut entry = entry?; let path: PathBuf = entry .path()? .components() .skip(1) // strip top-level directory .filter(|c| matches!(c, Normal(_))) // prevent traversal attacks .collect(); entry.unpack(dst.join(path))?; } Ok(()) } /// Installs the .tar.gz if it doesn't yet exist, returns the /// (package dir, entrypoint .wasm file path) pub fn install_package(wasmer_dir: &Path, url: &Url) -> Result { use fs_extra::dir::copy; let tempdir = tempdir::TempDir::new("download") .map_err(|e| anyhow::anyhow!("could not create download temp dir: {e}"))?; let target_targz_path = tempdir.path().join("package.tar.gz"); let unpacked_targz_path = tempdir.path().join("package"); std::fs::create_dir_all(&unpacked_targz_path).map_err(|e| { anyhow::anyhow!( "could not create dir {}: {e}", unpacked_targz_path.display() ) })?; get_targz_bytes(url, None, Some(target_targz_path.clone())) .map_err(|e| anyhow::anyhow!("failed to download {url}: {e}"))?; try_unpack_targz( target_targz_path.as_path(), unpacked_targz_path.as_path(), false, ) .with_context(|| anyhow::anyhow!("Could not unpack file downloaded from {url}"))?; // read {unpacked}/wasmer.toml to get the name + version number let toml_parsed = LocalPackage::read_toml(&unpacked_targz_path) .map_err(|e| anyhow::anyhow!("error reading package name / version number: {e}"))?; let version = toml_parsed.package.version.to_string(); let checkouts_dir = crate::get_checkouts_dir(wasmer_dir); let installation_path = checkouts_dir.join(format!("{}@{version}", Package::hash_url(url.as_ref()))); std::fs::create_dir_all(&installation_path) .map_err(|e| anyhow::anyhow!("could not create installation path for {url}: {e}"))?; let mut options = fs_extra::dir::CopyOptions::new(); options.content_only = true; options.overwrite = true; copy(&unpacked_targz_path, &installation_path, &options)?; #[cfg(not(target_os = "wasi"))] let _ = filetime::set_file_mtime( LocalPackage::get_wasmer_toml_path(&installation_path)?, filetime::FileTime::now(), ); Ok(installation_path) } pub fn whoami( wasmer_dir: &Path, registry: Option<&str>, token: Option<&str>, ) -> Result<(String, String), anyhow::Error> { use crate::queries::{who_am_i_query, WhoAmIQuery}; use graphql_client::GraphQLQuery; let config = WasmerConfig::from_file(wasmer_dir); let config = config .map_err(|e| anyhow::anyhow!("{e}")) .with_context(|| format!("{registry:?}"))?; let registry = match registry { Some(s) => format_graphql(s), None => config.registry.get_current_registry(), }; let login_token = token .map(|s| s.to_string()) .or_else(|| config.registry.get_login_token_for_registry(®istry)) .ok_or_else(|| anyhow::anyhow!("not logged into registry {:?}", registry))?; let q = WhoAmIQuery::build_query(who_am_i_query::Variables {}); let response: who_am_i_query::ResponseData = crate::graphql::execute_query(®istry, &login_token, &q) .with_context(|| format!("{registry:?}"))?; let username = response .viewer .as_ref() .ok_or_else(|| anyhow::anyhow!("not logged into registry {:?}", registry))? .username .to_string(); Ok((registry, username)) } pub fn test_if_registry_present(registry: &str) -> Result { use crate::queries::{test_if_registry_present, TestIfRegistryPresent}; use graphql_client::GraphQLQuery; let q = TestIfRegistryPresent::build_query(test_if_registry_present::Variables {}); crate::graphql::execute_query_modifier_inner_check_json( registry, "", &q, Some(Duration::from_secs(1)), |f| f, ) .map_err(|e| format!("{e}"))?; Ok(true) } pub fn get_all_available_registries(wasmer_dir: &Path) -> Result, String> { let config = WasmerConfig::from_file(wasmer_dir)?; let mut registries = Vec::new(); for login in config.registry.tokens { registries.push(format_graphql(&login.registry)); } Ok(registries) } #[derive(Debug, PartialEq, Clone)] pub struct RemoteWebcInfo { pub checksum: String, pub manifest: webc::Manifest, } pub fn install_webc_package( wasmer_dir: &Path, url: &Url, checksum: &str, ) -> Result<(), anyhow::Error> { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(async { install_webc_package_inner(wasmer_dir, url, checksum).await }) } async fn install_webc_package_inner( wasmer_dir: &Path, url: &Url, checksum: &str, ) -> Result<(), anyhow::Error> { use futures_util::StreamExt; let path = get_webc_dir(wasmer_dir); let _ = std::fs::create_dir_all(&path); let webc_path = path.join(checksum); let mut file = std::fs::File::create(&webc_path) .map_err(|e| anyhow::anyhow!("{e}")) .context(anyhow::anyhow!("{}", webc_path.display()))?; let client = { let builder = reqwest::Client::builder(); let builder = crate::graphql::proxy::maybe_set_up_proxy(builder)?; builder .redirect(reqwest::redirect::Policy::limited(10)) .build() .map_err(|e| anyhow::anyhow!("{e}")) .context("install_webc_package: failed to build reqwest Client")? }; let res = client .get(url.clone()) .header(ACCEPT, "application/webc") .send() .await .and_then(|response| response.error_for_status()) .map_err(|e| anyhow::anyhow!("{e}")) .context(anyhow::anyhow!("install_webc_package: failed to GET {url}"))?; let mut stream = res.bytes_stream(); while let Some(item) = stream.next().await { let item = item .map_err(|e| anyhow::anyhow!("{e}")) .context(anyhow::anyhow!("install_webc_package: failed to GET {url}"))?; file.write_all(&item) .map_err(|e| anyhow::anyhow!("{e}")) .context(anyhow::anyhow!( "install_webc_package: failed to write chunk to {}", webc_path.display() ))?; } Ok(()) } /// Returns a list of all installed webc packages pub fn get_all_installed_webc_packages(wasmer_dir: &Path) -> Vec { get_all_installed_webc_packages_inner(wasmer_dir) } fn get_all_installed_webc_packages_inner(wasmer_dir: &Path) -> Vec { let dir = get_webc_dir(wasmer_dir); let read_dir = match std::fs::read_dir(dir) { Ok(s) => s, Err(_) => return Vec::new(), }; read_dir .filter_map(|r| Some(r.ok()?.path())) .filter_map(|path| { webc::WebCMmap::parse( path, &webc::ParseOptions { parse_atoms: false, parse_volumes: false, ..Default::default() }, ) .ok() }) .filter_map(|webc| { let checksum = webc.checksum.as_ref().map(|s| &s.data)?.to_vec(); let hex_string = get_checksum_hash(&checksum); Some(RemoteWebcInfo { checksum: hex_string, manifest: webc.manifest.clone(), }) }) .collect() } /// The checksum of the webc file has a bunch of zeros at the end /// (it's currently encoded that way in the webc format). This function /// strips the zeros because otherwise the filename would become too long. /// /// So: /// /// `3ea47cb0000000000000` -> `3ea47cb` /// pub fn get_checksum_hash(bytes: &[u8]) -> String { let mut checksum = bytes.to_vec(); while checksum.last().copied() == Some(0) { checksum.pop(); } hex::encode(&checksum).chars().take(64).collect() } /// Returns the checksum of the .webc file, so that we can check whether the /// file is already installed before downloading it pub fn get_remote_webc_checksum(url: &Url) -> Result { let request_max_bytes = webc::WebC::get_signature_offset_start() + 4 + 1024 + 8 + 8; let data = get_webc_bytes(url, Some(0..request_max_bytes), None) .with_context(|| anyhow::anyhow!("note: use --registry to change the registry URL"))? .unwrap(); let checksum = webc::WebC::get_checksum_bytes(&data) .map_err(|e| anyhow::anyhow!("{e}"))? .to_vec(); Ok(get_checksum_hash(&checksum)) } /// Before fetching the entire file from a remote URL, just fetch the manifest /// so we can see if the package has already been installed pub fn get_remote_webc_manifest(url: &Url) -> Result { // Request up unti manifest size / manifest len let request_max_bytes = webc::WebC::get_signature_offset_start() + 4 + 1024 + 8 + 8; let data = get_webc_bytes(url, Some(0..request_max_bytes), None)?.unwrap(); let checksum = webc::WebC::get_checksum_bytes(&data) .map_err(|e| anyhow::anyhow!("{e}")) .context("WebC::get_checksum_bytes failed")? .to_vec(); let hex_string = get_checksum_hash(&checksum); let (manifest_start, manifest_len) = webc::WebC::get_manifest_offset_size(&data) .map_err(|e| anyhow::anyhow!("{e}")) .context("WebC::get_manifest_offset_size failed")?; let data_with_manifest = get_webc_bytes(url, Some(0..manifest_start + manifest_len), None)?.unwrap(); let manifest = webc::WebC::get_manifest(&data_with_manifest) .map_err(|e| anyhow::anyhow!("{e}")) .context("WebC::get_manifest failed")?; Ok(RemoteWebcInfo { checksum: hex_string, manifest, }) } fn setup_client( url: &Url, application_type: &'static str, ) -> Result { let client = { let builder = reqwest::blocking::Client::builder(); let builder = crate::graphql::proxy::maybe_set_up_proxy_blocking(builder) .context("setup_webc_client")?; builder .redirect(reqwest::redirect::Policy::limited(10)) .build() .map_err(|e| anyhow::anyhow!("{e}")) .context("setup_webc_client: builder.build() failed")? }; Ok(client.get(url.clone()).header(ACCEPT, application_type)) } fn get_webc_bytes( url: &Url, range: Option>, stream_response_into: Option, ) -> Result>, anyhow::Error> { get_bytes(url, range, "application/webc", stream_response_into) } fn get_targz_bytes( url: &Url, range: Option>, stream_response_into: Option, ) -> Result>, anyhow::Error> { get_bytes(url, range, "application/tar+gzip", stream_response_into) } fn get_bytes( url: &Url, range: Option>, application_type: &'static str, stream_response_into: Option, ) -> Result>, anyhow::Error> { // curl -r 0-500 -L https://wapm.dev/syrusakbary/python -H "Accept: application/webc" --output python.webc let mut res = setup_client(url, application_type)?; if let Some(range) = range.as_ref() { res = res.header(RANGE, format!("bytes={}-{}", range.start, range.end)); } let mut res = res .send() .map_err(|e| anyhow::anyhow!("{e}")) .context("send() failed")?; if res.status().is_redirection() { return Err(anyhow::anyhow!("redirect: {:?}", res.status())); } if res.status().is_server_error() { return Err(anyhow::anyhow!("server error: {:?}", res.status())); } if res.status().is_client_error() { return Err(anyhow::anyhow!("client error: {:?}", res.status())); } if let Some(path) = stream_response_into.as_ref() { let mut file = std::fs::File::create(&path).map_err(|e| { anyhow::anyhow!("failed to download {url} into {}: {e}", path.display()) })?; res.copy_to(&mut file) .map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| { anyhow::anyhow!("failed to download {url} into {}: {e}", path.display()) })?; if application_type == "application/webc" { let mut buf = vec![0; 100]; file.read_exact(&mut buf) .map_err(|e| anyhow::anyhow!("invalid webc downloaded from {url}: {e}"))?; if buf[0..webc::MAGIC.len()] != webc::MAGIC[..] { let first_100_bytes = String::from_utf8_lossy(&buf); return Err(anyhow::anyhow!("invalid webc bytes: {first_100_bytes:?}")); } } Ok(None) } else { let bytes = res .bytes() .map_err(|e| anyhow::anyhow!("{e}")) .context("bytes() failed")?; if application_type == "application/webc" && (range.is_none() || range.unwrap().start == 0) && bytes[0..webc::MAGIC.len()] != webc::MAGIC[..] { let bytes = bytes.iter().copied().take(100).collect::>(); let first_100_bytes = String::from_utf8_lossy(&bytes); return Err(anyhow::anyhow!("invalid webc bytes: {first_100_bytes:?}")); } // else if "application/tar+gzip" - we would need to uncompress the response here // since failure responses are very small, this will fail during unpacking instead Ok(Some(bytes.to_vec())) } } // TODO: this test is segfaulting only on linux-musl, no other OS // See https://github.com/wasmerio/wasmer/pull/3215 #[cfg(not(target_env = "musl"))] #[test] fn test_install_package() { println!("test install package..."); let registry = "https://registry.wapm.io/graphql"; if !test_if_registry_present(registry).unwrap_or(false) { panic!("registry.wapm.io not reachable, test will fail"); } println!("registry present"); let wabt = query_package_from_registry(registry, "wasmer/wabt", Some("1.0.29")).unwrap(); println!("wabt queried: {wabt:#?}"); assert_eq!(wabt.registry, registry); assert_eq!(wabt.package, "wasmer/wabt"); assert_eq!(wabt.version, "1.0.29"); assert_eq!( wabt.commands, "wat2wasm, wast2json, wasm2wat, wasm-interp, wasm-validate, wasm-strip" ); assert_eq!( wabt.url, "https://registry-cdn.wapm.io/packages/wasmer/wabt/wabt-1.0.29.tar.gz".to_string() ); let fake_wasmer_dir = tempdir::TempDir::new("tmp").unwrap(); let wasmer_dir = fake_wasmer_dir.path(); let path = install_package(wasmer_dir, &url::Url::parse(&wabt.url).unwrap()).unwrap(); println!("package installed: {path:?}"); assert_eq!( path, get_checkouts_dir(wasmer_dir).join(&format!("{}@1.0.29", Package::hash_url(&wabt.url))) ); let all_installed_packages = get_all_local_packages(wasmer_dir); let is_installed = all_installed_packages .iter() .any(|p| p.name == "wasmer/wabt" && p.version == "1.0.29"); if !is_installed { let panic_str = get_all_local_packages(wasmer_dir) .iter() .map(|p| format!("{} {} {}", p.registry, p.name, p.version)) .collect::>() .join("\r\n"); panic!("get all local packages: failed to install:\r\n{panic_str}"); } println!("ok, done"); } /// A library that exposes bindings to a WAPM package. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Bindings { /// A unique ID specifying this set of bindings. pub id: String, /// The URL which can be used to download the files that were generated /// (typically as a `*.tar.gz` file). pub url: String, /// The programming language these bindings are written in. pub language: ProgrammingLanguage, /// The generator used to generate these bindings. pub generator: BindingsGenerator, } /// The generator used to create [`Bindings`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct BindingsGenerator { /// A unique ID specifying this generator. pub id: String, /// The generator package's name (e.g. `wasmer/wasmer-pack`). pub package_name: String, /// The exact package version. pub version: String, /// The name of the command that was used for generating bindings. pub command: String, } impl fmt::Display for BindingsGenerator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let BindingsGenerator { package_name, version, command, .. } = self; write!(f, "{package_name}@{version}:{command}")?; Ok(()) } } /// List all bindings associated with a particular package. /// /// If a version number isn't provided, this will default to the most recently /// published version. pub fn list_bindings( registry: &str, name: &str, version: Option<&str>, ) -> Result, anyhow::Error> { use crate::queries::{ get_bindings_query::{ResponseData, Variables}, GetBindingsQuery, }; use graphql_client::GraphQLQuery; let variables = Variables { name: name.to_string(), version: version.map(String::from), }; let q = GetBindingsQuery::build_query(variables); let response: ResponseData = crate::graphql::execute_query(registry, "", &q)?; let package_version = response.package_version.context("Package not found")?; let mut bindings_packages = Vec::new(); for b in package_version.bindings.into_iter().flatten() { let pkg = Bindings { id: b.id, url: b.url, language: b.language, generator: BindingsGenerator { id: b.generator.package_version.id, package_name: b.generator.package_version.package.name, version: b.generator.package_version.version, command: b.generator.command_name, }, }; bindings_packages.push(pkg); } Ok(bindings_packages) }