use crate::delete_bitmap::DeleteBitmap;
use crate::error::ColumnarError;
use crate::format::{HEADER_SIZE, SegmentFooter, SegmentHeader};
use crate::predicate::ScanPredicate;
use super::block_decode::{
append_null_fill, decode_block, empty_decoded, result_valid_len, result_valid_slice_mut,
};
use super::types::DecodedColumn;
fn checked_slice(data: &[u8], start: usize, len: usize) -> Result<&[u8], ColumnarError> {
let end = start
.checked_add(len)
.ok_or_else(|| ColumnarError::Corruption {
segment_id: None,
reason: "column block range overflow".into(),
offset: None,
})?;
data.get(start..end).ok_or(ColumnarError::TruncatedSegment {
expected: end,
got: data.len(),
})
}
pub struct SegmentReader<'a> {
pub(super) data: &'a [u8],
pub(super) footer: SegmentFooter,
}
#[derive(Debug)]
pub struct OwnedSegmentReader {
plaintext: Vec<u8>,
footer: SegmentFooter,
}
impl OwnedSegmentReader {
fn from_plaintext(plaintext: Vec<u8>) -> Result<Self, ColumnarError> {
SegmentHeader::from_bytes(&plaintext)?;
let footer = SegmentFooter::from_segment_tail(&plaintext)?;
Ok(Self { plaintext, footer })
}
pub fn open_with_kek(
blob: &[u8],
kek: Option<&nodedb_wal::crypto::WalEncryptionKey>,
) -> Result<Self, ColumnarError> {
let is_encrypted = blob.len() >= 4 && blob[0..4] == crate::encrypt::SEGC_MAGIC;
if is_encrypted {
let key = kek.ok_or(ColumnarError::MissingKek)?;
let plaintext = crate::encrypt::decrypt_segment(key, blob)?;
Self::from_plaintext(plaintext)
} else if kek.is_some() {
Err(ColumnarError::KekRequired)
} else {
Self::from_plaintext(blob.to_vec())
}
}
pub fn reader(&self) -> SegmentReader<'_> {
SegmentReader {
data: &self.plaintext,
footer: self.footer.clone(),
}
}
pub fn footer(&self) -> &SegmentFooter {
&self.footer
}
pub fn row_count(&self) -> u64 {
self.footer.row_count
}
}
impl<'a> SegmentReader<'a> {
pub fn open(data: &'a [u8]) -> Result<Self, ColumnarError> {
if data.len() >= 4 && data[0..4] == crate::encrypt::SEGC_MAGIC {
return Err(ColumnarError::MissingKek);
}
SegmentHeader::from_bytes(data)?;
let footer = SegmentFooter::from_segment_tail(data)?;
Ok(Self { data, footer })
}
pub fn footer(&self) -> &SegmentFooter {
&self.footer
}
pub fn row_count(&self) -> u64 {
self.footer.row_count
}
pub fn column_count(&self) -> usize {
self.footer.column_count as usize
}
pub fn read_column(&self, col_idx: usize) -> Result<DecodedColumn, ColumnarError> {
self.read_column_filtered(col_idx, &[])
}
pub fn read_column_filtered(
&self,
col_idx: usize,
predicates: &[ScanPredicate],
) -> Result<DecodedColumn, ColumnarError> {
self.read_column_impl(col_idx, predicates, &DeleteBitmap::new())
}
pub fn read_columns(
&self,
col_indices: &[usize],
predicates: &[ScanPredicate],
) -> Result<Vec<DecodedColumn>, ColumnarError> {
col_indices
.iter()
.map(|&idx| self.read_column_filtered(idx, predicates))
.collect()
}
pub fn read_column_with_deletes(
&self,
col_idx: usize,
predicates: &[ScanPredicate],
deletes: &DeleteBitmap,
) -> Result<DecodedColumn, ColumnarError> {
self.read_column_impl(col_idx, predicates, deletes)
}
pub fn read_columns_with_deletes(
&self,
col_indices: &[usize],
predicates: &[ScanPredicate],
deletes: &DeleteBitmap,
) -> Result<Vec<DecodedColumn>, ColumnarError> {
col_indices
.iter()
.map(|&idx| self.read_column_with_deletes(idx, predicates, deletes))
.collect()
}
fn read_column_impl(
&self,
col_idx: usize,
predicates: &[ScanPredicate],
deletes: &DeleteBitmap,
) -> Result<DecodedColumn, ColumnarError> {
if col_idx >= self.footer.columns.len() {
return Err(ColumnarError::ColumnOutOfRange {
index: col_idx,
count: self.footer.columns.len(),
});
}
let col_meta = &self.footer.columns[col_idx];
let my_preds: Vec<&ScanPredicate> =
predicates.iter().filter(|p| p.col_idx == col_idx).collect();
let col_offset =
usize::try_from(col_meta.offset).map_err(|_| ColumnarError::Corruption {
segment_id: None,
reason: "column offset does not fit usize".into(),
offset: None,
})?;
let col_start =
HEADER_SIZE
.checked_add(col_offset)
.ok_or_else(|| ColumnarError::Corruption {
segment_id: None,
reason: "column offset range overflow".into(),
offset: None,
})?;
if col_start > self.data.len() {
return Err(ColumnarError::TruncatedSegment {
expected: col_start,
got: self.data.len(),
});
}
let mut cursor = col_start;
let mut result = empty_decoded(col_meta.layout);
let mut global_row: u32 = 0;
for block_stat in &col_meta.block_stats {
let block_row_count = block_stat.row_count;
let header_end = cursor
.checked_add(4)
.ok_or_else(|| ColumnarError::Corruption {
segment_id: None,
reason: "column block header range overflow".into(),
offset: None,
})?;
let header =
self.data
.get(cursor..header_end)
.ok_or(ColumnarError::TruncatedSegment {
expected: header_end,
got: self.data.len(),
})?;
let block_len = usize::try_from(u32::from_le_bytes([
header[0], header[1], header[2], header[3],
]))
.map_err(|_| ColumnarError::Corruption {
segment_id: None,
reason: "column block length does not fit usize".into(),
offset: None,
})?;
let block_data = checked_slice(self.data, header_end, block_len)?;
cursor =
header_end
.checked_add(block_len)
.ok_or_else(|| ColumnarError::Corruption {
segment_id: None,
reason: "column block range overflow".into(),
offset: None,
})?;
let pred_skip = my_preds.iter().any(|p| p.can_skip_block(block_stat));
let delete_skip =
!deletes.is_empty() && deletes.is_block_fully_deleted(global_row, block_row_count);
if pred_skip || delete_skip {
append_null_fill(&mut result, block_row_count as usize);
global_row += block_row_count;
continue;
}
let pre_len = result_valid_len(&result);
decode_block(
&mut result,
block_data,
col_meta.layout,
col_meta.codec,
block_row_count as usize,
col_meta.dictionary.as_deref(),
)?;
if !deletes.is_empty() {
let valid_slice = result_valid_slice_mut(&mut result, pre_len);
deletes.apply_to_validity(valid_slice, global_row);
}
global_row += block_row_count;
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::checked_slice;
use crate::error::ColumnarError;
#[test]
fn block_range_rejects_truncated_and_overflowed_lengths() {
assert!(matches!(
checked_slice(&[0; 4], 2, 4),
Err(ColumnarError::TruncatedSegment {
expected: 6,
got: 4
})
));
assert!(matches!(
checked_slice(&[], usize::MAX, 1),
Err(ColumnarError::Corruption { .. })
));
}
}