wasmer_registry/package/
mod.rs

1#[cfg(feature = "build-package")]
2pub mod builder;
3
4use crate::WasmerConfig;
5use regex::Regex;
6use std::path::{Path, PathBuf};
7use std::{fmt, str::FromStr};
8use url::Url;
9
10const REGEX_PACKAGE_WITH_VERSION: &str =
11    r"^([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)(@([a-zA-Z0-9\.\-_]+*))?$";
12
13lazy_static::lazy_static! {
14    static ref PACKAGE_WITH_VERSION: Regex = regex::Regex::new(REGEX_PACKAGE_WITH_VERSION).unwrap();
15}
16
17#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
18pub struct Package {
19    pub namespace: String,
20    pub name: String,
21    pub version: Option<String>,
22}
23
24impl fmt::Display for Package {
25    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26        write!(f, "{}", self.file())
27    }
28}
29
30impl Package {
31    /// Checks whether the package is already installed, if yes, returns the path to the root dir
32    pub fn already_installed(&self, wasmer_dir: &Path) -> Option<PathBuf> {
33        let checkouts_dir = crate::get_checkouts_dir(wasmer_dir);
34        let config = WasmerConfig::from_file(wasmer_dir).ok()?;
35        let current_registry = config.registry.get_current_registry();
36        let hash = self.get_hash(&current_registry);
37
38        let found = std::fs::read_dir(&checkouts_dir)
39            .ok()?
40            .filter_map(|e| Some(e.ok()?.file_name().to_str()?.to_string()))
41            .find(|s| match self.version.as_ref() {
42                None => s.contains(&hash),
43                Some(v) => s.contains(&hash) && s.ends_with(v),
44            })?;
45        Some(checkouts_dir.join(found))
46    }
47
48    /// Checks if the URL is already installed, note that `{url}@{version}`
49    /// and `{url}` are treated the same
50    pub fn is_url_already_installed(url: &Url, wasmer_dir: &Path) -> Option<PathBuf> {
51        let checkouts_dir = crate::get_checkouts_dir(wasmer_dir);
52
53        let url_string = url.to_string();
54        let (url, version) = match url_string.split('@').collect::<Vec<_>>()[..] {
55            [url, version] => (url.to_string(), Some(version)),
56            _ => (url_string, None),
57        };
58        let hash = Self::hash_url(&url);
59        let found = std::fs::read_dir(&checkouts_dir)
60            .ok()?
61            .filter_map(|e| Some(e.ok()?.file_name().to_str()?.to_string()))
62            .find(|s| match version.as_ref() {
63                None => s.contains(&hash),
64                Some(v) => s.contains(&hash) && s.ends_with(v),
65            })?;
66        Some(checkouts_dir.join(found))
67    }
68
69    /// Returns the hash of the URL with a maximum of 128 bytes length
70    /// (necessary for not erroring on filesystem limitations)
71    pub fn hash_url(url: &str) -> String {
72        hex::encode(url).chars().take(128).collect()
73    }
74
75    /// Returns the hash of the URL with a maximum of 64 bytes length
76    pub fn unhash_url(hashed: &str) -> String {
77        String::from_utf8_lossy(&hex::decode(hashed).unwrap_or_default()).to_string()
78    }
79
80    /// Returns the hash of the package URL without the version
81    /// (because the version is encoded as @version and isn't part of the hash itself)
82    pub fn get_hash(&self, registry: &str) -> String {
83        let url = self.get_url_without_version(registry);
84        Self::hash_url(&url.unwrap_or_default())
85    }
86
87    fn get_url_without_version(&self, registry: &str) -> Result<String, anyhow::Error> {
88        let url = self.url(registry);
89        Ok(format!(
90            "{}/{}/{}",
91            url?.origin().ascii_serialization(),
92            self.namespace,
93            self.name
94        ))
95    }
96
97    /// Returns the filename for this package
98    pub fn file(&self) -> String {
99        let version = self
100            .version
101            .as_ref()
102            .map(|f| format!("@{f}"))
103            .unwrap_or_default();
104        format!("{}/{}{version}", self.namespace, self.name)
105    }
106
107    /// Returns the {namespace}/{name} package name
108    pub fn package(&self) -> String {
109        format!("{}/{}", self.namespace, self.name)
110    }
111
112    /// Returns the full URL including the version for this package
113    pub fn url(&self, registry: &str) -> Result<Url, anyhow::Error> {
114        let registry_tld = tldextract::TldExtractor::new(tldextract::TldOption::default())
115            .extract(registry)
116            .map_err(|e| anyhow::anyhow!("Invalid registry: {}: {e}", registry))?;
117
118        let registry_tld = format!(
119            "{}.{}",
120            registry_tld.domain.as_deref().unwrap_or(""),
121            registry_tld.suffix.as_deref().unwrap_or(""),
122        );
123
124        let version = self
125            .version
126            .as_ref()
127            .map(|f| format!("@{f}"))
128            .unwrap_or_default();
129        let url = format!(
130            "https://{registry_tld}/{}/{}{version}",
131            self.namespace, self.name
132        );
133        url::Url::parse(&url).map_err(|e| anyhow::anyhow!("error parsing {url}: {e}"))
134    }
135
136    /// Returns the path to the installation directory.
137    /// Does not check whether the installation directory already exists.
138    pub fn get_path(&self, wasmer_dir: &Path) -> Result<PathBuf, anyhow::Error> {
139        let checkouts_dir = crate::get_checkouts_dir(wasmer_dir);
140        let config = WasmerConfig::from_file(wasmer_dir)
141            .map_err(|e| anyhow::anyhow!("could not load config {e}"))?;
142        let hash = self.get_hash(&config.registry.get_current_registry());
143
144        match self.version.as_ref() {
145            Some(v) => Ok(checkouts_dir.join(format!("{}@{}", hash, v))),
146            None => Ok(checkouts_dir.join(&hash)),
147        }
148    }
149}
150
151impl FromStr for Package {
152    type Err = anyhow::Error;
153
154    fn from_str(s: &str) -> Result<Self, Self::Err> {
155        let captures = PACKAGE_WITH_VERSION
156            .captures(s.trim())
157            .map(|c| {
158                c.iter()
159                    .flatten()
160                    .map(|m| m.as_str().to_owned())
161                    .collect::<Vec<_>>()
162            })
163            .unwrap_or_default();
164
165        match captures.len() {
166            // namespace/package
167            3 => {
168                let namespace = captures[1].to_string();
169                let name = captures[2].to_string();
170                Ok(Package {
171                    namespace,
172                    name,
173                    version: None,
174                })
175            }
176            // namespace/package@version
177            5 => {
178                let namespace = captures[1].to_string();
179                let name = captures[2].to_string();
180                let version = captures[4].to_string();
181                Ok(Package {
182                    namespace,
183                    name,
184                    version: Some(version),
185                })
186            }
187            other => Err(anyhow::anyhow!("invalid package {other}")),
188        }
189    }
190}