1use std::fs::{self, File};
7use std::io::{self, ErrorKind, Write as _};
8use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Duration;
11
12use rig_core::tool::{FileChange, FileState, Hunk, HunkLine, Lines, ToolExecutionError};
13use serde::Serialize;
14use serde::de::DeserializeOwned;
15
16use crate::workspace::{Contents, Listing, Local, Stat, Workspace};
17
18pub fn read_text(path: &str) -> Result<String, ToolExecutionError> {
23 local().read_text(Path::new(path))
24}
25
26pub struct Prefix {
28 pub text: String,
31 pub size: u64,
33}
34
35impl Prefix {
36 pub fn cut(&self) -> bool {
38 self.size > self.text.len() as u64
39 }
40
41 pub fn of(contents: Contents, cap: usize) -> Self {
45 let Contents { bytes, size } = contents;
46 let mut text = match String::from_utf8(bytes) {
47 Ok(text) => text,
48 Err(error) => {
49 let valid = error.utf8_error().valid_up_to();
50 let mut bytes = error.into_bytes();
51 if size > cap as u64 && bytes.len().saturating_sub(valid) < 4 {
52 bytes.truncate(valid);
53 }
54 String::from_utf8_lossy(&bytes).into_owned()
55 }
56 };
57 if let Some(rest) = text.strip_prefix('\u{feff}') {
58 text = rest.to_owned();
59 }
60 Self { text, size }
61 }
62}
63
64pub fn read_prefix(path: &Path, cap: usize) -> io::Result<Prefix> {
67 Ok(Prefix::of(Local.read(path, cap as u64)?, cap))
68}
69
70pub fn write_atomic(path: &Path, contents: &[u8]) -> io::Result<()> {
83 let (target, permissions) = match fs::metadata(path) {
84 Ok(meta) if !meta.is_file() => {
85 return Err(io::Error::new(
86 ErrorKind::InvalidInput,
87 "not a regular file",
88 ));
89 }
90 Ok(meta) if meta.permissions().readonly() => {
91 return Err(io::Error::new(
92 ErrorKind::PermissionDenied,
93 "the file is read-only; leave it unchanged or ask the user",
94 ));
95 }
96 Ok(meta) => (fs::canonicalize(path)?, Some(meta.permissions())),
97 Err(error) if error.kind() == ErrorKind::NotFound => (link_target(path), None),
98 Err(error) => return Err(error),
99 };
100 if let Some(parent) = target
101 .parent()
102 .filter(|parent| !parent.as_os_str().is_empty())
103 {
104 fs::create_dir_all(parent)?;
105 }
106 let name = target
107 .file_name()
108 .ok_or_else(|| io::Error::new(ErrorKind::InvalidInput, "names no file"))?;
109 let temporary = temporary_beside(&target, name);
110 let written = File::create_new(&temporary).and_then(|mut file| {
111 if let Some(permissions) = permissions {
112 file.set_permissions(permissions)?;
113 }
114 file.write_all(contents)?;
115 file.sync_all()?;
116 fs::rename(&temporary, &target)
117 });
118 written.inspect_err(|_| {
119 fs::remove_file(&temporary).ok();
120 })
121}
122
123pub(crate) fn write_scratch(path: &Path, contents: &[u8]) -> io::Result<()> {
128 let name = path
129 .file_name()
130 .ok_or_else(|| io::Error::new(ErrorKind::InvalidInput, "names no file"))?;
131 let temporary = temporary_beside(path, name);
132 let written = File::create_new(&temporary)
133 .and_then(|mut file| file.write_all(contents))
134 .and_then(|()| fs::rename(&temporary, path));
135 written.inspect_err(|_| {
136 fs::remove_file(&temporary).ok();
137 })
138}
139
140fn temporary_beside(target: &Path, name: &std::ffi::OsStr) -> PathBuf {
143 static NEXT: AtomicU64 = AtomicU64::new(0);
145 target.with_file_name(format!(
146 ".{}.{}-{}.tmp",
147 name.to_string_lossy(),
148 std::process::id(),
149 NEXT.fetch_add(1, Ordering::Relaxed)
150 ))
151}
152
153pub fn read_json<T: DeserializeOwned>(path: &Path) -> Option<T> {
156 serde_json::from_slice(&fs::read(path).ok()?).ok()
157}
158
159pub fn write_json(path: &Path, value: &impl Serialize) -> io::Result<()> {
162 write_atomic(path, &serde_json::to_vec_pretty(value)?)
163}
164
165fn link_target(path: &Path) -> PathBuf {
168 const MAX_HOPS: usize = 40;
170 let mut target = path.to_path_buf();
171 for _ in 0..MAX_HOPS {
172 let Ok(link) = fs::read_link(&target) else {
173 break;
174 };
175 target = target.parent().unwrap_or(Path::new("")).join(link);
176 }
177 target
178}
179
180pub fn io_error(path: &str, error: io::Error) -> ToolExecutionError {
182 let message = format!("{path}: {error}");
183 match error.kind() {
184 ErrorKind::NotFound => ToolExecutionError::not_found(message),
185 ErrorKind::PermissionDenied => ToolExecutionError::permission_denied(message),
186 _ => ToolExecutionError::other(message),
187 }
188}
189
190pub(crate) fn listed(
195 workspace: &dyn Workspace,
196 root: &Path,
197 glob: Option<&str>,
198 listing: Listing,
199) -> Result<Vec<PathBuf>, ToolExecutionError> {
200 let shown = root.to_string_lossy();
201 let missing = || {
202 let missing = io::Error::new(ErrorKind::NotFound, "no such file or directory");
203 io_error(&shown, missing)
204 };
205 let files = workspace
206 .files(root, glob, listing)
207 .map_err(|error| match error.kind() {
208 ErrorKind::InvalidInput => ToolExecutionError::invalid_args(error.to_string()),
209 ErrorKind::NotFound => missing(),
210 _ => io_error(&shown, error),
211 })?;
212 if files.is_empty()
214 && workspace
215 .stat(root)
216 .map_err(|error| io_error(&shown, error))?
217 == Stat::Missing
218 {
219 return Err(missing());
220 }
221 Ok(files)
222}
223
224pub fn resolve(base: Option<&Path>, path: &str) -> PathBuf {
243 local().resolve(base, path)
244}
245
246pub fn place(
262 base: Option<&Path>,
263 confine: bool,
264 path: &str,
265) -> Result<PathBuf, ToolExecutionError> {
266 local().place(base, confine, path)
267}
268
269fn local() -> &'static dyn Workspace {
271 &Local
272}
273
274pub fn file_change(path: &str, before: Option<&str>, after: Option<&str>) -> FileChange {
300 let mut change = diff(path, before, after);
301 change.before_text = before.map(str::to_owned);
302 change.before = before.map(|text| FileState::of(text.as_bytes()));
303 change.after = after.map(|text| FileState::of(text.as_bytes()));
304 change
305}
306
307pub(crate) fn diff(path: &str, before: Option<&str>, after: Option<&str>) -> FileChange {
310 let diff = similar::TextDiff::configure()
312 .timeout(Duration::from_millis(100))
313 .diff_lines(before.unwrap_or_default(), after.unwrap_or_default());
314 let hunks: Vec<Hunk> = diff
315 .grouped_ops(3)
316 .iter()
317 .filter_map(|ops| hunk(&diff, ops))
318 .collect();
319 let lines = hunks.iter().flat_map(|hunk| &hunk.lines);
320 let counted = |sign| lines.clone().filter(|line| line.sign() == sign).count();
321 let (added, removed) = (counted('+'), counted('-'));
322 let mut change = match (before, after) {
323 (_, None) => FileChange::deleted(path, removed),
324 (None, Some(_)) => FileChange::created(path, added),
325 (Some(_), Some(_)) => FileChange::modified(path, added, removed),
326 };
327 change.hunks = hunks;
328 change
329}
330
331fn hunk(diff: &similar::TextDiff<'_, '_, '_, str>, ops: &[similar::DiffOp]) -> Option<Hunk> {
333 let (first, last) = (ops.first()?, ops.last()?);
334 let span = |start: usize, end: usize| Lines {
335 start: start + 1,
336 count: end.saturating_sub(start),
337 };
338 let lines = ops
339 .iter()
340 .flat_map(|op| diff.iter_changes(op))
341 .map(|change| {
342 let text = change.value().to_owned();
343 match change.tag() {
344 similar::ChangeTag::Equal => HunkLine::Same(text),
345 similar::ChangeTag::Delete => HunkLine::Removed(text),
346 similar::ChangeTag::Insert => HunkLine::Added(text),
347 }
348 });
349 Some(Hunk {
350 old: span(first.old_range().start, last.old_range().end),
351 new: span(first.new_range().start, last.new_range().end),
352 lines: lines.collect(),
353 })
354}
355
356#[derive(Default)]
358pub(crate) struct Previous {
359 pub(crate) text: String,
362 pub(crate) exact: bool,
365 pub(crate) state: Option<FileState>,
367}
368
369impl Previous {
370 pub(crate) fn of(bytes: Vec<u8>) -> Self {
372 let state = Some(FileState::of(&bytes));
373 let (text, exact) = match String::from_utf8(bytes) {
374 Ok(text) => (text, true),
375 Err(error) => (String::from_utf8_lossy(error.as_bytes()).into(), false),
376 };
377 Self { text, exact, state }
378 }
379
380 pub(crate) fn read(read: io::Result<Contents>) -> Option<Self> {
385 match read {
386 Err(error) if error.kind() == ErrorKind::NotFound => None,
387 Ok(contents) if contents.size > crate::MAX_FILE_BYTES => {
388 Some(Self::unread(contents.size))
389 }
390 Ok(contents) => Some(Self::of(contents.bytes)),
391 Err(_) => Some(Self::default()),
392 }
393 }
394
395 pub(crate) fn unread(size: u64) -> Self {
397 Self {
398 text: String::new(),
399 exact: false,
400 state: Some(FileState::unread(size)),
401 }
402 }
403
404 pub(crate) fn change(this: Option<Self>, path: &str, after: Option<&[u8]>) -> FileChange {
408 let text = after.map(String::from_utf8_lossy);
409 let before = this.as_ref().map(|it| it.text.as_str());
410 let mut change = diff(path, before, text.as_deref());
411 change.after = after.map(FileState::of);
412 if let Some(this) = this {
413 change.before = this.state;
414 change.before_text = this.exact.then_some(this.text);
415 }
416 change
417 }
418}
419
420#[cfg(test)]
421mod tests;