use anyhow::Result;
use chardetng::EncodingDetector;
use encoding_rs::Encoding;
use std::path::Path;
use tokio::fs;
use walkdir::WalkDir;
use std::io::{self, Write};
fn should_process_file(path: &Path, allowed_extensions: &[String]) -> bool {
if let Some(extension) = path.extension() {
if let Some(ext) = extension.to_str() {
return allowed_extensions.iter().any(|allowed| allowed.eq_ignore_ascii_case(ext));
}
}
false
}
fn default_allowed_extensions() -> Vec<String> {
vec![
"c", "h", "cpp", "hpp", "cc", "cxx", "rs", "ts", "tsx", "js", "jsx", "txt", "html", "htm", "xml", "py", "java", "go", "md", "markdown", "json", "yaml", "yml", "toml", "sh", "bash", "sql", "css", "scss", "sass", ]
.into_iter()
.map(String::from)
.collect()
}
pub async fn scan_directory(dir_path: &str, convert: bool, verbose: bool, types: Option<Vec<String>>) -> Result<()> {
let allowed_extensions = types.unwrap_or_else(default_allowed_extensions);
let mut found_non_utf8 = false;
for entry in WalkDir::new(dir_path).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let path = entry.path();
if !should_process_file(path, &allowed_extensions) {
if verbose {
println!("Skip: {}", path.display());
}
continue;
}
if let Some(encoding_info) = detect_file_encoding(path).await? {
if encoding_info.0 != "UTF-8" {
found_non_utf8 = true;
if verbose {
let content = fs::read(path).await?;
println!(
"File: {}\nEncoding: {}\nSize: {} bytes\n",
path.display(),
encoding_info.0,
content.len(),
);
} else {
println!("{} {}", path.display(), encoding_info.0);
}
}
}
}
}
if !found_non_utf8 {
return Ok(());
}
let should_convert = if convert {
true
} else {
print!("\nConvert to UTF-8? (y/n): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
input.trim().eq_ignore_ascii_case("y")
};
if should_convert {
for entry in WalkDir::new(dir_path).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let path = entry.path();
if !should_process_file(path, &allowed_extensions) {
continue;
}
if let Some(encoding_info) = detect_file_encoding(path).await? {
if encoding_info.0 != "UTF-8" {
convert_to_utf8(path, encoding_info.1).await?;
println!("Converted: {}", path.display());
}
}
}
}
}
Ok(())
}
async fn detect_file_encoding(path: &Path) -> Result<Option<(&'static str, &'static Encoding)>> {
let content = fs::read(path).await?;
let mut detector = EncodingDetector::new();
detector.feed(&content, true);
let encoding = detector.guess(None, true);
if encoding.name() != "UTF-8" {
Ok(Some((encoding.name(), encoding)))
} else {
Ok(None)
}
}
async fn convert_to_utf8(path: &Path, encoding: &'static Encoding) -> Result<()> {
let content = fs::read(path).await?;
let (decoded, _, had_errors) = encoding.decode(&content);
if had_errors {
println!("Warning: decoding error in {}", path.display());
}
fs::write(path, decoded.as_bytes()).await?;
Ok(())
}