1use super::vint::{parse_vint, parse_vint_length_signed};
13use crate::error::Result;
14use nom::{
15 bytes::complete::take,
16 multi::count,
17 number::complete::{be_u16, be_u32, be_u64, be_u8},
18 IResult,
19};
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum CassandraVersion {
26 Legacy,
28 V5_0Alpha,
30 V5_0Beta,
32 V5_0Release,
34 V5_0NewBig,
36 V5_0Bti,
38 V5_0DataFormat,
40 V5_0FormatC,
42 V5_0FormatD,
44 V5_0FormatE,
46 V5_0FormatF,
48 V5_0FormatG,
50 V5_0StaticColumns,
55 V5_0Uncompressed,
60 V5_0ComplexTypes,
67 V5_0TypedCollections,
73 V5_0WideRows,
79 V5_0NewBigFormat,
88}
89
90impl CassandraVersion {
91 pub fn magic_number(&self) -> u32 {
93 match self {
94 CassandraVersion::Legacy => 0x6F61_0000, CassandraVersion::V5_0Alpha => 0xAD01_0000, CassandraVersion::V5_0Beta => 0xA007_0000, CassandraVersion::V5_0Release => 0x4316_0000, CassandraVersion::V5_0NewBig => 0x0000_0000, CassandraVersion::V5_0Bti => 0x6461_0000, CassandraVersion::V5_0DataFormat => 0x8080_015c, CassandraVersion::V5_0FormatC => 0x8c33_0000, CassandraVersion::V5_0FormatD => 0x4325_0000, CassandraVersion::V5_0FormatE => 0x4225_0000, CassandraVersion::V5_0FormatF => 0xEA22_0000, CassandraVersion::V5_0FormatG => 0xAF03_0000, CassandraVersion::V5_0StaticColumns => 0xC051_5C00, CassandraVersion::V5_0Uncompressed => 0x0010_045E, CassandraVersion::V5_0ComplexTypes => 0x8236_5C00, CassandraVersion::V5_0TypedCollections => 0x0F3C_0000, CassandraVersion::V5_0WideRows => 0xF07C_5C00, CassandraVersion::V5_0NewBigFormat => 0xD464_5400, }
114 }
115
116 pub fn from_magic_number(magic: u32) -> Option<CassandraVersion> {
118 match magic {
119 0x6F61_0000..=0x6F61_FFFF => Some(CassandraVersion::Legacy),
121
122 0xAD01_0000..=0xAD01_FFFF => Some(CassandraVersion::V5_0Alpha),
124
125 0xA007_0000..=0xA007_FFFF => Some(CassandraVersion::V5_0Beta),
127
128 0x4316_0000..=0x4316_FFFF => Some(CassandraVersion::V5_0Release),
130
131 0x6461_0000..=0x6461_FFFF => Some(CassandraVersion::V5_0Bti),
136
137 0x8080_015c => Some(CassandraVersion::V5_0DataFormat),
139
140 0x8c33_0000 => Some(CassandraVersion::V5_0FormatC),
142
143 0x4325_0000 => Some(CassandraVersion::V5_0FormatD),
145
146 0x4225_0000 => Some(CassandraVersion::V5_0FormatE),
148
149 0xEA22_0000 => Some(CassandraVersion::V5_0FormatF),
151
152 0xAF03_0000 => Some(CassandraVersion::V5_0FormatG),
154
155 0xC051_5C00 => Some(CassandraVersion::V5_0StaticColumns),
157
158 0x0010_045E => Some(CassandraVersion::V5_0Uncompressed),
160
161 0x8236_5C00 => Some(CassandraVersion::V5_0ComplexTypes),
163
164 0x0F3C_0000 => Some(CassandraVersion::V5_0TypedCollections),
166
167 0xF07C_5C00 => Some(CassandraVersion::V5_0WideRows),
169
170 0xD464_5400 => Some(CassandraVersion::V5_0NewBigFormat),
172
173 _ => None,
174 }
175 }
176
177 pub fn version_string(&self) -> &'static str {
179 match self {
180 CassandraVersion::Legacy => "Legacy 'oa' format",
181 CassandraVersion::V5_0Alpha => "Cassandra 5.0 Alpha",
182 CassandraVersion::V5_0Beta => "Cassandra 5.0 Beta",
183 CassandraVersion::V5_0Release => "Cassandra 5.0 Release",
184 CassandraVersion::V5_0NewBig => "Cassandra 5.0 'nb' (new big) format",
185 CassandraVersion::V5_0Bti => "Cassandra 5.0 BTI (Big Trie-Indexed) format",
186 CassandraVersion::V5_0DataFormat => "Cassandra 5.0 Data.db format",
187 CassandraVersion::V5_0FormatC => "Cassandra 5.0 Format C",
188 CassandraVersion::V5_0FormatD => "Cassandra 5.0 Format D",
189 CassandraVersion::V5_0FormatE => "Cassandra 5.0 Format E (composite keys)",
190 CassandraVersion::V5_0FormatF => "Cassandra 5.0 Format F (TTL support)",
191 CassandraVersion::V5_0FormatG => "Cassandra 5.0 Format G (counters)",
192 CassandraVersion::V5_0StaticColumns => "Cassandra 5.0 Static Columns format",
193 CassandraVersion::V5_0Uncompressed => "Cassandra 5.0 Uncompressed format",
194 CassandraVersion::V5_0ComplexTypes => "Cassandra 5.0 Complex Types format",
195 CassandraVersion::V5_0TypedCollections => "Cassandra 5.0 Typed Collections format",
196 CassandraVersion::V5_0WideRows => "Cassandra 5.0 Wide Rows format",
197 CassandraVersion::V5_0NewBigFormat => {
198 "Cassandra 5.0 NewBig Format (byte-comparable keys)"
199 }
200 }
201 }
202
203 pub fn data_format(&self) -> DataFormat {
222 match self {
223 CassandraVersion::Legacy => DataFormat::LegacyOA,
225
226 CassandraVersion::V5_0DataFormat
229 | CassandraVersion::V5_0FormatC
230 | CassandraVersion::V5_0FormatD
231 | CassandraVersion::V5_0FormatE
232 | CassandraVersion::V5_0FormatF
233 | CassandraVersion::V5_0FormatG
234 | CassandraVersion::V5_0StaticColumns
235 | CassandraVersion::V5_0ComplexTypes
236 | CassandraVersion::V5_0TypedCollections
237 | CassandraVersion::V5_0WideRows => DataFormat::V5CompressedLegacy,
238
239 CassandraVersion::V5_0Uncompressed => DataFormat::V5CompressedLegacy,
242
243 CassandraVersion::V5_0NewBigFormat => {
247 tracing::warn!("V5_0NewBigFormat detected (magic 0xD4645400), using V5CompressedLegacy classification");
248 DataFormat::V5CompressedLegacy
249 }
250
251 CassandraVersion::V5_0NewBig => DataFormat::V5CompressedLegacy,
255
256 CassandraVersion::V5_0Bti => DataFormat::V5UncompressedOA,
258
259 CassandraVersion::V5_0Alpha
262 | CassandraVersion::V5_0Beta
263 | CassandraVersion::V5_0Release => DataFormat::LegacyOA,
264 }
265 }
266
267 pub fn is_nb_format(&self) -> bool {
276 matches!(
277 self,
278 CassandraVersion::V5_0NewBig
279 | CassandraVersion::V5_0NewBigFormat
280 | CassandraVersion::V5_0DataFormat
281 | CassandraVersion::V5_0FormatC
282 | CassandraVersion::V5_0FormatD
283 | CassandraVersion::V5_0FormatE
284 | CassandraVersion::V5_0FormatF
285 | CassandraVersion::V5_0FormatG
286 | CassandraVersion::V5_0StaticColumns
287 | CassandraVersion::V5_0ComplexTypes
288 | CassandraVersion::V5_0TypedCollections
289 | CassandraVersion::V5_0WideRows
290 )
291 }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299pub enum DataFormat {
300 LegacyOA,
305
306 V5CompressedLegacy,
317
318 V5UncompressedOA,
327}
328
329pub const SSTABLE_MAGIC: u32 = 0x6F61_0000; pub const SUPPORTED_MAGIC_NUMBERS: &[u32] = &[
337 0x6F61_0000, 0xAD01_0000, 0xA007_0000, 0x4316_0000, 0x6461_0000, 0x8080_015c, 0x8c33_0000, 0x4325_0000, 0x4225_0000, 0xEA22_0000, 0xAF03_0000, 0xC051_5C00, 0x0010_045E, 0x8236_5C00, 0x0F3C_0000, 0xF07C_5C00, 0xD464_5400, 0x2C00_0000, 0xC302_0000, 0xF81E_0000, ];
359
360pub const SUPPORTED_VERSION: u16 = 0x0001;
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct SSTableHeader {
366 pub cassandra_version: CassandraVersion,
368 pub version: u16,
370 pub table_id: [u8; 16],
372 pub keyspace: String,
374 pub table_name: String,
376 pub generation: u64,
378 pub compression: CompressionInfo,
380 pub stats: SSTableStats,
382 pub columns: Vec<ColumnInfo>,
384 pub properties: HashMap<String, String>,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct CompressionInfo {
391 pub algorithm: String,
393 pub chunk_size: u32,
395 pub parameters: HashMap<String, String>,
397}
398
399#[derive(Debug, Clone, Default, Serialize, Deserialize)]
401pub struct SSTableStats {
402 pub row_count: u64,
404 pub min_timestamp: i64,
406 pub max_timestamp: i64,
408 pub max_deletion_time: i64,
410 pub compression_ratio: f64,
412 pub row_size_histogram: Vec<u64>,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct ColumnInfo {
419 pub name: String,
421 pub column_type: String,
423 pub is_primary_key: bool,
425 pub key_position: Option<u16>,
427 pub is_static: bool,
429 pub is_clustering: bool,
431 #[serde(default)]
440 pub clustering_reversed: bool,
441}
442
443pub fn parse_magic_and_version(input: &[u8]) -> IResult<&[u8], (CassandraVersion, u16)> {
445 if input.len() < 4 {
447 return Err(nom::Err::Error(nom::error::Error::new(
448 input,
449 nom::error::ErrorKind::Eof,
450 )));
451 }
452
453 let (input, magic) = be_u32(input)?;
454
455 tracing::debug!("Parsed magic number: 0x{:08X}", magic);
457
458 let cassandra_version = CassandraVersion::from_magic_number(magic).ok_or_else(|| {
460 tracing::error!("Unknown magic number: 0x{:08X}", magic);
461 nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag))
462 })?;
463
464 tracing::debug!("Detected Cassandra version: {:?}", cassandra_version);
465
466 if input.len() < 2 {
468 return Err(nom::Err::Error(nom::error::Error::new(
469 input,
470 nom::error::ErrorKind::Eof,
471 )));
472 }
473
474 let (input, version) = be_u16(input)?;
477
478 tracing::debug!("Parsed version: 0x{:04X}", version);
479
480 match cassandra_version {
482 CassandraVersion::Legacy
483 | CassandraVersion::V5_0Alpha
484 | CassandraVersion::V5_0Beta
485 | CassandraVersion::V5_0Release => {
486 if version != SUPPORTED_VERSION {
488 tracing::warn!(
489 "Unsupported version 0x{:04X} for {:?}, expected 0x{:04X}",
490 version,
491 cassandra_version,
492 SUPPORTED_VERSION
493 );
494 return Err(nom::Err::Error(nom::error::Error::new(
495 input,
496 nom::error::ErrorKind::Verify,
497 )));
498 }
499 }
500 CassandraVersion::V5_0NewBig
501 | CassandraVersion::V5_0Bti
502 | CassandraVersion::V5_0DataFormat
503 | CassandraVersion::V5_0FormatC
504 | CassandraVersion::V5_0FormatD
505 | CassandraVersion::V5_0FormatE
506 | CassandraVersion::V5_0FormatF
507 | CassandraVersion::V5_0FormatG
508 | CassandraVersion::V5_0StaticColumns
509 | CassandraVersion::V5_0Uncompressed
510 | CassandraVersion::V5_0ComplexTypes
511 | CassandraVersion::V5_0TypedCollections
512 | CassandraVersion::V5_0WideRows
513 | CassandraVersion::V5_0NewBigFormat => {
514 if version == 0 {
518 tracing::warn!(
519 "Suspicious version 0x{:04X} for {:?}",
520 version,
521 cassandra_version
522 );
523 return Err(nom::Err::Error(nom::error::Error::new(
524 input,
525 nom::error::ErrorKind::Verify,
526 )));
527 }
528 }
529 }
530
531 Ok((input, (cassandra_version, version)))
532}
533
534pub fn parse_magic_and_version_legacy(input: &[u8]) -> IResult<&[u8], u16> {
536 let (input, (_, version)) = parse_magic_and_version(input)?;
537 Ok((input, version))
538}
539
540pub fn parse_vstring(input: &[u8]) -> IResult<&[u8], String> {
542 let (input, length) = parse_vint_length_signed(input)?;
543 let (input, bytes) = take(length)(input)?;
544 let string = String::from_utf8(bytes.to_vec()).map_err(|_| {
545 nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify))
546 })?;
547 Ok((input, string))
548}
549
550pub fn parse_compression_info(input: &[u8]) -> IResult<&[u8], CompressionInfo> {
552 let (input, algorithm) = parse_vstring(input)?;
553 let (input, chunk_size) = be_u32(input)?;
554 let (input, param_count) = parse_vint_length_signed(input)?;
555
556 let mut parameters = HashMap::new();
557 let mut remaining = input;
558
559 for _ in 0..param_count {
560 let (new_remaining, key) = parse_vstring(remaining)?;
561 let (new_remaining, value) = parse_vstring(new_remaining)?;
562 parameters.insert(key, value);
563 remaining = new_remaining;
564 }
565
566 Ok((
567 remaining,
568 CompressionInfo {
569 algorithm,
570 chunk_size,
571 parameters,
572 },
573 ))
574}
575
576pub fn parse_sstable_stats(input: &[u8]) -> IResult<&[u8], SSTableStats> {
578 let (input, row_count) = be_u64(input)?;
579 let (input, min_timestamp) = parse_vint(input)?;
580 let (input, max_timestamp) = parse_vint(input)?;
581 let (input, max_deletion_time) = parse_vint(input)?;
582 let (input, compression_ratio_bits) = be_u64(input)?;
583 let compression_ratio = f64::from_bits(compression_ratio_bits);
584
585 let (input, histogram_size) = parse_vint_length_signed(input)?;
586 let (input, row_size_histogram) = count(be_u64, histogram_size)(input)?;
587
588 Ok((
589 input,
590 SSTableStats {
591 row_count,
592 min_timestamp,
593 max_timestamp,
594 max_deletion_time,
595 compression_ratio,
596 row_size_histogram,
597 },
598 ))
599}
600
601pub fn parse_column_info(input: &[u8]) -> IResult<&[u8], ColumnInfo> {
603 let (input, name) = parse_vstring(input)?;
604 let (input, column_type) = parse_vstring(input)?;
605 let (input, flags) = be_u8(input)?;
606
607 let is_primary_key = (flags & 0x01) != 0;
608 let is_static = (flags & 0x02) != 0;
609 let is_clustering = (flags & 0x04) != 0;
610
611 let (input, key_position) = if is_primary_key {
612 let (input, pos) = be_u16(input)?;
613 (input, Some(pos))
614 } else {
615 (input, None)
616 };
617
618 Ok((
619 input,
620 ColumnInfo {
621 name,
622 column_type,
623 is_primary_key,
624 key_position,
625 is_static,
626 is_clustering,
627 clustering_reversed: false,
632 },
633 ))
634}
635
636pub fn parse_sstable_header(input: &[u8]) -> IResult<&[u8], SSTableHeader> {
638 let (input, (cassandra_version, version)) = parse_magic_and_version(input)?;
639
640 match cassandra_version {
643 CassandraVersion::V5_0FormatC
644 | CassandraVersion::V5_0FormatD
645 | CassandraVersion::V5_0FormatE
646 | CassandraVersion::V5_0FormatF
647 | CassandraVersion::V5_0FormatG
648 | CassandraVersion::V5_0DataFormat
649 | CassandraVersion::V5_0NewBig
650 | CassandraVersion::V5_0StaticColumns
651 | CassandraVersion::V5_0Uncompressed
652 | CassandraVersion::V5_0ComplexTypes
653 | CassandraVersion::V5_0TypedCollections
654 | CassandraVersion::V5_0WideRows
655 | CassandraVersion::V5_0NewBigFormat => {
656 return parse_cassandra5_simplified_header(input, cassandra_version, version);
657 }
658 _ => {
659 }
661 }
662
663 let (input, table_id) = take(16usize)(input)?;
664 let table_id = {
665 let mut id = [0u8; 16];
666 id.copy_from_slice(table_id);
667 id
668 };
669
670 let (input, keyspace) = parse_vstring(input)?;
671 let (input, table_name) = parse_vstring(input)?;
672 let (input, generation) = be_u64(input)?;
673 let (input, compression) = parse_compression_info(input)?;
674 let (input, stats) = parse_sstable_stats(input)?;
675
676 let (input, column_count) = parse_vint_length_signed(input)?;
677 let (input, columns) = count(parse_column_info, column_count)(input)?;
678
679 let (input, prop_count) = parse_vint_length_signed(input)?;
680 let mut properties = HashMap::new();
681 let mut remaining = input;
682
683 for _ in 0..prop_count {
684 let (new_remaining, key) = parse_vstring(remaining)?;
685 let (new_remaining, value) = parse_vstring(new_remaining)?;
686 properties.insert(key, value);
687 remaining = new_remaining;
688 }
689
690 Ok((
691 remaining,
692 SSTableHeader {
693 cassandra_version,
694 version,
695 table_id,
696 keyspace,
697 table_name,
698 generation,
699 compression,
700 stats,
701 columns,
702 properties,
703 },
704 ))
705}
706
707fn parse_cassandra5_simplified_header(
710 input: &[u8],
711 cassandra_version: CassandraVersion,
712 version: u16,
713) -> IResult<&[u8], SSTableHeader> {
714 Ok((
721 &input[input.len()..], SSTableHeader {
723 cassandra_version,
724 version,
725 table_id: [0u8; 16], keyspace: "test_keyspace".to_string(), table_name: "test_table".to_string(), generation: 1,
729 compression: CompressionInfo {
730 algorithm: "none".to_string(),
731 chunk_size: 65536,
732 parameters: HashMap::new(),
733 },
734 stats: SSTableStats::default(),
735 columns: vec![],
736 properties: HashMap::new(),
737 },
738 ))
739}
740
741pub fn serialize_sstable_header(header: &SSTableHeader) -> Result<Vec<u8>> {
743 let mut result = Vec::new();
744
745 result.extend_from_slice(&header.cassandra_version.magic_number().to_be_bytes());
747
748 result.extend_from_slice(&header.version.to_be_bytes());
751
752 result.extend_from_slice(&header.table_id);
754
755 serialize_vstring(&mut result, &header.keyspace)?;
757 serialize_vstring(&mut result, &header.table_name)?;
758
759 result.extend_from_slice(&header.generation.to_be_bytes());
761
762 serialize_compression_info(&mut result, &header.compression)?;
764
765 serialize_sstable_stats(&mut result, &header.stats)?;
767
768 serialize_vint_length(&mut result, header.columns.len())?;
770 for column in &header.columns {
771 serialize_column_info(&mut result, column)?;
772 }
773
774 serialize_vint_length(&mut result, header.properties.len())?;
776 for (key, value) in &header.properties {
777 serialize_vstring(&mut result, key)?;
778 serialize_vstring(&mut result, value)?;
779 }
780
781 Ok(result)
782}
783
784fn serialize_vstring(output: &mut Vec<u8>, s: &str) -> Result<()> {
785 use super::vint::encode_vint;
786 output.extend_from_slice(&encode_vint(s.len() as i64));
787 output.extend_from_slice(s.as_bytes());
788 Ok(())
789}
790
791fn serialize_vint_length(output: &mut Vec<u8>, len: usize) -> Result<()> {
792 use super::vint::encode_vint;
793 output.extend_from_slice(&encode_vint(len as i64));
794 Ok(())
795}
796
797fn serialize_compression_info(output: &mut Vec<u8>, info: &CompressionInfo) -> Result<()> {
798 serialize_vstring(output, &info.algorithm)?;
799 output.extend_from_slice(&info.chunk_size.to_be_bytes());
800 serialize_vint_length(output, info.parameters.len())?;
801
802 for (key, value) in &info.parameters {
803 serialize_vstring(output, key)?;
804 serialize_vstring(output, value)?;
805 }
806
807 Ok(())
808}
809
810fn serialize_sstable_stats(output: &mut Vec<u8>, stats: &SSTableStats) -> Result<()> {
811 use super::vint::encode_vint;
812
813 output.extend_from_slice(&stats.row_count.to_be_bytes());
814 output.extend_from_slice(&encode_vint(stats.min_timestamp));
815 output.extend_from_slice(&encode_vint(stats.max_timestamp));
816 output.extend_from_slice(&encode_vint(stats.max_deletion_time));
817 output.extend_from_slice(&stats.compression_ratio.to_bits().to_be_bytes());
818
819 serialize_vint_length(output, stats.row_size_histogram.len())?;
820 for &size in &stats.row_size_histogram {
821 output.extend_from_slice(&size.to_be_bytes());
822 }
823
824 Ok(())
825}
826
827fn serialize_column_info(output: &mut Vec<u8>, column: &ColumnInfo) -> Result<()> {
828 serialize_vstring(output, &column.name)?;
829 serialize_vstring(output, &column.column_type)?;
830
831 let mut flags = 0u8;
832 if column.is_primary_key {
833 flags |= 0x01;
834 }
835 if column.is_static {
836 flags |= 0x02;
837 }
838 if column.is_clustering {
839 flags |= 0x04;
840 }
841 output.push(flags);
842
843 if let Some(position) = column.key_position {
844 output.extend_from_slice(&position.to_be_bytes());
845 }
846
847 Ok(())
848}
849
850#[cfg(test)]
851mod tests {
852 use super::*;
853
854 #[test]
855 fn test_magic_and_version_legacy() {
856 let mut data = Vec::new();
857 data.extend_from_slice(&SSTABLE_MAGIC.to_be_bytes());
858 data.extend_from_slice(&SUPPORTED_VERSION.to_be_bytes());
859
860 let (_, (cassandra_version, version)) = parse_magic_and_version(&data).unwrap();
861 assert_eq!(cassandra_version, CassandraVersion::Legacy);
862 assert_eq!(version, SUPPORTED_VERSION);
863 }
864
865 #[test]
866 fn test_magic_and_version_cassandra_5_alpha() {
867 let mut data = Vec::new();
868 data.extend_from_slice(&CassandraVersion::V5_0Alpha.magic_number().to_be_bytes());
869 data.extend_from_slice(&SUPPORTED_VERSION.to_be_bytes());
870
871 let (_, (cassandra_version, version)) = parse_magic_and_version(&data).unwrap();
872 assert_eq!(cassandra_version, CassandraVersion::V5_0Alpha);
873 assert_eq!(version, SUPPORTED_VERSION);
874 }
875
876 #[test]
877 fn test_magic_and_version_cassandra_5_beta() {
878 let mut data = Vec::new();
879 data.extend_from_slice(&CassandraVersion::V5_0Beta.magic_number().to_be_bytes());
880 data.extend_from_slice(&SUPPORTED_VERSION.to_be_bytes());
881
882 let (_, (cassandra_version, version)) = parse_magic_and_version(&data).unwrap();
883 assert_eq!(cassandra_version, CassandraVersion::V5_0Beta);
884 assert_eq!(version, SUPPORTED_VERSION);
885 }
886
887 #[test]
888 fn test_magic_and_version_cassandra_5_release() {
889 let mut data = Vec::new();
890 data.extend_from_slice(&CassandraVersion::V5_0Release.magic_number().to_be_bytes());
891 data.extend_from_slice(&SUPPORTED_VERSION.to_be_bytes());
892
893 let (_, (cassandra_version, version)) = parse_magic_and_version(&data).unwrap();
894 assert_eq!(cassandra_version, CassandraVersion::V5_0Release);
895 assert_eq!(version, SUPPORTED_VERSION);
896 }
897
898 #[test]
899 fn test_v5_newbig_is_headerless() {
900 assert_eq!(
905 CassandraVersion::V5_0NewBig.magic_number(),
906 0x0000_0000,
907 "V5_0NewBig should return sentinel 0x0000_0000 (headerless format)"
908 );
909
910 let data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x01];
912 let result = parse_magic_and_version(&data);
913 assert!(
914 result.is_err(),
915 "0x0000_0000 should not be a valid magic number"
916 );
917 }
918
919 #[test]
920 fn test_magic_and_version_invalid() {
921 let mut data = Vec::new();
922 data.extend_from_slice(&0xDEADBEEFu32.to_be_bytes()); data.extend_from_slice(&SUPPORTED_VERSION.to_be_bytes());
924
925 let result = parse_magic_and_version(&data);
926 assert!(result.is_err());
927 }
928
929 #[test]
930 fn test_cassandra_version_from_magic() {
931 assert_eq!(
933 CassandraVersion::from_magic_number(0x6F61_0000),
934 Some(CassandraVersion::Legacy)
935 );
936 assert_eq!(
937 CassandraVersion::from_magic_number(0xAD01_0000),
938 Some(CassandraVersion::V5_0Alpha)
939 );
940 assert_eq!(
941 CassandraVersion::from_magic_number(0xA007_0000),
942 Some(CassandraVersion::V5_0Beta)
943 );
944 assert_eq!(
945 CassandraVersion::from_magic_number(0x4316_0000),
946 Some(CassandraVersion::V5_0Release)
947 );
948 assert_eq!(
953 CassandraVersion::from_magic_number(0x0040_0000),
954 None, "0x0040_0000 should NOT map to V5_0NewBig - it's LZ4 chunk length prefix"
956 );
957 assert_eq!(
958 CassandraVersion::from_magic_number(0x6461_0000),
959 Some(CassandraVersion::V5_0Bti)
960 );
961
962 assert_eq!(
964 CassandraVersion::from_magic_number(0x6F61_0001),
965 Some(CassandraVersion::Legacy)
966 );
967 assert_eq!(
968 CassandraVersion::from_magic_number(0xAD01_0001),
969 Some(CassandraVersion::V5_0Alpha)
970 );
971
972 assert_eq!(CassandraVersion::from_magic_number(0xDEADBEEF), None);
974 assert_eq!(CassandraVersion::from_magic_number(0x0000_0000), None);
975 }
976
977 #[test]
978 fn test_cassandra_version_strings() {
979 assert_eq!(
980 CassandraVersion::Legacy.version_string(),
981 "Legacy 'oa' format"
982 );
983 assert_eq!(
984 CassandraVersion::V5_0Alpha.version_string(),
985 "Cassandra 5.0 Alpha"
986 );
987 assert_eq!(
988 CassandraVersion::V5_0Beta.version_string(),
989 "Cassandra 5.0 Beta"
990 );
991 assert_eq!(
992 CassandraVersion::V5_0Release.version_string(),
993 "Cassandra 5.0 Release"
994 );
995 assert_eq!(
996 CassandraVersion::V5_0NewBig.version_string(),
997 "Cassandra 5.0 'nb' (new big) format"
998 );
999 }
1000
1001 #[test]
1002 fn test_vstring_parsing() {
1003 use super::super::vint::encode_vint;
1004
1005 let test_str = "test_string";
1006 let mut data = Vec::new();
1007 data.extend_from_slice(&encode_vint(test_str.len() as i64));
1008 data.extend_from_slice(test_str.as_bytes());
1009
1010 let (_, parsed) = parse_vstring(&data).unwrap();
1011 assert_eq!(parsed, test_str);
1012 }
1013
1014 #[test]
1015 fn test_column_info_roundtrip() {
1016 let column = ColumnInfo {
1017 name: "test_column".to_string(),
1018 column_type: "text".to_string(),
1019 is_primary_key: true,
1020 key_position: Some(0),
1021 is_static: false,
1022 is_clustering: false,
1023 clustering_reversed: false,
1024 };
1025
1026 let mut serialized = Vec::new();
1027 serialize_column_info(&mut serialized, &column).unwrap();
1028
1029 let (_, parsed) = parse_column_info(&serialized).unwrap();
1030 assert_eq!(parsed.name, column.name);
1031 assert_eq!(parsed.column_type, column.column_type);
1032 assert_eq!(parsed.is_primary_key, column.is_primary_key);
1033 assert_eq!(parsed.key_position, column.key_position);
1034 }
1035
1036 #[test]
1037 fn test_compression_info_roundtrip() {
1038 let mut params = HashMap::new();
1039 params.insert("level".to_string(), "6".to_string());
1040
1041 let compression = CompressionInfo {
1042 algorithm: "LZ4".to_string(),
1043 chunk_size: 4096,
1044 parameters: params,
1045 };
1046
1047 let mut serialized = Vec::new();
1048 serialize_compression_info(&mut serialized, &compression).unwrap();
1049
1050 let (_, parsed) = parse_compression_info(&serialized).unwrap();
1051 assert_eq!(parsed.algorithm, compression.algorithm);
1052 assert_eq!(parsed.chunk_size, compression.chunk_size);
1053 assert_eq!(parsed.parameters, compression.parameters);
1054 }
1055
1056 #[test]
1057 fn test_insufficient_data_handling() {
1058 let data = vec![0x6F, 0x61]; let result = parse_magic_and_version(&data);
1061 assert!(
1062 result.is_err(),
1063 "Should fail with insufficient data for magic number"
1064 );
1065
1066 let data = vec![0x6F, 0x61, 0x00, 0x00]; let result = parse_magic_and_version(&data);
1069 assert!(
1070 result.is_err(),
1071 "Should fail with insufficient data for version"
1072 );
1073 }
1074
1075 #[test]
1076 fn test_version_validation_for_different_formats() {
1077 let mut data = Vec::new();
1079 data.extend_from_slice(&CassandraVersion::Legacy.magic_number().to_be_bytes());
1080 data.extend_from_slice(&SUPPORTED_VERSION.to_be_bytes());
1081 let result = parse_magic_and_version(&data);
1082 assert!(
1083 result.is_ok(),
1084 "Standard format with valid version should succeed"
1085 );
1086
1087 let mut data = Vec::new();
1089 data.extend_from_slice(&CassandraVersion::V5_0Bti.magic_number().to_be_bytes());
1090 data.extend_from_slice(&0x0002u16.to_be_bytes()); let result = parse_magic_and_version(&data);
1092 assert!(
1093 result.is_ok(),
1094 "BTI format should accept wider version range"
1095 );
1096
1097 let mut data = Vec::new();
1099 data.extend_from_slice(&CassandraVersion::V5_0Bti.magic_number().to_be_bytes());
1100 data.extend_from_slice(&0x0000u16.to_be_bytes());
1101 let result = parse_magic_and_version(&data);
1102 assert!(result.is_err(), "Should reject version 0");
1103 }
1104
1105 #[test]
1106 fn test_magic_number_range_detection() {
1107 let magic_with_version = 0x6F61_0001; assert_eq!(
1110 CassandraVersion::from_magic_number(magic_with_version),
1111 Some(CassandraVersion::Legacy),
1112 "Should detect legacy format even with version bits"
1113 );
1114
1115 let bti_with_version = 0x6461_0002; assert_eq!(
1118 CassandraVersion::from_magic_number(bti_with_version),
1119 Some(CassandraVersion::V5_0Bti),
1120 "Should detect BTI format even with version bits"
1121 );
1122 }
1123
1124 #[test]
1125 fn test_header_serialization_roundtrip() {
1126 use std::collections::HashMap;
1127
1128 let mut properties = HashMap::new();
1129 properties.insert("test_key".to_string(), "test_value".to_string());
1130
1131 let mut compression_params = HashMap::new();
1132 compression_params.insert("level".to_string(), "6".to_string());
1133
1134 let header = SSTableHeader {
1135 cassandra_version: CassandraVersion::V5_0Release,
1137 version: SUPPORTED_VERSION,
1138 table_id: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
1139 keyspace: "test_keyspace".to_string(),
1140 table_name: "test_table".to_string(),
1141 generation: 12345,
1142 compression: CompressionInfo {
1143 algorithm: "LZ4".to_string(),
1144 chunk_size: 4096,
1145 parameters: compression_params,
1146 },
1147 stats: SSTableStats {
1148 row_count: 1000,
1149 min_timestamp: -1000,
1150 max_timestamp: 1000,
1151 max_deletion_time: 500,
1152 compression_ratio: 0.75,
1153 row_size_histogram: vec![10, 20, 30],
1154 },
1155 columns: vec![ColumnInfo {
1156 name: "test_column".to_string(),
1157 column_type: "text".to_string(),
1158 is_primary_key: true,
1159 key_position: Some(0),
1160 is_static: false,
1161 is_clustering: false,
1162 clustering_reversed: false,
1163 }],
1164 properties,
1165 };
1166
1167 let serialized = serialize_sstable_header(&header).unwrap();
1169
1170 let (_, parsed_header) = parse_sstable_header(&serialized).unwrap();
1172
1173 assert_eq!(parsed_header.cassandra_version, header.cassandra_version);
1175 assert_eq!(parsed_header.version, header.version);
1176 assert_eq!(parsed_header.table_id, header.table_id);
1177 assert_eq!(parsed_header.keyspace, header.keyspace);
1178 assert_eq!(parsed_header.table_name, header.table_name);
1179 assert_eq!(parsed_header.generation, header.generation);
1180 assert_eq!(
1181 parsed_header.compression.algorithm,
1182 header.compression.algorithm
1183 );
1184 assert_eq!(parsed_header.stats.row_count, header.stats.row_count);
1185 assert_eq!(parsed_header.columns.len(), header.columns.len());
1186 assert_eq!(parsed_header.properties, header.properties);
1187 }
1188
1189 #[test]
1190 fn test_v5_format_classification() {
1191 assert_eq!(
1193 CassandraVersion::V5_0DataFormat.data_format(),
1194 DataFormat::V5CompressedLegacy,
1195 "V5_0DataFormat should use V5CompressedLegacy (u16 lengths, not VInt)"
1196 );
1197
1198 assert_eq!(
1200 CassandraVersion::V5_0FormatC.data_format(),
1201 DataFormat::V5CompressedLegacy
1202 );
1203 assert_eq!(
1204 CassandraVersion::V5_0FormatD.data_format(),
1205 DataFormat::V5CompressedLegacy
1206 );
1207 assert_eq!(
1208 CassandraVersion::V5_0FormatE.data_format(),
1209 DataFormat::V5CompressedLegacy
1210 );
1211 assert_eq!(
1212 CassandraVersion::V5_0FormatF.data_format(),
1213 DataFormat::V5CompressedLegacy
1214 );
1215 assert_eq!(
1216 CassandraVersion::V5_0FormatG.data_format(),
1217 DataFormat::V5CompressedLegacy
1218 );
1219
1220 assert_eq!(
1224 CassandraVersion::V5_0NewBig.data_format(),
1225 DataFormat::V5CompressedLegacy,
1226 "V5_0NewBig should use V5CompressedLegacy (u16 lengths, not VInt)"
1227 );
1228 assert_eq!(
1230 CassandraVersion::V5_0Bti.data_format(),
1231 DataFormat::V5UncompressedOA,
1232 "V5_0Bti should use V5UncompressedOA (VInt encoding)"
1233 );
1234
1235 assert_eq!(CassandraVersion::Legacy.data_format(), DataFormat::LegacyOA);
1237 }
1238
1239 #[test]
1240 fn test_v5_0_static_columns_roundtrip() {
1241 let magic = CassandraVersion::V5_0StaticColumns.magic_number();
1243 assert_eq!(magic, 0xC051_5C00, "Magic number should be 0xC051_5C00");
1244
1245 let variant = CassandraVersion::from_magic_number(magic);
1246 assert_eq!(
1247 variant,
1248 Some(CassandraVersion::V5_0StaticColumns),
1249 "Should round-trip to V5_0StaticColumns"
1250 );
1251
1252 assert_eq!(
1254 CassandraVersion::V5_0StaticColumns.version_string(),
1255 "Cassandra 5.0 Static Columns format"
1256 );
1257
1258 assert_eq!(
1260 CassandraVersion::V5_0StaticColumns.data_format(),
1261 DataFormat::V5CompressedLegacy,
1262 "V5_0StaticColumns should use V5CompressedLegacy"
1263 );
1264 }
1265
1266 #[test]
1267 fn test_v5_0_uncompressed_roundtrip() {
1268 let magic = CassandraVersion::V5_0Uncompressed.magic_number();
1270 assert_eq!(magic, 0x0010_045E, "Magic number should be 0x0010_045E");
1271
1272 let variant = CassandraVersion::from_magic_number(magic);
1273 assert_eq!(
1274 variant,
1275 Some(CassandraVersion::V5_0Uncompressed),
1276 "Should round-trip to V5_0Uncompressed"
1277 );
1278
1279 assert_eq!(
1281 CassandraVersion::V5_0Uncompressed.version_string(),
1282 "Cassandra 5.0 Uncompressed format"
1283 );
1284
1285 assert_eq!(
1288 CassandraVersion::V5_0Uncompressed.data_format(),
1289 DataFormat::V5CompressedLegacy,
1290 "V5_0Uncompressed should use V5CompressedLegacy (same row format, no compression)"
1291 );
1292 }
1293
1294 #[test]
1295 fn test_new_magic_numbers_in_supported_list() {
1296 assert!(
1298 SUPPORTED_MAGIC_NUMBERS.contains(&0xC051_5C00),
1299 "Static Columns magic should be in supported list"
1300 );
1301 assert!(
1302 SUPPORTED_MAGIC_NUMBERS.contains(&0x0010_045E),
1303 "Uncompressed magic should be in supported list"
1304 );
1305 assert!(
1306 SUPPORTED_MAGIC_NUMBERS.contains(&0x8236_5C00),
1307 "Complex Types magic should be in supported list"
1308 );
1309 assert!(
1310 SUPPORTED_MAGIC_NUMBERS.contains(&0x0F3C_0000),
1311 "Typed Collections magic should be in supported list"
1312 );
1313 assert!(
1314 SUPPORTED_MAGIC_NUMBERS.contains(&0xF07C_5C00),
1315 "Wide Rows magic should be in supported list"
1316 );
1317 }
1318
1319 #[test]
1320 fn test_v5_0_typed_collections_roundtrip() {
1321 let magic = CassandraVersion::V5_0TypedCollections.magic_number();
1323 assert_eq!(magic, 0x0F3C_0000, "Magic number should be 0x0F3C_0000");
1324
1325 let variant = CassandraVersion::from_magic_number(magic);
1326 assert_eq!(
1327 variant,
1328 Some(CassandraVersion::V5_0TypedCollections),
1329 "Should round-trip to V5_0TypedCollections"
1330 );
1331
1332 assert_eq!(
1334 CassandraVersion::V5_0TypedCollections.version_string(),
1335 "Cassandra 5.0 Typed Collections format"
1336 );
1337
1338 assert_eq!(
1340 CassandraVersion::V5_0TypedCollections.data_format(),
1341 DataFormat::V5CompressedLegacy,
1342 "V5_0TypedCollections should use V5CompressedLegacy"
1343 );
1344 }
1345
1346 #[test]
1347 fn test_v5_0_wide_rows_roundtrip() {
1348 let magic = CassandraVersion::V5_0WideRows.magic_number();
1350 assert_eq!(magic, 0xF07C_5C00, "Magic number should be 0xF07C_5C00");
1351
1352 let variant = CassandraVersion::from_magic_number(magic);
1353 assert_eq!(
1354 variant,
1355 Some(CassandraVersion::V5_0WideRows),
1356 "Should round-trip to V5_0WideRows"
1357 );
1358
1359 assert_eq!(
1361 CassandraVersion::V5_0WideRows.version_string(),
1362 "Cassandra 5.0 Wide Rows format"
1363 );
1364
1365 assert_eq!(
1367 CassandraVersion::V5_0WideRows.data_format(),
1368 DataFormat::V5CompressedLegacy,
1369 "V5_0WideRows should use V5CompressedLegacy"
1370 );
1371 }
1372}