Skip to main content

cqlite_core/storage/sstable/
bulletproof_reader.rs

1//! Bulletproof SSTable reader with universal format support
2//!
3//! # DEPRECATED - DO NOT USE IN PRODUCTION
4//!
5//! This module is DEPRECATED for production use. Use `SSTableReader` instead.
6//!
7//! **Deprecation Notice (Issue #190):**
8//! - This reader is marked EXPERIMENTAL and should not be used in production code paths
9//! - For production use, prefer `crate::storage::sstable::reader::SSTableReader`
10//! - This module is retained only for testing and legacy compatibility purposes
11//!
12//! ⚠️  **EXPERIMENTAL WARNING for Modern Formats (4.x/5.x)**
13//!
14//! The 'oa' format parsing implementation in this module is EXPERIMENTAL and
15//! based on reverse engineering. It may not fully align with the official
16//! Cassandra Big format specification (CEP-25). For production use with modern
17//! formats, prefer the spec-accurate readers:
18//!
19//! - `crate::storage::sstable::reader::SSTableReader` - Production-ready spec-accurate reader
20//! - `row_cell_state_machine.rs` - Implements schema-driven parsing without heuristics
21//! - Follows exact Cassandra specification for BIG format row/cell parsing
22//! - Eliminates type guessing in favor of schema-aware decoding
23
24// Allow deprecated warnings within this module since the entire module is deprecated
25#![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/// Bulletproof SSTable reader with automatic format detection
42///
43/// # Deprecated
44///
45/// This reader is DEPRECATED for production use (Issue #190).
46/// Use `crate::storage::sstable::reader::SSTableReader` instead.
47#[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    /// SSTable information (format, generation, etc.)
53    info: SSTableInfo,
54    /// Base directory containing SSTable files
55    base_dir: PathBuf,
56    /// Chunk decompressor (if compression is used)
57    decompressor: Option<ChunkDecompressor>,
58    /// Data file reader
59    data_reader: Option<BufReader<File>>,
60}
61
62impl Default for BulletproofReader {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl BulletproofReader {
69    /// Create a new bulletproof reader with default settings (for testing)
70    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    /// Create a new bulletproof reader from any SSTable file path
80    ///
81    /// This will automatically detect the format version and set up
82    /// proper compression handling if needed.
83    pub fn open<P: AsRef<Path>>(sstable_path: P) -> Result<Self> {
84        let path = sstable_path.as_ref();
85
86        // #1249: reject ALL below-floor versions BEFORE any initialization or
87        // file-body read, using the SAME authoritative gate the production
88        // readers use (`reader/mod.rs::open_inner`, `statistics_reader.rs::open`).
89        // `VersionGates::from_path` derives the version from the filename alone
90        // (no I/O) and rejects every pre-`na` BIG (`la`/`ic`/`jb`/`ma`–`me`) and
91        // non-`da` BTI descriptor with a typed `UnsupportedVersion` naming the
92        // supported floor — including below-floor versions that
93        // `SSTableInfo::from_path` classifies as `Unknown` (e.g. `la`), which the
94        // old `V2x`/`V3x` format match silently bypassed. A structurally-
95        // unparseable descriptor falls through to current behaviour (it is not
96        // made fatal); the floor only fires on a *parsed* below-floor version.
97        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    /// Initialize the reader by setting up compression and opening files
125    fn initialize(&mut self) -> Result<()> {
126        // Set up compression if the format supports it
127        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        // Open the Data.db file
137        self.open_data_file()?;
138
139        Ok(())
140    }
141
142    /// Set up compression by reading CompressionInfo.db if it exists
143    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    /// Open the Data.db file for reading
163    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    /// Read raw data from the SSTable at specified offset and length
185    ///
186    /// This automatically handles compression if present
187    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            // Use chunk-based decompression
195            decompressor.read_data(reader, offset, length)
196        } else {
197            // Read directly from uncompressed file
198            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    /// Read the entire SSTable data (for debugging)
210    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            // Get file size
227            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            // Read entire file
234            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    /// Parse SSTable data using format-specific parser
244    ///
245    /// This is where we'll implement the actual SSTable parsing
246    /// based on the detected format version
247    pub fn parse_sstable_data(&mut self) -> Result<Vec<SSTableEntry>> {
248        // #1249: reject ALL below-floor versions BEFORE reading any row bytes,
249        // using the same authoritative gate as `open` and the production
250        // readers. `open` already rejects these, but readers built via `new()`
251        // reach here directly, so re-derive from the same descriptor (the
252        // `<version>-<id>-<format>` `base_name` plus the Data component). This
253        // catches every pre-`na` BIG (`la`/`ma`–`me`) and non-`da` BTI
254        // version — including ones classified as `Unknown` (e.g. `la`) that the
255        // old `V2x`/`V3x` format match silently bypassed — with a typed
256        // `UnsupportedVersion` before `read_all_data()`. A structurally-
257        // unparseable descriptor is NOT made fatal: it falls through to the
258        // format-family match below.
259        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            // Below-floor V2x/V3x (and below-floor Unknown like `la`) are
276            // rejected via VersionGates above before any read, so they never
277            // reach this match. An above-floor Unknown (e.g. `nc`/`ob`/`pa`,
278            // tracked separately in #1297) still surfaces UnsupportedFormat.
279            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    /// Parse modern SSTable format (4.x, 5.x) with EXPERIMENTAL 'oa' format parsing
289    ///
290    /// ⚠️ WARNING: EXPERIMENTAL IMPLEMENTATION
291    /// This 'oa' format parser is experimental and may not fully align with
292    /// the official Cassandra Big format specification. For production use,
293    /// prefer the spec-accurate readers in row_cell_state_machine.rs which
294    /// implement schema-driven parsing without heuristics.
295    ///
296    /// TODO: Align with CEP-25 Big format specification or deprecate in favor
297    /// of the spec-accurate state machine implementation.
298    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        // Parse the 'oa' format header
309        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        // Parse data blocks following the header
316        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    /// Parse Cassandra 'oa' format header (EXPERIMENTAL)
327    ///
328    /// ⚠️ EXPERIMENTAL: This header parsing implementation is based on
329    /// reverse engineering and may not match the official Cassandra Big
330    /// format specification. The magic number check and field interpretations
331    /// should be verified against CEP-25 specification.
332    ///
333    /// This function strictly parses only the 32-byte header portion as per
334    /// the Cassandra SSTable format specification, handling oversized input
335    /// by reading only the first 32 bytes.
336    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        // For header size compliance, we only read the first 32 bytes
344        // This ensures oversized input is handled correctly by ignoring extra data
345        let header_data = &data[..32];
346
347        // Read magic number (first 4 bytes) - should be 0x6F61_0000 for 'oa' format
348        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        // Read format version (next 2 bytes, big-endian)
362        let format_version = u16::from_be_bytes([header_data[4], header_data[5]]);
363        debug!("'oa' format version: {}", format_version);
364
365        // Validate version - only version 1 is supported
366        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        // Read flags (4 bytes, big-endian)
374        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        // The remaining bytes (10-31) are reserved and should be zero per spec
382        // We don't validate they are zero to maintain compatibility, but we acknowledge them
383
384        // Return header with basic structure - the 32-byte header is now fully parsed
385        // Additional metadata parsing (like partition count) should be done separately
386        // when actually parsing the SSTable content, not during header validation
387        Ok(OaFormatHeader {
388            magic_number: magic,
389            format_version,
390            partition_count: 0, // Will be populated during full SSTable parsing
391            metadata_size: 0,   // Will be populated during full SSTable parsing
392            header_size: 32,    // Fixed header size per specification
393        })
394    }
395
396    /// Parse data blocks following the 'oa' header
397    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 we're only doing header compliance testing (partition_count = 0),
402        // we don't need to parse actual data blocks
403        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                    // Try to advance by a reasonable amount to recover
434                    offset += 16; // Skip forward and try next potential partition
435                    continue;
436                }
437            }
438        }
439
440        Ok(entries)
441    }
442
443    /// Parse a single partition block
444    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        // Read partition key length using VInt
458        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        // Read partition key
468        let key_data = &data[offset..offset + key_length as usize];
469        offset += key_length as usize;
470
471        // Read row count using VInt
472        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        // Format key data as hex string without type assumptions
481        let key_str = key_data
482            .iter()
483            .map(|b| format!("{:02x}", b))
484            .collect::<Vec<_>>()
485            .join("");
486
487        // Skip row data for now (would need more complex parsing)
488        // For each row, we'd need to parse clustering keys, column data, etc.
489
490        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    /// Read Variable Length Integer (VInt) from data using Cassandra format
507    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                // VInts in SSTable can be negative (ZigZag encoded), but we return as u64
512                // The caller needs to interpret the sign appropriately
513                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    /// Read legacy varint format for backwards compatibility
523    #[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                // Most significant bit is 0, this is the last byte
538                result |= (byte as u64) << shift;
539                break;
540            } else {
541                // Most significant bit is 1, more bytes follow
542                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    /// Get information about the SSTable
554    pub fn info(&self) -> &SSTableInfo {
555        &self.info
556    }
557
558    /// Get compression information if available
559    pub fn compression_info(&self) -> Option<&CompressionInfo> {
560        self.decompressor.as_ref().map(|d| d.compression_info())
561    }
562
563    /// Get cache statistics if compression is enabled
564    pub fn cache_stats(&self) -> Option<(usize, usize)> {
565        self.decompressor.as_ref().map(|d| d.cache_stats())
566    }
567
568    /// Get header information (for compatibility)
569    pub async fn get_header(&self) -> Result<crate::parser::header::SSTableHeader> {
570        // Return a basic header based on format detection
571        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, // Use Legacy for V4
575                SSTableFormat::V3x(_) => crate::parser::header::CassandraVersion::Legacy, // Use Legacy for V3
576                SSTableFormat::V2x(_) => crate::parser::header::CassandraVersion::Legacy, // Use Legacy for V2
577                SSTableFormat::Unknown(_) => crate::parser::header::CassandraVersion::V5_0Release,
578            },
579            version: 1,
580            table_id: [0; 16], // Placeholder
581            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    /// Stream entries from the SSTable (for compatibility)
596    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    /// Get file path for the SSTable
605    pub fn get_file_path(&self) -> &Path {
606        &self.base_dir
607    }
608
609    /// Verify integrity of the SSTable
610    pub async fn verify_integrity(&self) -> Result<bool> {
611        // Basic integrity check - try to parse the data
612        match self.parse_sstable_data_readonly() {
613            Ok(_) => Ok(true),
614            Err(_) => Ok(false),
615        }
616    }
617
618    /// Parse SSTable data without mutable access (for compatibility)
619    fn parse_sstable_data_readonly(&self) -> Result<Vec<SSTableEntry>> {
620        // Read the data file directly
621        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        // Parse using the existing logic but with dummy values for new fields
632        self.parse_modern_format_readonly(&data)
633    }
634
635    /// Parse modern format without mutable access using EXPERIMENTAL 'oa' format
636    ///
637    /// ⚠️ EXPERIMENTAL: This readonly parsing is experimental and should be
638    /// replaced with the spec-accurate row_cell_state_machine implementation
639    /// for production use.
640    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        // Parse using the EXPERIMENTAL 'oa' format (may not align with Big format spec)
648        match self.parse_oa_header(data) {
649            Ok(header) => self.parse_data_blocks(data, &header),
650            Err(_) => {
651                // Fallback to basic parsing if header parsing fails
652                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/// EXPERIMENTAL Cassandra 'oa' format header structure
661///
662/// ⚠️ WARNING: This structure is based on reverse engineering and may not
663/// accurately represent the official Cassandra Big format header as specified
664/// in CEP-25. Field interpretations and byte layouts should be verified
665/// against the official specification.
666#[derive(Debug, Clone)]
667pub struct OaFormatHeader {
668    /// Magic number (EXPERIMENTAL: assumed to be 0x6F61_0000)
669    /// TODO: Verify against CEP-25 Big format specification
670    #[allow(dead_code)]
671    pub magic_number: u32,
672    /// Format version (interpretation may not match Big format spec)
673    pub format_version: u16,
674    /// Number of partitions in this SSTable (experimental field interpretation)
675    partition_count: u64,
676    /// Size of metadata section (experimental field interpretation)
677    #[allow(dead_code)]
678    metadata_size: u64,
679    /// Total header size in bytes
680    header_size: usize,
681}
682
683/// Parsed SSTable entry
684#[derive(Debug, Clone)]
685pub struct SSTableEntry {
686    /// Row key
687    pub key: crate::RowKey,
688    /// Column values
689    pub values: Vec<crate::Value>,
690    /// Write timestamp
691    pub timestamp: Option<i64>,
692    /// Generation number
693    pub generation: Option<u64>,
694    /// Format-specific information
695    pub format_info: String,
696}
697
698/// Stream of SSTable entries for iterating over large datasets
699pub struct SSTableEntryStream {
700    entries: Vec<SSTableEntry>,
701    position: usize,
702}
703
704impl SSTableEntryStream {
705    /// Get the next entry from the stream
706    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
717/// Utility function to test reading an SSTable directory
718pub 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    // Find Data.db files
724    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                    // Try to read first 1KB of data
748                    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                            // Try to parse the data
757                            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        // Test simple VInt (single byte)
801        // Value 5 ZigZag-encodes to 10 (0x0A)
802        let data = [0x0A]; // Value 5 in ZigZag VInt encoding
803        let (value, bytes_read) = reader.read_vint(&data)?;
804        assert_eq!(value, 5);
805        assert_eq!(bytes_read, 1);
806
807        // Test multi-byte VInt
808        // Value 128 ZigZag-encodes to 256, which needs [0x81, 0x00]
809        let data = [0x81, 0x00]; // Value 128 in ZigZag VInt encoding (256 raw -> 128 decoded)
810        let (value, bytes_read) = reader.read_vint(&data)?;
811        assert_eq!(value, 128);
812        assert_eq!(bytes_read, 2);
813
814        // Test legacy varint for backwards compatibility
815        let data = [0x80, 0x01]; // Value 128 in legacy varint
816        let (value, bytes_read) = reader.read_varint(&data)?;
817        assert_eq!(value, 128);
818        assert_eq!(bytes_read, 2);
819        Ok(())
820    }
821
822    /// #1249: a below-floor format (Cassandra 3.x BIG `ma`) must yield the typed
823    /// `UnsupportedVersion` error from `parse_sstable_data` BEFORE any row bytes
824    /// are read. The reader points at a non-existent Data.db: if the rejection
825    /// happened after `read_all_data()` we would instead get an IO/parse error,
826    /// so observing `UnsupportedVersion` proves no body read occurred.
827    #[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            // Intentionally bogus base dir + no data_reader so that any attempt
835            // to read the body would fail loudly with a non-version error.
836            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    /// #1249: `open` rejects a below-floor descriptor before initialization,
851    /// even when the Data.db body exists (here a truncated/garbage body). The
852    /// typed `UnsupportedVersion` must surface rather than a parse/corruption
853    /// error from reading the body.
854    #[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        // Non-empty, non-'oa' body: would error during parsing if ever read.
860        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    /// #1249 regression: a below-floor BIG descriptor that `SSTableInfo`
875    /// classifies as `SSTableFormat::Unknown` (Cassandra 2.x `la`) must STILL
876    /// be rejected by the floor. The old `V2x`/`V3x` format-match guard let
877    /// `la` (Unknown) bypass the floor entirely; routing through the
878    /// authoritative `VersionGates` rejects it via `BigVersionGates`'s `< na`
879    /// gate. We point at a non-existent Data.db so any post-read code path
880    /// would surface an IO/parse error instead of `UnsupportedVersion`.
881    #[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        // `la` is NOT classified as V2x/V3x — it falls into Unknown, which the
885        // old guard did not catch. This assertion documents that gap.
886        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    /// #1249 regression: `open` rejects a below-floor `Unknown`-classified BIG
905    /// descriptor (`la`) before initialization, even when a (garbage) Data.db
906    /// body exists on disk. Proves pre-read rejection via the authoritative
907    /// gate rather than a parse/corruption error from reading the body.
908    #[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        // Garbage body: would error during parsing if it were ever read.
914        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}