oxicode_agent/tools/
hashline_fs.rs1use async_trait::async_trait;
9use oxicode_hashline::mismatch::HashlineError;
10use oxicode_hashline::patcher::HashlineFs;
11use std::path::{Path, PathBuf};
12use tokio::fs;
13
14use super::file_mutation_queue::global_mutation_queue;
15use super::path_security::PathGuard;
16
17pub struct TokioHashlineFs {
20 root: PathBuf,
21}
22
23impl TokioHashlineFs {
24 pub fn new(root: PathBuf) -> Self {
26 Self { root }
27 }
28
29 fn validate(&self, path: &str) -> Result<PathBuf, HashlineError> {
30 let guard = PathGuard::new(&self.root);
31 guard
32 .validate_traversal(Path::new(path))
33 .map_err(|e| HashlineError::Io(std::io::Error::other(e.to_string())))
34 }
35}
36
37#[async_trait]
38impl HashlineFs for TokioHashlineFs {
39 async fn read_text(&self, path: &str) -> Result<String, HashlineError> {
40 let validated = self.validate(path)?;
41 match fs::read_to_string(&validated).await {
42 Ok(text) => Ok(text),
43 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(HashlineError::NotFound {
44 path: path.to_string(),
45 }),
46 Err(e) => Err(HashlineError::Io(e)),
47 }
48 }
49
50 async fn write_text(&self, path: &str, text: &str) -> Result<String, HashlineError> {
51 let validated = self.validate(path)?;
52 let text_owned = text.to_string();
53 let result_path = validated.clone();
54 global_mutation_queue()
55 .with_queue(&validated, || async {
56 fs::write(&validated, &text_owned).await
57 })
58 .await
59 .map(|_| path.to_string())
60 .map_err(|e: std::io::Error| {
61 let _ = &result_path; HashlineError::Io(e)
63 })
64 }
65
66 async fn preflight_write(&self, path: &str) -> Result<(), HashlineError> {
67 let validated = self.validate(path)?;
68 if let Some(parent) = validated.parent()
70 && !parent.as_os_str().is_empty()
71 && fs::metadata(parent).await.is_err()
72 {
73 return Err(HashlineError::Io(std::io::Error::new(
74 std::io::ErrorKind::NotFound,
75 format!("Parent directory does not exist: {}", parent.display()),
76 )));
77 }
78 Ok(())
79 }
80
81 fn canonical_path(&self, path: &str) -> String {
82 let guard = PathGuard::new(&self.root);
83 match guard.validate_traversal(Path::new(path)) {
84 Ok(p) => {
85 match p.strip_prefix(&self.root) {
87 Ok(rel) => rel.to_string_lossy().into_owned(),
88 Err(_) => p.to_string_lossy().into_owned(),
89 }
90 }
91 Err(_) => path.to_string(),
92 }
93 }
94}