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