use super::LogSeq;
use super::header::{WAL_HEADER_LEN, WalHeaderError, decode_header};
use super::record::{RecordError, WalRecord, decode_record};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CorruptionLocation {
Tail,
Middle,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CorruptionReason {
Record {
location: CorruptionLocation,
error: RecordError,
},
FirstSequenceMismatch {
expected: LogSeq,
actual: LogSeq,
location: CorruptionLocation,
},
SequenceGap {
expected: LogSeq,
actual: LogSeq,
location: CorruptionLocation,
},
SequenceRegression {
expected: LogSeq,
actual: LogSeq,
location: CorruptionLocation,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReplayTerminator {
InvalidHeader(WalHeaderError),
CleanEnd,
CorruptAt {
offset: usize,
reason: CorruptionReason,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReplayResult<'a> {
pub records: Vec<WalRecord<'a>>,
pub terminator: ReplayTerminator,
}
#[must_use]
pub fn replay(bytes: &[u8]) -> ReplayResult<'_> {
let mut records = Vec::new();
let header = match decode_header(bytes) {
Ok(header) => header,
Err(error) => {
return ReplayResult {
records,
terminator: ReplayTerminator::InvalidHeader(error),
};
}
};
let mut offset = WAL_HEADER_LEN;
let mut expected = header.first_seq;
while let Some(remaining) = bytes
.get(offset..)
.filter(|remaining| !remaining.is_empty())
{
let decoded = match decode_record(remaining) {
Ok(decoded) => decoded,
Err(error) => {
let location = error
.framed_length()
.and_then(|length| offset.checked_add(length))
.map_or(CorruptionLocation::Tail, |next| {
successor_location(bytes, next)
});
return ReplayResult {
records,
terminator: ReplayTerminator::CorruptAt {
offset,
reason: CorruptionReason::Record { location, error },
},
};
}
};
let next = offset.saturating_add(decoded.encoded_len);
if decoded.record.seq != expected {
let location = successor_location(bytes, next);
let reason = if records.is_empty() {
CorruptionReason::FirstSequenceMismatch {
expected,
actual: decoded.record.seq,
location,
}
} else if decoded.record.seq > expected {
CorruptionReason::SequenceGap {
expected,
actual: decoded.record.seq,
location,
}
} else {
CorruptionReason::SequenceRegression {
expected,
actual: decoded.record.seq,
location,
}
};
return ReplayResult {
records,
terminator: ReplayTerminator::CorruptAt { offset, reason },
};
}
records.push(decoded.record);
offset = next;
expected = LogSeq::new(expected.get().saturating_add(1));
}
ReplayResult {
records,
terminator: ReplayTerminator::CleanEnd,
}
}
fn successor_location(bytes: &[u8], offset: usize) -> CorruptionLocation {
bytes
.get(offset..)
.filter(|successor| !successor.is_empty())
.and_then(|successor| decode_record(successor).ok())
.map_or(CorruptionLocation::Tail, |_| CorruptionLocation::Middle)
}