1use crate::error::Result;
2use ignore::{WalkBuilder, DirEntry};
3use rayon::prelude::*;
4use std::path::{Path, PathBuf};
5use std::fs;
6
7pub struct PathRewriter {
8 src_path: PathBuf,
9 dest_path: PathBuf,
10}
11
12impl PathRewriter {
13 pub fn new<P: AsRef<Path>, Q: AsRef<Path>>(src_path: P, dest_path: Q) -> Self {
14 Self {
15 src_path: src_path.as_ref().to_path_buf(),
16 dest_path: dest_path.as_ref().to_path_buf(),
17 }
18 }
19
20 pub fn rewrite_paths(&self) -> Result<()> {
22 let src_str = self.src_path.to_string_lossy();
23 let dest_str = self.dest_path.to_string_lossy();
24
25 let files: Vec<PathBuf> = WalkBuilder::new(&self.dest_path)
27 .hidden(false) .git_ignore(true) .build()
30 .filter_map(|entry| {
31 match entry {
32 Ok(entry) => {
33 if entry.file_type()?.is_file() {
34 Some(entry.path().to_path_buf())
35 } else {
36 None
37 }
38 }
39 Err(_) => None,
40 }
41 })
42 .collect();
43
44 files.par_iter().for_each(|file_path| {
46 if let Err(e) = self.rewrite_file(file_path, &src_str, &dest_str) {
47 log::warn!("Failed to rewrite paths in {}: {}", file_path.display(), e);
48 }
49 });
50
51 Ok(())
52 }
53
54 fn rewrite_file(&self, file_path: &Path, src_str: &str, dest_str: &str) -> Result<()> {
56 let content = match fs::read_to_string(file_path) {
58 Ok(content) => content,
59 Err(_) => {
60 return Ok(());
62 }
63 };
64
65 if !content.contains(src_str) {
67 return Ok(());
68 }
69
70 if self.is_likely_binary(&content) {
72 return Ok(());
73 }
74
75 let new_content = content.replace(src_str, dest_str);
77
78 if new_content != content {
80 fs::write(file_path, new_content)?;
81 log::debug!("Rewrote paths in: {}", file_path.display());
82 }
83
84 Ok(())
85 }
86
87 fn is_likely_binary(&self, content: &str) -> bool {
89 content.contains('\0') ||
91 {
93 let total = content.len();
94 if total == 0 {
95 return false;
96 }
97
98 let printable = content.chars()
99 .filter(|c| c.is_ascii_graphic() || c.is_ascii_whitespace())
100 .count();
101
102 let printable_ratio = printable as f64 / total as f64;
103 printable_ratio < 0.95
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use tempfile::tempdir;
112 use std::fs;
113
114 #[test]
115 fn test_path_rewriting() {
116 let temp_dir = tempdir().unwrap();
117 let src_dir = temp_dir.path().join("src");
118 let dest_dir = temp_dir.path().join("dest");
119
120 fs::create_dir_all(&src_dir).unwrap();
122 fs::create_dir_all(&dest_dir).unwrap();
123
124 let test_content = format!("export PATH=\"{}:$PATH\"", src_dir.display());
126 fs::write(dest_dir.join("activate.sh"), &test_content).unwrap();
127
128 fs::write(dest_dir.join(".gitignore"), "activate.sh").unwrap();
130
131 let rewriter = PathRewriter::new(&src_dir, &dest_dir);
133 rewriter.rewrite_paths().unwrap();
134
135 let rewritten_content = fs::read_to_string(dest_dir.join("activate.sh")).unwrap();
137 assert!(rewritten_content.contains(&dest_dir.to_string_lossy().to_string()));
138 assert!(!rewritten_content.contains(&src_dir.to_string_lossy().to_string()));
139 }
140
141 #[test]
142 fn test_binary_detection() {
143 let rewriter = PathRewriter::new("/tmp", "/tmp2");
144
145 assert!(!rewriter.is_likely_binary("Hello, world!\nThis is text."));
147
148 assert!(rewriter.is_likely_binary("Hello\0world"));
150
151 let binary_like: String = (0..100).map(|_| '\x01').collect();
153 assert!(rewriter.is_likely_binary(&binary_like));
154 }
155}