Skip to main content

wapm_cli/
util.rs

1use crate::constants::{DEFAULT_RUNTIME, WAPM_RUNTIME_ENV_KEY};
2use crate::data::manifest::PACKAGES_DIR_NAME;
3use crate::graphql::execute_query;
4use graphql_client::*;
5use license_exprs;
6use semver::Version;
7use std::path::{Path, PathBuf};
8use std::{env, fs, io};
9use thiserror::Error;
10
11pub static MAX_NAME_LENGTH: usize = 50;
12
13#[derive(Debug, Error)]
14pub enum NameError {
15    #[error("Please enter a name")]
16    Empty,
17    #[error("The name \"{0}\" is too long. It must be {1} characters or fewer")]
18    NameTooLong(String, usize),
19    #[error(
20        "The name \"{0}\" contains invalid characters. Please use alpha-numeric characters, '-', and '_'",
21    )]
22    InvalidCharacters(String),
23}
24
25/// Checks whether a given package name is acceptable or not
26pub fn validate_name(name: &str) -> Result<String, NameError> {
27    if name.len() > MAX_NAME_LENGTH {
28        return Err(NameError::NameTooLong(name.to_string(), MAX_NAME_LENGTH));
29    }
30
31    let re = regex::Regex::new("^[-a-zA-Z0-9_]+").unwrap();
32
33    if !re.is_match(name) {
34        return Err(NameError::InvalidCharacters(name.to_string()));
35    }
36
37    Ok(name.to_owned())
38}
39
40/// Checks whether a given command / runner name is acceptable or not
41pub fn validate_runner(runner: &str) -> Result<String, NameError> {
42    if runner.trim().is_empty() {
43        Err(NameError::Empty)
44    } else {
45        Ok(runner.trim().to_string())
46    }
47}
48
49#[derive(Debug, Error)]
50pub enum LicenseError {
51    #[error("\"{0}\" is not a valid SPDX license")]
52    UnknownLicenseId(String),
53    #[error("License should be a valid SPDX license expression (without \"LicenseRef\")")]
54    InvalidStructure(),
55}
56
57/// Checks whether a given package name is acceptable or not
58pub fn validate_license(license: &str) -> Result<String, LicenseError> {
59    match license_exprs::validate_license_expr(license) {
60        Ok(_) => Ok(license.to_owned()),
61        Err(license_exprs::ParseError::UnknownLicenseId(word)) => {
62            Err(LicenseError::UnknownLicenseId(word.to_owned()))
63        }
64        Err(license_exprs::ParseError::InvalidStructure(_)) => {
65            Err(LicenseError::InvalidStructure())
66        }
67    }
68}
69
70#[derive(GraphQLQuery)]
71#[graphql(
72    schema_path = "graphql/schema.graphql",
73    query_path = "graphql/queries/whoami.graphql",
74    response_derives = "Debug"
75)]
76struct WhoAmIQuery;
77
78pub fn get_username() -> anyhow::Result<Option<String>> {
79    let q = WhoAmIQuery::build_query(who_am_i_query::Variables {});
80    let response: who_am_i_query::ResponseData = execute_query(&q)?;
81    Ok(response.viewer.map(|viewer| viewer.username))
82}
83
84#[cfg(feature = "telemetry")]
85pub fn telemetry_is_enabled() -> bool {
86    let mut config = if let Ok(c) = crate::config::Config::from_file() {
87        c
88    } else {
89        // TODO: change this to false when wapm becomes more stable
90        // defaulting to on is for the alpha and we should be very conservative about
91        // telemetry once we have more confidence in wapm's stability/userbase size
92        return true;
93    };
94    let telemetry_str = crate::config::get(&mut config, "telemetry.enabled".to_string())
95        .unwrap_or_else(|_| "true".to_string());
96
97    // if we fail to parse, someone probably tried to turn it off
98    telemetry_str.parse::<bool>().unwrap_or(false)
99}
100
101#[inline]
102pub fn get_package_namespace_and_name(package_name: &str) -> anyhow::Result<(&str, &str)> {
103    let split: Vec<&str> = package_name.split('/').collect();
104    match &split[..] {
105        [namespace, name] => Ok((*namespace, *name)),
106        [global_package_name] => {
107            info!(
108                "Interpreting unqualified global package name \"{}\" as \"_/{}\"",
109                package_name, global_package_name
110            );
111            Ok(("_", *global_package_name))
112        }
113        _ => bail!("Package name is invalid"),
114    }
115}
116
117#[inline]
118pub fn fully_qualified_package_display_name(
119    package_name: &str,
120    package_version: &Version,
121) -> String {
122    format!("{}@{}", package_name, package_version)
123}
124
125pub fn create_package_dir(
126    project_dir: &Path,
127    namespace_dir: &str,
128    fully_qualified_package_name: &str,
129) -> Result<PathBuf, io::Error> {
130    let mut package_dir = project_dir.join(PACKAGES_DIR_NAME);
131    package_dir.push(namespace_dir);
132    package_dir.push(fully_qualified_package_name);
133    fs::create_dir_all(&package_dir)?;
134    Ok(package_dir)
135}
136
137pub fn wapm_should_print_color() -> bool {
138    std::env::var("WAPM_DISABLE_COLOR")
139        .map(|_| false)
140        .unwrap_or(true)
141}
142
143use lazy_static::lazy_static;
144use std::sync::Mutex;
145
146#[derive(Debug, Default)]
147/// A wrapper type that ensures that the inner type is only set once
148pub struct SetOnce<T: Default> {
149    set: bool,
150    value: T,
151}
152
153impl<T: Default> SetOnce<T> {
154    pub fn new() -> Self {
155        Self {
156            set: false,
157            value: T::default(),
158        }
159    }
160    pub fn set(&mut self, value: T) -> Option<()> {
161        if self.set {
162            return None;
163        }
164
165        self.value = value;
166        self.set = true;
167        Some(())
168    }
169
170    pub fn get(&self) -> &T {
171        &self.value
172    }
173}
174
175lazy_static! {
176    /// Global variable that determines the behavior of prompts
177    pub static ref WAPM_FORCE_YES_TO_PROMPTS: Mutex<SetOnce<bool>> = Mutex::new(SetOnce::new());
178}
179
180/// If true, prompts should not ask for user input
181pub fn wapm_should_accept_all_prompts() -> bool {
182    let guard = WAPM_FORCE_YES_TO_PROMPTS.lock().unwrap();
183    *guard.get()
184}
185
186pub fn set_wapm_should_accept_all_prompts(val: bool) -> Option<()> {
187    let mut guard = WAPM_FORCE_YES_TO_PROMPTS.lock().unwrap();
188    guard.set(val)
189}
190
191/// Asks the user to confirm something. Returns a boolean indicating if the user consented
192/// or if the `WAPM_FORCE_YES_TO_PROMPTS` variable is set
193pub fn prompt_user_for_yes(prompt: &str) -> anyhow::Result<bool> {
194    use std::io::Write;
195
196    print!("{}\n[y/n] ", prompt);
197    std::io::stdout().flush()?;
198    if wapm_should_accept_all_prompts() {
199        Ok(true)
200    } else {
201        let mut input_str = String::new();
202        std::io::stdin().read_line(&mut input_str)?;
203        match input_str.to_lowercase().trim_end() {
204            "yes" | "y" => Ok(true),
205            _ => Ok(false),
206        }
207    }
208}
209
210#[cfg(feature = "prehash-module")]
211/// This function hashes the Wasm module to generate a key.
212/// We use it to speed up the time required to run a commands
213/// since it doesn't require doing the hash of the module at runtime.
214pub fn get_hashed_module_key(path: &Path) -> Option<String> {
215    debug!("Creating hash of wasm module at {:?}", path);
216    let bytes = match std::fs::read(path) {
217        Ok(bytes) => bytes,
218        Err(e) => {
219            error!(
220                "Could not read wasm module at {} when attempting to generate hash: {}",
221                path.to_string_lossy().to_string(),
222                e.to_string()
223            );
224            return None;
225        }
226    };
227    let hash = blake3::hash(&bytes[..]);
228    let str_hash = hex::encode(&hash.as_bytes());
229    Some(str_hash)
230}
231
232#[cfg(not(feature = "prehash-module"))]
233pub fn get_hashed_module_key(_path: &Path) -> Option<String> {
234    None
235}
236
237#[cfg(feature = "update-notifications")]
238pub fn get_latest_runtime_version(runtime: &str) -> Result<String, String> {
239    use std::process::Command;
240
241    let output = Command::new(runtime)
242        .arg("-V")
243        .output()
244        .map_err(|err| err.to_string())?;
245    let stdout_str = std::str::from_utf8(&output.stdout).map_err(|err| err.to_string())?;
246    let mut whitespace_iter = stdout_str.split_whitespace();
247    let _first = whitespace_iter.next();
248    debug_assert_eq!(_first, Some(runtime));
249
250    match whitespace_iter.next() {
251        Some(v) => Ok(v.to_string()),
252        None => Err("Can't find the version of wasmer".to_string()),
253    }
254}
255
256#[cfg(feature = "update-notifications")]
257/// Returns `None` if versions can't be taken out of the string.
258/// Returns `Some(bool)` where `bool` is whether or not the old version
259/// is greater than or equal to the old version.  This is useful for checking
260/// if there needs to be an update.
261pub fn compare_versions(old: &str, new: &str) -> Option<bool> {
262    let old_ver_pieces = old.split('.').collect::<Vec<&str>>();
263    let new_ver_pieces = new.split('.').collect::<Vec<&str>>();
264
265    if !(old_ver_pieces.len() == 3 && new_ver_pieces.len() == 3) {
266        return None;
267    }
268    let parse = |pieces: Vec<&str>| -> Option<(usize, usize, usize)> {
269        Some((
270            pieces[0].parse::<usize>().ok()?,
271            pieces[1].parse::<usize>().ok()?,
272            pieces[2].parse::<usize>().ok()?,
273        ))
274    };
275    if let (Some(old_ver), Some(new_ver)) = (parse(old_ver_pieces), parse(new_ver_pieces)) {
276        Some(old_ver >= new_ver)
277    } else {
278        None
279    }
280}
281
282/// Returns the value of the WAPM_RUNTIME env var if it exists.
283/// Otherwise returns wasmer
284fn get_runtime() -> String {
285    env::var(WAPM_RUNTIME_ENV_KEY).unwrap_or_else(|_| DEFAULT_RUNTIME.to_owned())
286}
287
288/// Splits the runtime from the rest of arguments
289fn split_runtime_and_args(runtime: String) -> (String, Vec<String>) {
290    let runtime_split = runtime.split_whitespace();
291    if let Some((split_runtime, split_runtime_args)) = runtime_split
292        .map(|s| s.to_string())
293        .collect::<Vec<String>>()
294        .split_first()
295    {
296        return (split_runtime.to_string(), split_runtime_args.to_vec());
297    }
298    (runtime, vec![])
299}
300
301/// We put this in a new function, to be clear that runtime can be both
302/// 1. A string with the runtime value (eg. "wasmer")
303/// 2. A string with the runtime value and the args (eg. "wasmer --backend=singlepass")
304pub fn get_runtime_with_args() -> (String, Vec<String>) {
305    split_runtime_and_args(get_runtime())
306}
307
308#[cfg(not(target_os = "wasi"))]
309pub fn create_temp_dir() -> Result<tempfile::TempDir, std::io::Error> {
310    tempfile::TempDir::new()
311}
312
313#[cfg(target_os = "wasi")]
314pub fn create_temp_dir() -> Result<std::path::PathBuf, std::io::Error> {
315    let mut buf = [0u8; 4];
316    getrandom::getrandom(&mut buf)?;
317    let path = format!("/tmp/{:#10x}", u32::from_be_bytes(buf));
318    let ret: std::path::PathBuf = path.into();
319    Ok(ret)
320}
321
322#[cfg(target_os = "wasi")]
323pub fn whoami_distro() -> String {
324    whoami::os().to_lowercase()
325}
326
327#[cfg(not(target_os = "wasi"))]
328pub fn whoami_distro() -> String {
329    whoami::distro().to_lowercase()
330}
331
332#[cfg(test)]
333mod test {
334    use super::*;
335
336    #[cfg(feature = "update-notifications")]
337    #[test]
338    pub fn compare_ver_test() {
339        assert_eq!(compare_versions("0.1.0", "0.1.0"), Some(true));
340        assert_eq!(compare_versions("1.1.0", "0.1.0"), Some(true));
341        assert_eq!(compare_versions("1.0.0", "0.2.5"), Some(true));
342        assert_eq!(compare_versions("1.0.0", "2.2.5"), Some(false));
343        assert_eq!(compare_versions("1.0.0", "2.0.5"), Some(false));
344        assert_eq!(compare_versions("1.1.0", "2.0.5"), Some(false));
345        assert_eq!(compare_versions("1.1.6", "2.0.0"), Some(false));
346        assert_eq!(compare_versions("0.1.1", "0.1.0"), Some(true));
347        assert_eq!(compare_versions("0.1.1", "0.2.0"), Some(false));
348    }
349
350    #[test]
351    pub fn test_split_runtime_and_args() {
352        assert_eq!(
353            split_runtime_and_args("wasmer".to_owned()),
354            ("wasmer".to_owned(), vec![])
355        );
356        assert_eq!(
357            split_runtime_and_args("wasmer --backend=llvm".to_owned()),
358            ("wasmer".to_owned(), vec!["--backend=llvm".to_owned()])
359        );
360        assert_eq!(
361            split_runtime_and_args("wasmer run".to_owned()),
362            ("wasmer".to_owned(), vec!["run".to_owned()])
363        );
364
365        // Weird spacing
366        assert_eq!(
367            split_runtime_and_args("  wasmer   ".to_owned()),
368            ("wasmer".to_owned(), vec![])
369        );
370        assert_eq!(
371            split_runtime_and_args("wasmer  run  ".to_owned()),
372            ("wasmer".to_owned(), vec!["run".to_owned()])
373        );
374        assert_eq!(
375            split_runtime_and_args("  wasmer  run  ".to_owned()),
376            ("wasmer".to_owned(), vec!["run".to_owned()])
377        );
378    }
379}