Skip to main content

zwire_host/
api.rs

1//! High-level, in-process helpers for crates that embed this one as a library.
2//!
3//! Sibling hosts such as `zpwrchrome-host` depend on `zwire-host` to **crawl the
4//! filesystem** and **run commands** without speaking the wire protocol or
5//! standing up a daemon — they just call these functions and get native Rust
6//! values back:
7//!
8//! ```no_run
9//! // in zpwrchrome-host:
10//! for entry in zwire_host::api::walk("~/src", Some("rs")) {
11//!     println!("{}", entry.path.display());
12//! }
13//! let out = zwire_host::api::exec("git", ["status", "--porcelain"]).unwrap();
14//! println!("git exited {:?}:\n{}", out.code, String::from_utf8_lossy(&out.stdout));
15//! ```
16//!
17//! Everything here is a thin, allocation-light wrapper over the same capability
18//! functions the transports call, so behaviour is identical in-process and over
19//! the socket.
20use crate::proto::b64_decode;
21use serde_json::{json, Value};
22use std::path::PathBuf;
23
24/// Captured result of [`exec`].
25#[derive(Debug, Clone)]
26pub struct ExecOutput {
27    /// Process exit code, or `None` if it was killed by a signal.
28    pub code: Option<i64>,
29    /// Raw stdout bytes.
30    pub stdout: Vec<u8>,
31    /// Raw stderr bytes.
32    pub stderr: Vec<u8>,
33}
34
35impl ExecOutput {
36    /// stdout decoded lossily as UTF-8, trailing newline trimmed.
37    pub fn stdout_str(&self) -> String {
38        String::from_utf8_lossy(&self.stdout).trim_end().to_string()
39    }
40}
41
42/// Run `program args...` to completion in-process and capture its output.
43/// Errors carry the failure reason (e.g. the program was not found).
44pub fn exec<I, S>(program: &str, args: I) -> Result<ExecOutput, String>
45where
46    I: IntoIterator<Item = S>,
47    S: AsRef<str>,
48{
49    let args: Vec<String> = args.into_iter().map(|s| s.as_ref().to_string()).collect();
50    let v = crate::exec::run(&json!({ "program": program, "args": args }));
51    if v["ok"] != json!(true) {
52        return Err(v["err"].as_str().unwrap_or("exec_failed").to_string());
53    }
54    Ok(ExecOutput {
55        code: v["code"].as_i64(),
56        stdout: v["stdout"]
57            .as_str()
58            .and_then(b64_decode)
59            .unwrap_or_default(),
60        stderr: v["stderr"]
61            .as_str()
62            .and_then(b64_decode)
63            .unwrap_or_default(),
64    })
65}
66
67/// Run `program` with `input` fed to its stdin.
68pub fn exec_stdin<I, S>(program: &str, args: I, input: &str) -> Result<ExecOutput, String>
69where
70    I: IntoIterator<Item = S>,
71    S: AsRef<str>,
72{
73    let args: Vec<String> = args.into_iter().map(|s| s.as_ref().to_string()).collect();
74    let v = crate::exec::run(&json!({ "program": program, "args": args, "stdin": input }));
75    if v["ok"] != json!(true) {
76        return Err(v["err"].as_str().unwrap_or("exec_failed").to_string());
77    }
78    Ok(ExecOutput {
79        code: v["code"].as_i64(),
80        stdout: v["stdout"]
81            .as_str()
82            .and_then(b64_decode)
83            .unwrap_or_default(),
84        stderr: v["stderr"]
85            .as_str()
86            .and_then(b64_decode)
87            .unwrap_or_default(),
88    })
89}
90
91/// One node found by [`walk`].
92#[derive(Debug, Clone)]
93pub struct Entry {
94    /// Absolute path to the entry.
95    pub path: PathBuf,
96    /// Leaf file name.
97    pub name: String,
98    /// Whether the entry is a directory.
99    pub dir: bool,
100    /// File size in bytes (0 for directories / on stat failure).
101    pub size: u64,
102}
103
104/// Recursively crawl `root` (a `~`-expandable path). `ext`, when given, keeps
105/// only files with that extension (no leading dot). Capped internally so a crawl
106/// of a huge tree can't run away.
107pub fn walk(root: &str, ext: Option<&str>) -> Vec<Entry> {
108    let mut req = json!({ "path": root });
109    if let Some(x) = ext {
110        req["ext"] = json!(x);
111    }
112    entries_from(crate::fsops::handle("fs_walk", &req))
113}
114
115/// Like [`walk`] but with the full filter set: `depth`, `dirs_only`, `ext`,
116/// `contains` (substring match on the leaf name).
117pub fn walk_filtered(
118    root: &str,
119    depth: Option<usize>,
120    dirs_only: bool,
121    ext: Option<&str>,
122    contains: Option<&str>,
123) -> Vec<Entry> {
124    let mut req = json!({ "path": root, "dirs_only": dirs_only });
125    if let Some(d) = depth {
126        req["depth"] = json!(d);
127    }
128    if let Some(x) = ext {
129        req["ext"] = json!(x);
130    }
131    if let Some(c) = contains {
132        req["contains"] = json!(c);
133    }
134    entries_from(crate::fsops::handle("fs_walk", &req))
135}
136
137fn entries_from(v: Value) -> Vec<Entry> {
138    v["entries"]
139        .as_array()
140        .map(|a| {
141            a.iter()
142                .map(|e| Entry {
143                    path: PathBuf::from(e["path"].as_str().unwrap_or("")),
144                    name: e["name"].as_str().unwrap_or("").to_string(),
145                    dir: e["dir"].as_bool().unwrap_or(false),
146                    size: e["size"].as_u64().unwrap_or(0),
147                })
148                .collect()
149        })
150        .unwrap_or_default()
151}
152
153/// Read a whole file (`~`-expandable path) into bytes.
154pub fn read_file(path: &str) -> Result<Vec<u8>, String> {
155    let v = crate::fsops::handle("fs_read", &json!({ "path": path }));
156    if v["ok"] != json!(true) {
157        return Err(v["err"].as_str().unwrap_or("read_failed").to_string());
158    }
159    Ok(v["b64"].as_str().and_then(b64_decode).unwrap_or_default())
160}
161
162/// Write bytes to a file (`~`-expandable path), creating parent dirs.
163pub fn write_file(path: &str, bytes: &[u8]) -> Result<(), String> {
164    let v = crate::fsops::handle(
165        "fs_write",
166        &json!({ "path": path, "b64": crate::proto::b64_encode(bytes) }),
167    );
168    if v["ok"] == json!(true) {
169        Ok(())
170    } else {
171        Err(v["err"].as_str().unwrap_or("write_failed").to_string())
172    }
173}