Files
wasmer/lib/registry/src/lib.rs
2022-12-22 11:29:39 +01:00

1050 lines
34 KiB
Rust

//! 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<String>,
}
pub fn get_package_local_dir(wasmer_dir: &Path, url: &str, version: &str) -> Option<PathBuf> {
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<LocalPackage> {
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<PathBuf, String> {
Ok(self.path.clone())
}
/// Returns the wasmer.toml path if it exists
pub fn get_wasmer_toml_path(base_path: &Path) -> Result<PathBuf, anyhow::Error> {
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<wasmer_toml::Manifest, String> {
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::Manifest>(&wasmer_toml)
.map_err(|e| format!("Could not parse toml for {:?}: {e}", base_path.display()))?;
Ok(wasmer_toml)
}
pub fn get_commands(&self) -> Result<Vec<String>, 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::<Vec<_>>().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::<Result<Vec<_>, 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<LocalPackage> {
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<LocalPackage> {
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<PackageDownloadInfo, String> {
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<String>,
},
}
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<String>,
},
}
/// 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<PackageDownloadInfo, QueryPackageError> {
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::<wasmer_toml::Manifest>(&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::<Vec<_>>()
.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<P: AsRef<Path>>(
target_targz_path: P,
target_path: P,
strip_toplevel: bool,
) -> Result<PathBuf, anyhow::Error> {
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<u8> = 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<PathBuf, anyhow::Error> {
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<R>(mut archive: tar::Archive<R>, 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<PathBuf, anyhow::Error> {
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(&registry))
.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(&registry, &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<bool, String> {
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<Vec<String>, 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<RemoteWebcInfo> {
get_all_installed_webc_packages_inner(wasmer_dir)
}
fn get_all_installed_webc_packages_inner(wasmer_dir: &Path) -> Vec<RemoteWebcInfo> {
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<String, anyhow::Error> {
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<RemoteWebcInfo, anyhow::Error> {
// 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<reqwest::blocking::RequestBuilder, anyhow::Error> {
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<Range<usize>>,
stream_response_into: Option<PathBuf>,
) -> Result<Option<Vec<u8>>, anyhow::Error> {
get_bytes(url, range, "application/webc", stream_response_into)
}
fn get_targz_bytes(
url: &Url,
range: Option<Range<usize>>,
stream_response_into: Option<PathBuf>,
) -> Result<Option<Vec<u8>>, anyhow::Error> {
get_bytes(url, range, "application/tar+gzip", stream_response_into)
}
fn get_bytes(
url: &Url,
range: Option<Range<usize>>,
application_type: &'static str,
stream_response_into: Option<PathBuf>,
) -> Result<Option<Vec<u8>>, 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::<Vec<_>>();
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::<Vec<_>>()
.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<Vec<Bindings>, 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)
}