warpgate_pdk/
funcs.rs

1use crate::api::populate_send_request_output;
2use crate::{exec_command, send_request};
3use extism_pdk::*;
4use serde::de::DeserializeOwned;
5use std::ffi::OsStr;
6use std::path::PathBuf;
7use std::vec;
8use warpgate_api::{
9    AnyResult, ExecCommandInput, ExecCommandOutput, HostEnvironment, HostOS, SendRequestInput,
10    SendRequestOutput, TestEnvironment, VirtualPath, anyhow,
11};
12
13#[host_fn]
14extern "ExtismHost" {
15    fn exec_command(input: Json<ExecCommandInput>) -> Json<ExecCommandOutput>;
16    fn from_virtual_path(input: String) -> String;
17    fn get_env_var(key: String) -> String;
18    fn send_request(input: Json<SendRequestInput>) -> Json<SendRequestOutput>;
19    fn set_env_var(name: String, value: String);
20    fn to_virtual_path(input: String) -> Json<VirtualPath>;
21}
22
23/// Fetch the requested input and return a response.
24pub fn fetch(input: SendRequestInput) -> AnyResult<SendRequestOutput> {
25    let url = input.url.clone();
26    let response = send_request!(input, input);
27    let status = response.status;
28
29    if status != 200 {
30        let body = response.text()?;
31
32        debug!(
33            "Response body for <url>{}</url>: <muted>{}</muted>",
34            url, body
35        );
36
37        return Err(anyhow!(
38            "Failed to request <url>{url}</url> <mutedlight>({})</mutedlight>",
39            status
40        ));
41    }
42
43    if response.body.is_empty() {
44        return Err(anyhow!("Invalid response from <url>{url}</url>, no body"));
45    }
46
47    Ok(response)
48}
49
50/// Fetch the provided URL and return the response as bytes.
51pub fn fetch_bytes<U>(url: U) -> AnyResult<Vec<u8>>
52where
53    U: AsRef<str>,
54{
55    Ok(fetch(SendRequestInput::new(url))?.body)
56}
57
58/// Fetch the provided URL and deserialize the response as JSON.
59pub fn fetch_json<U, R>(url: U) -> AnyResult<R>
60where
61    U: AsRef<str>,
62    R: DeserializeOwned,
63{
64    fetch(SendRequestInput::new(url))?.json()
65}
66
67/// Fetch the provided URL and return the response as text.
68pub fn fetch_text<U>(url: U) -> AnyResult<String>
69where
70    U: AsRef<str>,
71{
72    fetch(SendRequestInput::new(url))?.text()
73}
74
75/// Execute a command on the host with the provided input.
76pub fn exec(input: ExecCommandInput) -> AnyResult<ExecCommandOutput> {
77    Ok(exec_command!(input, input))
78}
79
80/// Execute a command on the host and capture its output (pipe).
81pub fn exec_captured<C, I, A>(command: C, args: I) -> AnyResult<ExecCommandOutput>
82where
83    C: AsRef<str>,
84    I: IntoIterator<Item = A>,
85    A: AsRef<str>,
86{
87    exec(ExecCommandInput::pipe(command, args))
88}
89
90/// Execute a command on the host and stream its output to the console (inherit).
91pub fn exec_streamed<C, I, A>(command: C, args: I) -> AnyResult<ExecCommandOutput>
92where
93    C: AsRef<str>,
94    I: IntoIterator<Item = A>,
95    A: AsRef<str>,
96{
97    exec(ExecCommandInput::inherit(command, args))
98}
99
100/// Load all Git tags from the provided remote URL.
101/// The `git` binary must exist on the host machine.
102pub fn load_git_tags<U>(url: U) -> AnyResult<Vec<String>>
103where
104    U: AsRef<str>,
105{
106    let url = url.as_ref();
107
108    debug!("Loading Git tags from remote <url>{}</url>", url);
109
110    let mut tags: Vec<String> = vec![];
111    let output = exec_captured(
112        "git",
113        ["ls-remote", "--tags", "--sort", "version:refname", url],
114    )?;
115
116    if output.exit_code != 0 {
117        debug!("Failed to load Git tags");
118
119        return Ok(tags);
120    }
121
122    for line in output.stdout.split('\n') {
123        // https://superuser.com/questions/1445823/what-does-mean-in-the-tags
124        if line.ends_with("^{}") {
125            continue;
126        }
127
128        let parts = line.split('\t').collect::<Vec<_>>();
129
130        if parts.len() < 2 {
131            continue;
132        }
133
134        if let Some(tag) = parts[1].strip_prefix("refs/tags/") {
135            tags.push(tag.to_owned());
136        }
137    }
138
139    debug!("Loaded {} Git tags", tags.len());
140
141    Ok(tags)
142}
143
144/// Check whether a command exists or not on the host machine.
145pub fn command_exists(env: &HostEnvironment, command: &str) -> bool {
146    debug!(
147        "Checking if command <shell>{}</shell> exists on the host",
148        command
149    );
150
151    let result = if env.os == HostOS::Windows {
152        exec_captured(
153            "powershell",
154            ["-Command", format!("Get-Command {command}").as_str()],
155        )
156    } else {
157        exec_captured("which", [command])
158    };
159
160    if result.is_ok_and(|res| res.exit_code == 0) {
161        debug!("Command does exist");
162
163        return true;
164    }
165
166    debug!("Command does NOT exist");
167
168    false
169}
170
171/// Return the value of an environment variable on the host machine.
172pub fn get_host_env_var<K>(key: K) -> AnyResult<Option<String>>
173where
174    K: AsRef<str>,
175{
176    let inner = unsafe { get_env_var(key.as_ref().into())? };
177
178    Ok(if inner.is_empty() { None } else { Some(inner) })
179}
180
181/// Set the value of an environment variable on the host machine.
182pub fn set_host_env_var<K, V>(key: K, value: V) -> AnyResult<()>
183where
184    K: AsRef<str>,
185    V: AsRef<str>,
186{
187    unsafe { set_env_var(key.as_ref().into(), value.as_ref().into())? };
188
189    Ok(())
190}
191
192/// Append paths to the `PATH` environment variable on the host machine.
193pub fn add_host_paths<I, P>(paths: I) -> AnyResult<()>
194where
195    I: IntoIterator<Item = P>,
196    P: AsRef<str>,
197{
198    let paths = paths
199        .into_iter()
200        .map(|p| p.as_ref().to_owned())
201        .collect::<Vec<_>>();
202
203    set_host_env_var("PATH", paths.join(":"))
204}
205
206/// Convert the provided path into a [`PathBuf`] instance,
207/// with the prefix resolved absolutely to the host.
208pub fn into_real_path<P>(path: P) -> AnyResult<PathBuf>
209where
210    P: AsRef<OsStr>,
211{
212    Ok(PathBuf::from(unsafe {
213        from_virtual_path(path.as_ref().to_string_lossy().into())?
214    }))
215}
216
217/// Convert the provided path into a [`VirtualPath`] instance,
218/// with the prefix resolved to the WASM virtual whitelist.
219pub fn into_virtual_path<P>(path: P) -> AnyResult<VirtualPath>
220where
221    P: AsRef<OsStr>,
222{
223    let data = unsafe { to_virtual_path(path.as_ref().to_string_lossy().into())? };
224
225    Ok(data.0)
226}
227
228/// Return the ID for the current plugin.
229pub fn get_plugin_id() -> AnyResult<String> {
230    Ok(config::get("plugin_id")?.expect("Missing plugin ID!"))
231}
232
233/// Return information about the host environment.
234pub fn get_host_environment() -> AnyResult<HostEnvironment> {
235    let config = config::get("host_environment")?.expect("Missing host environment!");
236    let config: HostEnvironment = json::from_str(&config)?;
237
238    Ok(config)
239}
240
241/// Return information about the testing environment.
242pub fn get_test_environment() -> AnyResult<Option<TestEnvironment>> {
243    if let Some(config) = config::get("test_environment")? {
244        return Ok(json::from_str(&config)?);
245    }
246
247    Ok(None)
248}