use crate::error::Result;
use ignore::{WalkBuilder, DirEntry};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::fs;
pub struct PathRewriter {
src_path: PathBuf,
dest_path: PathBuf,
}
impl PathRewriter {
pub fn new<P: AsRef<Path>, Q: AsRef<Path>>(src_path: P, dest_path: Q) -> Self {
Self {
src_path: src_path.as_ref().to_path_buf(),
dest_path: dest_path.as_ref().to_path_buf(),
}
}
pub fn rewrite_paths(&self) -> Result<()> {
let src_str = self.src_path.to_string_lossy();
let dest_str = self.dest_path.to_string_lossy();
let files: Vec<PathBuf> = WalkBuilder::new(&self.dest_path)
.hidden(false) .git_ignore(true) .build()
.filter_map(|entry| {
match entry {
Ok(entry) => {
if entry.file_type()?.is_file() {
Some(entry.path().to_path_buf())
} else {
None
}
}
Err(_) => None,
}
})
.collect();
files.par_iter().for_each(|file_path| {
if let Err(e) = self.rewrite_file(file_path, &src_str, &dest_str) {
log::warn!("Failed to rewrite paths in {}: {}", file_path.display(), e);
}
});
Ok(())
}
fn rewrite_file(&self, file_path: &Path, src_str: &str, dest_str: &str) -> Result<()> {
let content = match fs::read_to_string(file_path) {
Ok(content) => content,
Err(_) => {
return Ok(());
}
};
if !content.contains(src_str) {
return Ok(());
}
if self.is_likely_binary(&content) {
return Ok(());
}
let new_content = content.replace(src_str, dest_str);
if new_content != content {
fs::write(file_path, new_content)?;
log::debug!("Rewrote paths in: {}", file_path.display());
}
Ok(())
}
fn is_likely_binary(&self, content: &str) -> bool {
content.contains('\0') ||
{
let total = content.len();
if total == 0 {
return false;
}
let printable = content.chars()
.filter(|c| c.is_ascii_graphic() || c.is_ascii_whitespace())
.count();
let printable_ratio = printable as f64 / total as f64;
printable_ratio < 0.95
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::fs;
#[test]
fn test_path_rewriting() {
let temp_dir = tempdir().unwrap();
let src_dir = temp_dir.path().join("src");
let dest_dir = temp_dir.path().join("dest");
fs::create_dir_all(&src_dir).unwrap();
fs::create_dir_all(&dest_dir).unwrap();
let test_content = format!("export PATH=\"{}:$PATH\"", src_dir.display());
fs::write(dest_dir.join("activate.sh"), &test_content).unwrap();
fs::write(dest_dir.join(".gitignore"), "activate.sh").unwrap();
let rewriter = PathRewriter::new(&src_dir, &dest_dir);
rewriter.rewrite_paths().unwrap();
let rewritten_content = fs::read_to_string(dest_dir.join("activate.sh")).unwrap();
assert!(rewritten_content.contains(&dest_dir.to_string_lossy().to_string()));
assert!(!rewritten_content.contains(&src_dir.to_string_lossy().to_string()));
}
#[test]
fn test_binary_detection() {
let rewriter = PathRewriter::new("/tmp", "/tmp2");
assert!(!rewriter.is_likely_binary("Hello, world!\nThis is text."));
assert!(rewriter.is_likely_binary("Hello\0world"));
let binary_like: String = (0..100).map(|_| '\x01').collect();
assert!(rewriter.is_likely_binary(&binary_like));
}
}