use crate::error::BatchError;
use memmap2::MmapMut;
use rustywallet_keys::private_key::PrivateKey;
use std::fs::OpenOptions;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Raw,
Hex,
Wif,
}
impl OutputFormat {
pub fn bytes_per_key(&self) -> usize {
match self {
OutputFormat::Raw => 32,
OutputFormat::Hex => 65, OutputFormat::Wif => 53, }
}
}
pub struct MmapWriter {
mmap: MmapMut,
position: usize,
format: OutputFormat,
capacity: usize,
written: usize,
path: String,
}
impl MmapWriter {
pub fn create<P: AsRef<Path>>(
path: P,
capacity: usize,
format: OutputFormat,
) -> Result<Self, BatchError> {
let path_str = path.as_ref().to_string_lossy().to_string();
let file_size = capacity * format.bytes_per_key();
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)
.map_err(|e| BatchError::io_error(format!("Failed to create file: {}", e)))?;
file.set_len(file_size as u64)
.map_err(|e| BatchError::io_error(format!("Failed to set file size: {}", e)))?;
let mmap = unsafe {
MmapMut::map_mut(&file)
.map_err(|e| BatchError::io_error(format!("Failed to mmap file: {}", e)))?
};
Ok(Self {
mmap,
position: 0,
format,
capacity,
written: 0,
path: path_str,
})
}
pub fn write_key(&mut self, key: &PrivateKey) -> Result<(), BatchError> {
if self.written >= self.capacity {
return Err(BatchError::io_error("Writer capacity exceeded"));
}
let bytes = match self.format {
OutputFormat::Raw => {
let b = key.to_bytes();
self.mmap[self.position..self.position + 32].copy_from_slice(&b);
self.position += 32;
32
}
OutputFormat::Hex => {
let hex = key.to_hex();
let line = format!("{}\n", hex);
let bytes = line.as_bytes();
self.mmap[self.position..self.position + bytes.len()].copy_from_slice(bytes);
self.position += bytes.len();
bytes.len()
}
OutputFormat::Wif => {
let wif = key.to_wif(rustywallet_keys::network::Network::Mainnet);
let line = format!("{}\n", wif);
let bytes = line.as_bytes();
self.mmap[self.position..self.position + bytes.len()].copy_from_slice(bytes);
self.position += bytes.len();
bytes.len()
}
};
self.written += 1;
let _ = bytes; Ok(())
}
pub fn write_keys(&mut self, keys: &[PrivateKey]) -> Result<usize, BatchError> {
let mut count = 0;
for key in keys {
if self.written >= self.capacity {
break;
}
self.write_key(key)?;
count += 1;
}
Ok(count)
}
pub fn written(&self) -> usize {
self.written
}
pub fn remaining(&self) -> usize {
self.capacity - self.written
}
pub fn path(&self) -> &str {
&self.path
}
pub fn finish(self) -> Result<usize, BatchError> {
self.mmap
.flush()
.map_err(|e| BatchError::io_error(format!("Failed to flush mmap: {}", e)))?;
let file = OpenOptions::new()
.write(true)
.open(&self.path)
.map_err(|e| BatchError::io_error(format!("Failed to open file for truncate: {}", e)))?;
file.set_len(self.position as u64)
.map_err(|e| BatchError::io_error(format!("Failed to truncate file: {}", e)))?;
Ok(self.written)
}
}
pub struct MmapBatchGenerator {
path: String,
count: usize,
format: OutputFormat,
chunk_size: usize,
parallel: bool,
}
impl MmapBatchGenerator {
pub fn new<P: AsRef<Path>>(path: P, count: usize) -> Self {
Self {
path: path.as_ref().to_string_lossy().to_string(),
count,
format: OutputFormat::Hex,
chunk_size: 10_000,
parallel: true,
}
}
pub fn format(mut self, format: OutputFormat) -> Self {
self.format = format;
self
}
pub fn chunk_size(mut self, size: usize) -> Self {
self.chunk_size = size;
self
}
pub fn parallel(mut self, enabled: bool) -> Self {
self.parallel = enabled;
self
}
pub fn generate(self) -> Result<usize, BatchError> {
use crate::fast_gen::FastKeyGenerator;
let mut writer = MmapWriter::create(&self.path, self.count, self.format)?;
let mut remaining = self.count;
while remaining > 0 {
let chunk_count = remaining.min(self.chunk_size);
let keys = FastKeyGenerator::new(chunk_count)
.parallel(self.parallel)
.generate();
writer.write_keys(&keys)?;
remaining -= chunk_count;
}
writer.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_mmap_writer_hex() {
let dir = tempdir().unwrap();
let path = dir.path().join("keys.txt");
let mut writer = MmapWriter::create(&path, 100, OutputFormat::Hex).unwrap();
for _ in 0..100 {
let key = PrivateKey::random();
writer.write_key(&key).unwrap();
}
let written = writer.finish().unwrap();
assert_eq!(written, 100);
let content = fs::read_to_string(&path).unwrap();
let lines: Vec<_> = content.lines().collect();
assert_eq!(lines.len(), 100);
assert!(lines.iter().all(|l| l.len() == 64));
}
#[test]
fn test_mmap_writer_raw() {
let dir = tempdir().unwrap();
let path = dir.path().join("keys.bin");
let mut writer = MmapWriter::create(&path, 50, OutputFormat::Raw).unwrap();
for _ in 0..50 {
let key = PrivateKey::random();
writer.write_key(&key).unwrap();
}
let written = writer.finish().unwrap();
assert_eq!(written, 50);
let metadata = fs::metadata(&path).unwrap();
assert_eq!(metadata.len(), 50 * 32);
}
#[test]
fn test_mmap_batch_generator() {
let dir = tempdir().unwrap();
let path = dir.path().join("batch.txt");
let written = MmapBatchGenerator::new(&path, 1000)
.format(OutputFormat::Hex)
.chunk_size(100)
.parallel(true)
.generate()
.unwrap();
assert_eq!(written, 1000);
let content = fs::read_to_string(&path).unwrap();
let lines: Vec<_> = content.lines().collect();
assert_eq!(lines.len(), 1000);
}
}