use crate::error::{Error, Result};
use crate::types::LogIndex;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use super::IndexKey;
pub struct WalWriter {
writer: BufWriter<File>,
}
impl WalWriter {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path.as_ref())
.map_err(|e| Error::Internal(format!("failed to open WAL: {}", e)))?;
Ok(Self {
writer: BufWriter::new(file),
})
}
pub fn append(&mut self, idx: LogIndex, keys: &[IndexKey]) -> Result<()> {
if keys.is_empty() {
writeln!(self.writer, "{}", idx.value())
.map_err(|e| Error::Internal(format!("failed to write to WAL: {}", e)))?;
} else {
write!(self.writer, "{}", idx.value())
.map_err(|e| Error::Internal(format!("failed to write to WAL: {}", e)))?;
for key in keys {
write!(self.writer, " {}", hex::encode(key))
.map_err(|e| Error::Internal(format!("failed to write to WAL: {}", e)))?;
}
writeln!(self.writer)
.map_err(|e| Error::Internal(format!("failed to write to WAL: {}", e)))?;
}
Ok(())
}
pub fn flush(&mut self) -> Result<()> {
self.writer
.flush()
.map_err(|e| Error::Internal(format!("failed to flush WAL: {}", e)))?;
Ok(())
}
}
pub struct WalReader {
reader: BufReader<File>,
line_buf: String,
}
impl WalReader {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let file = File::open(path.as_ref())
.map_err(|e| Error::Internal(format!("failed to open WAL for reading: {}", e)))?;
Ok(Self {
reader: BufReader::new(file),
line_buf: String::new(),
})
}
pub fn next_entry(&mut self) -> Result<Option<(LogIndex, Vec<IndexKey>)>> {
self.line_buf.clear();
let bytes_read = self
.reader
.read_line(&mut self.line_buf)
.map_err(|e| Error::Internal(format!("failed to read from WAL: {}", e)))?;
if bytes_read == 0 {
return Ok(None);
}
parse_wal_line(&self.line_buf).map(Some)
}
}
fn parse_wal_line(line: &str) -> Result<(LogIndex, Vec<IndexKey>)> {
let line = line.trim();
if line.is_empty() {
return Err(Error::Internal("empty WAL line".into()));
}
let mut parts = line.split_whitespace();
let idx_str = parts
.next()
.ok_or_else(|| Error::Internal("missing index in WAL line".into()))?;
let idx: u64 = idx_str
.parse()
.map_err(|e| Error::Internal(format!("invalid index in WAL: {}", e)))?;
let mut keys = Vec::new();
for hex_key in parts {
let key_bytes = hex::decode(hex_key)
.map_err(|e| Error::Internal(format!("invalid hex key in WAL: {}", e)))?;
if key_bytes.len() != 32 {
return Err(Error::Internal(format!(
"invalid key length in WAL: expected 32, got {}",
key_bytes.len()
)));
}
let mut key = [0u8; 32];
key.copy_from_slice(&key_bytes);
keys.push(key);
}
Ok((LogIndex::new(idx), keys))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_wal_write_read() {
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
{
let mut writer = WalWriter::open(path).unwrap();
let key1 = [1u8; 32];
let key2 = [2u8; 32];
writer.append(LogIndex::new(0), &[key1]).unwrap();
writer.append(LogIndex::new(1), &[key1, key2]).unwrap();
writer.append(LogIndex::new(2), &[]).unwrap(); writer.flush().unwrap();
}
{
let mut reader = WalReader::open(path).unwrap();
let (idx, keys) = reader.next_entry().unwrap().unwrap();
assert_eq!(idx.value(), 0);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], [1u8; 32]);
let (idx, keys) = reader.next_entry().unwrap().unwrap();
assert_eq!(idx.value(), 1);
assert_eq!(keys.len(), 2);
assert_eq!(keys[0], [1u8; 32]);
assert_eq!(keys[1], [2u8; 32]);
let (idx, keys) = reader.next_entry().unwrap().unwrap();
assert_eq!(idx.value(), 2);
assert_eq!(keys.len(), 0);
assert!(reader.next_entry().unwrap().is_none());
}
}
#[test]
fn test_parse_wal_line() {
let line = "42 0101010101010101010101010101010101010101010101010101010101010101 0202020202020202020202020202020202020202020202020202020202020202";
let (idx, keys) = parse_wal_line(line).unwrap();
assert_eq!(idx.value(), 42);
assert_eq!(keys.len(), 2);
let line = "123";
let (idx, keys) = parse_wal_line(line).unwrap();
assert_eq!(idx.value(), 123);
assert_eq!(keys.len(), 0);
}
}