rustywallet_batch/
mmap.rs1use crate::error::BatchError;
7use memmap2::MmapMut;
8use rustywallet_keys::private_key::PrivateKey;
9use std::fs::OpenOptions;
10use std::path::Path;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OutputFormat {
15 Raw,
17 Hex,
19 Wif,
21}
22
23impl OutputFormat {
24 pub fn bytes_per_key(&self) -> usize {
26 match self {
27 OutputFormat::Raw => 32,
28 OutputFormat::Hex => 65, OutputFormat::Wif => 53, }
31 }
32}
33
34pub struct MmapWriter {
55 mmap: MmapMut,
57 position: usize,
59 format: OutputFormat,
61 capacity: usize,
63 written: usize,
65 path: String,
67}
68
69impl MmapWriter {
70 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 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 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 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; Ok(())
147 }
148
149 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 pub fn written(&self) -> usize {
164 self.written
165 }
166
167 pub fn remaining(&self) -> usize {
169 self.capacity - self.written
170 }
171
172 pub fn path(&self) -> &str {
174 &self.path
175 }
176
177 pub fn finish(self) -> Result<usize, BatchError> {
179 self.mmap
181 .flush()
182 .map_err(|e| BatchError::io_error(format!("Failed to flush mmap: {}", e)))?;
183
184 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
197pub struct MmapBatchGenerator {
202 path: String,
204 count: usize,
206 format: OutputFormat,
208 chunk_size: usize,
210 parallel: bool,
212}
213
214impl MmapBatchGenerator {
215 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 pub fn format(mut self, format: OutputFormat) -> Self {
228 self.format = format;
229 self
230 }
231
232 pub fn chunk_size(mut self, size: usize) -> Self {
234 self.chunk_size = size;
235 self
236 }
237
238 pub fn parallel(mut self, enabled: bool) -> Self {
240 self.parallel = enabled;
241 self
242 }
243
244 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 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 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 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}