rustywallet_vanity/
regex_pattern.rs1use crate::address_type::AddressType;
7use crate::error::PatternError;
8use regex::Regex;
9
10#[derive(Debug, Clone)]
26pub struct RegexPattern {
27 regex: Regex,
29 pattern_str: String,
31 case_insensitive: bool,
33}
34
35impl RegexPattern {
36 pub fn new(pattern: &str) -> Result<Self, PatternError> {
46 if pattern.is_empty() {
47 return Err(PatternError::EmptyPattern);
48 }
49
50 let regex = Regex::new(pattern)
51 .map_err(|e| PatternError::InvalidRegex(e.to_string()))?;
52
53 Ok(Self {
54 regex,
55 pattern_str: pattern.to_string(),
56 case_insensitive: false,
57 })
58 }
59
60 pub fn new_case_insensitive(pattern: &str) -> Result<Self, PatternError> {
62 if pattern.is_empty() {
63 return Err(PatternError::EmptyPattern);
64 }
65
66 let ci_pattern = format!("(?i){}", pattern);
68 let regex = Regex::new(&ci_pattern)
69 .map_err(|e| PatternError::InvalidRegex(e.to_string()))?;
70
71 Ok(Self {
72 regex,
73 pattern_str: pattern.to_string(),
74 case_insensitive: true,
75 })
76 }
77
78 pub fn matches(&self, address: &str) -> bool {
80 self.regex.is_match(address)
81 }
82
83 pub fn as_str(&self) -> &str {
85 &self.pattern_str
86 }
87
88 pub fn is_case_insensitive(&self) -> bool {
90 self.case_insensitive
91 }
92
93 pub fn estimate_difficulty(&self, address_type: AddressType) -> f64 {
97 let alphabet_size: f64 = match address_type {
99 AddressType::P2PKH => 58.0,
100 AddressType::P2WPKH => 32.0,
101 AddressType::P2TR => 32.0,
102 AddressType::Ethereum => 16.0,
103 };
104
105 let effective_len = self.pattern_str
108 .chars()
109 .filter(|c| c.is_alphanumeric())
110 .count();
111
112 if effective_len == 0 {
113 return 1.0;
114 }
115
116 let case_factor = if self.case_insensitive {
118 let letter_count = self.pattern_str
119 .chars()
120 .filter(|c| c.is_alphabetic())
121 .count();
122 2.0_f64.powi(letter_count as i32)
123 } else {
124 1.0
125 };
126
127 alphabet_size.powi(effective_len as i32) / case_factor
128 }
129}
130
131impl std::fmt::Display for RegexPattern {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 write!(f, "regex:{}", self.pattern_str)
134 }
135}
136
137pub struct CommonPatterns;
139
140impl CommonPatterns {
141 pub fn starts_with(text: &str) -> Result<RegexPattern, PatternError> {
143 RegexPattern::new(&format!("^{}", regex::escape(text)))
144 }
145
146 pub fn ends_with(text: &str) -> Result<RegexPattern, PatternError> {
148 RegexPattern::new(&format!("{}$", regex::escape(text)))
149 }
150
151 pub fn contains(text: &str) -> Result<RegexPattern, PatternError> {
153 RegexPattern::new(®ex::escape(text))
154 }
155
156 pub fn repeated_char(c: char, count: usize) -> Result<RegexPattern, PatternError> {
168 if count == 0 {
169 return Err(PatternError::EmptyPattern);
170 }
171 RegexPattern::new(&format!("{}{{{}}}", regex::escape(&c.to_string()), count))
172 }
173
174 pub fn alternating(chars: &str) -> Result<RegexPattern, PatternError> {
176 if chars.len() < 2 {
177 return Err(PatternError::InvalidPattern("Need at least 2 characters".into()));
178 }
179 let pattern: String = chars.chars()
180 .map(|c| regex::escape(&c.to_string()))
181 .collect::<Vec<_>>()
182 .join("");
183 RegexPattern::new(&pattern)
184 }
185
186 pub fn word(word: &str) -> Result<RegexPattern, PatternError> {
188 RegexPattern::new_case_insensitive(®ex::escape(word))
189 }
190
191 pub fn numeric_sequence(min_length: usize) -> Result<RegexPattern, PatternError> {
203 if min_length == 0 {
204 return Err(PatternError::EmptyPattern);
205 }
206 RegexPattern::new(&format!(r"\d{{{},}}", min_length))
207 }
208
209 pub fn letter_sequence(min_length: usize) -> Result<RegexPattern, PatternError> {
211 if min_length == 0 {
212 return Err(PatternError::EmptyPattern);
213 }
214 RegexPattern::new(&format!(r"[A-Za-z]{{{},}}", min_length))
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_regex_pattern_basic() {
224 let pattern = RegexPattern::new(r"^1[A-Z]{3}").unwrap();
225 assert!(pattern.matches("1ABC123"));
226 assert!(pattern.matches("1XYZ456"));
227 assert!(!pattern.matches("1abc123")); assert!(!pattern.matches("2ABC123")); }
230
231 #[test]
232 fn test_regex_pattern_case_insensitive() {
233 let pattern = RegexPattern::new_case_insensitive(r"^1love").unwrap();
234 assert!(pattern.matches("1Love123"));
235 assert!(pattern.matches("1LOVE123"));
236 assert!(pattern.matches("1love123"));
237 }
238
239 #[test]
240 fn test_common_patterns_starts_with() {
241 let pattern = CommonPatterns::starts_with("1BTC").unwrap();
242 assert!(pattern.matches("1BTC123"));
243 assert!(!pattern.matches("2BTC123"));
244 }
245
246 #[test]
247 fn test_common_patterns_ends_with() {
248 let pattern = CommonPatterns::ends_with("BTC").unwrap();
249 assert!(pattern.matches("1abcBTC"));
250 assert!(!pattern.matches("1BTCabc"));
251 }
252
253 #[test]
254 fn test_common_patterns_repeated() {
255 let pattern = CommonPatterns::repeated_char('A', 3).unwrap();
256 assert!(pattern.matches("1AAA123"));
257 assert!(pattern.matches("1AAAA123")); assert!(!pattern.matches("1AA123"));
259 }
260
261 #[test]
262 fn test_common_patterns_numeric() {
263 let pattern = CommonPatterns::numeric_sequence(4).unwrap();
264 assert!(pattern.matches("1abc1234xyz"));
265 assert!(pattern.matches("1abc12345xyz"));
266 assert!(!pattern.matches("1abc123xyz"));
267 }
268
269 #[test]
270 fn test_difficulty_estimate() {
271 let pattern = RegexPattern::new(r"^1[A-Z]{2}").unwrap();
272 let diff = pattern.estimate_difficulty(AddressType::P2PKH);
273 assert!(diff > 1000.0);
275 }
276
277 #[test]
278 fn test_invalid_regex() {
279 let result = RegexPattern::new(r"[invalid");
280 assert!(result.is_err());
281 }
282
283 #[test]
284 fn test_empty_pattern() {
285 assert!(RegexPattern::new("").is_err());
286 }
287}