1pub use extism_pdk::{self, plugin_fn, FnResult, FromBytes, ToBytes};
7pub use serde;
8pub use serde_json;
9
10use base64::Engine;
11use extism_pdk::*;
12use serde::de::DeserializeOwned;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct WsRequest {
21 #[serde(default)]
22 pub action: String,
23 #[serde(default)]
24 pub data: serde_json::Value,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct PackageResult {
30 pub status: String,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub data: Option<serde_json::Value>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub error: Option<String>,
35}
36
37impl PackageResult {
38 pub fn ok(data: serde_json::Value) -> Self {
39 Self {
40 status: "ok".into(),
41 data: Some(data),
42 error: None,
43 }
44 }
45
46 pub fn ok_empty() -> Self {
47 Self {
48 status: "ok".into(),
49 data: None,
50 error: None,
51 }
52 }
53
54 pub fn err(msg: impl Into<String>) -> Self {
55 Self {
56 status: "error".into(),
57 data: None,
58 error: Some(msg.into()),
59 }
60 }
61
62 pub fn to_json(&self) -> String {
63 serde_json::to_string(self)
64 .unwrap_or_else(|_| r#"{"status":"error","error":"serialize failed"}"#.into())
65 }
66}
67
68#[host_fn]
72extern "ExtismHost" {
73 pub fn host_log(input: String) -> String;
74 pub fn host_kv_get(key: String) -> String;
75 pub fn host_kv_set(input: String);
76 pub fn host_kv_list(prefix: String) -> String;
77 pub fn host_kv_delete(key: String);
78 pub fn host_env_get(key: String) -> String;
79 pub fn host_read_file(path: String) -> String;
80 pub fn host_write_file(input: String);
81 pub fn host_write_file_base64(input: String) -> String;
82 pub fn host_list_dir(path: String) -> String;
83 pub fn host_exec(input: String) -> String;
84 pub fn host_exec_advanced(input: String) -> String;
85 pub fn host_chat_completion(input: String) -> String;
86 pub fn host_chat_completion_stream(input: String) -> String;
87 pub fn host_http_request(input: String) -> String;
88 pub fn host_call_package(input: String) -> String;
89 pub fn host_call_package_ws(input: String) -> String;
90 pub fn host_capability_call(input: String) -> String;
91 pub fn host_process_spawn(config: String) -> String;
92 pub fn host_process_stop(name: String) -> String;
93 pub fn host_process_status(name: String) -> String;
94 pub fn host_process_write_stdin(input: String) -> String;
95 pub fn host_process_read_stdout(input: String) -> String;
96 pub fn host_sqlite_query(input: String) -> String;
97 pub fn host_sqlite_execute(input: String) -> String;
98 pub fn host_sqlite_batch(input: String) -> String;
99 pub fn host_now_ms(input: String) -> String;
100}
101
102pub fn log(level: &str, msg: &str) {
106 let input = serde_json::json!([level, msg]).to_string();
107 let _ = unsafe { host_log(input) };
108}
109
110pub fn log_info(msg: &str) {
111 log("info", msg);
112}
113pub fn log_warn(msg: &str) {
114 log("warn", msg);
115}
116pub fn log_error(msg: &str) {
117 log("error", msg);
118}
119pub fn log_debug(msg: &str) {
120 log("debug", msg);
121}
122
123pub fn kv_get(key: &str) -> Option<String> {
125 match unsafe { host_kv_get(key.to_string()) } {
126 Ok(v) if v.is_empty() => None,
127 Ok(v) => Some(v),
128 Err(_) => None,
129 }
130}
131
132pub fn kv_set(key: &str, value: &str) {
134 let input = serde_json::json!([key, value]).to_string();
135 let _ = unsafe { host_kv_set(input) };
136}
137
138pub fn kv_list(prefix: &str) -> Result<Vec<String>, String> {
139 match unsafe { host_kv_list(prefix.to_string()) } {
140 Ok(json) => parse_host_json(&json),
141 Err(e) => Err(format!("{}", e)),
142 }
143}
144
145pub fn kv_delete(key: &str) -> Result<(), String> {
146 match unsafe { host_kv_delete(key.to_string()) } {
147 Ok(_) => Ok(()),
148 Err(e) => Err(format!("{}", e)),
149 }
150}
151
152pub fn env_get(key: &str) -> Option<String> {
153 match unsafe { host_env_get(key.to_string()) } {
154 Ok(value) if value.is_empty() => None,
155 Ok(value) => Some(value),
156 Err(_) => None,
157 }
158}
159
160pub fn read_file(path: &str) -> Result<String, String> {
162 match unsafe { host_read_file(path.to_string()) } {
163 Ok(content) => {
164 if content.starts_with(r#"{"error":"#) {
165 Err(content)
166 } else {
167 Ok(content)
168 }
169 }
170 Err(e) => Err(format!("{}", e)),
171 }
172}
173
174pub fn write_file(path: &str, content: &str) {
176 let input = serde_json::json!([path, content]).to_string();
177 let _ = unsafe { host_write_file(input) };
178}
179
180pub fn write_file_base64(path: &str, b64: &str) -> Result<String, String> {
183 let input = serde_json::json!([path, b64]).to_string();
184 let raw = unsafe { host_write_file_base64(input) }
185 .map_err(|e| format!("host_write_file_base64 failed: {e}"))?;
186 let parsed: serde_json::Value = serde_json::from_str(&raw)
187 .map_err(|e| format!("host_write_file_base64 returned invalid json: {e}"))?;
188 if let Some(err) = parsed.get("error").and_then(|v| v.as_str()) {
189 if !err.trim().is_empty() {
190 return Err(err.to_string());
191 }
192 }
193 Ok(parsed
194 .get("path")
195 .and_then(|v| v.as_str())
196 .unwrap_or(path)
197 .to_string())
198}
199
200pub fn list_dir(path: &str) -> Result<Vec<DirEntry>, String> {
202 match unsafe { host_list_dir(path.to_string()) } {
203 Ok(json) => parse_host_json(&json),
204 Err(e) => Err(format!("{}", e)),
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct DirEntry {
210 pub name: String,
211 pub is_dir: bool,
212}
213
214pub fn exec_command(cmd: &str, args: &[&str]) -> Result<ExecResult, String> {
216 let input = serde_json::json!({
217 "command": cmd,
218 "args": args,
219 })
220 .to_string();
221 match unsafe { host_exec(input) } {
222 Ok(json) => parse_host_json(&json),
223 Err(e) => Err(format!("{}", e)),
224 }
225}
226
227#[derive(Debug, Clone, Default, Serialize, Deserialize)]
228pub struct ExecAdvancedOptions {
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub stdin: Option<String>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub stdin_base64: Option<String>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub workdir: Option<String>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub timeout_ms: Option<u64>,
237 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
238 pub env: HashMap<String, String>,
239}
240
241pub fn exec_command_advanced_with_options(
242 cmd: &str,
243 args: &[&str],
244 options: &ExecAdvancedOptions,
245) -> Result<ExecResult, String> {
246 let input = serde_json::json!({
247 "command": cmd,
248 "args": args,
249 "stdin": options.stdin,
250 "stdin_base64": options.stdin_base64,
251 "workdir": options.workdir,
252 "timeout_ms": options.timeout_ms,
253 "env": options.env,
254 })
255 .to_string();
256
257 match unsafe { host_exec_advanced(input) } {
258 Ok(json) => parse_host_json(&json),
259 Err(e) => Err(format!("{}", e)),
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct ExecResult {
265 pub status: i32,
266 pub stdout: String,
267 pub stderr: String,
268 #[serde(default)]
269 pub stdout_base64: Option<String>,
270 #[serde(default)]
271 pub stderr_base64: Option<String>,
272}
273
274impl ExecResult {
275 pub fn stdout_text(&self) -> String {
276 decode_exec_base64_text(&self.stdout_base64).unwrap_or_else(|| self.stdout.clone())
277 }
278
279 pub fn stderr_text(&self) -> String {
280 decode_exec_base64_text(&self.stderr_base64).unwrap_or_else(|| self.stderr.clone())
281 }
282}
283
284#[derive(Debug, Clone, Serialize)]
285struct HostChatCompletionInput<'a> {
286 request_label: &'a str,
287 endpoint: &'a str,
288 body: &'a str,
289}
290
291pub fn http_request(
294 method: &str,
295 url: &str,
296 headers: &[(&str, &str)],
297 body: &str,
298) -> Result<String, String> {
299 let header_map: std::collections::HashMap<&str, &str> = headers.iter().copied().collect();
300 let input = serde_json::json!({
301 "method": method,
302 "url": url,
303 "headers": header_map,
304 "body": body,
305 })
306 .to_string();
307
308 let raw = unsafe { host_http_request(input) }
309 .map_err(|e| format!("host_http_request failed: {e}"))?;
310 let parsed: serde_json::Value = serde_json::from_str(&raw)
311 .map_err(|e| format!("host_http_request returned invalid json: {e}"))?;
312 if let Some(err) = parsed.get("error").and_then(|v| v.as_str()) {
313 if !err.trim().is_empty() {
314 return Err(err.to_string());
315 }
316 }
317 let status = parsed.get("status").and_then(|v| v.as_u64()).unwrap_or(0);
318 let resp_body = parsed
319 .get("body")
320 .and_then(|v| v.as_str())
321 .unwrap_or("")
322 .to_string();
323 if (200..300).contains(&status) {
324 Ok(resp_body)
325 } else {
326 Err(format!("http status {status}: {resp_body}"))
327 }
328}
329
330pub fn chat_completion(request_label: &str, endpoint: &str, body: &str) -> Result<String, String> {
331 let input = serde_json::to_string(&HostChatCompletionInput {
332 request_label,
333 endpoint,
334 body,
335 })
336 .map_err(|error| format!("failed to serialize host chat completion input: {}", error))?;
337
338 match unsafe { host_chat_completion(input) } {
339 Ok(result) => {
340 if let Ok(error) = serde_json::from_str::<HostErrorResult>(&result) {
341 if !error.error.trim().is_empty() {
342 return Err(error.error);
343 }
344 }
345 Ok(result)
346 }
347 Err(e) => Err(format!("{}", e)),
348 }
349}
350
351#[derive(Debug, Clone, Serialize)]
352struct HostChatCompletionStreamInput<'a> {
353 request_label: &'a str,
354 body: &'a str,
355 session_id: &'a str,
356}
357
358pub fn chat_completion_stream(
359 request_label: &str,
360 _endpoint: &str,
361 body: &str,
362 session_id: &str,
363) -> Result<String, String> {
364 let input = serde_json::to_string(&HostChatCompletionStreamInput {
365 request_label,
366 body,
367 session_id,
368 })
369 .map_err(|e| format!("failed to serialize stream input: {}", e))?;
370
371 match unsafe { host_chat_completion_stream(input) } {
372 Ok(result) => {
373 if let Ok(error) = serde_json::from_str::<HostErrorResult>(&result) {
374 if !error.error.trim().is_empty() {
375 return Err(error.error);
376 }
377 }
378 Ok(result)
379 }
380 Err(e) => Err(format!("{}", e)),
381 }
382}
383
384pub fn call_package(package: &str, func: &str, args: &str) -> Result<String, String> {
386 let input = serde_json::json!({
387 "package": package,
388 "func": func,
389 "args": args,
390 })
391 .to_string();
392 match unsafe { host_call_package(input) } {
393 Ok(result) => {
394 if result.contains(r#""error""#) {
395 Err(result)
396 } else {
397 Ok(result)
398 }
399 }
400 Err(e) => Err(format!("{}", e)),
401 }
402}
403
404pub fn call_package_ws_action(
405 package: &str,
406 action: &str,
407 data: &serde_json::Value,
408) -> Result<String, String> {
409 let input = serde_json::json!({
410 "package": package,
411 "action": action,
412 "data": data,
413 })
414 .to_string();
415 match unsafe { host_call_package_ws(input) } {
416 Ok(result) => Ok(result),
417 Err(e) => Err(format!("{}", e)),
418 }
419}
420
421pub fn call_capability_action(
422 capability: &str,
423 action: &str,
424 data: &serde_json::Value,
425) -> Result<String, String> {
426 let input = serde_json::json!({
427 "capability": capability,
428 "action": action,
429 "data": data,
430 })
431 .to_string();
432 match unsafe { host_capability_call(input) } {
433 Ok(result) => Ok(result),
434 Err(e) => Err(format!("{}", e)),
435 }
436}
437
438pub fn process_spawn(config_json: &str) -> Result<String, String> {
440 match unsafe { host_process_spawn(config_json.to_string()) } {
441 Ok(r) => Ok(r),
442 Err(e) => Err(format!("{}", e)),
443 }
444}
445
446pub fn process_stop(name: &str) -> Result<String, String> {
448 match unsafe { host_process_stop(name.to_string()) } {
449 Ok(r) => Ok(r),
450 Err(e) => Err(format!("{}", e)),
451 }
452}
453
454pub fn process_status(name: &str) -> Result<String, String> {
456 match unsafe { host_process_status(name.to_string()) } {
457 Ok(r) => Ok(r),
458 Err(e) => Err(format!("{}", e)),
459 }
460}
461
462pub fn process_write_stdin(name: &str, input: &str) -> Result<String, String> {
463 let payload = serde_json::json!({
464 "name": name,
465 "input": input,
466 })
467 .to_string();
468 match unsafe { host_process_write_stdin(payload) } {
469 Ok(r) => Ok(r),
470 Err(e) => Err(format!("{}", e)),
471 }
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct ProcessStdoutRead {
476 pub status: String,
477 pub name: String,
478 pub next_offset: usize,
479 pub chunk: Vec<u8>,
480}
481
482pub fn process_read_stdout(name: &str, offset: usize) -> Result<ProcessStdoutRead, String> {
483 let payload = serde_json::json!({
484 "name": name,
485 "offset": offset,
486 })
487 .to_string();
488 match unsafe { host_process_read_stdout(payload) } {
489 Ok(json) => parse_host_json(&json),
490 Err(e) => Err(format!("{}", e)),
491 }
492}
493
494pub fn now_ms() -> u64 {
495 unsafe { host_now_ms(String::new()) }
496 .ok()
497 .and_then(|s| s.trim().parse::<u64>().ok())
498 .unwrap_or(0)
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct SqliteQueryResult {
503 pub columns: Vec<String>,
504 pub rows: Vec<Vec<serde_json::Value>>,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct SqliteExecResult {
509 pub rows_affected: u64,
510}
511
512pub fn sqlite_query(
513 path: &str,
514 sql: &str,
515 params: &[serde_json::Value],
516) -> Result<SqliteQueryResult, String> {
517 let input = serde_json::json!({
518 "path": path,
519 "sql": sql,
520 "params": params,
521 })
522 .to_string();
523 match unsafe { host_sqlite_query(input) } {
524 Ok(json) => parse_host_json(&json),
525 Err(e) => Err(format!("{}", e)),
526 }
527}
528
529pub fn sqlite_execute(
530 path: &str,
531 sql: &str,
532 params: &[serde_json::Value],
533) -> Result<SqliteExecResult, String> {
534 let input = serde_json::json!({
535 "path": path,
536 "sql": sql,
537 "params": params,
538 })
539 .to_string();
540 match unsafe { host_sqlite_execute(input) } {
541 Ok(json) => parse_host_json(&json),
542 Err(e) => Err(format!("{}", e)),
543 }
544}
545
546pub fn sqlite_batch(
547 path: &str,
548 statements: &[(String, Vec<serde_json::Value>)],
549) -> Result<SqliteExecResult, String> {
550 let payload: Vec<serde_json::Value> = statements
551 .iter()
552 .map(|(sql, params)| serde_json::json!({ "sql": sql, "params": params }))
553 .collect();
554 let input = serde_json::json!({
555 "path": path,
556 "statements": payload,
557 })
558 .to_string();
559 match unsafe { host_sqlite_batch(input) } {
560 Ok(json) => parse_host_json(&json),
561 Err(e) => Err(format!("{}", e)),
562 }
563}
564
565#[derive(Debug, Deserialize)]
566struct HostErrorResult {
567 error: String,
568}
569
570fn parse_host_json<T>(json: &str) -> Result<T, String>
571where
572 T: DeserializeOwned,
573{
574 match serde_json::from_str::<T>(json) {
575 Ok(value) => Ok(value),
576 Err(parse_error) => {
577 if let Ok(error) = serde_json::from_str::<HostErrorResult>(json) {
578 if !error.error.is_empty() {
579 return Err(error.error);
580 }
581 }
582 Err(format!("parse error: {}", parse_error))
583 }
584 }
585}
586
587fn decode_exec_base64_text(encoded: &Option<String>) -> Option<String> {
588 encoded
589 .as_deref()
590 .and_then(|value| base64::engine::general_purpose::STANDARD.decode(value).ok())
591 .and_then(|bytes| String::from_utf8(bytes).ok())
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn parse_host_json_returns_structured_value() {
600 let parsed = parse_host_json::<ExecResult>(r#"{"status":0,"stdout":"ok","stderr":""}"#)
601 .expect("expected exec result");
602
603 assert_eq!(parsed.status, 0);
604 assert_eq!(parsed.stdout, "ok");
605 assert_eq!(parsed.stderr, "");
606 }
607
608 #[test]
609 fn parse_host_json_surfaces_host_error_without_wrapper_parse_failure() {
610 let err = parse_host_json::<ExecResult>(
611 r#"{"error":"The system cannot find the path specified."}"#,
612 )
613 .expect_err("expected host error");
614
615 assert_eq!(err, "The system cannot find the path specified.");
616 }
617
618 #[test]
619 fn parse_host_json_accepts_optional_base64_exec_fields() {
620 let parsed = parse_host_json::<ExecResult>(
621 r#"{"status":0,"stdout":"fallback","stderr":"","stdout_base64":"aGVsbG8=","stderr_base64":null}"#,
622 )
623 .expect("expected exec result with base64 fields");
624
625 assert_eq!(parsed.status, 0);
626 assert_eq!(parsed.stdout, "fallback");
627 assert_eq!(parsed.stdout_base64.as_deref(), Some("aGVsbG8="));
628 assert_eq!(parsed.stdout_text(), "hello");
629 assert_eq!(parsed.stderr_text(), "");
630 }
631
632 #[test]
633 fn exec_text_helpers_fall_back_to_plain_text() {
634 let parsed = ExecResult {
635 status: 0,
636 stdout: "plain stdout".into(),
637 stderr: "plain stderr".into(),
638 stdout_base64: None,
639 stderr_base64: Some("%%%invalid%%%".into()),
640 };
641
642 assert_eq!(parsed.stdout_text(), "plain stdout");
643 assert_eq!(parsed.stderr_text(), "plain stderr");
644 }
645}