1use async_trait::async_trait;
7use std::path::Path;
8use std::sync::Arc;
9
10use super::{
11 BackendError, BackendResult, ConflictError, KernelBackend, PatchOp, ReadRange,
12 ToolInfo, ToolResult, WriteMode,
13};
14use crate::tools::{ToolArgs, ToolCtx, ToolRegistry};
15use crate::vfs::{DirEntry, Filesystem, MountInfo, VfsRouter};
16
17pub struct LocalBackend {
23 vfs: Arc<VfsRouter>,
25 tools: Option<Arc<ToolRegistry>>,
27}
28
29impl LocalBackend {
30 pub fn new(vfs: Arc<VfsRouter>) -> Self {
32 Self { vfs, tools: None }
33 }
34
35 pub fn with_tools(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>) -> Self {
37 Self {
38 vfs,
39 tools: Some(tools),
40 }
41 }
42
43 pub fn vfs(&self) -> &Arc<VfsRouter> {
45 &self.vfs
46 }
47
48 pub fn tools(&self) -> Option<&Arc<ToolRegistry>> {
50 self.tools.as_ref()
51 }
52
53 pub fn apply_patch_op(content: &mut String, op: &PatchOp) -> BackendResult<()> {
57 match op {
58 PatchOp::Insert { offset, content: insert_content } => {
59 if *offset > content.len() {
60 return Err(BackendError::InvalidOperation(format!(
61 "insert offset {} exceeds content length {}",
62 offset,
63 content.len()
64 )));
65 }
66 content.insert_str(*offset, insert_content);
67 }
68
69 PatchOp::Delete { offset, len, expected } => {
70 let end = offset.saturating_add(*len);
71 if end > content.len() {
72 return Err(BackendError::InvalidOperation(format!(
73 "delete range {}..{} exceeds content length {}",
74 offset, end, content.len()
75 )));
76 }
77 if let Some(expected_content) = expected {
79 let actual = &content[*offset..end];
80 if actual != expected_content {
81 return Err(BackendError::Conflict(ConflictError {
82 location: format!("offset {}", offset),
83 expected: expected_content.clone(),
84 actual: actual.to_string(),
85 }));
86 }
87 }
88 content.drain(*offset..end);
89 }
90
91 PatchOp::Replace {
92 offset,
93 len,
94 content: replace_content,
95 expected,
96 } => {
97 let end = offset.saturating_add(*len);
98 if end > content.len() {
99 return Err(BackendError::InvalidOperation(format!(
100 "replace range {}..{} exceeds content length {}",
101 offset, end, content.len()
102 )));
103 }
104 if let Some(expected_content) = expected {
106 let actual = &content[*offset..end];
107 if actual != expected_content {
108 return Err(BackendError::Conflict(ConflictError {
109 location: format!("offset {}", offset),
110 expected: expected_content.clone(),
111 actual: actual.to_string(),
112 }));
113 }
114 }
115 content.replace_range(*offset..end, replace_content);
116 }
117
118 PatchOp::InsertLine { line, content: insert_content } => {
119 let lines: Vec<&str> = content.lines().collect();
120 let line_idx = line.saturating_sub(1); if line_idx > lines.len() {
122 return Err(BackendError::InvalidOperation(format!(
123 "line {} exceeds line count {}",
124 line,
125 lines.len()
126 )));
127 }
128 let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
129 new_lines.insert(line_idx, insert_content.clone());
130 *content = new_lines.join("\n");
131 if !content.is_empty() && !content.ends_with('\n') {
133 content.push('\n');
134 }
135 }
136
137 PatchOp::DeleteLine { line, expected } => {
138 let lines: Vec<&str> = content.lines().collect();
139 let line_idx = line.saturating_sub(1); if line_idx >= lines.len() {
141 return Err(BackendError::InvalidOperation(format!(
142 "line {} exceeds line count {}",
143 line,
144 lines.len()
145 )));
146 }
147 if let Some(expected_content) = expected {
149 let actual = lines[line_idx];
150 if actual != expected_content {
151 return Err(BackendError::Conflict(ConflictError {
152 location: format!("line {}", line),
153 expected: expected_content.clone(),
154 actual: actual.to_string(),
155 }));
156 }
157 }
158 let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
159 new_lines.remove(line_idx);
160 *content = new_lines.join("\n");
161 if !content.is_empty() && !content.ends_with('\n') {
162 content.push('\n');
163 }
164 }
165
166 PatchOp::ReplaceLine {
167 line,
168 content: replace_content,
169 expected,
170 } => {
171 let lines: Vec<&str> = content.lines().collect();
172 let line_idx = line.saturating_sub(1); if line_idx >= lines.len() {
174 return Err(BackendError::InvalidOperation(format!(
175 "line {} exceeds line count {}",
176 line,
177 lines.len()
178 )));
179 }
180 if let Some(expected_content) = expected {
182 let actual = lines[line_idx];
183 if actual != expected_content {
184 return Err(BackendError::Conflict(ConflictError {
185 location: format!("line {}", line),
186 expected: expected_content.clone(),
187 actual: actual.to_string(),
188 }));
189 }
190 }
191 let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
192 new_lines[line_idx] = replace_content.clone();
193 *content = new_lines.join("\n");
194 if !content.is_empty() && !content.ends_with('\n') {
195 content.push('\n');
196 }
197 }
198
199 PatchOp::Append { content: append_content } => {
200 content.push_str(append_content);
201 }
202 }
203 Ok(())
204 }
205
206}
207
208#[async_trait]
209impl KernelBackend for LocalBackend {
210 async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>> {
215 Ok(self.vfs.read_range(path, range).await?)
218 }
219
220 async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()> {
221 match mode {
222 WriteMode::CreateNew => {
223 if self.vfs.exists(path).await {
225 return Err(BackendError::AlreadyExists(path.display().to_string()));
226 }
227 self.vfs.write(path, content).await?;
228 }
229 WriteMode::Overwrite | WriteMode::Truncate => {
230 self.vfs.write(path, content).await?;
231 }
232 WriteMode::UpdateOnly => {
233 if !self.vfs.exists(path).await {
234 return Err(BackendError::NotFound(path.display().to_string()));
235 }
236 self.vfs.write(path, content).await?;
237 }
238 _ => {
240 self.vfs.write(path, content).await?;
241 }
242 }
243 Ok(())
244 }
245
246 async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()> {
247 self.vfs.append(path, content).await?;
248 Ok(())
249 }
250
251 async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
252 let data = self.vfs.read(path).await?;
254 let mut content = String::from_utf8(data)
255 .map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
256
257 for op in ops {
259 Self::apply_patch_op(&mut content, op)?;
260 }
261
262 self.vfs.write(path, content.as_bytes()).await?;
264 Ok(())
265 }
266
267 async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>> {
272 Ok(self.vfs.list(path).await?)
273 }
274
275 async fn stat(&self, path: &Path) -> BackendResult<DirEntry> {
276 Ok(self.vfs.stat(path).await?)
277 }
278
279 async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> BackendResult<()> {
280 self.vfs.set_mtime(path, mtime).await?;
281 Ok(())
282 }
283
284 async fn mkdir(&self, path: &Path) -> BackendResult<()> {
285 self.vfs.mkdir(path).await?;
286 Ok(())
287 }
288
289 async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
290 if recursive {
291 if let Ok(entry) = self.vfs.lstat(path).await
296 && entry.is_dir()
297 {
298 if let Ok(entries) = self.vfs.list(path).await {
300 for entry in entries {
301 let child_path = path.join(&entry.name);
302 Box::pin(self.remove(&child_path, true)).await?;
304 }
305 }
306 }
307 }
308 self.vfs.remove(path).await?;
309 Ok(())
310 }
311
312 async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
313 self.vfs.rename(from, to).await?;
314 Ok(())
315 }
316
317 async fn exists(&self, path: &Path) -> bool {
318 self.vfs.exists(path).await
319 }
320
321 async fn lstat(&self, path: &Path) -> BackendResult<DirEntry> {
326 Ok(self.vfs.lstat(path).await?)
327 }
328
329 async fn read_link(&self, path: &Path) -> BackendResult<std::path::PathBuf> {
330 Ok(self.vfs.read_link(path).await?)
331 }
332
333 async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
334 self.vfs.symlink(target, link).await?;
335 Ok(())
336 }
337
338 async fn call_tool(
343 &self,
344 name: &str,
345 args: ToolArgs,
346 ctx: &mut dyn ToolCtx,
347 ) -> BackendResult<ToolResult> {
348 let registry = self.tools.as_ref().ok_or_else(|| {
349 BackendError::ToolNotFound(format!("no tool registry configured for: {}", name))
350 })?;
351
352 let tool = registry.get(name).ok_or_else(|| {
353 BackendError::ToolNotFound(format!("{}: command not found", name))
354 })?;
355
356 let exec_result = tool.execute(args, ctx).await;
358 Ok(exec_result.into())
359 }
360
361 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
362 match &self.tools {
363 Some(registry) => {
364 let schemas = registry.schemas();
365 Ok(schemas
366 .into_iter()
367 .map(|schema| ToolInfo {
368 name: schema.name.clone(),
369 description: schema.description.clone(),
370 schema,
371 })
372 .collect())
373 }
374 None => Ok(Vec::new()),
375 }
376 }
377
378 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
379 match &self.tools {
380 Some(registry) => match registry.get(name) {
381 Some(tool) => {
382 let schema = tool.schema();
383 Ok(Some(ToolInfo {
384 name: schema.name.clone(),
385 description: schema.description.clone(),
386 schema,
387 }))
388 }
389 None => Ok(None),
390 },
391 None => Ok(None),
392 }
393 }
394
395 fn read_only(&self) -> bool {
400 self.vfs.read_only()
401 }
402
403 fn backend_type(&self) -> &str {
404 "local"
405 }
406
407 fn mounts(&self) -> Vec<MountInfo> {
408 self.vfs.list_mounts()
409 }
410
411 fn resolve_real_path(&self, path: &Path) -> Option<std::path::PathBuf> {
412 self.vfs.resolve_real_path(path)
413 }
414}
415
416impl std::fmt::Debug for LocalBackend {
417 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418 f.debug_struct("LocalBackend")
419 .field("vfs", &self.vfs)
420 .field("has_tools", &self.tools.is_some())
421 .finish()
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use crate::vfs::MemoryFs;
429 use std::path::PathBuf;
430
431 async fn make_backend() -> LocalBackend {
432 let mut vfs = VfsRouter::new();
433 let mem = MemoryFs::new();
434 mem.write(Path::new("test.txt"), b"hello world")
435 .await
436 .unwrap();
437 mem.write(Path::new("lines.txt"), b"line1\nline2\nline3\n")
438 .await
439 .unwrap();
440 mem.mkdir(Path::new("dir")).await.unwrap();
441 mem.write(Path::new("dir/nested.txt"), b"nested content")
442 .await
443 .unwrap();
444 vfs.mount("/", mem);
445 LocalBackend::new(Arc::new(vfs))
446 }
447
448 #[tokio::test]
449 async fn test_read_full() {
450 let backend = make_backend().await;
451 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
452 assert_eq!(content, b"hello world");
453 }
454
455 #[tokio::test]
456 async fn test_read_with_byte_range() {
457 let backend = make_backend().await;
458 let range = ReadRange::bytes(0, 5);
459 let content = backend.read(Path::new("/test.txt"), Some(range)).await.unwrap();
460 assert_eq!(content, b"hello");
461 }
462
463 #[tokio::test]
464 async fn test_read_with_line_range() {
465 let backend = make_backend().await;
466 let range = ReadRange::lines(2, 3);
467 let content = backend.read(Path::new("/lines.txt"), Some(range)).await.unwrap();
468 assert_eq!(std::str::from_utf8(&content).unwrap(), "line2\nline3");
469 }
470
471 #[tokio::test]
472 async fn test_write_overwrite() {
473 let backend = make_backend().await;
474 backend
475 .write(Path::new("/test.txt"), b"new content", WriteMode::Overwrite)
476 .await
477 .unwrap();
478 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
479 assert_eq!(content, b"new content");
480 }
481
482 #[tokio::test]
483 async fn test_write_create_new() {
484 let backend = make_backend().await;
485 backend
486 .write(Path::new("/new.txt"), b"created", WriteMode::CreateNew)
487 .await
488 .unwrap();
489 let content = backend.read(Path::new("/new.txt"), None).await.unwrap();
490 assert_eq!(content, b"created");
491 }
492
493 #[tokio::test]
494 async fn test_write_create_new_fails_if_exists() {
495 let backend = make_backend().await;
496 let result = backend
497 .write(Path::new("/test.txt"), b"fail", WriteMode::CreateNew)
498 .await;
499 assert!(matches!(result, Err(BackendError::AlreadyExists(_))));
500 }
501
502 #[tokio::test]
503 async fn test_write_update_only() {
504 let backend = make_backend().await;
505 backend
506 .write(Path::new("/test.txt"), b"updated", WriteMode::UpdateOnly)
507 .await
508 .unwrap();
509 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
510 assert_eq!(content, b"updated");
511 }
512
513 #[tokio::test]
514 async fn test_write_update_only_fails_if_not_exists() {
515 let backend = make_backend().await;
516 let result = backend
517 .write(Path::new("/nonexistent.txt"), b"fail", WriteMode::UpdateOnly)
518 .await;
519 assert!(matches!(result, Err(BackendError::NotFound(_))));
520 }
521
522 #[tokio::test]
523 async fn test_append() {
524 let backend = make_backend().await;
525 backend.append(Path::new("/test.txt"), b" appended").await.unwrap();
526 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
527 assert_eq!(content, b"hello world appended");
528 }
529
530 #[tokio::test]
531 async fn test_patch_insert() {
532 let backend = make_backend().await;
533 let ops = vec![PatchOp::Insert {
534 offset: 5,
535 content: " there".to_string(),
536 }];
537 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
538 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
539 assert_eq!(std::str::from_utf8(&content).unwrap(), "hello there world");
540 }
541
542 #[tokio::test]
543 async fn test_patch_delete() {
544 let backend = make_backend().await;
545 let ops = vec![PatchOp::Delete {
546 offset: 5,
547 len: 6,
548 expected: None,
549 }];
550 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
551 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
552 assert_eq!(std::str::from_utf8(&content).unwrap(), "hello");
553 }
554
555 #[tokio::test]
556 async fn test_patch_delete_with_cas() {
557 let backend = make_backend().await;
558 let ops = vec![PatchOp::Delete {
559 offset: 0,
560 len: 5,
561 expected: Some("hello".to_string()),
562 }];
563 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
564 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
565 assert_eq!(std::str::from_utf8(&content).unwrap(), " world");
566 }
567
568 #[tokio::test]
569 async fn test_patch_delete_cas_conflict() {
570 let backend = make_backend().await;
571 let ops = vec![PatchOp::Delete {
572 offset: 0,
573 len: 5,
574 expected: Some("wrong".to_string()),
575 }];
576 let result = backend.patch(Path::new("/test.txt"), &ops).await;
577 assert!(matches!(result, Err(BackendError::Conflict(_))));
578 }
579
580 #[tokio::test]
581 async fn test_patch_replace() {
582 let backend = make_backend().await;
583 let ops = vec![PatchOp::Replace {
584 offset: 0,
585 len: 5,
586 content: "hi".to_string(),
587 expected: None,
588 }];
589 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
590 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
591 assert_eq!(std::str::from_utf8(&content).unwrap(), "hi world");
592 }
593
594 #[tokio::test]
595 async fn test_patch_replace_line() {
596 let backend = make_backend().await;
597 let ops = vec![PatchOp::ReplaceLine {
598 line: 2,
599 content: "replaced".to_string(),
600 expected: None,
601 }];
602 backend.patch(Path::new("/lines.txt"), &ops).await.unwrap();
603 let content = backend.read(Path::new("/lines.txt"), None).await.unwrap();
604 let text = std::str::from_utf8(&content).unwrap();
605 assert!(text.contains("line1"));
606 assert!(text.contains("replaced"));
607 assert!(text.contains("line3"));
608 assert!(!text.contains("line2"));
609 }
610
611 #[tokio::test]
612 async fn test_patch_delete_line() {
613 let backend = make_backend().await;
614 let ops = vec![PatchOp::DeleteLine {
615 line: 2,
616 expected: None,
617 }];
618 backend.patch(Path::new("/lines.txt"), &ops).await.unwrap();
619 let content = backend.read(Path::new("/lines.txt"), None).await.unwrap();
620 let text = std::str::from_utf8(&content).unwrap();
621 assert!(text.contains("line1"));
622 assert!(!text.contains("line2"));
623 assert!(text.contains("line3"));
624 }
625
626 #[tokio::test]
627 async fn test_patch_insert_line() {
628 let backend = make_backend().await;
629 let ops = vec![PatchOp::InsertLine {
630 line: 2,
631 content: "inserted".to_string(),
632 }];
633 backend.patch(Path::new("/lines.txt"), &ops).await.unwrap();
634 let content = backend.read(Path::new("/lines.txt"), None).await.unwrap();
635 let text = std::str::from_utf8(&content).unwrap();
636 let lines: Vec<&str> = text.lines().collect();
637 assert_eq!(lines[0], "line1");
638 assert_eq!(lines[1], "inserted");
639 assert_eq!(lines[2], "line2");
640 }
641
642 #[tokio::test]
643 async fn test_patch_append() {
644 let backend = make_backend().await;
645 let ops = vec![PatchOp::Append {
646 content: "!".to_string(),
647 }];
648 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
649 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
650 assert_eq!(std::str::from_utf8(&content).unwrap(), "hello world!");
651 }
652
653 #[tokio::test]
654 async fn test_list() {
655 let backend = make_backend().await;
656 let entries = backend.list(Path::new("/")).await.unwrap();
657 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
658 assert!(names.contains(&"test.txt"));
659 assert!(names.contains(&"lines.txt"));
660 assert!(names.contains(&"dir"));
661 }
662
663 #[tokio::test]
664 async fn test_stat() {
665 let backend = make_backend().await;
666 let info = backend.stat(Path::new("/test.txt")).await.unwrap();
667 assert!(info.is_file());
668 assert_eq!(info.size, 11); let info = backend.stat(Path::new("/dir")).await.unwrap();
671 assert!(info.is_dir());
672 }
673
674 #[tokio::test]
675 async fn test_mkdir() {
676 let backend = make_backend().await;
677 backend.mkdir(Path::new("/newdir")).await.unwrap();
678 assert!(backend.exists(Path::new("/newdir")).await);
679 let info = backend.stat(Path::new("/newdir")).await.unwrap();
680 assert!(info.is_dir());
681 }
682
683 #[tokio::test]
684 async fn test_remove() {
685 let backend = make_backend().await;
686 assert!(backend.exists(Path::new("/test.txt")).await);
687 backend.remove(Path::new("/test.txt"), false).await.unwrap();
688 assert!(!backend.exists(Path::new("/test.txt")).await);
689 }
690
691 #[tokio::test]
692 async fn test_remove_recursive() {
693 let backend = make_backend().await;
694 assert!(backend.exists(Path::new("/dir/nested.txt")).await);
695 backend.remove(Path::new("/dir"), true).await.unwrap();
696 assert!(!backend.exists(Path::new("/dir")).await);
697 assert!(!backend.exists(Path::new("/dir/nested.txt")).await);
698 }
699
700 #[tokio::test]
701 async fn test_exists() {
702 let backend = make_backend().await;
703 assert!(backend.exists(Path::new("/test.txt")).await);
704 assert!(!backend.exists(Path::new("/nonexistent.txt")).await);
705 }
706
707 #[tokio::test]
708 async fn test_backend_info() {
709 let backend = make_backend().await;
710 assert_eq!(backend.backend_type(), "local");
711 assert!(!backend.read_only());
712 let mounts = backend.mounts();
713 assert!(!mounts.is_empty());
714 }
715
716 #[tokio::test]
717 async fn test_list_includes_symlinks() {
718 use crate::vfs::Filesystem;
719
720 let mut vfs = VfsRouter::new();
721 let mem = MemoryFs::new();
722 mem.write(Path::new("target.txt"), b"content").await.unwrap();
723 mem.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
724 vfs.mount("/", mem);
725 let backend = LocalBackend::new(Arc::new(vfs));
726
727 let entries = backend.list(Path::new("/")).await.unwrap();
728
729 let link_entry = entries.iter().find(|e| e.name == "link.txt").unwrap();
730 assert!(link_entry.is_symlink(), "link.txt should be a symlink");
731 assert_eq!(link_entry.symlink_target, Some(PathBuf::from("target.txt")));
732 }
733}