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