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