1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
use crate::error::WarpgateError;
use miette::IntoDiagnostic;
use reqwest::Url;
use starbase_archive::Archiver;
use starbase_utils::{fs, glob};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use warpgate_api::VirtualPath;

pub fn extract_prefix_from_slug(slug: &str) -> &str {
    slug.split('/').next().expect("Expected an owner scope!")
}

pub fn extract_suffix_from_slug(slug: &str) -> &str {
    slug.split('/')
        .nth(1)
        .expect("Expected a package or repository name!")
}

pub fn determine_cache_extension(value: &str) -> &str {
    for ext in [".toml", ".json", ".yaml", ".yml"] {
        if value.ends_with(ext) {
            return ext;
        }
    }

    ".wasm"
}

pub fn create_wasm_file_prefix(name: &str) -> String {
    let mut name = name.to_lowercase().replace('-', "_");

    if !name.ends_with("_plugin") {
        name.push_str("_plugin");
    }

    name
}

pub async fn download_from_url_to_file(
    source_url: &str,
    temp_file: &Path,
    client: &reqwest::Client,
) -> miette::Result<()> {
    let url = Url::parse(source_url).into_diagnostic()?;

    // Fetch the file from the HTTP source
    let response = client
        .get(url)
        .send()
        .await
        .map_err(|error| WarpgateError::Http {
            error,
            url: source_url.to_owned(),
        })?;
    let status = response.status();

    if status.as_u16() == 404 {
        return Err(WarpgateError::DownloadNotFound {
            url: source_url.to_owned(),
        }
        .into());
    }

    if !status.is_success() {
        return Err(WarpgateError::DownloadFailed {
            url: source_url.to_owned(),
            status: status.to_string(),
        }
        .into());
    }

    // Write the bytes to our temporary file
    fs::write_file(
        temp_file,
        response
            .bytes()
            .await
            .map_err(|error| WarpgateError::Http {
                error,
                url: source_url.to_owned(),
            })?,
    )?;

    Ok(())
}

pub fn move_or_unpack_download(temp_file: &Path, dest_file: &Path) -> miette::Result<()> {
    match temp_file.extension().map(|e| e.to_str().unwrap()) {
        // Move these files as-is
        Some("wasm" | "toml" | "json" | "yaml" | "yml") => {
            fs::rename(temp_file, dest_file)?;
        }

        // Unpack archives to temp and move the wasm file
        Some("tar" | "gz" | "xz" | "tgz" | "txz" | "zst" | "zstd" | "zip") => {
            let out_dir = temp_file.parent().unwrap().join("out");

            Archiver::new(&out_dir, dest_file).unpack_from_ext()?;

            let wasm_files = glob::walk_files(&out_dir, ["**/*.wasm"])?;

            if wasm_files.is_empty() {
                return Err(miette::miette!(
                    "No applicable `.wasm` file could be found in downloaded plugin.",
                ));

                // Find a release file first, as some archives include the target folder
            } else if let Some(release_wasm) = wasm_files
                .iter()
                .find(|f| f.to_string_lossy().contains("release"))
            {
                fs::rename(release_wasm, dest_file)?;

                // Otherwise, move the first wasm file available
            } else {
                fs::rename(&wasm_files[0], dest_file)?;
            }

            fs::remove_file(temp_file)?;
            fs::remove_dir_all(out_dir)?;
        }

        Some(x) => {
            return Err(miette::miette!(
                "Unsupported file extension `{}` for downloaded plugin.",
                x
            ));
        }

        None => {
            return Err(miette::miette!(
                "Unsure how to handle downloaded plugin as no file extension/type could be derived."
            ));
        }
    };

    Ok(())
}

/// Sort virtual paths from longest to shortest host path,
/// so that prefix replacing is deterministic and accurate.
fn sort_virtual_paths(map: &BTreeMap<PathBuf, PathBuf>) -> Vec<(&PathBuf, &PathBuf)> {
    let mut list = map.iter().collect::<Vec<_>>();
    list.sort_by(|a, d| d.0.cmp(a.0));
    list
}

/// Convert the provided virtual guest path to an absolute host path.
pub fn from_virtual_path(paths_map: &BTreeMap<PathBuf, PathBuf>, path: &Path) -> PathBuf {
    for (host_path, guest_path) in sort_virtual_paths(paths_map) {
        if let Ok(rel_path) = path.strip_prefix(guest_path) {
            return host_path.join(rel_path);
        }
    }

    path.to_owned()
}

/// Convert the provided absolute host path to a virtual guest path suitable
/// for WASI sandboxed runtimes.
pub fn to_virtual_path(paths_map: &BTreeMap<PathBuf, PathBuf>, path: &Path) -> VirtualPath {
    for (host_path, guest_path) in sort_virtual_paths(paths_map) {
        if let Ok(rel_path) = path.strip_prefix(host_path) {
            let path = guest_path.join(rel_path);

            // Only forward slashes are allowed in WASI
            let path = if cfg!(windows) {
                PathBuf::from(path.to_string_lossy().replace('\\', "/"))
            } else {
                path
            };

            return VirtualPath::WithReal {
                path,
                virtual_prefix: guest_path.to_path_buf(),
                real_prefix: host_path.to_path_buf(),
            };
        }
    }

    VirtualPath::Only(path.to_owned())
}