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