use std::path::Path;
use tokio::fs;
use chrono::{Local, DateTime};
use regex::Regex;
pub struct PatternResult {
pub name: String,
pub warnings: Vec<String>,
}
pub async fn apply_rename_pattern(
file_path: &Path,
pattern: &str,
counter: usize,
) -> Result<PatternResult, Box<dyn std::error::Error>> {
let file_name = file_path
.file_name()
.and_then(|n| n.to_str())
.ok_or("Invalid file name")?;
let (name, ext) = if let Some(dot_pos) = file_name.rfind('.') {
let (n, e) = file_name.split_at(dot_pos);
(n, &e[1..])
} else {
(file_name, "")
};
let mut result = String::new();
let mut warnings = Vec::new();
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
let _valid_placeholders = "CNEFPDH";
let valid_modifiers = "LUTRMX";
while i < chars.len() {
if chars[i] == '%' && i + 1 < chars.len() {
let next_char = chars[i + 1].to_ascii_uppercase();
if i + 2 < chars.len() {
let token = format!("{}{}", next_char, chars[i + 2].to_ascii_uppercase());
if token == "FD" || token == "FH" {
if let Ok(metadata) = fs::metadata(file_path).await {
if let Ok(mtime) = metadata.modified() {
let dt: DateTime<Local> = mtime.into();
if token == "FD" {
result.push_str(&dt.format("%Y-%m-%d").to_string());
} else {
result.push_str(&dt.format("%H-%M-%S").to_string());
}
i += 3;
continue;
}
}
}
}
match next_char {
'C' => {
let mut j = i + 2;
let mut digits = String::new();
while j < chars.len() && chars[j].is_ascii_digit() {
digits.push(chars[j]);
j += 1;
}
if !digits.is_empty() {
let width: usize = digits.parse().unwrap_or(1);
result.push_str(&format!("{:0width$}", counter, width = width));
i = j;
continue;
} else {
result.push_str(&counter.to_string());
i += 2;
continue;
}
}
'N' => {
let j = i + 2;
let (range_opt, next_j) = parse_range(&chars, j, name.len());
if let Some((start, end)) = range_opt {
if start < end && start < name.len() {
result.push_str(&name[start..end]);
}
i = next_j;
continue;
}
result.push_str(name);
i += 2;
continue;
}
'E' => {
let j = i + 2;
let (range_opt, next_j) = parse_range(&chars, j, ext.len());
if let Some((start, end)) = range_opt {
if start < end && start < ext.len() {
result.push_str(&ext[start..end]);
}
i = next_j;
continue;
}
result.push_str(ext);
i += 2;
continue;
}
'F' => {
let j = i + 2;
result.push_str(file_name);
i = j;
continue;
}
'P' => {
let j = i + 2;
let parent_name = if let Some(parent) = file_path.parent().and_then(|p| p.file_name()).and_then(|n| n.to_str()) {
parent.to_string()
} else if let Ok(abs) = fs::canonicalize(file_path).await {
abs.parent().and_then(|p| p.file_name()).and_then(|n| n.to_str()).unwrap_or("").to_string()
} else {
"".to_string()
};
let (range_opt, next_j) = parse_range(&chars, j, parent_name.len());
if let Some((start, end)) = range_opt {
if start < end && start < parent_name.len() {
result.push_str(&parent_name[start..end]);
}
i = next_j;
} else {
result.push_str(&parent_name);
i = j;
}
continue;
}
'D' => {
let j = i + 2;
result.push_str(&Local::now().format("%Y-%m-%d").to_string());
i = j;
continue;
}
'H' => {
let j = i + 2;
result.push_str(&Local::now().format("%H-%M-%S").to_string());
i = j;
continue;
}
_ => {
if valid_modifiers.contains(next_char) {
result.push(chars[i]); result.push(chars[i + 1]); i += 2;
continue;
}
if next_char.is_alphabetic() {
warnings.push(format!("Unknown token: %{}", chars[i+1]));
}
result.push(chars[i]);
i += 1;
continue;
}
}
}
result.push(chars[i]);
i += 1;
}
result = apply_modifiers_left_to_right(&result)?;
Ok(PatternResult { name: result, warnings })
}
fn apply_modifiers_left_to_right(pattern: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut result = String::new();
let mut i = 0;
let mut mode = ModifierMode::None;
let chars: Vec<char> = pattern.chars().collect();
while i < chars.len() {
if i < chars.len() - 1 && chars[i] == '%' {
match chars[i + 1] {
'L' | 'l' => {
result = result.to_ascii_lowercase();
mode = ModifierMode::Lowercase;
i += 2;
continue;
}
'U' | 'u' => {
result = result.to_ascii_uppercase();
mode = ModifierMode::Uppercase;
i += 2;
continue;
}
'T' | 't' => {
result = to_title_case(&result);
mode = ModifierMode::TitleCase;
i += 2;
continue;
}
'M' | 'm' => {
result = result.trim().to_string();
i += 2;
continue;
}
_ => {}
}
}
let mut found_modifier = false;
let delimiters = ['/', '|', ':', ',', '@'];
for delimiter in delimiters {
if chars.len() >= 3 && i < chars.len() - 3
&& chars[i] == '%' && (chars[i + 1] == 'R' || chars[i + 1] == 'r')
&& chars[i + 2] == delimiter {
if let Some((old, new, end_pos)) = parse_pattern_args(&chars, i + 3, delimiter)? {
result = result.replace(&old, &new);
i = end_pos;
found_modifier = true;
break;
}
}
if chars.len() >= 3 && i < chars.len() - 3
&& chars[i] == '%' && (chars[i + 1] == 'X' || chars[i + 1] == 'x')
&& chars[i + 2] == delimiter {
if let Some((pattern_str, new, end_pos)) = parse_pattern_args(&chars, i + 3, delimiter)? {
let re = Regex::new(&pattern_str)?;
result = re.replace_all(&result, new.as_str()).to_string();
i = end_pos;
found_modifier = true;
break;
}
}
}
if found_modifier {
continue;
}
let ch = chars[i];
match mode {
ModifierMode::Lowercase => result.push(ch.to_ascii_lowercase()),
ModifierMode::Uppercase => result.push(ch.to_ascii_uppercase()),
ModifierMode::TitleCase => {
result.push(ch);
result = to_title_case(&result);
}
ModifierMode::None => result.push(ch),
}
i += 1;
}
Ok(result)
}
#[derive(PartialEq)]
enum ModifierMode {
None,
Lowercase,
Uppercase,
TitleCase,
}
fn to_title_case(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = true;
for c in s.chars() {
if c.is_whitespace() || c == '_' || c == '.' || c == '-' {
result.push(c);
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c.to_ascii_lowercase());
}
}
result
}
fn parse_range(chars: &[char], start_pos: usize, len: usize) -> (Option<(usize, usize)>, usize) {
let mut j = start_pos;
let mut start_str = String::new();
while j < chars.len() && chars[j].is_ascii_digit() {
start_str.push(chars[j]);
j += 1;
}
if j < chars.len() && chars[j] == '-' {
j += 1;
let mut is_negative = false;
if j < chars.len() && chars[j] == '-' {
is_negative = true;
j += 1;
}
let mut end_str = String::new();
while j < chars.len() && chars[j].is_ascii_digit() {
end_str.push(chars[j]);
j += 1;
}
let start = if start_str.is_empty() {
0
} else {
start_str.parse::<usize>().unwrap_or(1).saturating_sub(1)
};
let end = if end_str.is_empty() {
len
} else {
let end_val = end_str.parse::<usize>().unwrap_or(len);
if is_negative {
len.saturating_sub(end_val)
} else {
end_val.min(len)
}
};
return (Some((start, end)), j);
}
(None, start_pos)
}
fn parse_pattern_args(
chars: &[char],
start: usize,
delimiter: char,
) -> Result<Option<(String, String, usize)>, Box<dyn std::error::Error>> {
let mut i = start;
let mut arg1 = String::new();
let mut arg2 = String::new();
let mut found_delimiter = false;
while i < chars.len() {
if chars[i] == delimiter {
found_delimiter = true;
i += 1;
break;
}
arg1.push(chars[i]);
i += 1;
}
if !found_delimiter {
return Ok(None);
}
while i < chars.len() {
if chars[i] == '.' {
break;
}
if i < chars.len() - 1 && chars[i] == '%' {
let next = chars[i + 1].to_ascii_uppercase();
if "RUL T MN EC FPDH X".contains(next) {
break;
}
}
arg2.push(chars[i]);
i += 1;
}
Ok(Some((arg1, arg2, i)))
}