1#![allow(deprecated)]
26
27use std::fs::File;
28use std::io::BufReader;
29use std::path::{Path, PathBuf};
30use tracing::{debug, info, warn};
31
32use super::{
33 chunk_decompressor::{create_decompressor_from_file, ChunkDecompressor},
34 compression_info::CompressionInfo,
35 format_detector::{SSTableComponent, SSTableFormat, SSTableInfo},
36 version_gate::VersionGates,
37};
38use crate::parser::vint::parse_vint;
39use crate::{Error, Result};
40
41#[deprecated(
48 since = "0.1.0",
49 note = "Use SSTableReader instead. This reader is EXPERIMENTAL and not suitable for production. See Issue #190."
50)]
51pub struct BulletproofReader {
52 info: SSTableInfo,
54 base_dir: PathBuf,
56 decompressor: Option<ChunkDecompressor>,
58 data_reader: Option<BufReader<File>>,
60}
61
62impl Default for BulletproofReader {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl BulletproofReader {
69 pub fn new() -> Self {
71 Self {
72 info: SSTableInfo::default(),
73 base_dir: PathBuf::new(),
74 decompressor: None,
75 data_reader: None,
76 }
77 }
78
79 pub fn open<P: AsRef<Path>>(sstable_path: P) -> Result<Self> {
84 let path = sstable_path.as_ref();
85
86 if let Err(e @ Error::UnsupportedVersion { .. }) = VersionGates::from_path(path) {
98 return Err(e);
99 }
100
101 let info = SSTableInfo::from_path(path)?;
102
103 let base_dir = path
104 .parent()
105 .ok_or_else(|| Error::InvalidPath("No parent directory".to_string()))?
106 .to_path_buf();
107
108 info!(
109 "Opening SSTable with bulletproof reader: format={:?}, generation={}, size={}, component={:?}, base={}",
110 info.format, info.generation_numeric().unwrap_or(0), info.size, info.component, info.base_name
111 );
112
113 let mut reader = Self {
114 info,
115 base_dir,
116 decompressor: None,
117 data_reader: None,
118 };
119
120 reader.initialize()?;
121 Ok(reader)
122 }
123
124 fn initialize(&mut self) -> Result<()> {
126 if self.info.format.supports_compression() {
128 if let Err(e) = self.setup_compression() {
129 warn!(
130 "Compression setup failed: {}, trying without compression",
131 e
132 );
133 }
134 }
135
136 self.open_data_file()?;
138
139 Ok(())
140 }
141
142 fn setup_compression(&mut self) -> Result<()> {
144 let compression_info_path = self
145 .info
146 .companion_path(SSTableComponent::CompressionInfo, &self.base_dir);
147
148 if compression_info_path.exists() {
149 debug!("Found CompressionInfo.db, setting up decompression");
150
151 let decompressor = create_decompressor_from_file(&compression_info_path)?;
152 self.decompressor = Some(decompressor);
153
154 debug!("Compression setup complete");
155 } else {
156 debug!("No CompressionInfo.db found, assuming uncompressed data");
157 }
158
159 Ok(())
160 }
161
162 fn open_data_file(&mut self) -> Result<()> {
164 let data_path = self
165 .info
166 .companion_path(SSTableComponent::Data, &self.base_dir);
167
168 if !data_path.exists() {
169 return Err(Error::InvalidPath(format!(
170 "Data.db file not found: {:?}",
171 data_path
172 )));
173 }
174
175 let file = File::open(&data_path).map_err(Error::Io)?;
176 let reader = BufReader::new(file);
177
178 self.data_reader = Some(reader);
179
180 debug!("Data.db file opened: {:?}", data_path);
181 Ok(())
182 }
183
184 pub fn read_raw_data(&mut self, offset: u64, length: usize) -> Result<Vec<u8>> {
188 let reader = self
189 .data_reader
190 .as_mut()
191 .ok_or_else(|| Error::InvalidState("Data reader not initialized".to_string()))?;
192
193 if let Some(decompressor) = &mut self.decompressor {
194 decompressor.read_data(reader, offset, length)
196 } else {
197 use std::io::{Read, Seek, SeekFrom};
199
200 reader.seek(SeekFrom::Start(offset)).map_err(Error::Io)?;
201
202 let mut buffer = vec![0u8; length];
203 reader.read_exact(&mut buffer).map_err(Error::Io)?;
204
205 Ok(buffer)
206 }
207 }
208
209 pub fn read_all_data(&mut self) -> Result<Vec<u8>> {
211 if let Some(decompressor) = &mut self.decompressor {
212 let reader = self
213 .data_reader
214 .as_mut()
215 .ok_or_else(|| Error::InvalidState("Data reader not initialized".to_string()))?;
216
217 decompressor.read_all_data(reader)
218 } else {
219 let reader = self
220 .data_reader
221 .as_mut()
222 .ok_or_else(|| Error::InvalidState("Data reader not initialized".to_string()))?;
223
224 use std::io::{Read, Seek, SeekFrom};
225
226 let current_pos = reader.stream_position().map_err(Error::Io)?;
228 let file_size = reader.seek(SeekFrom::End(0)).map_err(Error::Io)?;
229 reader
230 .seek(SeekFrom::Start(current_pos))
231 .map_err(Error::Io)?;
232
233 reader.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
235
236 let mut buffer = Vec::with_capacity(file_size as usize);
237 reader.read_to_end(&mut buffer).map_err(Error::Io)?;
238
239 Ok(buffer)
240 }
241 }
242
243 pub fn parse_sstable_data(&mut self) -> Result<Vec<SSTableEntry>> {
248 if let Err(e @ Error::UnsupportedVersion { .. }) =
260 VersionGates::from_path(Path::new(&format!("{}-Data.db", self.info.base_name)))
261 {
262 return Err(e);
263 }
264
265 let data = self.read_all_data()?;
266
267 info!(
268 "Parsing SSTable data ({} bytes) with format {:?}",
269 data.len(),
270 self.info.format
271 );
272
273 match &self.info.format {
274 SSTableFormat::V4x(_) | SSTableFormat::V5x(_) => self.parse_modern_format(&data),
275 SSTableFormat::V2x(version)
280 | SSTableFormat::V3x(version)
281 | SSTableFormat::Unknown(version) => Err(Error::UnsupportedFormat(format!(
282 "Unknown SSTable version: {}",
283 version
284 ))),
285 }
286 }
287
288 fn parse_modern_format(&self, data: &[u8]) -> Result<Vec<SSTableEntry>> {
299 warn!("EXPERIMENTAL: Parsing modern SSTable format with custom 'oa' format parsing");
300 warn!("This implementation may not fully align with Cassandra Big format specification");
301
302 if data.len() < 8 {
303 return Err(Error::InvalidFormat(
304 "Data too short for 'oa' format header".to_string(),
305 ));
306 }
307
308 let header = self.parse_oa_header(data)?;
310 debug!(
311 "Parsed 'oa' header: version={}, partition_count={}",
312 header.format_version, header.partition_count
313 );
314
315 let entries = self.parse_data_blocks(data, &header)?;
317
318 info!(
319 "Parsed {} entries from {} bytes using structured parsing",
320 entries.len(),
321 data.len()
322 );
323 Ok(entries)
324 }
325
326 pub fn parse_oa_header(&self, data: &[u8]) -> Result<OaFormatHeader> {
337 if data.len() < 32 {
338 return Err(Error::InvalidFormat(
339 "OA header must be exactly 32 bytes".to_string(),
340 ));
341 }
342
343 let header_data = &data[..32];
346
347 let magic = u32::from_be_bytes([
349 header_data[0],
350 header_data[1],
351 header_data[2],
352 header_data[3],
353 ]);
354 if magic != 0x6F61_0000 {
355 return Err(Error::InvalidFormat(format!(
356 "Invalid magic number: expected 0x6F61_0000, got 0x{:08x}",
357 magic
358 )));
359 }
360
361 let format_version = u16::from_be_bytes([header_data[4], header_data[5]]);
363 debug!("'oa' format version: {}", format_version);
364
365 if format_version != 1 {
367 return Err(Error::InvalidFormat(format!(
368 "Unsupported OA format version: {}. Only version 1 is supported.",
369 format_version
370 )));
371 }
372
373 let _flags = u32::from_be_bytes([
375 header_data[6],
376 header_data[7],
377 header_data[8],
378 header_data[9],
379 ]);
380
381 Ok(OaFormatHeader {
388 magic_number: magic,
389 format_version,
390 partition_count: 0, metadata_size: 0, header_size: 32, })
394 }
395
396 fn parse_data_blocks(&self, data: &[u8], header: &OaFormatHeader) -> Result<Vec<SSTableEntry>> {
398 let mut entries = Vec::new();
399 let mut offset = header.header_size;
400
401 if header.partition_count == 0 {
404 debug!("Header-only parsing mode - no data blocks to parse");
405 return Ok(entries);
406 }
407
408 debug!(
409 "Parsing {} partitions starting at offset {}",
410 header.partition_count, offset
411 );
412
413 for partition_idx in 0..header.partition_count {
414 if offset >= data.len() {
415 warn!(
416 "Reached end of data while parsing partition {}",
417 partition_idx
418 );
419 break;
420 }
421
422 match self.parse_partition_block(&data[offset..], partition_idx) {
423 Ok((entry, bytes_consumed)) => {
424 entries.push(entry);
425 offset += bytes_consumed;
426
427 if offset >= data.len() {
428 break;
429 }
430 }
431 Err(e) => {
432 warn!("Failed to parse partition {}: {}", partition_idx, e);
433 offset += 16; continue;
436 }
437 }
438 }
439
440 Ok(entries)
441 }
442
443 fn parse_partition_block(
445 &self,
446 data: &[u8],
447 partition_idx: u64,
448 ) -> Result<(SSTableEntry, usize)> {
449 if data.len() < 4 {
450 return Err(Error::InvalidFormat(
451 "Insufficient data for partition block".to_string(),
452 ));
453 }
454
455 let mut offset = 0;
456
457 let (key_length, vint_bytes) = self.read_vint(&data[offset..])?;
459 offset += vint_bytes;
460
461 if offset + key_length as usize > data.len() {
462 return Err(Error::InvalidFormat(
463 "Partition key extends beyond data".to_string(),
464 ));
465 }
466
467 let key_data = &data[offset..offset + key_length as usize];
469 offset += key_length as usize;
470
471 let (row_count, vint_bytes) = self.read_vint(&data[offset..])?;
473 offset += vint_bytes;
474
475 debug!(
476 "Partition {}: key_length={}, row_count={}",
477 partition_idx, key_length, row_count
478 );
479
480 let key_str = key_data
482 .iter()
483 .map(|b| format!("{:02x}", b))
484 .collect::<Vec<_>>()
485 .join("");
486
487 let entry = SSTableEntry {
491 key: crate::RowKey::from(key_data.to_vec()),
492 values: vec![crate::Value::Text(key_str.into())],
493 timestamp: Some(
494 std::time::SystemTime::now()
495 .duration_since(std::time::UNIX_EPOCH)
496 .unwrap()
497 .as_millis() as i64,
498 ),
499 generation: Some(self.info.generation_numeric().unwrap_or(0)),
500 format_info: format!("oa_format:partition={}", partition_idx),
501 };
502
503 Ok((entry, offset))
504 }
505
506 pub fn read_vint(&self, data: &[u8]) -> Result<(u64, usize)> {
508 match parse_vint(data) {
509 Ok((remaining, value)) => {
510 let bytes_consumed = data.len() - remaining.len();
511 Ok((value as u64, bytes_consumed))
514 }
515 Err(nom_error) => Err(Error::InvalidFormat(format!(
516 "VInt parsing failed: {:?}",
517 nom_error
518 ))),
519 }
520 }
521
522 #[allow(dead_code)]
524 fn read_varint(&self, data: &[u8]) -> Result<(u64, usize)> {
525 if data.is_empty() {
526 return Err(Error::InvalidFormat("Empty data for varint".to_string()));
527 }
528
529 let mut result = 0u64;
530 let mut shift = 0;
531 let mut bytes_read = 0;
532
533 for &byte in data {
534 bytes_read += 1;
535
536 if byte & 0x80 == 0 {
537 result |= (byte as u64) << shift;
539 break;
540 } else {
541 result |= ((byte & 0x7F) as u64) << shift;
543 shift += 7;
544
545 if shift >= 64 {
546 return Err(Error::InvalidFormat("Varint overflow".to_string()));
547 }
548 }
549 }
550
551 Ok((result, bytes_read))
552 }
553 pub fn info(&self) -> &SSTableInfo {
555 &self.info
556 }
557
558 pub fn compression_info(&self) -> Option<&CompressionInfo> {
560 self.decompressor.as_ref().map(|d| d.compression_info())
561 }
562
563 pub fn cache_stats(&self) -> Option<(usize, usize)> {
565 self.decompressor.as_ref().map(|d| d.cache_stats())
566 }
567
568 pub async fn get_header(&self) -> Result<crate::parser::header::SSTableHeader> {
570 Ok(crate::parser::header::SSTableHeader {
572 cassandra_version: match &self.info.format {
573 SSTableFormat::V5x(_) => crate::parser::header::CassandraVersion::V5_0Release,
574 SSTableFormat::V4x(_) => crate::parser::header::CassandraVersion::Legacy, SSTableFormat::V3x(_) => crate::parser::header::CassandraVersion::Legacy, SSTableFormat::V2x(_) => crate::parser::header::CassandraVersion::Legacy, SSTableFormat::Unknown(_) => crate::parser::header::CassandraVersion::V5_0Release,
578 },
579 version: 1,
580 table_id: [0; 16], keyspace: "unknown".to_string(),
582 table_name: "unknown".to_string(),
583 generation: self.info.generation_numeric().unwrap_or(0),
584 compression: crate::parser::header::CompressionInfo {
585 algorithm: "NONE".to_string(),
586 chunk_size: 65536,
587 parameters: std::collections::HashMap::new(),
588 },
589 stats: crate::parser::header::SSTableStats::default(),
590 columns: vec![],
591 properties: std::collections::HashMap::new(),
592 })
593 }
594
595 pub async fn stream_entries(&self) -> Result<SSTableEntryStream> {
597 let entries = self.parse_sstable_data_readonly()?;
598 Ok(SSTableEntryStream {
599 entries,
600 position: 0,
601 })
602 }
603
604 pub fn get_file_path(&self) -> &Path {
606 &self.base_dir
607 }
608
609 pub async fn verify_integrity(&self) -> Result<bool> {
611 match self.parse_sstable_data_readonly() {
613 Ok(_) => Ok(true),
614 Err(_) => Ok(false),
615 }
616 }
617
618 fn parse_sstable_data_readonly(&self) -> Result<Vec<SSTableEntry>> {
620 use std::fs::File;
622 use std::io::Read;
623
624 let data_file_path = self
625 .base_dir
626 .join(format!("{}-Data.db", self.info.base_name));
627 let mut file = File::open(&data_file_path)?;
628 let mut data = Vec::new();
629 file.read_to_end(&mut data)?;
630
631 self.parse_modern_format_readonly(&data)
633 }
634
635 fn parse_modern_format_readonly(&self, data: &[u8]) -> Result<Vec<SSTableEntry>> {
641 if data.len() < 8 {
642 return Err(Error::InvalidFormat(
643 "Data too short for 'oa' format".to_string(),
644 ));
645 }
646
647 match self.parse_oa_header(data) {
649 Ok(header) => self.parse_data_blocks(data, &header),
650 Err(_) => {
651 warn!("EXPERIMENTAL: 'oa' header parsing failed, using fallback");
653 warn!("Consider using spec-accurate readers for production");
654 Ok(Vec::new())
655 }
656 }
657 }
658}
659
660#[derive(Debug, Clone)]
667pub struct OaFormatHeader {
668 #[allow(dead_code)]
671 pub magic_number: u32,
672 pub format_version: u16,
674 partition_count: u64,
676 #[allow(dead_code)]
678 metadata_size: u64,
679 header_size: usize,
681}
682
683#[derive(Debug, Clone)]
685pub struct SSTableEntry {
686 pub key: crate::RowKey,
688 pub values: Vec<crate::Value>,
690 pub timestamp: Option<i64>,
692 pub generation: Option<u64>,
694 pub format_info: String,
696}
697
698pub struct SSTableEntryStream {
700 entries: Vec<SSTableEntry>,
701 position: usize,
702}
703
704impl SSTableEntryStream {
705 pub async fn next(&mut self) -> Result<Option<SSTableEntry>> {
707 if self.position < self.entries.len() {
708 let entry = self.entries[self.position].clone();
709 self.position += 1;
710 Ok(Some(entry))
711 } else {
712 Ok(None)
713 }
714 }
715}
716
717pub fn test_read_sstable_directory<P: AsRef<Path>>(dir_path: P) -> Result<()> {
719 let dir = dir_path.as_ref();
720
721 info!("Testing bulletproof SSTable reading in: {:?}", dir);
722
723 let entries = std::fs::read_dir(dir).map_err(Error::Io)?;
725
726 for entry in entries {
727 let entry = entry.map_err(Error::Io)?;
728 let path = entry.path();
729
730 if path
731 .file_name()
732 .and_then(|s| s.to_str())
733 .map(|s| s.ends_with("-Data.db"))
734 .unwrap_or(false)
735 {
736 debug!("Testing SSTable: {:?}", path);
737
738 match BulletproofReader::open(path) {
739 Ok(mut reader) => {
740 info!("Successfully opened SSTable");
741
742 if let Some(compression_info) = reader.compression_info() {
743 debug!("Compression: {}", compression_info.algorithm);
744 debug!("Chunk size: {} bytes", compression_info.chunk_length);
745 }
746
747 match reader.read_raw_data(0, 1024) {
749 Ok(data) => {
750 debug!("Read {} bytes successfully", data.len());
751 debug!(
752 "First 32 bytes: {:02x?}",
753 &data[..std::cmp::min(32, data.len())]
754 );
755
756 match reader.parse_sstable_data() {
758 Ok(entries) => {
759 info!("Parsed {} entries", entries.len());
760 for (i, entry) in entries.iter().take(3).enumerate() {
761 debug!(
762 "Entry {}: key='{:?}' ({})",
763 i, entry.key, entry.format_info
764 );
765 }
766 }
767 Err(e) => {
768 warn!("Parsing failed (this is expected for now): {}", e);
769 }
770 }
771 }
772 Err(e) => {
773 warn!("Failed to read data: {}", e);
774 }
775 }
776 }
777 Err(e) => {
778 warn!("Failed to open SSTable: {}", e);
779 }
780 }
781 }
782 }
783
784 Ok(())
785}
786
787#[cfg(test)]
788mod tests {
789 use super::*;
790
791 #[test]
792 fn test_vint_reading() -> Result<()> {
793 let reader = BulletproofReader {
794 info: SSTableInfo::from_path(&std::path::PathBuf::from("nb-1-big-Data.db")).unwrap(),
795 base_dir: std::path::PathBuf::new(),
796 decompressor: None,
797 data_reader: None,
798 };
799
800 let data = [0x0A]; let (value, bytes_read) = reader.read_vint(&data)?;
804 assert_eq!(value, 5);
805 assert_eq!(bytes_read, 1);
806
807 let data = [0x81, 0x00]; let (value, bytes_read) = reader.read_vint(&data)?;
811 assert_eq!(value, 128);
812 assert_eq!(bytes_read, 2);
813
814 let data = [0x80, 0x01]; let (value, bytes_read) = reader.read_varint(&data)?;
817 assert_eq!(value, 128);
818 assert_eq!(bytes_read, 2);
819 Ok(())
820 }
821
822 #[test]
828 fn test_below_floor_rejected_before_read() {
829 let info = SSTableInfo::from_path(&std::path::PathBuf::from("ma-1-big-Data.db")).unwrap();
830 assert!(matches!(info.format, SSTableFormat::V3x(_)));
831
832 let mut reader = BulletproofReader {
833 info,
834 base_dir: std::path::PathBuf::from("/nonexistent/cqlite-1249"),
837 decompressor: None,
838 data_reader: None,
839 };
840
841 match reader.parse_sstable_data() {
842 Err(Error::UnsupportedVersion { version, floor }) => {
843 assert_eq!(version, "ma");
844 assert_eq!(floor, "na");
845 }
846 other => panic!("expected UnsupportedVersion, got {:?}", other),
847 }
848 }
849
850 #[test]
855 fn test_open_below_floor_rejected_with_body_present() {
856 let dir = std::env::temp_dir().join(format!("cqlite-1249-{}", std::process::id()));
857 std::fs::create_dir_all(&dir).unwrap();
858 let data_path = dir.join("ma-1-big-Data.db");
859 std::fs::write(&data_path, b"not a valid sstable body").unwrap();
861
862 let result = BulletproofReader::open(&data_path);
863 let _ = std::fs::remove_dir_all(&dir);
864
865 match result {
866 Err(Error::UnsupportedVersion { version, floor }) => {
867 assert_eq!(version, "ma");
868 assert_eq!(floor, "na");
869 }
870 other => panic!("expected UnsupportedVersion, got {:?}", other.map(|_| ())),
871 }
872 }
873
874 #[test]
882 fn test_below_floor_unknown_la_rejected_before_read() {
883 let info = SSTableInfo::from_path(&std::path::PathBuf::from("la-1-big-Data.db")).unwrap();
884 assert!(matches!(info.format, SSTableFormat::Unknown(ref v) if v == "la"));
887
888 let mut reader = BulletproofReader {
889 info,
890 base_dir: std::path::PathBuf::from("/nonexistent/cqlite-1249-la"),
891 decompressor: None,
892 data_reader: None,
893 };
894
895 match reader.parse_sstable_data() {
896 Err(Error::UnsupportedVersion { version, floor }) => {
897 assert_eq!(version, "la");
898 assert_eq!(floor, "na");
899 }
900 other => panic!("expected UnsupportedVersion for la, got {:?}", other),
901 }
902 }
903
904 #[test]
909 fn test_open_below_floor_unknown_la_rejected_with_body_present() {
910 let dir = std::env::temp_dir().join(format!("cqlite-1249-la-{}", std::process::id()));
911 std::fs::create_dir_all(&dir).unwrap();
912 let data_path = dir.join("la-1-big-Data.db");
913 std::fs::write(&data_path, b"not a valid sstable body").unwrap();
915
916 let result = BulletproofReader::open(&data_path);
917 let _ = std::fs::remove_dir_all(&dir);
918
919 match result {
920 Err(Error::UnsupportedVersion { version, floor }) => {
921 assert_eq!(version, "la");
922 assert_eq!(floor, "na");
923 }
924 other => panic!(
925 "expected UnsupportedVersion for la, got {:?}",
926 other.map(|_| ())
927 ),
928 }
929 }
930}