use anyhow::Result;
use std::fs;
use std::path::Path;
pub struct EolNormalizer;
impl EolNormalizer {
pub fn detect_eol(file_path: &Path) -> Result<String> {
if !file_path.exists() {
return Ok(Self::platform_default());
}
let content = fs::read_to_string(file_path)?;
Self::detect_eol_from_content(&content)
}
pub fn detect_eol_from_content(content: &str) -> Result<String> {
if content.contains("\r\n") {
return Ok("\r\n".to_string());
}
if content.contains('\n') {
return Ok("\n".to_string());
}
if content.contains('\r') {
return Ok("\r".to_string());
}
Ok(Self::platform_default())
}
pub fn platform_default() -> String {
if cfg!(windows) {
"\r\n".to_string()
} else {
"\n".to_string()
}
}
pub fn normalize_to_eol(content: &str, target_eol: &str) -> String {
let normalized = content
.replace("\r\n", "\n") .replace('\r', "\n");
normalized.replace('\n', target_eol)
}
pub fn normalize_to_match_file(content: &str, target_file: &Path) -> Result<String> {
let target_eol = Self::detect_eol(target_file)?;
Ok(Self::normalize_to_eol(content, &target_eol))
}
}
pub struct SkipIfGenerator;
impl SkipIfGenerator {
pub fn generate_exact_match(content: &str) -> String {
let escaped = regex::escape(content);
format!("(?s){}", escaped) }
pub fn content_exists_in_file(content: &str, file_path: &Path) -> Result<bool> {
if !file_path.exists() {
return Ok(false);
}
let file_content = fs::read_to_string(file_path)?;
Ok(file_content.contains(content))
}
pub fn generate_idempotent_pattern(content: &str) -> String {
Self::generate_exact_match(content)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_detect_eol_crlf() -> Result<()> {
let temp_file = NamedTempFile::new()?;
fs::write(temp_file.path(), "line1\r\nline2\r\n")?;
let eol = EolNormalizer::detect_eol(temp_file.path())?;
assert_eq!(eol, "\r\n");
Ok(())
}
#[test]
fn test_detect_eol_lf() -> Result<()> {
let temp_file = NamedTempFile::new()?;
fs::write(temp_file.path(), "line1\nline2\n")?;
let eol = EolNormalizer::detect_eol(temp_file.path())?;
assert_eq!(eol, "\n");
Ok(())
}
#[test]
fn test_detect_eol_cr() -> Result<()> {
let temp_file = NamedTempFile::new()?;
fs::write(temp_file.path(), "line1\rline2\r")?;
let eol = EolNormalizer::detect_eol(temp_file.path())?;
assert_eq!(eol, "\r");
Ok(())
}
#[test]
fn test_detect_eol_no_eol() -> Result<()> {
let temp_file = NamedTempFile::new()?;
fs::write(temp_file.path(), "single line")?;
let eol = EolNormalizer::detect_eol(temp_file.path())?;
assert_eq!(eol, EolNormalizer::platform_default());
Ok(())
}
#[test]
fn test_detect_eol_nonexistent_file() -> Result<()> {
let eol = EolNormalizer::detect_eol(Path::new("/nonexistent/file"))?;
assert_eq!(eol, EolNormalizer::platform_default());
Ok(())
}
#[test]
fn test_normalize_to_eol() {
let content = "line1\r\nline2\rline3\nline4";
let normalized_lf = EolNormalizer::normalize_to_eol(content, "\n");
assert_eq!(normalized_lf, "line1\nline2\nline3\nline4");
let normalized_crlf = EolNormalizer::normalize_to_eol(content, "\r\n");
assert_eq!(normalized_crlf, "line1\r\nline2\r\nline3\r\nline4");
let normalized_cr = EolNormalizer::normalize_to_eol(content, "\r");
assert_eq!(normalized_cr, "line1\rline2\rline3\rline4");
}
#[test]
fn test_normalize_to_match_file() -> Result<()> {
let temp_file = NamedTempFile::new()?;
fs::write(temp_file.path(), "existing\r\ncontent")?;
let content_to_inject = "new\ncontent";
let normalized =
EolNormalizer::normalize_to_match_file(content_to_inject, temp_file.path())?;
assert_eq!(normalized, "new\r\ncontent");
Ok(())
}
#[test]
fn test_generate_exact_match() {
let content = "function hello() {\n console.log('world');\n}";
let pattern = SkipIfGenerator::generate_exact_match(content);
assert!(regex::Regex::new(&pattern).is_ok());
let regex = regex::Regex::new(&pattern).unwrap();
assert!(regex.is_match(content));
}
#[test]
fn test_generate_exact_match_with_special_chars() {
let content = "function test() {\n return /^[a-z]+$/;\n}";
let pattern = SkipIfGenerator::generate_exact_match(content);
let regex = regex::Regex::new(&pattern).unwrap();
assert!(regex.is_match(content));
let different_content = "function test() {\n return /^[A-Z]+$/;\n}";
assert!(!regex.is_match(different_content));
}
#[test]
fn test_content_exists_in_file() -> Result<()> {
let temp_file = NamedTempFile::new()?;
let content = "function hello() {\n console.log('world');\n}";
fs::write(
temp_file.path(),
&format!("// Header\n{}\n// Footer", content),
)?;
assert!(SkipIfGenerator::content_exists_in_file(
content,
temp_file.path()
)?);
let different_content = "function goodbye() {\n console.log('moon');\n}";
assert!(!SkipIfGenerator::content_exists_in_file(
different_content,
temp_file.path()
)?);
Ok(())
}
#[test]
fn test_content_exists_in_nonexistent_file() -> Result<()> {
let result =
SkipIfGenerator::content_exists_in_file("content", Path::new("/nonexistent/file"))?;
assert!(!result);
Ok(())
}
#[test]
fn test_generate_idempotent_pattern() {
let content = "function test() {}";
let pattern = SkipIfGenerator::generate_idempotent_pattern(content);
assert!(regex::Regex::new(&pattern).is_ok());
let regex = regex::Regex::new(&pattern).unwrap();
assert!(regex.is_match(content));
}
#[test]
fn test_platform_default() {
let default = EolNormalizer::platform_default();
assert!(default == "\n" || default == "\r\n");
}
}