use std::fs;
use std::path::Path;
use crate::storage::commitlog::descriptor::CommitLogDescriptor;
use crate::storage::commitlog::frame::{FrameStep, FrameWalker};
use crate::storage::commitlog::mutation::{decode_mutation, Mutation};
use crate::storage::commitlog::schema::SchemaSet;
use crate::{Error, Result};
pub const MAX_SEGMENT_BYTES: u64 = 128 * 1024 * 1024;
pub struct CommitLogReader {
bytes: Vec<u8>,
descriptor: CommitLogDescriptor,
schemas: SchemaSet,
}
impl CommitLogReader {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::open_with_schemas(path, SchemaSet::new())
}
pub fn open_with_schemas<P: AsRef<Path>>(path: P, schemas: SchemaSet) -> Result<Self> {
let path = path.as_ref();
let meta = fs::metadata(path)?;
if meta.len() > MAX_SEGMENT_BYTES {
return Err(Error::UnsupportedFormat(format!(
"CommitLog segment {} is {} bytes, exceeding the {}-byte reader limit",
path.display(),
meta.len(),
MAX_SEGMENT_BYTES
)));
}
let bytes = fs::read(path)?;
let descriptor = CommitLogDescriptor::parse(&bytes)?;
if let Some(class) = &descriptor.compression_class {
return Err(Error::UnsupportedFormat(format!(
"CommitLog segment uses commitlog compression ({class}); \
compressed segments are not supported (issue #2389 v1 is \
uncompressed-only) — refusing to decode compressed bytes as plain"
)));
}
if descriptor.encrypted {
return Err(Error::UnsupportedFormat(
"CommitLog segment declares an encryption context; \
encrypted segments are not supported"
.to_string(),
));
}
Ok(Self {
bytes,
descriptor,
schemas,
})
}
pub fn descriptor(&self) -> &CommitLogDescriptor {
&self.descriptor
}
pub fn mutations(&self) -> MutationIter<'_> {
MutationIter {
walker: FrameWalker::new(&self.bytes, self.descriptor.id, self.descriptor.header_len),
schemas: &self.schemas,
truncated: false,
done: false,
}
}
}
pub struct MutationIter<'a> {
walker: FrameWalker<'a>,
schemas: &'a SchemaSet,
truncated: bool,
done: bool,
}
impl MutationIter<'_> {
pub fn truncated(&self) -> bool {
self.truncated
}
}
impl Iterator for MutationIter<'_> {
type Item = Result<Mutation>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
match self.walker.next_frame() {
Ok(FrameStep::Record(body)) => Some(decode_mutation(body, self.schemas)),
Ok(FrameStep::End) => {
self.done = true;
None
}
Ok(FrameStep::Truncated) => {
self.truncated = true;
self.done = true;
None
}
Err(e) => {
self.done = true;
Some(Err(e))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::commitlog::descriptor::tests::build_header;
fn write_segment(params: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let bytes = build_header(7, 1_689_012_345_678_i64, params);
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("CommitLog-7-1.log");
fs::write(&path, &bytes).expect("write segment");
(dir, path)
}
#[test]
fn open_fails_closed_on_cassandra_shaped_compression() {
let (_dir, path) = write_segment(r#"{"compressionClass":"LZ4Compressor"}"#);
assert!(matches!(
CommitLogReader::open(&path),
Err(Error::UnsupportedFormat(_))
));
}
#[test]
fn open_fails_closed_on_cassandra_shaped_encryption() {
let (_dir, path) =
write_segment(r#"{"encCipher":"AES/CBC/PKCS5Padding","encKeyAlias":"testing:1"}"#);
assert!(matches!(
CommitLogReader::open(&path),
Err(Error::UnsupportedFormat(_))
));
}
#[test]
fn open_succeeds_on_uncompressed_unencrypted_header() {
let (_dir, path) = write_segment("{}");
let reader = CommitLogReader::open(&path).expect("plain segment opens");
assert!(!reader.descriptor().is_unsupported_payload());
}
}