1use anyhow::Result;
2use std::fs;
3use std::path::Path;
4
5pub struct EolNormalizer;
7
8impl EolNormalizer {
9 pub fn detect_eol(file_path: &Path) -> Result<String> {
14 if !file_path.exists() {
15 return Ok(Self::platform_default());
16 }
17
18 let content = fs::read_to_string(file_path)?;
19 Self::detect_eol_from_content(&content)
20 }
21
22 pub fn detect_eol_from_content(content: &str) -> Result<String> {
24 if content.contains("\r\n") {
26 return Ok("\r\n".to_string());
27 }
28
29 if content.contains('\n') {
31 return Ok("\n".to_string());
32 }
33
34 if content.contains('\r') {
36 return Ok("\r".to_string());
37 }
38
39 Ok(Self::platform_default())
41 }
42
43 pub fn platform_default() -> String {
45 if cfg!(windows) {
46 "\r\n".to_string()
47 } else {
48 "\n".to_string()
49 }
50 }
51
52 pub fn normalize_to_eol(content: &str, target_eol: &str) -> String {
54 let normalized = content
56 .replace("\r\n", "\n") .replace('\r', "\n"); normalized.replace('\n', target_eol)
61 }
62
63 pub fn normalize_to_match_file(content: &str, target_file: &Path) -> Result<String> {
65 let target_eol = Self::detect_eol(target_file)?;
66 Ok(Self::normalize_to_eol(content, &target_eol))
67 }
68}
69
70pub struct SkipIfGenerator;
72
73impl SkipIfGenerator {
74 pub fn generate_exact_match(content: &str) -> String {
79 let escaped = regex::escape(content);
81 format!("(?s){}", escaped) }
83
84 pub fn content_exists_in_file(content: &str, file_path: &Path) -> Result<bool> {
86 if !file_path.exists() {
87 return Ok(false);
88 }
89
90 let file_content = fs::read_to_string(file_path)?;
91 Ok(file_content.contains(content))
92 }
93
94 pub fn generate_idempotent_pattern(content: &str) -> String {
100 Self::generate_exact_match(content)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use tempfile::NamedTempFile;
111
112 #[test]
113 fn test_detect_eol_crlf() -> Result<()> {
114 let temp_file = NamedTempFile::new()?;
115 fs::write(temp_file.path(), "line1\r\nline2\r\n")?;
116
117 let eol = EolNormalizer::detect_eol(temp_file.path())?;
118 assert_eq!(eol, "\r\n");
119
120 Ok(())
121 }
122
123 #[test]
124 fn test_detect_eol_lf() -> Result<()> {
125 let temp_file = NamedTempFile::new()?;
126 fs::write(temp_file.path(), "line1\nline2\n")?;
127
128 let eol = EolNormalizer::detect_eol(temp_file.path())?;
129 assert_eq!(eol, "\n");
130
131 Ok(())
132 }
133
134 #[test]
135 fn test_detect_eol_cr() -> Result<()> {
136 let temp_file = NamedTempFile::new()?;
137 fs::write(temp_file.path(), "line1\rline2\r")?;
138
139 let eol = EolNormalizer::detect_eol(temp_file.path())?;
140 assert_eq!(eol, "\r");
141
142 Ok(())
143 }
144
145 #[test]
146 fn test_detect_eol_no_eol() -> Result<()> {
147 let temp_file = NamedTempFile::new()?;
148 fs::write(temp_file.path(), "single line")?;
149
150 let eol = EolNormalizer::detect_eol(temp_file.path())?;
151 assert_eq!(eol, EolNormalizer::platform_default());
152
153 Ok(())
154 }
155
156 #[test]
157 fn test_detect_eol_nonexistent_file() -> Result<()> {
158 let eol = EolNormalizer::detect_eol(Path::new("/nonexistent/file"))?;
159 assert_eq!(eol, EolNormalizer::platform_default());
160
161 Ok(())
162 }
163
164 #[test]
165 fn test_normalize_to_eol() {
166 let content = "line1\r\nline2\rline3\nline4";
167
168 let normalized_lf = EolNormalizer::normalize_to_eol(content, "\n");
170 assert_eq!(normalized_lf, "line1\nline2\nline3\nline4");
171
172 let normalized_crlf = EolNormalizer::normalize_to_eol(content, "\r\n");
174 assert_eq!(normalized_crlf, "line1\r\nline2\r\nline3\r\nline4");
175
176 let normalized_cr = EolNormalizer::normalize_to_eol(content, "\r");
178 assert_eq!(normalized_cr, "line1\rline2\rline3\rline4");
179 }
180
181 #[test]
182 fn test_normalize_to_match_file() -> Result<()> {
183 let temp_file = NamedTempFile::new()?;
184 fs::write(temp_file.path(), "existing\r\ncontent")?;
185
186 let content_to_inject = "new\ncontent";
187 let normalized =
188 EolNormalizer::normalize_to_match_file(content_to_inject, temp_file.path())?;
189
190 assert_eq!(normalized, "new\r\ncontent");
191
192 Ok(())
193 }
194
195 #[test]
196 fn test_generate_exact_match() {
197 let content = "function hello() {\n console.log('world');\n}";
198 let pattern = SkipIfGenerator::generate_exact_match(content);
199
200 assert!(regex::Regex::new(&pattern).is_ok());
202
203 let regex = regex::Regex::new(&pattern).unwrap();
205 assert!(regex.is_match(content));
206 }
207
208 #[test]
209 fn test_generate_exact_match_with_special_chars() {
210 let content = "function test() {\n return /^[a-z]+$/;\n}";
211 let pattern = SkipIfGenerator::generate_exact_match(content);
212
213 let regex = regex::Regex::new(&pattern).unwrap();
215 assert!(regex.is_match(content));
216
217 let different_content = "function test() {\n return /^[A-Z]+$/;\n}";
219 assert!(!regex.is_match(different_content));
220 }
221
222 #[test]
223 fn test_content_exists_in_file() -> Result<()> {
224 let temp_file = NamedTempFile::new()?;
225 let content = "function hello() {\n console.log('world');\n}";
226 fs::write(
227 temp_file.path(),
228 &format!("// Header\n{}\n// Footer", content),
229 )?;
230
231 assert!(SkipIfGenerator::content_exists_in_file(
233 content,
234 temp_file.path()
235 )?);
236
237 let different_content = "function goodbye() {\n console.log('moon');\n}";
239 assert!(!SkipIfGenerator::content_exists_in_file(
240 different_content,
241 temp_file.path()
242 )?);
243
244 Ok(())
245 }
246
247 #[test]
248 fn test_content_exists_in_nonexistent_file() -> Result<()> {
249 let result =
250 SkipIfGenerator::content_exists_in_file("content", Path::new("/nonexistent/file"))?;
251 assert!(!result);
252
253 Ok(())
254 }
255
256 #[test]
257 fn test_generate_idempotent_pattern() {
258 let content = "function test() {}";
259 let pattern = SkipIfGenerator::generate_idempotent_pattern(content);
260
261 assert!(regex::Regex::new(&pattern).is_ok());
263
264 let regex = regex::Regex::new(&pattern).unwrap();
266 assert!(regex.is_match(content));
267 }
268
269 #[test]
270 fn test_platform_default() {
271 let default = EolNormalizer::platform_default();
272
273 assert!(default == "\n" || default == "\r\n");
275 }
276}