#![forbid(unsafe_code)]
use std::path::{Path, PathBuf};
pub fn apply_edit(root: &str, file: &str, old: &str, new: &str) -> Result<bool, String> {
if old.trim().is_empty() {
return Err("apply_edit: `old` text is empty — refusing to edit".to_string());
}
let full: PathBuf = if Path::new(file).is_absolute() {
PathBuf::from(file)
} else {
Path::new(root).join(file)
};
let path_str = full.to_string_lossy().to_string();
let content = std::fs::read_to_string(&full)
.map_err(|e| format!("read {}: {}", path_str, e))?;
if let Some(idx) = content.find(old) {
let mut out = content.clone();
out.replace_range(idx..idx + old.len(), new);
write_file(&full, &out)?;
return Ok(true);
}
let old_norm = collapse_ws(old);
if let Some((start, end)) = fuzzy_span(&content, &old_norm) {
let mut out = content.clone();
out.replace_range(start..end, new);
write_file(&full, &out)?;
return Ok(true);
}
Ok(false)
}
fn fuzzy_span(hay: &str, needle: &str) -> Option<(usize, usize)> {
if needle.is_empty() {
return Some((0, 0));
}
let needle_toks: Vec<char> = needle.chars().filter(|c| !c.is_whitespace()).collect();
if needle_toks.is_empty() {
return Some((0, 0));
}
let need_len = needle_toks.len();
let hay_chars: Vec<(usize, char)> = hay
.char_indices()
.filter(|(_, c)| !c.is_whitespace())
.collect();
if hay_chars.len() < need_len {
return None;
}
'win: for w in 0..=hay_chars.len() - need_len {
for k in 0..need_len {
if hay_chars[w + k].1 != needle_toks[k] {
continue 'win;
}
}
let start = hay_chars[w].0;
let last = hay_chars[w + need_len - 1];
let end = last.0 + last.1.len_utf8();
return Some((start, end));
}
None
}
fn collapse_ws(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_ws = false;
for c in s.chars() {
if c.is_whitespace() {
if !prev_ws {
out.push(' ');
}
prev_ws = true;
} else {
out.push(c);
prev_ws = false;
}
}
out
}
fn write_file(path: &Path, content: &str) -> Result<(), String> {
let tmp = path.with_extension("reliary-tmp");
std::fs::write(&tmp, content).map_err(|e| format!("write {}: {}", tmp.display(), e))?;
std::fs::rename(&tmp, path).map_err(|e| format!("rename {}: {}", path.display(), e))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::sync::atomic::{AtomicUsize, Ordering};
static CTR: AtomicUsize = AtomicUsize::new(0);
fn tmp_(content: &str) -> (PathBuf, std::fs::File) {
let n = CTR.fetch_add(1, Ordering::SeqCst);
let mut p = std::env::temp_dir();
p.push(format!("reliary_edit_test_{}_{}.rs", std::process::id(), n));
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(content.as_bytes()).unwrap();
(p, f)
}
#[test]
fn exact_replace() {
let (p, _) = tmp_("fn foo() { bar(); }");
let root = p.parent().unwrap().to_str().unwrap();
let file = p.file_name().unwrap().to_str().unwrap();
let ok = apply_edit(root, file, "bar()", "baz()").unwrap();
assert!(ok);
assert_eq!(std::fs::read_to_string(&p).unwrap(), "fn foo() { baz(); }");
}
#[test]
fn whitespace_insensitive() {
let (p, _) = tmp_("fn foo( a : i32, b : i32 ) { x; }");
let root = p.to_str().unwrap();
let ok = apply_edit(root, p.to_str().unwrap(), "fn foo(a: i32, b: i32)", "fn foo(a: i64, b: i32)").unwrap();
assert!(ok);
let out = std::fs::read_to_string(&p).unwrap();
assert!(out.contains("fn foo(a: i64, b: i32)"));
}
#[test]
fn no_match() {
let (p, _) = tmp_("fn foo() { x; }");
let root = p.to_str().unwrap();
let ok = apply_edit(root, p.to_str().unwrap(), "fn foo()", "fn bar()").unwrap();
assert!(ok);
}
}
#[cfg(test)]
mod v80_tests {
use super::*;
#[test]
fn empty_old_is_refused() {
let d = std::env::temp_dir().join(format!("reliary_edit_{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
let f = d.join("a.rs");
std::fs::write(&f, "fn foo() {}\n").unwrap();
let r = apply_edit(d.to_str().unwrap(), "a.rs", "", "XXX");
assert!(r.is_err(), "empty old must be refused, got {:?}", r);
assert_eq!(std::fs::read_to_string(&f).unwrap(), "fn foo() {}\n", "file must be unchanged");
let r2 = apply_edit(d.to_str().unwrap(), "a.rs", " \n ", "XXX");
assert!(r2.is_err(), "whitespace-only old must be refused");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn normal_edit_still_works() {
let d = std::env::temp_dir().join(format!("reliary_edit2_{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
let f = d.join("a.rs");
std::fs::write(&f, "fn foo() { 1 }\n").unwrap();
let r = apply_edit(d.to_str().unwrap(), "a.rs", "1", "2").unwrap();
assert!(r);
assert_eq!(std::fs::read_to_string(&f).unwrap(), "fn foo() { 2 }\n");
let _ = std::fs::remove_dir_all(&d);
}
}