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