wasmer_cli/utils/
mod.rs

1//! Utility functions for the WebAssembly module
2
3pub(crate) mod package_wizard;
4pub(crate) mod prompts;
5pub(crate) mod render;
6pub(crate) mod timestamp;
7pub(crate) mod unpack;
8
9use std::{
10    path::{Path, PathBuf},
11    str::FromStr,
12};
13
14use anyhow::{Context as _, Result, bail};
15use once_cell::sync::Lazy;
16use regex::Regex;
17use wasmer_wasix::runners::MappedDirectory;
18
19fn retrieve_alias_pathbuf(alias: &str, real_dir: &str) -> Result<MappedDirectory> {
20    let pb = PathBuf::from(&real_dir).canonicalize()?;
21    if let Ok(pb_metadata) = pb.metadata() {
22        if !pb_metadata.is_dir() {
23            bail!("\"{}\" exists, but it is not a directory", &real_dir);
24        }
25    } else {
26        bail!("Directory \"{}\" does not exist", &real_dir);
27    }
28    Ok(MappedDirectory {
29        guest: alias.to_string(),
30        host: pb,
31    })
32}
33
34/// Parses a mapdir from a string
35pub fn parse_mapdir(entry: &str) -> Result<MappedDirectory> {
36    // We try first splitting by `::`
37    if let [alias, real_dir] = entry.split("::").collect::<Vec<&str>>()[..] {
38        retrieve_alias_pathbuf(alias, real_dir)
39    }
40    // And then we try splitting by `:` (for compatibility with previous API)
41    else if let [alias, real_dir] = entry.splitn(2, ':').collect::<Vec<&str>>()[..] {
42        retrieve_alias_pathbuf(alias, real_dir)
43    } else {
44        bail!(
45            "Directory mappings must consist of two paths separate by a `::` or `:`. Found {}",
46            &entry
47        )
48    }
49}
50
51/// Parses an environment variable.
52pub fn parse_envvar(entry: &str) -> Result<(String, String)> {
53    let entry = entry.trim();
54
55    match entry.find('=') {
56        None => bail!(
57            "Environment variable must be of the form `<name>=<value>`; found `{}`",
58            &entry
59        ),
60
61        Some(0) => bail!(
62            "Environment variable is not well formed, the `name` is missing in `<name>=<value>`; got `{}`",
63            &entry
64        ),
65
66        Some(position) if position == entry.len() - 1 => bail!(
67            "Environment variable is not well formed, the `value` is missing in `<name>=<value>`; got `{}`",
68            &entry
69        ),
70
71        Some(position) => Ok((entry[..position].into(), entry[position + 1..].into())),
72    }
73}
74
75pub(crate) const DEFAULT_PACKAGE_MANIFEST_FILE: &str = "wasmer.toml";
76
77/// Load a package manifest from the manifest file.
78///
79/// Path can either be a directory, or a concrete file path.
80pub fn load_package_manifest(
81    path: &Path,
82) -> Result<Option<(PathBuf, wasmer_config::package::Manifest)>, anyhow::Error> {
83    let file_path = if path.is_file() {
84        path.to_owned()
85    } else {
86        path.join(DEFAULT_PACKAGE_MANIFEST_FILE)
87    };
88
89    let contents = match std::fs::read_to_string(&file_path) {
90        Ok(c) => c,
91        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
92        Err(err) => {
93            return Err(err).with_context(|| {
94                format!(
95                    "Could not read package manifest at '{}'",
96                    file_path.display()
97                )
98            });
99        }
100    };
101
102    let manifest = wasmer_config::package::Manifest::parse(&contents).with_context(|| {
103        format!(
104            "Could not parse package config at: '{}' - full config: {}",
105            file_path.display(),
106            contents
107        )
108    })?;
109
110    Ok(Some((file_path, manifest)))
111}
112
113/// The identifier for an app or package in the form, `owner/package@version`,
114/// where the `owner` and `version` are optional.
115#[derive(Debug, Clone, PartialEq)]
116pub(crate) struct Identifier {
117    /// The package's name.
118    pub name: String,
119    /// The package's owner, typically a username or namespace.
120    pub owner: Option<String>,
121    /// The package's version number.
122    pub version: Option<String>,
123}
124
125impl Identifier {
126    pub fn new(name: impl Into<String>) -> Self {
127        Identifier {
128            name: name.into(),
129            owner: None,
130            version: None,
131        }
132    }
133
134    pub fn with_owner(self, owner: impl Into<String>) -> Self {
135        Identifier {
136            owner: Some(owner.into()),
137            ..self
138        }
139    }
140
141    pub fn with_version(self, version: impl Into<String>) -> Self {
142        Identifier {
143            version: Some(version.into()),
144            ..self
145        }
146    }
147}
148
149impl FromStr for Identifier {
150    type Err = anyhow::Error;
151
152    fn from_str(s: &str) -> Result<Self, Self::Err> {
153        const PATTERN: &str = r"^(?x)
154            (?:
155                (?P<owner>[a-zA-Z][\w\d_.-]*)
156                /
157            )?
158            (?P<name>[a-zA-Z][\w\d_.-]*)
159            (?:
160                @
161                (?P<version>[\w\d.]+)
162            )?
163            $
164        ";
165        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(PATTERN).unwrap());
166
167        let caps = RE.captures(s).context(
168            "Invalid package identifier, expected something like namespace/package@version",
169        )?;
170
171        let mut identifier = Identifier::new(&caps["name"]);
172
173        if let Some(owner) = caps.name("owner") {
174            identifier = identifier.with_owner(owner.as_str());
175        }
176
177        if let Some(version) = caps.name("version") {
178            identifier = identifier.with_version(version.as_str());
179        }
180
181        Ok(identifier)
182    }
183}
184
185/// Merge two yaml values by recursively merging maps from b into a.
186///
187/// Preserves old values that were not in b.
188pub(crate) fn merge_yaml_values(a: &serde_yaml::Value, b: &serde_yaml::Value) -> serde_yaml::Value {
189    use serde_yaml::Value as V;
190    match (a, b) {
191        (V::Mapping(a), V::Mapping(b)) => {
192            let mut m = a.clone();
193            for (k, v) in b.iter() {
194                let newval = if let Some(old) = a.get(k) {
195                    merge_yaml_values(old, v)
196                } else {
197                    v.clone()
198                };
199                m.insert(k.clone(), newval);
200            }
201            V::Mapping(m)
202        }
203        _ => b.clone(),
204    }
205}
206
207// pub(crate) fn shell(cmd: &str, args: &[&str]) -> anyhow::Result<Option<i32>> {
208//     let ecode = Command::new(cmd).args(args).spawn()?.wait()?;
209//     if ecode.success() {
210//         Ok(ecode.code())
211//     } else {
212//         Err(anyhow::format_err!(
213//             "failed to execute linux command (code={:?})",
214//             ecode.code()
215//         ))
216//     }
217// }
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_merge_yaml_values() {
225        use serde_yaml::Value;
226        let v1 = r#"
227a: a
228b:
229  b1: b1
230c: c
231        "#;
232        let v2 = r#"
233a: a1
234b:
235  b2: b2
236        "#;
237        let v3 = r#"
238a: a1
239b:
240  b1: b1
241  b2: b2
242c: c
243        "#;
244
245        let a: Value = serde_yaml::from_str(v1).unwrap();
246        let b: Value = serde_yaml::from_str(v2).unwrap();
247        let c: Value = serde_yaml::from_str(v3).unwrap();
248        let merged = merge_yaml_values(&a, &b);
249        assert_eq!(merged, c);
250    }
251
252    #[test]
253    fn parse_valid_identifiers() {
254        let inputs = [
255            ("python", Identifier::new("python")),
256            (
257                "syrusakbary/python",
258                Identifier::new("python").with_owner("syrusakbary"),
259            ),
260            (
261                "wasmer/wasmer.io",
262                Identifier::new("wasmer.io").with_owner("wasmer"),
263            ),
264            (
265                "syrusakbary/python@1.2.3",
266                Identifier::new("python")
267                    .with_owner("syrusakbary")
268                    .with_version("1.2.3"),
269            ),
270            (
271                "python@1.2.3",
272                Identifier::new("python").with_version("1.2.3"),
273            ),
274        ];
275
276        for (src, expected) in inputs {
277            let identifier = Identifier::from_str(src).expect(src);
278            assert_eq!(identifier, expected);
279        }
280    }
281
282    #[test]
283    fn invalid_package_identifiers() {
284        let inputs = ["", "$", "python/", "/python", "python@", "."];
285
286        for input in inputs {
287            let result = Identifier::from_str(input);
288            assert!(result.is_err(), "Got {result:?} from {input:?}");
289        }
290    }
291
292    #[test]
293    fn test_parse_envvar() {
294        assert_eq!(
295            parse_envvar("A").unwrap_err().to_string(),
296            "Environment variable must be of the form `<name>=<value>`; found `A`"
297        );
298        assert_eq!(
299            parse_envvar("=A").unwrap_err().to_string(),
300            "Environment variable is not well formed, the `name` is missing in `<name>=<value>`; got `=A`"
301        );
302        assert_eq!(
303            parse_envvar("A=").unwrap_err().to_string(),
304            "Environment variable is not well formed, the `value` is missing in `<name>=<value>`; got `A=`"
305        );
306        assert_eq!(parse_envvar("A=B").unwrap(), ("A".into(), "B".into()));
307        assert_eq!(parse_envvar("   A=B\t").unwrap(), ("A".into(), "B".into()));
308        assert_eq!(
309            parse_envvar("A=B=C=D").unwrap(),
310            ("A".into(), "B=C=D".into())
311        );
312    }
313}