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