use serde_json::json;
use std::path::Path;
use zacor_package::io::fs::FileType;
use zacor_package::protocol::{self, CapabilityError, CapabilityReq, CapabilityRes, CapabilityResult};
pub fn handle(req: &CapabilityReq) -> CapabilityRes {
let result = match req.domain.as_str() {
"fs" => handle_fs(&req.op, &req.params),
"clipboard" => handle_clipboard(&req.op, &req.params),
"prompt" => handle_prompt(&req.op, &req.params),
_ => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown capability domain: {}", req.domain),
)),
};
CapabilityRes {
id: req.id,
result: match result {
Ok(data) => CapabilityResult::Ok { data },
Err(e) => CapabilityResult::Error {
error: CapabilityError::from_io(&e),
},
},
}
}
fn resolve_path(path_str: &str) -> String {
let cwd = std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let resolved = protocol::resolve_path(path_str, &cwd);
resolved.replace('/', std::path::MAIN_SEPARATOR_STR)
}
fn handle_fs(op: &str, params: &serde_json::Value) -> std::io::Result<serde_json::Value> {
let path_str = params
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("");
let resolved = resolve_path(path_str);
match op {
"read_string" => {
let content = std::fs::read_to_string(&resolved)?;
Ok(json!({"content": content}))
}
"read" => {
let content = std::fs::read(&resolved)?;
let encoded = protocol::base64_encode(&content);
Ok(json!({"content": encoded}))
}
"read_dir" => {
let mut entries = Vec::new();
for entry in std::fs::read_dir(&resolved)? {
let entry = entry?;
let meta = entry.metadata()?;
let ft: FileType = meta.file_type().into();
let mut e = json!({
"name": entry.file_name().to_string_lossy(),
"size": meta.len(),
"file_type": ft,
});
if let Ok(modified) = meta.modified() {
if let Ok(epoch) = modified.duration_since(std::time::UNIX_EPOCH) {
e["modified"] = json!(epoch.as_secs_f64());
}
}
entries.push(e);
}
Ok(json!({"entries": entries}))
}
"stat" => {
let meta = std::fs::metadata(&resolved)?;
let ft: FileType = meta.file_type().into();
let mut result = json!({"size": meta.len(), "file_type": ft});
if let Ok(modified) = meta.modified() {
if let Ok(epoch) = modified.duration_since(std::time::UNIX_EPOCH) {
result["modified"] = json!(epoch.as_secs_f64());
}
}
if let Ok(created) = meta.created() {
if let Ok(epoch) = created.duration_since(std::time::UNIX_EPOCH) {
result["created"] = json!(epoch.as_secs_f64());
}
}
Ok(result)
}
"exists" => Ok(json!({"exists": Path::new(&resolved).exists()})),
"write" => {
let content = params
.get("content")
.and_then(|v| v.as_str())
.unwrap_or("");
let decoded = protocol::base64_decode(content)?;
std::fs::write(&resolved, decoded)?;
Ok(json!({}))
}
_ => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown fs operation: {}", op),
)),
}
}
fn handle_clipboard(
op: &str,
params: &serde_json::Value,
) -> std::io::Result<serde_json::Value> {
match op {
"read" => {
let mut clipboard = arboard::Clipboard::new()
.map_err(|e| std::io::Error::other(e.to_string()))?;
let text = clipboard
.get_text()
.map_err(|e| std::io::Error::other(e.to_string()))?;
Ok(json!({"text": text}))
}
"write" => {
let text = params
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("");
let mut clipboard = arboard::Clipboard::new()
.map_err(|e| std::io::Error::other(e.to_string()))?;
clipboard
.set_text(text.to_string())
.map_err(|e| std::io::Error::other(e.to_string()))?;
Ok(json!({}))
}
_ => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown clipboard operation: {}", op),
)),
}
}
fn handle_prompt(
op: &str,
params: &serde_json::Value,
) -> std::io::Result<serde_json::Value> {
use std::io::{IsTerminal, Write};
if !std::io::stdin().is_terminal() {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"prompt not available in piped mode",
));
}
let message = params
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("");
match op {
"confirm" => {
eprint!("{} [y/N] ", message);
std::io::stderr().flush()?;
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
let answer = matches!(line.trim().to_lowercase().as_str(), "y" | "yes");
Ok(json!({"answer": answer}))
}
"choose" => {
let options = params
.get("options")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<_>>()
})
.unwrap_or_default();
eprintln!("{}", message);
for (i, opt) in options.iter().enumerate() {
eprintln!(" {}) {}", i + 1, opt);
}
eprint!("Choice: ");
std::io::stderr().flush()?;
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
let choice = line.trim();
if let Ok(n) = choice.parse::<usize>() {
if n >= 1 && n <= options.len() {
return Ok(json!({"answer": options[n - 1]}));
}
}
if options.iter().any(|o| o == choice) {
return Ok(json!({"answer": choice}));
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid choice: {}", choice),
))
}
"text" => {
eprint!("{}: ", message);
std::io::stderr().flush()?;
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
Ok(json!({"answer": line.trim_end()}))
}
_ => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown prompt operation: {}", op),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use zacor_package::protocol::CapabilityReq;
fn make_req(domain: &str, op: &str, params: serde_json::Value) -> CapabilityReq {
CapabilityReq {
id: 1,
domain: domain.into(),
op: op.into(),
params,
}
}
#[test]
fn fs_read_string() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "hello").unwrap();
let req = make_req("fs", "read_string", json!({"path": file.to_str().unwrap()}));
let res = handle(&req);
match res.result {
CapabilityResult::Ok { data } => {
assert_eq!(data["content"], "hello");
}
CapabilityResult::Error { error } => panic!("unexpected error: {}", error.message),
}
}
#[test]
fn fs_exists() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
let req = make_req("fs", "exists", json!({"path": file.to_str().unwrap()}));
let res = handle(&req);
match res.result {
CapabilityResult::Ok { data } => assert_eq!(data["exists"], false),
_ => panic!("expected ok"),
}
std::fs::write(&file, "data").unwrap();
let res = handle(&req);
match res.result {
CapabilityResult::Ok { data } => assert_eq!(data["exists"], true),
_ => panic!("expected ok"),
}
}
#[test]
fn fs_read_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.txt"), "").unwrap();
std::fs::write(dir.path().join("b.txt"), "").unwrap();
let req = make_req(
"fs",
"read_dir",
json!({"path": dir.path().to_str().unwrap()}),
);
let res = handle(&req);
match res.result {
CapabilityResult::Ok { data } => {
let entries = data["entries"].as_array().unwrap();
assert_eq!(entries.len(), 2);
}
_ => panic!("expected ok"),
}
}
#[test]
fn fs_not_found() {
let req = make_req(
"fs",
"read_string",
json!({"path": "/nonexistent/path/file.txt"}),
);
let res = handle(&req);
match res.result {
CapabilityResult::Error { error } => {
assert_eq!(error.kind, "not_found");
}
_ => panic!("expected error"),
}
}
#[test]
fn fs_write_and_read() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("output.txt");
let encoded = protocol::base64_encode(b"written data");
let req = make_req(
"fs",
"write",
json!({"path": file.to_str().unwrap(), "content": encoded}),
);
let res = handle(&req);
assert!(matches!(res.result, CapabilityResult::Ok { .. }));
let content = std::fs::read_to_string(&file).unwrap();
assert_eq!(content, "written data");
}
#[test]
fn unknown_domain_returns_error() {
let req = make_req("unknown", "op", json!({}));
let res = handle(&req);
assert!(matches!(res.result, CapabilityResult::Error { .. }));
}
#[test]
fn unknown_fs_op_returns_error() {
let req = make_req("fs", "unknown_op", json!({}));
let res = handle(&req);
assert!(matches!(res.result, CapabilityResult::Error { .. }));
}
#[test]
fn response_id_matches_request() {
let req = CapabilityReq {
id: 42,
domain: "fs".into(),
op: "exists".into(),
params: json!({"path": "."}),
};
let res = handle(&req);
assert_eq!(res.id, 42);
}
}