rustywallet_batch/
mmap.rs

1//! Memory-mapped file output for batch key generation.
2//!
3//! This module provides efficient file output using memory-mapped files,
4//! allowing direct writes without buffering overhead.
5
6use crate::error::BatchError;
7use memmap2::MmapMut;
8use rustywallet_keys::private_key::PrivateKey;
9use std::fs::OpenOptions;
10use std::path::Path;
11
12/// Output format for memory-mapped file.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OutputFormat {
15    /// Raw 32-byte private keys (most compact)
16    Raw,
17    /// Hex-encoded keys, one per line (64 chars + newline)
18    Hex,
19    /// WIF-encoded keys, one per line
20    Wif,
21}
22
23impl OutputFormat {
24    /// Get the size in bytes per key for this format.
25    pub fn bytes_per_key(&self) -> usize {
26        match self {
27            OutputFormat::Raw => 32,
28            OutputFormat::Hex => 65, // 64 hex chars + newline
29            OutputFormat::Wif => 53, // ~52 chars + newline (compressed WIF)
30        }
31    }
32}
33
34/// Memory-mapped file writer for batch key output.
35///
36/// This writer uses memory-mapped files for efficient output,
37/// avoiding buffering overhead and enabling direct disk writes.
38///
39/// # Example
40///
41/// ```no_run
42/// use rustywallet_batch::mmap::{MmapWriter, OutputFormat};
43/// use rustywallet_keys::private_key::PrivateKey;
44///
45/// let mut writer = MmapWriter::create("keys.txt", 1000, OutputFormat::Hex).unwrap();
46///
47/// for _ in 0..1000 {
48///     let key = PrivateKey::random();
49///     writer.write_key(&key).unwrap();
50/// }
51///
52/// writer.finish().unwrap();
53/// ```
54pub struct MmapWriter {
55    /// Memory-mapped region
56    mmap: MmapMut,
57    /// Current write position
58    position: usize,
59    /// Output format
60    format: OutputFormat,
61    /// Total capacity in keys
62    capacity: usize,
63    /// Number of keys written
64    written: usize,
65    /// File path for reference
66    path: String,
67}
68
69impl MmapWriter {
70    /// Create a new memory-mapped file writer.
71    ///
72    /// # Arguments
73    ///
74    /// * `path` - Path to the output file
75    /// * `capacity` - Maximum number of keys to write
76    /// * `format` - Output format for keys
77    pub fn create<P: AsRef<Path>>(
78        path: P,
79        capacity: usize,
80        format: OutputFormat,
81    ) -> Result<Self, BatchError> {
82        let path_str = path.as_ref().to_string_lossy().to_string();
83        let file_size = capacity * format.bytes_per_key();
84
85        // Create and size the file
86        let file = OpenOptions::new()
87            .read(true)
88            .write(true)
89            .create(true)
90            .truncate(true)
91            .open(&path)
92            .map_err(|e| BatchError::io_error(format!("Failed to create file: {}", e)))?;
93
94        file.set_len(file_size as u64)
95            .map_err(|e| BatchError::io_error(format!("Failed to set file size: {}", e)))?;
96
97        // Memory-map the file
98        let mmap = unsafe {
99            MmapMut::map_mut(&file)
100                .map_err(|e| BatchError::io_error(format!("Failed to mmap file: {}", e)))?
101        };
102
103        Ok(Self {
104            mmap,
105            position: 0,
106            format,
107            capacity,
108            written: 0,
109            path: path_str,
110        })
111    }
112
113    /// Write a single key to the file.
114    pub fn write_key(&mut self, key: &PrivateKey) -> Result<(), BatchError> {
115        if self.written >= self.capacity {
116            return Err(BatchError::io_error("Writer capacity exceeded"));
117        }
118
119        let bytes = match self.format {
120            OutputFormat::Raw => {
121                let b = key.to_bytes();
122                self.mmap[self.position..self.position + 32].copy_from_slice(&b);
123                self.position += 32;
124                32
125            }
126            OutputFormat::Hex => {
127                let hex = key.to_hex();
128                let line = format!("{}\n", hex);
129                let bytes = line.as_bytes();
130                self.mmap[self.position..self.position + bytes.len()].copy_from_slice(bytes);
131                self.position += bytes.len();
132                bytes.len()
133            }
134            OutputFormat::Wif => {
135                let wif = key.to_wif(rustywallet_keys::network::Network::Mainnet);
136                let line = format!("{}\n", wif);
137                let bytes = line.as_bytes();
138                self.mmap[self.position..self.position + bytes.len()].copy_from_slice(bytes);
139                self.position += bytes.len();
140                bytes.len()
141            }
142        };
143
144        self.written += 1;
145        let _ = bytes; // suppress unused warning
146        Ok(())
147    }
148
149    /// Write multiple keys to the file.
150    pub fn write_keys(&mut self, keys: &[PrivateKey]) -> Result<usize, BatchError> {
151        let mut count = 0;
152        for key in keys {
153            if self.written >= self.capacity {
154                break;
155            }
156            self.write_key(key)?;
157            count += 1;
158        }
159        Ok(count)
160    }
161
162    /// Get the number of keys written.
163    pub fn written(&self) -> usize {
164        self.written
165    }
166
167    /// Get the remaining capacity.
168    pub fn remaining(&self) -> usize {
169        self.capacity - self.written
170    }
171
172    /// Get the file path.
173    pub fn path(&self) -> &str {
174        &self.path
175    }
176
177    /// Finish writing and truncate file to actual size.
178    pub fn finish(self) -> Result<usize, BatchError> {
179        // Flush the mmap
180        self.mmap
181            .flush()
182            .map_err(|e| BatchError::io_error(format!("Failed to flush mmap: {}", e)))?;
183
184        // Truncate file to actual written size
185        let file = OpenOptions::new()
186            .write(true)
187            .open(&self.path)
188            .map_err(|e| BatchError::io_error(format!("Failed to open file for truncate: {}", e)))?;
189
190        file.set_len(self.position as u64)
191            .map_err(|e| BatchError::io_error(format!("Failed to truncate file: {}", e)))?;
192
193        Ok(self.written)
194    }
195}
196
197/// Batch file generator using memory-mapped output.
198///
199/// Combines batch generation with memory-mapped file output
200/// for maximum throughput.
201pub struct MmapBatchGenerator {
202    /// Output path
203    path: String,
204    /// Number of keys to generate
205    count: usize,
206    /// Output format
207    format: OutputFormat,
208    /// Chunk size for parallel generation
209    chunk_size: usize,
210    /// Use parallel generation
211    parallel: bool,
212}
213
214impl MmapBatchGenerator {
215    /// Create a new mmap batch generator.
216    pub fn new<P: AsRef<Path>>(path: P, count: usize) -> Self {
217        Self {
218            path: path.as_ref().to_string_lossy().to_string(),
219            count,
220            format: OutputFormat::Hex,
221            chunk_size: 10_000,
222            parallel: true,
223        }
224    }
225
226    /// Set the output format.
227    pub fn format(mut self, format: OutputFormat) -> Self {
228        self.format = format;
229        self
230    }
231
232    /// Set the chunk size for parallel generation.
233    pub fn chunk_size(mut self, size: usize) -> Self {
234        self.chunk_size = size;
235        self
236    }
237
238    /// Enable or disable parallel generation.
239    pub fn parallel(mut self, enabled: bool) -> Self {
240        self.parallel = enabled;
241        self
242    }
243
244    /// Generate keys and write directly to file.
245    pub fn generate(self) -> Result<usize, BatchError> {
246        use crate::fast_gen::FastKeyGenerator;
247
248        let mut writer = MmapWriter::create(&self.path, self.count, self.format)?;
249
250        // Generate in chunks to balance memory and parallelism
251        let mut remaining = self.count;
252        while remaining > 0 {
253            let chunk_count = remaining.min(self.chunk_size);
254            let keys = FastKeyGenerator::new(chunk_count)
255                .parallel(self.parallel)
256                .generate();
257
258            writer.write_keys(&keys)?;
259            remaining -= chunk_count;
260        }
261
262        writer.finish()
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use std::fs;
270    use tempfile::tempdir;
271
272    #[test]
273    fn test_mmap_writer_hex() {
274        let dir = tempdir().unwrap();
275        let path = dir.path().join("keys.txt");
276
277        let mut writer = MmapWriter::create(&path, 100, OutputFormat::Hex).unwrap();
278
279        for _ in 0..100 {
280            let key = PrivateKey::random();
281            writer.write_key(&key).unwrap();
282        }
283
284        let written = writer.finish().unwrap();
285        assert_eq!(written, 100);
286
287        // Verify file content
288        let content = fs::read_to_string(&path).unwrap();
289        let lines: Vec<_> = content.lines().collect();
290        assert_eq!(lines.len(), 100);
291        assert!(lines.iter().all(|l| l.len() == 64));
292    }
293
294    #[test]
295    fn test_mmap_writer_raw() {
296        let dir = tempdir().unwrap();
297        let path = dir.path().join("keys.bin");
298
299        let mut writer = MmapWriter::create(&path, 50, OutputFormat::Raw).unwrap();
300
301        for _ in 0..50 {
302            let key = PrivateKey::random();
303            writer.write_key(&key).unwrap();
304        }
305
306        let written = writer.finish().unwrap();
307        assert_eq!(written, 50);
308
309        // Verify file size
310        let metadata = fs::metadata(&path).unwrap();
311        assert_eq!(metadata.len(), 50 * 32);
312    }
313
314    #[test]
315    fn test_mmap_batch_generator() {
316        let dir = tempdir().unwrap();
317        let path = dir.path().join("batch.txt");
318
319        let written = MmapBatchGenerator::new(&path, 1000)
320            .format(OutputFormat::Hex)
321            .chunk_size(100)
322            .parallel(true)
323            .generate()
324            .unwrap();
325
326        assert_eq!(written, 1000);
327
328        let content = fs::read_to_string(&path).unwrap();
329        let lines: Vec<_> = content.lines().collect();
330        assert_eq!(lines.len(), 1000);
331    }
332}