use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ClusteringValueLayout {
Fixed(usize),
Variable,
}
pub(crate) fn resolve_clustering_value_layout(type_name: &str) -> Option<ClusteringValueLayout> {
let inner = strip_reversed(type_name);
let ctor = inner.split('(').next().unwrap_or(inner);
let simple = ctor.rsplit('.').next().unwrap_or(ctor);
let fixed = match simple {
"BooleanType" => Some(1),
"Int32Type" | "FloatType" => Some(4),
"LongType" | "DoubleType" | "TimestampType" | "DateType" => Some(8),
"UUIDType" | "TimeUUIDType" | "LexicalUUIDType" => Some(16),
"EmptyType" => Some(0),
_ => None,
};
if let Some(n) = fixed {
return Some(ClusteringValueLayout::Fixed(n));
}
let variable = matches!(
simple,
"UTF8Type"
| "AsciiType"
| "BytesType"
| "IntegerType"
| "DecimalType"
| "InetAddressType"
| "ByteType"
| "ShortType"
| "TimeType"
| "SimpleDateType"
| "DurationType"
| "MapType"
| "SetType"
| "ListType"
| "TupleType"
| "UserType"
| "FrozenType"
| "CompositeType"
| "DynamicCompositeType"
| "VectorType"
);
if variable {
return Some(ClusteringValueLayout::Variable);
}
None
}
fn strip_reversed(type_name: &str) -> &str {
let ctor_full = type_name.split('(').next().unwrap_or(type_name);
let ctor = ctor_full.rsplit('.').next().unwrap_or(ctor_full);
if ctor == "ReversedType" {
if let Some(open) = type_name.find('(') {
let body = &type_name[open + 1..];
return body.strip_suffix(')').unwrap_or(body);
}
}
type_name
}
pub(crate) trait ByteSkip {
fn read_u8(&mut self) -> Result<u8>;
fn read_u16(&mut self) -> Result<u16>;
fn read_unsigned_vint(&mut self) -> Result<u64>;
fn skip(&mut self, n: usize) -> Result<()>;
}
pub(crate) fn skip_covered_slice<C: ByteSkip>(
c: &mut C,
layouts: &[Option<ClusteringValueLayout>],
) -> Result<bool> {
for _ in 0..2 {
let _kind = c.read_u8()?; let size = c.read_u16()? as usize; if size == 0 {
continue;
}
if size > layouts.len() {
return Ok(false);
}
if !skip_values_without_size(c, size, layouts)? {
return Ok(false);
}
}
Ok(true)
}
fn skip_values_without_size<C: ByteSkip>(
c: &mut C,
size: usize,
layouts: &[Option<ClusteringValueLayout>],
) -> Result<bool> {
let mut offset = 0usize;
while offset < size {
let header = c.read_unsigned_vint()?;
let limit = size.min(offset + 32);
while offset < limit {
let shift = (offset * 2) % 64;
let is_null = (header >> (shift + 1)) & 1 == 1;
let is_empty = (header >> shift) & 1 == 1;
if !is_null && !is_empty {
match layouts[offset] {
Some(ClusteringValueLayout::Fixed(n)) => c.skip(n)?,
Some(ClusteringValueLayout::Variable) => {
let len = c.read_unsigned_vint()? as usize;
c.skip(len)?;
}
None => return Ok(false), }
}
offset += 1;
}
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
#[test]
fn fixed_widths_match_cassandra() {
use ClusteringValueLayout::*;
let cases = [
("org.apache.cassandra.db.marshal.Int32Type", Fixed(4)),
("org.apache.cassandra.db.marshal.LongType", Fixed(8)),
("org.apache.cassandra.db.marshal.TimestampType", Fixed(8)),
("org.apache.cassandra.db.marshal.DateType", Fixed(8)),
("org.apache.cassandra.db.marshal.DoubleType", Fixed(8)),
("org.apache.cassandra.db.marshal.FloatType", Fixed(4)),
("org.apache.cassandra.db.marshal.BooleanType", Fixed(1)),
("org.apache.cassandra.db.marshal.UUIDType", Fixed(16)),
("org.apache.cassandra.db.marshal.TimeUUIDType", Fixed(16)),
("org.apache.cassandra.db.marshal.LexicalUUIDType", Fixed(16)),
("org.apache.cassandra.db.marshal.EmptyType", Fixed(0)),
];
for (name, want) in cases {
assert_eq!(
resolve_clustering_value_layout(name),
Some(want),
"type {name}"
);
}
}
#[test]
fn variable_types_resolve_variable() {
for name in [
"org.apache.cassandra.db.marshal.UTF8Type",
"org.apache.cassandra.db.marshal.AsciiType",
"org.apache.cassandra.db.marshal.BytesType",
"org.apache.cassandra.db.marshal.IntegerType",
"org.apache.cassandra.db.marshal.DecimalType",
"org.apache.cassandra.db.marshal.ByteType",
"org.apache.cassandra.db.marshal.ShortType",
"org.apache.cassandra.db.marshal.SimpleDateType",
"org.apache.cassandra.db.marshal.TimeType",
] {
assert_eq!(
resolve_clustering_value_layout(name),
Some(ClusteringValueLayout::Variable),
"type {name}"
);
}
}
#[test]
fn reversed_delegates_to_base() {
assert_eq!(
resolve_clustering_value_layout(
"org.apache.cassandra.db.marshal.ReversedType(org.apache.cassandra.db.marshal.Int32Type)"
),
Some(ClusteringValueLayout::Fixed(4))
);
assert_eq!(
resolve_clustering_value_layout(
"org.apache.cassandra.db.marshal.ReversedType(org.apache.cassandra.db.marshal.UTF8Type)"
),
Some(ClusteringValueLayout::Variable)
);
}
#[test]
fn parametric_collections_are_variable() {
for name in [
"org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.UTF8Type)",
"org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.Int32Type)",
"org.apache.cassandra.db.marshal.MapType(org.apache.cassandra.db.marshal.UTF8Type,org.apache.cassandra.db.marshal.UTF8Type)",
] {
assert_eq!(
resolve_clustering_value_layout(name),
Some(ClusteringValueLayout::Variable),
"type {name}"
);
}
}
#[test]
fn unknown_type_is_none() {
assert_eq!(
resolve_clustering_value_layout("org.apache.cassandra.db.marshal.SomeFutureType"),
None
);
}
struct Buf<'a> {
b: &'a [u8],
p: usize,
}
impl ByteSkip for Buf<'_> {
fn read_u8(&mut self) -> Result<u8> {
let v = *self
.b
.get(self.p)
.ok_or_else(|| Error::Corruption("eof".into()))?;
self.p += 1;
Ok(v)
}
fn read_u16(&mut self) -> Result<u16> {
let hi = self.read_u8()? as u16;
let lo = self.read_u8()? as u16;
Ok((hi << 8) | lo)
}
fn read_unsigned_vint(&mut self) -> Result<u64> {
Ok(self.read_u8()? as u64)
}
fn skip(&mut self, n: usize) -> Result<()> {
let end = self
.p
.checked_add(n)
.filter(|&e| e <= self.b.len())
.ok_or_else(|| Error::Corruption("eof".into()))?;
self.p = end;
Ok(())
}
}
#[test]
fn skip_empty_slice() {
let bytes = [1u8, 0, 0, 6, 0, 0];
let mut c = Buf { b: &bytes, p: 0 };
assert!(skip_covered_slice(&mut c, &[]).unwrap());
assert_eq!(c.p, bytes.len());
}
#[test]
fn skip_slice_with_fixed_int_values() {
let layouts = vec![Some(ClusteringValueLayout::Fixed(4))];
let mut bytes = Vec::new();
for kind in [1u8, 6u8] {
bytes.push(kind);
bytes.extend_from_slice(&1u16.to_be_bytes()); bytes.push(0x00); bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); }
let mut c = Buf { b: &bytes, p: 0 };
assert!(skip_covered_slice(&mut c, &layouts).unwrap());
assert_eq!(c.p, bytes.len());
}
#[test]
fn skip_slice_with_variable_value() {
let layouts = vec![Some(ClusteringValueLayout::Variable)];
let mut bytes = Vec::new();
bytes.push(1); bytes.extend_from_slice(&1u16.to_be_bytes()); bytes.push(0x00); bytes.push(3); bytes.extend_from_slice(b"abc");
bytes.push(6); bytes.extend_from_slice(&0u16.to_be_bytes()); let mut c = Buf { b: &bytes, p: 0 };
assert!(skip_covered_slice(&mut c, &layouts).unwrap());
assert_eq!(c.p, bytes.len());
}
#[test]
fn skip_slice_unknown_type_bails_false() {
let layouts = vec![None];
let mut bytes = Vec::new();
bytes.push(1);
bytes.extend_from_slice(&1u16.to_be_bytes());
bytes.push(0x00);
let mut c = Buf { b: &bytes, p: 0 };
assert!(!skip_covered_slice(&mut c, &layouts).unwrap());
}
#[test]
fn skip_slice_size_exceeds_types_bails_false() {
let layouts = vec![Some(ClusteringValueLayout::Fixed(4))];
let mut bytes = Vec::new();
bytes.push(1);
bytes.extend_from_slice(&2u16.to_be_bytes()); let mut c = Buf { b: &bytes, p: 0 };
assert!(!skip_covered_slice(&mut c, &layouts).unwrap());
}
fn assert_variable_covered_skip(type_name: &str, natural_width: usize) {
assert_eq!(
resolve_clustering_value_layout(type_name),
Some(ClusteringValueLayout::Variable),
"type {type_name} must resolve Variable (no valueLengthIfFixed override)"
);
let layouts = vec![Some(ClusteringValueLayout::Variable)];
let value: Vec<u8> = (0..natural_width as u8).collect();
let mut bytes = Vec::new();
bytes.push(1u8);
bytes.extend_from_slice(&1u16.to_be_bytes());
bytes.push(0x00); bytes.push(natural_width as u8); bytes.extend_from_slice(&value);
let start_len = bytes.len();
bytes.push(6u8);
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.push(0xAB);
let mut c = Buf { b: &bytes, p: 0 };
assert!(
skip_covered_slice(&mut c, &layouts).unwrap(),
"covered slice for {type_name} should skip cleanly"
);
assert_eq!(
c.p,
bytes.len() - 1,
"type {type_name}: skip must advance past kind+size+header+vint+{natural_width}B value \
and the empty end bound, stopping before pendingRepair"
);
assert_eq!(start_len, 1 + 2 + 1 + 1 + natural_width);
}
#[test]
fn skip_slice_tinyint_clustering_advances_vint_prefixed() {
assert_variable_covered_skip("org.apache.cassandra.db.marshal.ByteType", 1);
}
#[test]
fn skip_slice_smallint_clustering_advances_vint_prefixed() {
assert_variable_covered_skip("org.apache.cassandra.db.marshal.ShortType", 2);
}
#[test]
fn skip_slice_date_clustering_advances_vint_prefixed() {
assert_variable_covered_skip("org.apache.cassandra.db.marshal.SimpleDateType", 4);
}
#[test]
fn skip_slice_time_clustering_advances_vint_prefixed() {
assert_variable_covered_skip("org.apache.cassandra.db.marshal.TimeType", 8);
}
#[test]
fn skip_slice_null_value_consumes_no_bytes() {
let layouts = vec![Some(ClusteringValueLayout::Fixed(4))];
let mut bytes = Vec::new();
bytes.push(1);
bytes.extend_from_slice(&1u16.to_be_bytes());
bytes.push(0x02); bytes.push(6);
bytes.extend_from_slice(&0u16.to_be_bytes());
let mut c = Buf { b: &bytes, p: 0 };
assert!(skip_covered_slice(&mut c, &layouts).unwrap());
assert_eq!(c.p, bytes.len());
}
}