1use thiserror::Error;
2use std::path::Path;
3
4#[derive(Error, Debug)]
6pub enum EspError {
7 #[error("Invalid file format")]
8 InvalidFormat,
9
10 #[error("Unsupported record type: {0}")]
11 UnsupportedRecordType(String),
12
13 #[error("Compression error: {0}")]
14 CompressionError(String),
15
16 #[error("IO error: {0}")]
17 IoError(#[from] std::io::Error),
18
19 #[error("JSON error: {0}")]
20 JsonError(#[from] serde_json::Error),
21}
22
23struct StringValidationConfig {
25 blacklist: &'static [&'static str],
26 whitelist: &'static [&'static str],
27}
28
29impl StringValidationConfig {
30 const fn new() -> Self {
31 Self {
32 blacklist: &["<p>"],
33 whitelist: &["Orcax"],
34 }
35 }
36}
37
38pub fn is_valid_string(text: &str) -> bool {
40 let text = text.trim();
41
42 if text.is_empty() {
43 return false;
44 }
45
46 let config = StringValidationConfig::new();
47
48 if config.blacklist.contains(&text) {
50 return false;
51 }
52
53 if is_whitelisted(text, &config) {
55 return true;
56 }
57
58 if is_variable_name(text) {
60 return false;
61 }
62
63 text.chars().all(|c| !c.is_control() || c.is_whitespace())
65}
66
67fn is_whitelisted(text: &str, config: &StringValidationConfig) -> bool {
69 config.whitelist.iter().any(|&w| text.contains(w)) || text.contains("<Alias")
70}
71
72fn is_variable_name(text: &str) -> bool {
74 is_camel_case(text) || is_snake_case(text)
75}
76
77fn is_camel_case(text: &str) -> bool {
79 if text.len() < 3 || !text.chars().all(|c| c.is_ascii_alphanumeric()) {
80 return false;
81 }
82
83 let has_uppercase = text.chars().skip(2).any(|c| c.is_ascii_uppercase());
84 let not_all_uppercase = !text.chars().all(|c| c.is_ascii_uppercase());
85
86 has_uppercase && not_all_uppercase
87}
88
89fn is_snake_case(text: &str) -> bool {
91 !text.contains(' ') && text.contains('_')
92}
93
94pub fn create_backup(file_path: &Path) -> Result<std::path::PathBuf, EspError> {
96 if !file_path.exists() {
97 return Err(EspError::IoError(std::io::Error::new(
98 std::io::ErrorKind::NotFound,
99 "原文件不存在"
100 )));
101 }
102
103 let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S");
104 let backup_path = file_path.with_extension(format!("{}.bak", timestamp));
105
106 std::fs::copy(file_path, &backup_path)
107 .map_err(EspError::IoError)?;
108
109 Ok(backup_path)
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_string_validation() {
118 assert!(is_valid_string("Iron Sword"));
120 assert!(is_valid_string("This is a valid description."));
121 assert!(is_valid_string("铁剑"));
122 assert!(is_valid_string("这是一个有效的描述。"));
123 assert!(is_valid_string("Mixed 中英文 text"));
124
125 assert!(!is_valid_string("CamelCaseVariable"));
127 assert!(!is_valid_string("snake_case_var"));
128 assert!(!is_valid_string(""));
129 assert!(!is_valid_string("<p>"));
130 }
131
132 #[test]
133 fn test_camel_case() {
134 assert!(is_camel_case("CamelCase"));
135 assert!(is_camel_case("myVariable"));
136 assert!(!is_camel_case("lowercase"));
137 assert!(!is_camel_case("UPPERCASE"));
138 assert!(!is_camel_case("my"));
139 }
140
141 #[test]
142 fn test_snake_case() {
143 assert!(is_snake_case("snake_case"));
144 assert!(is_snake_case("my_variable"));
145 assert!(!is_snake_case("normal text"));
146 assert!(!is_snake_case("CamelCase"));
147 }
148}