Skip to main content

btc_vanity/
file.rs

1//! # File Reading and Writing Module
2//!
3//! This module provides functionality for:
4//! - Parsing input files containing vanity patterns and flags.
5//! - Writing generated vanity wallet details to output files.
6
7use std::fs::{self, OpenOptions};
8use std::io::{self, Write};
9use std::path::Path;
10
11use crate::error::VanityError;
12use crate::flags::VanityFlags;
13use crate::vanity_addr_generator::chain::Chain;
14use crate::VanityMode;
15
16/// Represents a single line item from an input file,
17/// containing a vanity pattern and associated flags.
18#[derive(Debug, Clone)]
19pub struct FileLineItem {
20    /// The vanity pattern to match (e.g., "emiv").
21    pub pattern: String,
22    /// The associated `VanityFlags` configuration.
23    pub flags: VanityFlags,
24}
25
26/// Parses a single line from an input file into a [FileLineItem].
27///
28/// # Arguments
29/// - `line`: A string slice representing a line from the input file.
30///
31/// # Returns
32/// - `Some(FileLineItem)` if the line is valid and parsable.
33/// - `None` if the line is empty or starts with a comment (`#`).
34fn parse_line(line: &str) -> Option<FileLineItem> {
35    if line.starts_with('#') {
36        return None;
37    }
38
39    let mut tokens = line.split_whitespace();
40
41    // The first token is the pattern
42    let pattern = tokens.next()?.to_string();
43    // The rest are "flags"
44    let flags_vec: Vec<&str> = tokens.collect();
45
46    // If no flags, then everything is default
47    if flags_vec.is_empty() {
48        return Some(FileLineItem {
49            pattern,
50            flags: VanityFlags {
51                force_flags: false,
52                is_case_sensitive: false,
53                disable_fast_mode: false,
54                output_file_name: None,
55                vanity_mode: None,
56                chain: None,
57                threads: 16,
58            },
59        });
60    }
61
62    let is_case_sensitive = flags_vec.contains(&"-c") || flags_vec.contains(&"--case-sensitive");
63    let disable_fast_mode = flags_vec.contains(&"-d") || flags_vec.contains(&"--disable-fast");
64
65    // chain
66    let chain = if flags_vec.contains(&"--eth") {
67        Some(Chain::Ethereum)
68    } else if flags_vec.contains(&"--sol") {
69        Some(Chain::Solana)
70    } else if flags_vec.contains(&"--btc") {
71        Some(Chain::Bitcoin)
72    } else {
73        None
74    };
75
76    // vanity mode
77    let vanity_mode = if flags_vec.contains(&"-r") || flags_vec.contains(&"--regex") {
78        Some(VanityMode::Regex)
79    } else if flags_vec.contains(&"-a") || flags_vec.contains(&"--anywhere") {
80        Some(VanityMode::Anywhere)
81    } else if flags_vec.contains(&"-s") || flags_vec.contains(&"--suffix") {
82        Some(VanityMode::Suffix)
83    } else if flags_vec.contains(&"-p") || flags_vec.contains(&"--prefix") {
84        Some(VanityMode::Prefix)
85    } else {
86        None
87    };
88
89    // output file name: look for `-o` or `--output-file` plus the next token
90    let mut output_file_name: Option<String> = None;
91    for (i, &flag) in flags_vec.iter().enumerate() {
92        if flag == "-o" || flag == "--output-file" {
93            if let Some(next_flag) = flags_vec.get(i + 1) {
94                output_file_name = Some(next_flag.to_string());
95            }
96        }
97    }
98
99    Some(FileLineItem {
100        pattern,
101        flags: VanityFlags {
102            force_flags: false,
103            is_case_sensitive,
104            disable_fast_mode,
105            output_file_name,
106            vanity_mode,
107            chain,
108            threads: 0,
109        },
110    })
111}
112
113/// Reads and parses an input file, converting each line into a [FileLineItem].
114///
115/// # Arguments
116/// - `path`: A string slice representing the file path.
117///
118/// # Returns
119/// - `Ok(Vec<FileLineItem>)`: A vector of parsed [FileLineItem] objects.
120/// - `Err(VanityError)`: An error if the file cannot be read or parsed.
121///
122/// # Errors
123/// - Returns `VanityError::FileError` if the file cannot be read.
124pub fn parse_input_file(path: &str) -> Result<Vec<FileLineItem>, VanityError> {
125    let contents = fs::read_to_string(path)?;
126    let mut items = Vec::new();
127
128    for line in contents.lines() {
129        let line = line.trim();
130        if line.is_empty() || line.starts_with('#') {
131            continue;
132        }
133
134        if let Some(item) = parse_line(line) {
135            items.push(item);
136        }
137    }
138    Ok(items)
139}
140
141/// Writes a string buffer to an output file. If the file does not exist, it will be created.
142///
143/// # Arguments
144/// - `output_path`: The path to the output file.
145/// - `buffer`: The content to write to the file.
146///
147/// # Returns
148/// - `Ok(())` on successful write.
149/// - `Err(VanityError)` if the operation fails.
150///
151/// # Errors
152/// - Returns `VanityError::FileError` if the operation fails,
153///   such as due to invalid input or a write failure.
154pub fn write_output_file(output_path: &Path, buffer: &str) -> Result<(), VanityError> {
155    // Attempt to open the file in append mode
156    let file_result = OpenOptions::new()
157        .append(true)
158        .create(true)
159        .open(output_path);
160    let mut file = match file_result {
161        Ok(file) => file,
162        Err(e) => {
163            return Err(VanityError::FileError(io::Error::other(format!(
164                "Failed to open or create file: {e}"
165            ))))
166        }
167    };
168
169    // Write the buffer to the file
170    if let Err(e) = file.write_all(buffer.as_bytes()) {
171        return Err(VanityError::FileError(io::Error::new(
172            io::ErrorKind::WriteZero,
173            format!("Failed to write to file: {e}"),
174        )));
175    }
176
177    Ok(())
178}
179
180#[cfg(test)]
181mod tests {
182    use super::{parse_input_file, parse_line};
183    use crate::VanityMode;
184
185    #[test]
186    fn test_parse_line_with_valid_flags() {
187        let line = "test -p -c";
188        let item = parse_line(line).expect("Failed to parse valid line");
189
190        assert_eq!(item.pattern, "test");
191        assert_eq!(item.flags.vanity_mode, Some(VanityMode::Prefix));
192        assert!(item.flags.is_case_sensitive);
193    }
194
195    #[test]
196    fn test_parse_line_with_invalid_flags() {
197        let line = "test -z";
198        let item = parse_line(line).expect("Failed to parse line with invalid flags");
199
200        assert_eq!(item.pattern, "test");
201        assert!(item.flags.vanity_mode.is_none());
202    }
203
204    #[test]
205    fn test_parse_line_with_no_flags() {
206        let line = "test";
207        let item = parse_line(line).expect("Failed to parse line without flags");
208
209        assert_eq!(item.pattern, "test");
210        assert!(item.flags.vanity_mode.is_none());
211        assert!(!item.flags.is_case_sensitive);
212        assert!(!item.flags.disable_fast_mode);
213    }
214
215    #[test]
216    fn test_parse_line_with_output_file_flag() {
217        let line = "test -p -o output.txt";
218        let item = parse_line(line).expect("Failed to parse line with output file flag");
219
220        assert_eq!(item.pattern, "test");
221        assert_eq!(item.flags.vanity_mode, Some(VanityMode::Prefix));
222        assert_eq!(item.flags.output_file_name, Some("output.txt".to_string()));
223    }
224
225    #[test]
226    fn test_parse_empty_line() {
227        let line = "";
228        let item = parse_line(line);
229
230        assert!(item.is_none(), "Empty line should not be parsed");
231    }
232
233    #[test]
234    fn test_parse_comment_line() {
235        let line = "# This is a comment";
236        let item = parse_line(line);
237
238        assert!(item.is_none(), "Comment line should not be parsed");
239    }
240
241    #[test]
242    fn test_parse_input_file_with_valid_lines() {
243        // Mock file content
244        let file_content = "test -p\nexample -s\nanywhere -a";
245        let file_path = "test_valid_input.txt";
246        std::fs::write(file_path, file_content).expect("Failed to create mock input file");
247
248        // Parse the file
249        let result = parse_input_file(file_path);
250        assert!(result.is_ok(), "Failed to parse valid input file");
251
252        let items = result.unwrap();
253        assert_eq!(items.len(), 3);
254
255        assert_eq!(items[0].pattern, "test");
256        assert_eq!(items[0].flags.vanity_mode, Some(VanityMode::Prefix));
257
258        assert_eq!(items[1].pattern, "example");
259        assert_eq!(items[1].flags.vanity_mode, Some(VanityMode::Suffix));
260
261        assert_eq!(items[2].pattern, "anywhere");
262        assert_eq!(items[2].flags.vanity_mode, Some(VanityMode::Anywhere));
263
264        // Clean up
265        std::fs::remove_file(file_path).expect("Failed to delete mock input file");
266    }
267
268    #[test]
269    fn test_parse_input_file_with_invalid_lines() {
270        // Mock file content
271        let file_content = "test -z\nexample --invalid";
272        let file_path = "test_invalid_input.txt";
273        std::fs::write(file_path, file_content).expect("Failed to create mock input file");
274
275        // Parse the file
276        let result = parse_input_file(file_path);
277        assert!(result.is_ok(), "Failed to parse file with invalid lines");
278
279        let items = result.unwrap();
280        assert_eq!(items.len(), 2);
281
282        assert_eq!(items[0].pattern, "test");
283        assert!(items[0].flags.vanity_mode.is_none());
284
285        assert_eq!(items[1].pattern, "example");
286        assert!(items[1].flags.vanity_mode.is_none());
287
288        // Clean up
289        std::fs::remove_file(file_path).expect("Failed to delete mock input file");
290    }
291
292    #[test]
293    fn test_parse_input_file_with_empty_lines() {
294        // Mock file content
295        let file_content = "\n\n";
296        let file_path = "test_empty_lines.txt";
297        std::fs::write(file_path, file_content).expect("Failed to create mock input file");
298
299        // Parse the file
300        let result = parse_input_file(file_path);
301        assert!(result.is_ok(), "Failed to parse file with empty lines");
302
303        let items = result.unwrap();
304        assert!(
305            items.is_empty(),
306            "Parsed items should be empty for a file with only empty lines"
307        );
308
309        // Clean up
310        std::fs::remove_file(file_path).expect("Failed to delete mock input file");
311    }
312
313    #[test]
314    fn test_parse_input_file_with_invalid_path() {
315        // Non-existent file path
316        let file_path = "non_existent_file.txt";
317
318        // Parse the file
319        let result = parse_input_file(file_path);
320        assert!(
321            result.is_err(),
322            "Parsing a non-existent file should return an error"
323        );
324
325        if let Err(err) = result {
326            assert!(
327                err.to_string().contains("No such file"),
328                "Unexpected error message: {}",
329                err
330            );
331        }
332    }
333
334    #[test]
335    fn test_parse_input_file_with_missing_flags() {
336        // Mock file content
337        let file_content = "test\nexample\nmissing_flags";
338        let file_path = "test_missing_flags.txt";
339        std::fs::write(file_path, file_content).expect("Failed to create mock input file");
340
341        // Parse the file
342        let result = parse_input_file(file_path);
343        assert!(result.is_ok(), "Failed to parse file with missing flags");
344
345        let items = result.unwrap();
346        assert_eq!(items.len(), 3);
347
348        assert_eq!(items[0].pattern, "test");
349        assert!(items[0].flags.vanity_mode.is_none());
350
351        assert_eq!(items[1].pattern, "example");
352        assert!(items[1].flags.vanity_mode.is_none());
353
354        assert_eq!(items[2].pattern, "missing_flags");
355        assert!(items[2].flags.vanity_mode.is_none());
356
357        // Clean up
358        std::fs::remove_file(file_path).expect("Failed to delete mock input file");
359    }
360}