use strum::FromRepr;
use super::{
AofChunkHeader, AofShardedHeader, AofShardedLogTransactionHeader, AofSingleLogTransactionHeader,
write_at,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr)]
#[repr(u8)]
pub enum AofHeaderType {
BasicHeader = 0,
ShardedHeader = 1,
SingleLogTransactionHeader = 2,
ShardedLogTransactionHeader = 3,
BasicChunkHeader = 4,
ShardedChunkHeader = 5,
}
impl AofHeaderType {
pub const ALL: [AofHeaderType; 6] = [
Self::BasicHeader,
Self::ShardedHeader,
Self::SingleLogTransactionHeader,
Self::ShardedLogTransactionHeader,
Self::BasicChunkHeader,
Self::ShardedChunkHeader,
];
#[inline]
pub const fn total_size(self) -> usize {
match self {
Self::BasicHeader => AofHeader::TOTAL_SIZE,
Self::ShardedHeader => AofShardedHeader::TOTAL_SIZE,
Self::SingleLogTransactionHeader => AofSingleLogTransactionHeader::TOTAL_SIZE,
Self::ShardedLogTransactionHeader => AofShardedLogTransactionHeader::TOTAL_SIZE,
Self::BasicChunkHeader => AofHeader::TOTAL_SIZE + AofChunkHeader::TOTAL_SIZE,
Self::ShardedChunkHeader => AofShardedHeader::TOTAL_SIZE + AofChunkHeader::TOTAL_SIZE,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AofHeader {
pub aof_header_version: u8,
pub flags: u8,
pub op_type: u8,
pub procedure_id: u8,
pub database_id: u8,
pub store_version: i64,
pub session_id: i32,
}
impl AofHeader {
pub const TOTAL_SIZE: usize = 16;
pub const AOF_HEADER_VERSION: u8 = 5;
pub const MAX_SUPPORTED_AOF_HEADER_VERSION: u8 = Self::AOF_HEADER_VERSION;
pub const AOF_HEADER_TYPE_MASK: u8 = 0b0111;
pub const CHUNKED_RECORD_FLAG: u8 = 0b0100;
pub const UNSAFE_TRUNCATE_LOG_FLAG: u8 = 0b1000;
#[inline]
pub const fn new() -> Self {
Self {
aof_header_version: Self::AOF_HEADER_VERSION,
flags: 0,
op_type: 0,
procedure_id: 0,
database_id: 0,
store_version: 0,
session_id: 0,
}
}
#[inline]
pub const fn unsafe_truncate_log(&self) -> bool {
(self.flags & Self::UNSAFE_TRUNCATE_LOG_FLAG) != 0
}
#[inline]
pub fn set_unsafe_truncate_log(&mut self, value: bool) {
if value {
self.flags |= Self::UNSAFE_TRUNCATE_LOG_FLAG;
} else {
self.flags &= !Self::UNSAFE_TRUNCATE_LOG_FLAG;
}
}
#[inline]
pub const fn header_type(&self) -> Option<AofHeaderType> {
match self.flags & Self::AOF_HEADER_TYPE_MASK {
0 => Some(AofHeaderType::BasicHeader),
1 => Some(AofHeaderType::ShardedHeader),
2 => Some(AofHeaderType::SingleLogTransactionHeader),
3 => Some(AofHeaderType::ShardedLogTransactionHeader),
4 => Some(AofHeaderType::BasicChunkHeader),
5 => Some(AofHeaderType::ShardedChunkHeader),
_ => None,
}
}
#[inline]
pub fn set_header_type(&mut self, value: AofHeaderType) {
debug_assert!((value as u8) <= Self::AOF_HEADER_TYPE_MASK);
self.flags = (self.flags & !Self::AOF_HEADER_TYPE_MASK) | value as u8;
}
#[inline]
pub const fn is_chunked(&self) -> bool {
(self.flags & Self::CHUNKED_RECORD_FLAG) != 0
}
#[inline]
pub const fn parse(entry: &[u8]) -> Option<Self> {
let Some(chunk) = entry.first_chunk::<{ Self::TOTAL_SIZE }>() else {
return None;
};
Some(Self {
aof_header_version: chunk[0],
flags: chunk[1],
op_type: chunk[2],
procedure_id: chunk[3],
database_id: chunk[3],
store_version: i64::from_le_bytes([
chunk[4], chunk[5], chunk[6], chunk[7], chunk[8], chunk[9], chunk[10], chunk[11],
]),
session_id: i32::from_le_bytes([chunk[12], chunk[13], chunk[14], chunk[15]]),
})
}
#[inline]
pub const fn to_bytes(&self) -> [u8; Self::TOTAL_SIZE] {
let prefix = [
self.aof_header_version,
self.flags,
self.op_type,
if self.procedure_id != 0 {
self.procedure_id
} else {
self.database_id
},
];
let mut out = [0u8; Self::TOTAL_SIZE];
write_at(&mut out, 0, prefix);
write_at(&mut out, 4, self.store_version.to_le_bytes());
write_at(&mut out, 12, self.session_id.to_le_bytes());
out
}
#[inline]
pub const fn skip_header(entry: &[u8]) -> Option<usize> {
let Some(header) = Self::parse(entry) else {
return None;
};
match header.header_type() {
Some(t) => Some(t.total_size()),
None => None,
}
}
#[inline]
pub fn sequence_number_of(entry: &[u8], fallback: i64) -> Option<i64> {
let header = Self::parse(entry)?;
match header.header_type()? {
AofHeaderType::ShardedHeader
| AofHeaderType::ShardedChunkHeader
| AofHeaderType::ShardedLogTransactionHeader => {
AofShardedHeader::parse(entry).map(|sh| sh.sequence_number)
}
_ => Some(fallback),
}
}
#[inline]
pub const fn get_chunked_header_ref(entry: &[u8]) -> Option<(usize, AofChunkHeader)> {
let Some(header) = Self::parse(entry) else {
return None;
};
let Some(ht) = header.header_type() else {
return None;
};
let offset = match ht {
AofHeaderType::BasicChunkHeader => Self::TOTAL_SIZE,
AofHeaderType::ShardedChunkHeader => AofShardedHeader::TOTAL_SIZE,
_ => return None,
};
if entry.len() < offset + AofChunkHeader::TOTAL_SIZE {
return None;
}
let chunk_slice = entry.split_at(offset).1;
let Some(chunk) = AofChunkHeader::parse(chunk_slice) else {
return None;
};
Some((offset, chunk))
}
}
impl Default for AofHeader {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::{AofHeader, AofHeaderType};
#[test]
fn test_aof_header_roundtrip_and_flags() {
let mut h = AofHeader::new();
h.set_header_type(AofHeaderType::BasicHeader);
h.op_type = 0x01;
h.store_version = 42;
h.session_id = -7;
let bytes = h.to_bytes();
let parsed = AofHeader::parse(&bytes).unwrap();
assert_eq!(parsed, h);
assert_eq!(parsed.header_type(), Some(AofHeaderType::BasicHeader));
assert!(!parsed.is_chunked());
assert!(!parsed.unsafe_truncate_log());
h.set_unsafe_truncate_log(true);
h.set_header_type(AofHeaderType::ShardedChunkHeader);
assert!(h.unsafe_truncate_log());
assert!(h.is_chunked());
assert_eq!(h.header_type(), Some(AofHeaderType::ShardedChunkHeader));
let bytes2 = h.to_bytes();
let parsed2 = AofHeader::parse(&bytes2).unwrap();
assert_eq!(parsed2, h);
assert!(parsed2.unsafe_truncate_log());
assert!(parsed2.is_chunked());
}
}