1use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
7use serde::{Deserialize, Serialize};
8
9pub const PROTOCOL_VERSION: u32 = 1;
11
12#[derive(Debug, Clone, Serialize)]
14pub struct AgentRequest {
15 pub id: u64,
16 #[serde(rename = "m")]
17 pub method: String,
18 #[serde(rename = "p")]
19 pub params: serde_json::Value,
20}
21
22impl AgentRequest {
23 pub fn new(id: u64, method: impl Into<String>, params: serde_json::Value) -> Self {
24 Self {
25 id,
26 method: method.into(),
27 params,
28 }
29 }
30
31 pub fn to_json_line(&self) -> String {
32 serde_json::to_string(self).unwrap() + "\n"
33 }
34}
35
36#[derive(Debug, Clone, Deserialize)]
38pub struct AgentResponse {
39 pub id: u64,
40 #[serde(rename = "d")]
42 pub data: Option<serde_json::Value>,
43 #[serde(rename = "r")]
45 pub result: Option<serde_json::Value>,
46 #[serde(rename = "e")]
48 pub error: Option<String>,
49 pub ok: Option<bool>,
51 #[serde(rename = "v")]
52 pub version: Option<u32>,
53}
54
55impl AgentResponse {
56 pub fn is_ready(&self) -> bool {
58 self.ok == Some(true)
59 }
60
61 pub fn is_data(&self) -> bool {
63 self.data.is_some()
64 }
65
66 pub fn is_final(&self) -> bool {
68 self.result.is_some() || self.error.is_some()
69 }
70}
71
72#[derive(Debug, Clone, Deserialize)]
74#[allow(dead_code)]
75pub struct RemoteDirEntry {
76 pub name: String,
77 pub path: String,
78 #[serde(default)]
79 pub dir: bool,
80 #[serde(default)]
81 pub file: bool,
82 #[serde(default)]
83 pub link: bool,
84 #[serde(default)]
85 pub link_dir: bool,
86 #[serde(default)]
87 pub size: u64,
88 #[serde(default)]
89 pub mtime: i64,
90 #[serde(default)]
91 pub mode: u32,
92}
93
94#[derive(Debug, Clone, Deserialize)]
96#[allow(dead_code)]
97pub struct RemoteMetadata {
98 pub size: u64,
99 pub mtime: i64,
100 pub mode: u32,
101 #[serde(default)]
102 pub uid: u32,
103 #[serde(default)]
104 pub gid: u32,
105 #[serde(default)]
106 pub dir: bool,
107 #[serde(default)]
108 pub file: bool,
109 #[serde(default)]
110 pub link: bool,
111}
112
113#[derive(Debug, Clone, Deserialize)]
115#[allow(dead_code)]
116pub struct ExecResult {
117 pub code: i32,
118}
119
120#[derive(Debug, Clone, Deserialize)]
122#[allow(dead_code)]
123pub struct ExecOutput {
124 #[serde(default)]
125 pub out: Option<String>,
126 #[serde(default)]
127 pub err: Option<String>,
128}
129
130pub fn encode_base64(data: &[u8]) -> String {
132 BASE64.encode(data)
133}
134
135pub fn decode_base64(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
137 BASE64.decode(s)
138}
139
140pub fn read_params(path: &str, offset: Option<u64>, len: Option<usize>) -> serde_json::Value {
142 let mut params = serde_json::json!({"path": path});
143 if let Some(off) = offset {
144 params["off"] = serde_json::json!(off);
145 }
146 if let Some(l) = len {
147 params["len"] = serde_json::json!(l);
148 }
149 params
150}
151
152pub fn write_params(path: &str, data: &[u8]) -> serde_json::Value {
154 serde_json::json!({
155 "path": path,
156 "data": encode_base64(data)
157 })
158}
159
160pub fn sudo_write_params(
162 path: &str,
163 data: &[u8],
164 mode: u32,
165 uid: u32,
166 gid: u32,
167) -> serde_json::Value {
168 serde_json::json!({
169 "path": path,
170 "data": encode_base64(data),
171 "mode": mode,
172 "uid": uid,
173 "gid": gid
174 })
175}
176
177pub fn stat_params(path: &str, follow_symlinks: bool) -> serde_json::Value {
179 serde_json::json!({
180 "path": path,
181 "link": follow_symlinks
182 })
183}
184
185pub fn ls_params(path: &str) -> serde_json::Value {
187 serde_json::json!({"path": path})
188}
189
190pub fn exec_params(cmd: &str, args: &[String], cwd: Option<&str>) -> serde_json::Value {
194 let mut params = serde_json::json!({
195 "cmd": cmd,
196 "args": args
197 });
198 if let Some(dir) = cwd {
199 params["cwd"] = serde_json::json!(dir);
200 }
201 params
202}
203
204pub fn cancel_params(request_id: u64) -> serde_json::Value {
206 serde_json::json!({"id": request_id})
207}
208
209pub fn append_params(path: &str, data: &[u8]) -> serde_json::Value {
211 serde_json::json!({
212 "path": path,
213 "data": encode_base64(data)
214 })
215}
216
217pub fn truncate_params(path: &str, len: u64) -> serde_json::Value {
219 serde_json::json!({
220 "path": path,
221 "len": len
222 })
223}
224
225#[derive(Debug, Clone, Serialize)]
227#[serde(untagged)]
228pub enum PatchOp {
229 Copy { copy: CopyRange },
231 Insert { insert: InsertData },
233}
234
235#[derive(Debug, Clone, Serialize)]
237pub struct CopyRange {
238 pub off: u64,
239 pub len: u64,
240}
241
242#[derive(Debug, Clone, Serialize)]
244pub struct InsertData {
245 pub data: String, }
247
248impl PatchOp {
249 pub fn copy(offset: u64, len: u64) -> Self {
251 PatchOp::Copy {
252 copy: CopyRange { off: offset, len },
253 }
254 }
255
256 pub fn insert(data: &[u8]) -> Self {
258 PatchOp::Insert {
259 insert: InsertData {
260 data: encode_base64(data),
261 },
262 }
263 }
264}
265
266pub fn patch_params(src: &str, dst: Option<&str>, ops: &[PatchOp]) -> serde_json::Value {
268 let mut params = serde_json::json!({
269 "src": src,
270 "ops": ops
271 });
272 if let Some(d) = dst {
273 params["dst"] = serde_json::json!(d);
274 }
275 params
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_request_serialization() {
284 let req = AgentRequest::new(1, "read", serde_json::json!({"path": "/test.txt"}));
285 let json = req.to_json_line();
286 assert!(json.contains("\"id\":1"));
287 assert!(json.contains("\"m\":\"read\""));
288 assert!(json.contains("\"p\":{\"path\":\"/test.txt\"}"));
289 }
290
291 #[test]
292 fn test_response_parsing() {
293 let ready = r#"{"id":0,"ok":true,"v":1}"#;
294 let resp: AgentResponse = serde_json::from_str(ready).unwrap();
295 assert!(resp.is_ready());
296 assert_eq!(resp.version, Some(1));
297
298 let data = r#"{"id":1,"d":{"data":"SGVsbG8="}}"#;
299 let resp: AgentResponse = serde_json::from_str(data).unwrap();
300 assert!(resp.is_data());
301 assert!(!resp.is_final());
302
303 let result = r#"{"id":1,"r":{"size":5}}"#;
304 let resp: AgentResponse = serde_json::from_str(result).unwrap();
305 assert!(resp.is_final());
306 assert!(resp.result.is_some());
307
308 let error = r#"{"id":1,"e":"not found"}"#;
309 let resp: AgentResponse = serde_json::from_str(error).unwrap();
310 assert!(resp.is_final());
311 assert_eq!(resp.error, Some("not found".to_string()));
312 }
313
314 #[test]
315 fn test_base64_roundtrip() {
316 let data = b"Hello, World!";
317 let encoded = encode_base64(data);
318 let decoded = decode_base64(&encoded).unwrap();
319 assert_eq!(data.as_slice(), decoded.as_slice());
320 }
321
322 #[test]
323 fn test_patch_op_copy_serialization() {
324 let op = PatchOp::copy(100, 500);
325 let json = serde_json::to_string(&op).unwrap();
326 assert!(json.contains("\"copy\""));
327 assert!(json.contains("\"off\":100"));
328 assert!(json.contains("\"len\":500"));
329 assert!(!json.contains("\"insert\""));
331 }
332
333 #[test]
334 fn test_patch_op_insert_serialization() {
335 let op = PatchOp::insert(b"hello");
336 let json = serde_json::to_string(&op).unwrap();
337 assert!(json.contains("\"insert\""));
338 assert!(json.contains("\"data\":\"aGVsbG8=\"")); assert!(!json.contains("\"copy\""));
341 }
342
343 #[test]
344 fn test_patch_params() {
345 let ops = vec![
346 PatchOp::copy(0, 100),
347 PatchOp::insert(b"new content"),
348 PatchOp::copy(200, 300),
349 ];
350
351 let params = patch_params("/path/to/file", None, &ops);
353 assert_eq!(params["src"], "/path/to/file");
354 assert!(params.get("dst").is_none() || params["dst"].is_null());
355 assert!(params["ops"].is_array());
356 assert_eq!(params["ops"].as_array().unwrap().len(), 3);
357
358 let params = patch_params("/src/file", Some("/dst/file"), &ops);
360 assert_eq!(params["src"], "/src/file");
361 assert_eq!(params["dst"], "/dst/file");
362 }
363}