use difference_rs::Changeset;
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::utils::RegexIR;
use crate::utils::is_verbose;
use crate::interactive::lua as lua_exec;
use crate::interactive::rhai as rhai_exec;
use crate::interactive::sh as sh_exec;
#[derive(Deserialize, Serialize, PartialEq, Default, Clone)]
pub struct PatchFile {
pub patches: Vec<Patch>,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Default, Clone)]
pub struct Patch {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files: Vec<FilePath>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decoder: Option<DecodeBy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encoder: Option<EncodeBy>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub patch_area: Vec<AreaRule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replace: Option<Replacer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub insert: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum AreaRule {
Contains(RegexIR),
NotContains(RegexIR),
Before(RegexIR),
After(RegexIR),
CursorAtBegin,
CursorAtEnd,
FindByLua(PathBuf),
FindByRhai(PathBuf),
FindBySh(PathBuf),
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum DecodeBy {
Lua(PathBuf),
Rhai(PathBuf),
Sh(PathBuf),
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EncodeBy {
Lua(PathBuf),
Rhai(PathBuf),
Sh(PathBuf),
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum Replacer {
FromTo(String, String),
FromToVar(String, String),
RegexTo(RegexIR, String),
ByLua(PathBuf),
ByRhai(PathBuf),
BySh(PathBuf),
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum FilePath {
Just(PathBuf),
Re(RegexIR),
}
enum CursorType {
Start,
End,
}
#[derive(Clone, Copy)]
enum CallType {
Test,
Patch,
}
impl PatchFile {
fn find_for_file(
&self,
patch_dir: &Path,
filepath: &Path,
cntr: &mut usize,
call_type: CallType,
) -> anyhow::Result<()> {
for patch in &self.patches {
if self.should_process_file(filepath, &patch.files) {
if is_verbose() {
println!("> File: {filepath:?}");
}
let Ok(content) = PatchFile::read(filepath, patch_dir, &patch.decoder) else { continue };
match PatchFile::apply_patch(content.clone(), patch_dir, patch, cntr) {
Ok(Some(res)) => {
if is_verbose() {
println!("> Applied with patch: {patch:?}");
}
match call_type {
CallType::Patch => PatchFile::write(filepath, patch_dir, &patch.encoder, &res)?,
CallType::Test => {
let diffs = Changeset::new(&content, &res, "");
println!("=========== DIFF ===========");
println!("{diffs}");
println!("=========== DIFF ===========");
}
}
}
Ok(None) if is_verbose() => {
println!("> Ignored with patch: {patch:?}");
}
_ => {}
}
}
}
Ok(())
}
fn find_for_folder(&self, root: &Path, patch_dir: &Path, call_type: CallType) -> anyhow::Result<usize> {
let mut cntr = 0;
if root.is_dir() {
for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if !path.is_file() {
continue;
}
self.find_for_file(patch_dir, path, &mut cntr, call_type)?;
}
} else {
self.find_for_file(patch_dir, root, &mut cntr, call_type)?;
}
Ok(cntr)
}
pub fn patch(&self, root: &Path, patch_dir: &Path) -> anyhow::Result<usize> {
self.find_for_folder(root, patch_dir, CallType::Patch)
}
pub fn test(&self, root: &Path, patch_dir: &Path) -> anyhow::Result<usize> {
self.find_for_folder(root, patch_dir, CallType::Test)
}
fn should_process_file(&self, path: &Path, file_patterns: &[FilePath]) -> bool {
if file_patterns.is_empty() {
return true;
}
let path_str = path.to_string_lossy();
file_patterns.iter().any(|pattern| match pattern {
FilePath::Just(exact_path) => path.ends_with(exact_path),
FilePath::Re(regex) => regex.inner().is_match(&path_str),
})
}
fn read(path: &Path, _patch_dir: &Path, decoder: &Option<DecodeBy>) -> anyhow::Result<String> {
match decoder {
None => {
let mut content = String::new();
File::open(path)?.read_to_string(&mut content)?;
Ok(content)
}
Some(DecodeBy::Sh(script)) => Ok(sh_exec::decode_by(_patch_dir, script, path.canonicalize()?)?),
Some(DecodeBy::Lua(script)) => {
let mut content = vec![];
File::open(path)?.read_to_end(&mut content)?;
Ok(lua_exec::decode_by(_patch_dir, script, &content)?)
}
Some(DecodeBy::Rhai(script)) => {
let mut content = vec![];
File::open(path)?.read_to_end(&mut content)?;
Ok(rhai_exec::decode_by(_patch_dir, script, &content)?)
}
}
}
fn write(path: &Path, _patch_dir: &Path, encoder: &Option<EncodeBy>, content: &str) -> anyhow::Result<()> {
match encoder {
None => {
fs::write(path, content)?;
Ok(())
}
Some(EncodeBy::Sh(script)) => {
sh_exec::encode_by(_patch_dir, script, content, path.canonicalize()?)?;
Ok(())
}
Some(EncodeBy::Lua(script)) => {
let content = lua_exec::encode_by(_patch_dir, script, content)?;
fs::write(path, content)?;
Ok(())
}
Some(EncodeBy::Rhai(script)) => {
let content = rhai_exec::encode_by(_patch_dir, script, content)?;
fs::write(path, content)?;
Ok(())
}
}
}
fn apply_patch(content: String, patch_dir: &Path, patch: &Patch, cntr: &mut usize) -> anyhow::Result<Option<String>> {
if let Some((range, cursor)) = PatchFile::find_section(&content, patch_dir, &patch.patch_area)? {
let before = &content[..range.start];
let after = &content[range.end..];
if is_verbose() { println!(">> Section was found (content[{}..{}]).", range.start, range.end); }
let mut middle = content[range.start..range.end].to_owned();
if !middle.is_empty()
&& let Some(replacer) = &patch.replace
{
match replacer {
Replacer::FromTo(from, to) => {
middle = middle.replace(from, to);
if is_verbose() { println!(">> Replaced by `from_to` rule."); }
}
Replacer::FromToVar(from, to_env_var) => {
let to = std::env::var(to_env_var)?;
middle = middle.replace(from, &to);
if is_verbose() { println!(">> Replaced by `from_to_var` rule."); }
}
Replacer::RegexTo(from_re, to) => {
middle = from_re.replace_all(&middle, to.as_str()).to_string();
if is_verbose() { println!(">> Replaced by `regex_to` rule."); }
}
Replacer::BySh(path) => {
middle = sh_exec::replace_by(patch_dir, path, &middle)?;
if is_verbose() { println!(">> Replaced by `by_sh` rule."); }
}
Replacer::ByLua(path) => {
middle = lua_exec::replace_by(patch_dir, path, &middle)?;
if is_verbose() { println!(">> Replaced by `by_lua` rule."); }
}
Replacer::ByRhai(path) => {
middle = rhai_exec::replace_by(patch_dir, path, &middle)?;
if is_verbose() { println!(">> Replaced by `by_rhai` rule."); }
}
}
}
if let Some(insert) = &patch.insert {
if middle.is_empty() {
middle = insert.to_owned();
} else {
middle = match cursor {
CursorType::Start => insert.to_owned() + middle.as_str(),
CursorType::End => middle + insert.as_str(),
}
}
}
let res = format!("{before}{middle}{after}");
#[cfg(test)]
{
println!("{}", content);
println!("{}", res);
}
if !res.as_str().eq(content.as_str()) {
*cntr += 1;
}
return Ok(Some(res));
}
if is_verbose() {
println!(">> Section wasn't found.");
}
Ok(None)
}
fn find_section(
content: &str,
_patch_dir: &Path,
rules: &[AreaRule],
) -> anyhow::Result<Option<(std::ops::Range<usize>, CursorType)>> {
let mut current_pos = 0;
let mut start_pos = None;
let mut end_pos = None;
let mut cursor = CursorType::Start;
for rule in rules {
match &rule {
AreaRule::Contains(regex) => {
if !regex.inner().is_match(&content[current_pos..]) {
return Ok(None);
}
}
AreaRule::NotContains(regex) => {
if regex.inner().is_match(&content[current_pos..]) {
return Ok(None);
}
}
AreaRule::Before(regex) => {
cursor = CursorType::End;
if let Some(mat) = regex.inner().find(&content[current_pos..]) {
end_pos = Some(current_pos + mat.start());
} else {
return Ok(None);
}
}
AreaRule::After(regex) => {
cursor = CursorType::Start;
if let Some(mat) = regex.inner().find(&content[current_pos..]) {
start_pos = Some(current_pos + mat.end());
current_pos += mat.end();
} else {
return Ok(None);
}
}
AreaRule::CursorAtBegin => {
cursor = CursorType::Start;
}
AreaRule::CursorAtEnd => {
cursor = CursorType::End;
}
AreaRule::FindBySh(path) => {
let (new_start, new_end, cursor_at_end) = sh_exec::find_by(_patch_dir, path, content, start_pos, end_pos)?;
if new_start.is_some() {
start_pos = new_start;
}
if new_end.is_some() {
end_pos = new_end;
}
if cursor_at_end {
cursor = CursorType::End;
} else {
cursor = CursorType::Start;
}
}
AreaRule::FindByLua(path) => {
let (new_start, new_end, cursor_at_end) = lua_exec::find_by(_patch_dir, path, content, start_pos, end_pos)?;
if start_pos.is_none() && new_start != 0 {
start_pos = Some(new_start);
}
if end_pos.is_none() && new_end != content.len() {
end_pos = Some(new_end);
}
if cursor_at_end {
cursor = CursorType::End;
} else {
cursor = CursorType::Start;
}
}
AreaRule::FindByRhai(path) => {
let (new_start, new_end, cursor_at_end) = rhai_exec::find_by(_patch_dir, path, content, start_pos, end_pos)?;
if start_pos.is_none() && new_start != 0 {
start_pos = Some(new_start);
}
if end_pos.is_none() && new_end != content.len() {
end_pos = Some(new_end);
}
if cursor_at_end {
cursor = CursorType::End;
} else {
cursor = CursorType::Start;
}
}
}
}
let range = match (start_pos, end_pos) {
(Some(start), Some(end)) if start <= end => Some(start..end),
(Some(start), None) => Some(start..content.len()),
(None, Some(end)) => Some(0..end),
_ => Some(0..content.len()),
};
if let Some(range) = range {
Ok(Some((range, cursor)))
} else {
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_v1() -> anyhow::Result<()> {
let patch = Patch {
files: vec![],
patch_area: vec![AreaRule::After(RegexIR::new("media")?)],
replace: Some(Replacer::FromTo(String::from("ttt"), String::from("yyy"))),
insert: None,
decoder: None,
encoder: None,
};
let content = String::from("ttt is a new media: there is only ttt");
let mut cntr = 0;
let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
assert_eq!(res, Some(String::from("ttt is a new media: there is only yyy")));
assert_eq!(cntr, 1);
Ok(())
}
#[test]
fn find_v2() -> anyhow::Result<()> {
let patch = Patch {
files: vec![],
patch_area: vec![AreaRule::After(RegexIR::new("media")?)],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let content = String::from("ttt is a new media: there is only ttt");
let mut cntr = 0;
let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
assert_eq!(cntr, 1);
Ok(())
}
#[test]
fn find_v3() -> anyhow::Result<()> {
let patch = Patch {
files: vec![],
patch_area: vec![AreaRule::Before(RegexIR::new("media")?)],
replace: None,
insert: Some(String::from("v2 ")),
decoder: None,
encoder: None,
};
let content = String::from("ttt is a new media: there is only ttt");
let mut cntr = 0;
let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
assert_eq!(res, Some(String::from("ttt is a new v2 media: there is only ttt")));
assert_eq!(cntr, 1);
Ok(())
}
#[test]
fn find_v4() -> anyhow::Result<()> {
let patch = Patch {
files: vec![],
patch_area: vec![
AreaRule::Before(RegexIR::new(": there")?),
AreaRule::After(RegexIR::new("new ")?),
AreaRule::FindBySh(PathBuf::from("tests/test_v4.py")),
],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let content = String::from("ttt is a new media: there is only ttt");
let mut cntr = 0;
let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
assert_eq!(cntr, 1);
Ok(())
}
#[test]
fn find_v5() -> anyhow::Result<()> {
let patch = Patch {
files: vec![FilePath::Just(PathBuf::from("test_v5.docx"))],
patch_area: vec![],
replace: Some(Replacer::FromTo("game".to_string(), "rock".to_string())),
insert: None,
decoder: Some(DecodeBy::Sh(PathBuf::from("tests/test_v5.py"))),
encoder: Some(EncodeBy::Sh(PathBuf::from("tests/test_v5.py"))),
};
let pf = PatchFile { patches: vec![patch] };
let cntr = pf.patch(&PathBuf::from("tests"), &PathBuf::from("."))?;
assert_eq!(cntr, 1);
Ok(())
}
#[test]
fn find_v6() -> anyhow::Result<()> {
let patch = Patch {
files: vec![],
patch_area: vec![
AreaRule::Before(RegexIR::new(": there")?),
AreaRule::After(RegexIR::new("new ")?),
AreaRule::FindByLua(PathBuf::from("tests/test_v6.lua")),
],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let content = String::from("ttt is a new media: there is only ttt");
let mut cntr = 0;
let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
assert_eq!(cntr, 1);
Ok(())
}
#[test]
fn find_v7() -> anyhow::Result<()> {
let patch = Patch {
files: vec![],
patch_area: vec![
AreaRule::Before(RegexIR::new(": there")?),
AreaRule::After(RegexIR::new("new ")?),
AreaRule::FindByRhai(PathBuf::from("tests/test_v7.rhai")),
],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let content = String::from("ttt is a new media: there is only ttt");
let mut cntr = 0;
let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
assert_eq!(cntr, 1);
Ok(())
}
#[test]
fn find_v8() -> anyhow::Result<()> {
let patch = Patch {
files: vec![],
patch_area: vec![],
replace: Some(Replacer::BySh(PathBuf::from("tests/test_v8.py"))),
insert: None,
decoder: None,
encoder: None,
};
let content = String::from("This is my number: +18235123154");
let mut cntr = 0;
let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
assert_eq!(res, Some(String::from("This is my number: +28235223254")));
assert_eq!(cntr, 1);
Ok(())
}
#[test]
fn find_v9() -> anyhow::Result<()> {
let patch = Patch {
files: vec![],
patch_area: vec![],
replace: Some(Replacer::BySh(PathBuf::from("tests/test_v9.py"))),
insert: None,
decoder: None,
encoder: None,
};
let content = String::from("This is my project name: {1}");
let mut cntr = 0;
let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
assert_eq!(res, Some(String::from("This is my project name: smart-patcher")));
assert_eq!(cntr, 1);
Ok(())
}
#[test]
fn find_v10() -> anyhow::Result<()> {
let patch = Patch {
files: vec![],
patch_area: vec![
AreaRule::Before(RegexIR::new(": there")?),
AreaRule::After(RegexIR::new("new ")?),
AreaRule::CursorAtEnd,
],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let content = String::from("ttt is a new media: there is only ttt");
let mut cntr = 0;
let res = PatchFile::apply_patch(content, &PathBuf::from("."), &patch, &mut cntr)?;
assert_eq!(res, Some(String::from("ttt is a new media v2: there is only ttt")));
assert_eq!(cntr, 1);
Ok(())
}
}
#[cfg(feature = "generate-examples")]
#[allow(dead_code)]
pub fn generate_examples() -> anyhow::Result<()> {
use std::io::BufWriter;
if !PathBuf::from("examples").exists() {
fs::create_dir("examples")?;
}
let patch = Patch {
files: vec![FilePath::Re(RegexIR::new("\\./tests/.*")?)],
patch_area: vec![AreaRule::After(RegexIR::new("media")?)],
replace: Some(Replacer::FromTo(String::from("ttt"), String::from("yyy"))),
insert: None,
decoder: None,
encoder: None,
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch1.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
let patch = Patch {
files: vec![FilePath::Re(RegexIR::new("\\./tests/.*")?)],
patch_area: vec![AreaRule::After(RegexIR::new("media")?)],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch2.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
let patch = Patch {
files: vec![FilePath::Re(RegexIR::new("\\./tests/.*")?)],
patch_area: vec![AreaRule::Before(RegexIR::new("media")?)],
replace: None,
insert: Some(String::from("v2 ")),
decoder: None,
encoder: None,
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch3.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
let patch = Patch {
files: vec![FilePath::Re(RegexIR::new("\\./tests/.*")?)],
patch_area: vec![
AreaRule::Before(RegexIR::new(": there")?),
AreaRule::After(RegexIR::new("new ")?),
AreaRule::FindBySh(PathBuf::from("tests/test_v4.py")),
],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch4.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
let patch = Patch {
files: vec![FilePath::Just(PathBuf::from("test_v5.docx"))],
patch_area: vec![],
replace: Some(Replacer::FromTo("game".to_string(), "rock".to_string())),
insert: None,
decoder: Some(DecodeBy::Sh(PathBuf::from("tests/test_v5.py"))),
encoder: Some(EncodeBy::Sh(PathBuf::from("tests/test_v5.py"))),
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch5.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
let patch = Patch {
files: vec![FilePath::Re(RegexIR::new("\\./tests/.*")?)],
patch_area: vec![
AreaRule::Before(RegexIR::new(": there")?),
AreaRule::After(RegexIR::new("new ")?),
AreaRule::FindByLua(PathBuf::from("tests/test_v6.lua")),
],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch6.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
let patch = Patch {
files: vec![FilePath::Re(RegexIR::new("\\./tests/.*")?)],
patch_area: vec![
AreaRule::Before(RegexIR::new(": there")?),
AreaRule::After(RegexIR::new("new ")?),
AreaRule::FindByRhai(PathBuf::from("tests/test_v7.rhai")),
],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch7.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
let patch = Patch {
files: vec![FilePath::Re(RegexIR::new("\\./tests/.*")?)],
patch_area: vec![],
replace: Some(Replacer::BySh(PathBuf::from("tests/test_v8.py"))),
insert: None,
decoder: None,
encoder: None,
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch8.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
let patch = Patch {
files: vec![FilePath::Re(RegexIR::new("\\./tests/.*")?)],
patch_area: vec![],
replace: Some(Replacer::BySh(PathBuf::from("tests/test_v9.py"))),
insert: None,
decoder: None,
encoder: None,
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch9.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
let patch = Patch {
files: vec![FilePath::Re(RegexIR::new("\\./tests/.*")?)],
patch_area: vec![
AreaRule::Before(RegexIR::new(": there")?),
AreaRule::After(RegexIR::new("new ")?),
AreaRule::CursorAtEnd,
],
replace: None,
insert: Some(String::from(" v2")),
decoder: None,
encoder: None,
};
let pf = PatchFile { patches: vec![patch] };
let f = File::create("examples/patch10.json")?;
let buf = BufWriter::new(f);
serde_json::to_writer_pretty(buf, &pf)?;
Ok(())
}