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 let mut existing = match self.vfs.read(path).await {
249 Ok(data) => data,
250 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
251 Err(e) => return Err(e.into()),
252 };
253 existing.extend_from_slice(content);
254 self.vfs.write(path, &existing).await?;
255 Ok(())
256 }
257
258 async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
259 let data = self.vfs.read(path).await?;
261 let mut content = String::from_utf8(data)
262 .map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
263
264 for op in ops {
266 Self::apply_patch_op(&mut content, op)?;
267 }
268
269 self.vfs.write(path, content.as_bytes()).await?;
271 Ok(())
272 }
273
274 async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>> {
279 Ok(self.vfs.list(path).await?)
280 }
281
282 async fn stat(&self, path: &Path) -> BackendResult<DirEntry> {
283 Ok(self.vfs.stat(path).await?)
284 }
285
286 async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> BackendResult<()> {
287 self.vfs.set_mtime(path, mtime).await?;
288 Ok(())
289 }
290
291 async fn mkdir(&self, path: &Path) -> BackendResult<()> {
292 self.vfs.mkdir(path).await?;
293 Ok(())
294 }
295
296 async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
297 if recursive {
298 if let Ok(entry) = self.vfs.lstat(path).await
303 && entry.is_dir()
304 {
305 if let Ok(entries) = self.vfs.list(path).await {
307 for entry in entries {
308 let child_path = path.join(&entry.name);
309 Box::pin(self.remove(&child_path, true)).await?;
311 }
312 }
313 }
314 }
315 self.vfs.remove(path).await?;
316 Ok(())
317 }
318
319 async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
320 self.vfs.rename(from, to).await?;
321 Ok(())
322 }
323
324 async fn exists(&self, path: &Path) -> bool {
325 self.vfs.exists(path).await
326 }
327
328 async fn lstat(&self, path: &Path) -> BackendResult<DirEntry> {
333 Ok(self.vfs.lstat(path).await?)
334 }
335
336 async fn read_link(&self, path: &Path) -> BackendResult<std::path::PathBuf> {
337 Ok(self.vfs.read_link(path).await?)
338 }
339
340 async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
341 self.vfs.symlink(target, link).await?;
342 Ok(())
343 }
344
345 async fn call_tool(
350 &self,
351 name: &str,
352 args: ToolArgs,
353 ctx: &mut dyn ToolCtx,
354 ) -> BackendResult<ToolResult> {
355 let registry = self.tools.as_ref().ok_or_else(|| {
356 BackendError::ToolNotFound(format!("no tool registry configured for: {}", name))
357 })?;
358
359 let tool = registry.get(name).ok_or_else(|| {
360 BackendError::ToolNotFound(format!("{}: command not found", name))
361 })?;
362
363 let exec_result = tool.execute(args, ctx).await;
365 Ok(exec_result.into())
366 }
367
368 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
369 match &self.tools {
370 Some(registry) => {
371 let schemas = registry.schemas();
372 Ok(schemas
373 .into_iter()
374 .map(|schema| ToolInfo {
375 name: schema.name.clone(),
376 description: schema.description.clone(),
377 schema,
378 })
379 .collect())
380 }
381 None => Ok(Vec::new()),
382 }
383 }
384
385 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
386 match &self.tools {
387 Some(registry) => match registry.get(name) {
388 Some(tool) => {
389 let schema = tool.schema();
390 Ok(Some(ToolInfo {
391 name: schema.name.clone(),
392 description: schema.description.clone(),
393 schema,
394 }))
395 }
396 None => Ok(None),
397 },
398 None => Ok(None),
399 }
400 }
401
402 fn read_only(&self) -> bool {
407 self.vfs.read_only()
408 }
409
410 fn backend_type(&self) -> &str {
411 "local"
412 }
413
414 fn mounts(&self) -> Vec<MountInfo> {
415 self.vfs.list_mounts()
416 }
417
418 fn resolve_real_path(&self, path: &Path) -> Option<std::path::PathBuf> {
419 self.vfs.resolve_real_path(path)
420 }
421}
422
423impl std::fmt::Debug for LocalBackend {
424 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
425 f.debug_struct("LocalBackend")
426 .field("vfs", &self.vfs)
427 .field("has_tools", &self.tools.is_some())
428 .finish()
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435 use crate::vfs::MemoryFs;
436 use std::path::PathBuf;
437
438 async fn make_backend() -> LocalBackend {
439 let mut vfs = VfsRouter::new();
440 let mem = MemoryFs::new();
441 mem.write(Path::new("test.txt"), b"hello world")
442 .await
443 .unwrap();
444 mem.write(Path::new("lines.txt"), b"line1\nline2\nline3\n")
445 .await
446 .unwrap();
447 mem.mkdir(Path::new("dir")).await.unwrap();
448 mem.write(Path::new("dir/nested.txt"), b"nested content")
449 .await
450 .unwrap();
451 vfs.mount("/", mem);
452 LocalBackend::new(Arc::new(vfs))
453 }
454
455 #[tokio::test]
456 async fn test_read_full() {
457 let backend = make_backend().await;
458 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
459 assert_eq!(content, b"hello world");
460 }
461
462 #[tokio::test]
463 async fn test_read_with_byte_range() {
464 let backend = make_backend().await;
465 let range = ReadRange::bytes(0, 5);
466 let content = backend.read(Path::new("/test.txt"), Some(range)).await.unwrap();
467 assert_eq!(content, b"hello");
468 }
469
470 #[tokio::test]
471 async fn test_read_with_line_range() {
472 let backend = make_backend().await;
473 let range = ReadRange::lines(2, 3);
474 let content = backend.read(Path::new("/lines.txt"), Some(range)).await.unwrap();
475 assert_eq!(std::str::from_utf8(&content).unwrap(), "line2\nline3");
476 }
477
478 #[tokio::test]
479 async fn test_write_overwrite() {
480 let backend = make_backend().await;
481 backend
482 .write(Path::new("/test.txt"), b"new content", WriteMode::Overwrite)
483 .await
484 .unwrap();
485 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
486 assert_eq!(content, b"new content");
487 }
488
489 #[tokio::test]
490 async fn test_write_create_new() {
491 let backend = make_backend().await;
492 backend
493 .write(Path::new("/new.txt"), b"created", WriteMode::CreateNew)
494 .await
495 .unwrap();
496 let content = backend.read(Path::new("/new.txt"), None).await.unwrap();
497 assert_eq!(content, b"created");
498 }
499
500 #[tokio::test]
501 async fn test_write_create_new_fails_if_exists() {
502 let backend = make_backend().await;
503 let result = backend
504 .write(Path::new("/test.txt"), b"fail", WriteMode::CreateNew)
505 .await;
506 assert!(matches!(result, Err(BackendError::AlreadyExists(_))));
507 }
508
509 #[tokio::test]
510 async fn test_write_update_only() {
511 let backend = make_backend().await;
512 backend
513 .write(Path::new("/test.txt"), b"updated", WriteMode::UpdateOnly)
514 .await
515 .unwrap();
516 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
517 assert_eq!(content, b"updated");
518 }
519
520 #[tokio::test]
521 async fn test_write_update_only_fails_if_not_exists() {
522 let backend = make_backend().await;
523 let result = backend
524 .write(Path::new("/nonexistent.txt"), b"fail", WriteMode::UpdateOnly)
525 .await;
526 assert!(matches!(result, Err(BackendError::NotFound(_))));
527 }
528
529 #[tokio::test]
530 async fn test_append() {
531 let backend = make_backend().await;
532 backend.append(Path::new("/test.txt"), b" appended").await.unwrap();
533 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
534 assert_eq!(content, b"hello world appended");
535 }
536
537 #[tokio::test]
538 async fn test_patch_insert() {
539 let backend = make_backend().await;
540 let ops = vec![PatchOp::Insert {
541 offset: 5,
542 content: " there".to_string(),
543 }];
544 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
545 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
546 assert_eq!(std::str::from_utf8(&content).unwrap(), "hello there world");
547 }
548
549 #[tokio::test]
550 async fn test_patch_delete() {
551 let backend = make_backend().await;
552 let ops = vec![PatchOp::Delete {
553 offset: 5,
554 len: 6,
555 expected: None,
556 }];
557 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
558 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
559 assert_eq!(std::str::from_utf8(&content).unwrap(), "hello");
560 }
561
562 #[tokio::test]
563 async fn test_patch_delete_with_cas() {
564 let backend = make_backend().await;
565 let ops = vec![PatchOp::Delete {
566 offset: 0,
567 len: 5,
568 expected: Some("hello".to_string()),
569 }];
570 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
571 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
572 assert_eq!(std::str::from_utf8(&content).unwrap(), " world");
573 }
574
575 #[tokio::test]
576 async fn test_patch_delete_cas_conflict() {
577 let backend = make_backend().await;
578 let ops = vec![PatchOp::Delete {
579 offset: 0,
580 len: 5,
581 expected: Some("wrong".to_string()),
582 }];
583 let result = backend.patch(Path::new("/test.txt"), &ops).await;
584 assert!(matches!(result, Err(BackendError::Conflict(_))));
585 }
586
587 #[tokio::test]
588 async fn test_patch_replace() {
589 let backend = make_backend().await;
590 let ops = vec![PatchOp::Replace {
591 offset: 0,
592 len: 5,
593 content: "hi".to_string(),
594 expected: None,
595 }];
596 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
597 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
598 assert_eq!(std::str::from_utf8(&content).unwrap(), "hi world");
599 }
600
601 #[tokio::test]
602 async fn test_patch_replace_line() {
603 let backend = make_backend().await;
604 let ops = vec![PatchOp::ReplaceLine {
605 line: 2,
606 content: "replaced".to_string(),
607 expected: None,
608 }];
609 backend.patch(Path::new("/lines.txt"), &ops).await.unwrap();
610 let content = backend.read(Path::new("/lines.txt"), None).await.unwrap();
611 let text = std::str::from_utf8(&content).unwrap();
612 assert!(text.contains("line1"));
613 assert!(text.contains("replaced"));
614 assert!(text.contains("line3"));
615 assert!(!text.contains("line2"));
616 }
617
618 #[tokio::test]
619 async fn test_patch_delete_line() {
620 let backend = make_backend().await;
621 let ops = vec![PatchOp::DeleteLine {
622 line: 2,
623 expected: None,
624 }];
625 backend.patch(Path::new("/lines.txt"), &ops).await.unwrap();
626 let content = backend.read(Path::new("/lines.txt"), None).await.unwrap();
627 let text = std::str::from_utf8(&content).unwrap();
628 assert!(text.contains("line1"));
629 assert!(!text.contains("line2"));
630 assert!(text.contains("line3"));
631 }
632
633 #[tokio::test]
634 async fn test_patch_insert_line() {
635 let backend = make_backend().await;
636 let ops = vec![PatchOp::InsertLine {
637 line: 2,
638 content: "inserted".to_string(),
639 }];
640 backend.patch(Path::new("/lines.txt"), &ops).await.unwrap();
641 let content = backend.read(Path::new("/lines.txt"), None).await.unwrap();
642 let text = std::str::from_utf8(&content).unwrap();
643 let lines: Vec<&str> = text.lines().collect();
644 assert_eq!(lines[0], "line1");
645 assert_eq!(lines[1], "inserted");
646 assert_eq!(lines[2], "line2");
647 }
648
649 #[tokio::test]
650 async fn test_patch_append() {
651 let backend = make_backend().await;
652 let ops = vec![PatchOp::Append {
653 content: "!".to_string(),
654 }];
655 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
656 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
657 assert_eq!(std::str::from_utf8(&content).unwrap(), "hello world!");
658 }
659
660 #[tokio::test]
661 async fn test_list() {
662 let backend = make_backend().await;
663 let entries = backend.list(Path::new("/")).await.unwrap();
664 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
665 assert!(names.contains(&"test.txt"));
666 assert!(names.contains(&"lines.txt"));
667 assert!(names.contains(&"dir"));
668 }
669
670 #[tokio::test]
671 async fn test_stat() {
672 let backend = make_backend().await;
673 let info = backend.stat(Path::new("/test.txt")).await.unwrap();
674 assert!(info.is_file());
675 assert_eq!(info.size, 11); let info = backend.stat(Path::new("/dir")).await.unwrap();
678 assert!(info.is_dir());
679 }
680
681 #[tokio::test]
682 async fn test_mkdir() {
683 let backend = make_backend().await;
684 backend.mkdir(Path::new("/newdir")).await.unwrap();
685 assert!(backend.exists(Path::new("/newdir")).await);
686 let info = backend.stat(Path::new("/newdir")).await.unwrap();
687 assert!(info.is_dir());
688 }
689
690 #[tokio::test]
691 async fn test_remove() {
692 let backend = make_backend().await;
693 assert!(backend.exists(Path::new("/test.txt")).await);
694 backend.remove(Path::new("/test.txt"), false).await.unwrap();
695 assert!(!backend.exists(Path::new("/test.txt")).await);
696 }
697
698 #[tokio::test]
699 async fn test_remove_recursive() {
700 let backend = make_backend().await;
701 assert!(backend.exists(Path::new("/dir/nested.txt")).await);
702 backend.remove(Path::new("/dir"), true).await.unwrap();
703 assert!(!backend.exists(Path::new("/dir")).await);
704 assert!(!backend.exists(Path::new("/dir/nested.txt")).await);
705 }
706
707 #[tokio::test]
708 async fn test_exists() {
709 let backend = make_backend().await;
710 assert!(backend.exists(Path::new("/test.txt")).await);
711 assert!(!backend.exists(Path::new("/nonexistent.txt")).await);
712 }
713
714 #[tokio::test]
715 async fn test_backend_info() {
716 let backend = make_backend().await;
717 assert_eq!(backend.backend_type(), "local");
718 assert!(!backend.read_only());
719 let mounts = backend.mounts();
720 assert!(!mounts.is_empty());
721 }
722
723 #[tokio::test]
724 async fn test_list_includes_symlinks() {
725 use crate::vfs::Filesystem;
726
727 let mut vfs = VfsRouter::new();
728 let mem = MemoryFs::new();
729 mem.write(Path::new("target.txt"), b"content").await.unwrap();
730 mem.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
731 vfs.mount("/", mem);
732 let backend = LocalBackend::new(Arc::new(vfs));
733
734 let entries = backend.list(Path::new("/")).await.unwrap();
735
736 let link_entry = entries.iter().find(|e| e.name == "link.txt").unwrap();
737 assert!(link_entry.is_symlink(), "link.txt should be a symlink");
738 assert_eq!(link_entry.symlink_target, Some(PathBuf::from("target.txt")));
739 }
740}