1use crate::block_handle::BlockHandle;
2use crate::data_block::{DataBlock, DataBlockReader};
3use crate::error::{Error, Result};
4use crate::footer::Footer;
5use crate::types::CompressionType;
6use std::fs::File;
7use std::io::{BufReader, Read, Seek, SeekFrom};
8use std::path::Path;
9
10pub struct SstReader {
11 reader: BufReader<File>,
12 footer: Footer,
13 file_size: u64,
14}
15
16impl SstReader {
17 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
18 let file = File::open(path)?;
19 let mut reader = BufReader::new(file);
20
21 let file_size = reader.seek(std::io::SeekFrom::End(0))?;
22 reader.seek(std::io::SeekFrom::Start(0))?;
23
24 let footer = Footer::read_from(&mut reader)?;
25
26 Ok(SstReader {
27 reader,
28 file_size,
29 footer,
30 })
31 }
32
33 pub fn get_footer(&self) -> &Footer {
34 &self.footer
35 }
36
37 pub fn file_size(&self) -> u64 {
38 self.file_size
39 }
40
41 pub(crate) fn read_block(&mut self, handle: BlockHandle) -> Result<Vec<u8>> {
42 if handle.offset + handle.size > self.file_size {
43 return Err(Error::InvalidBlockHandle(
44 "Block extends beyond file size".to_string(),
45 ));
46 }
47
48 self.reader.seek(SeekFrom::Start(handle.offset))?;
49 let mut buffer = vec![0u8; handle.size as usize];
50 self.reader.read_exact(&mut buffer)?;
51 Ok(buffer)
52 }
53
54 pub fn read_data_block(
55 &mut self,
56 handle: BlockHandle,
57 compression_type: CompressionType,
58 ) -> Result<DataBlock> {
59 let block_data = self.read_block(handle)?;
60 DataBlock::new(&block_data, compression_type)
61 }
62
63 pub fn read_data_block_reader(
64 &mut self,
65 handle: BlockHandle,
66 compression_type: CompressionType,
67 ) -> Result<DataBlockReader> {
68 let block_data = self.read_block(handle)?;
69 DataBlockReader::new(&block_data, compression_type)
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use crate::types::ChecksumType;
76
77 use super::*;
78 use std::path::PathBuf;
79
80 fn fixture_path(version: u32, checksum: &str, compression: &str) -> PathBuf {
81 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
82 path.push("fixtures");
83 path.push("sst_files");
84 path.push(format!("v{}", version));
85 path.push(format!("v{}_{}_{}.sst", version, checksum, compression));
86 path
87 }
88
89 #[test]
90 fn test_open_nonexistent_file() {
91 let result = SstReader::open("nonexistent.sst");
92 assert!(result.is_err());
93 }
94
95 #[test]
96 fn test_format_v5_crc32_snappy() -> Result<()> {
97 let path = fixture_path(5, "crc32c", "snappy");
98 let reader = SstReader::open(&path)?;
99
100 assert_eq!(reader.get_footer().checksum_type, ChecksumType::CRC32c);
101 assert_eq!(reader.get_footer().format_version, 5);
102
103 assert_eq!(
107 reader.get_footer().metaindex_handle.offset,
108 1470,
109 "First parsed handle (metaindex) offset should be 1470"
110 );
111 assert_eq!(
112 reader.get_footer().metaindex_handle.size,
113 80,
114 "First parsed handle (metaindex) size should be 80"
115 );
116 assert_eq!(
117 reader.get_footer().index_handle.offset,
118 456,
119 "Second parsed handle (index) offset should be 456"
120 );
121 assert_eq!(
122 reader.get_footer().index_handle.size,
123 19,
124 "Second parsed handle (index) size should be 19"
125 );
126 assert!(reader.get_footer().base_context_checksum.is_none());
128
129 Ok(())
130 }
131
132 #[test]
133 fn test_format_v5_xxh64_snappy() -> Result<()> {
134 let path = fixture_path(5, "xxhash64", "snappy");
135 let reader = SstReader::open(&path)?;
136
137 assert_eq!(reader.get_footer().checksum_type, ChecksumType::Hash64);
138 assert_eq!(reader.get_footer().format_version, 5);
139
140 assert_eq!(
143 reader.get_footer().metaindex_handle.offset,
144 1471,
145 "First parsed handle (metaindex) offset should be 1471"
146 );
147 assert_eq!(
148 reader.get_footer().metaindex_handle.size,
149 80,
150 "First parsed handle (metaindex) size should be 80"
151 );
152 assert_eq!(
153 reader.get_footer().index_handle.offset,
154 457,
155 "Second parsed handle (index) offset should be 457"
156 );
157 assert_eq!(
158 reader.get_footer().index_handle.size,
159 19,
160 "Second parsed handle (index) size should be 19"
161 );
162 assert!(reader.get_footer().base_context_checksum.is_none());
164
165 Ok(())
166 }
167
168 #[test]
169 fn test_format_v7_crc32_snappy() -> Result<()> {
170 let path = fixture_path(7, "crc32c", "snappy");
171 let reader = SstReader::open(&path)?;
172
173 assert_eq!(reader.get_footer().checksum_type, ChecksumType::CRC32c);
174 assert_eq!(reader.get_footer().format_version, 7);
175
176 assert_eq!(
178 reader.get_footer().metaindex_handle.offset,
179 1477,
180 "Metaindex offset should be 1477 (from SST dump)"
181 );
182 assert_eq!(
183 reader.get_footer().metaindex_handle.size,
184 103,
185 "Metaindex size should be 103 (from SST dump)"
186 );
187 assert_eq!(
189 reader.get_footer().index_handle.offset,
190 0,
191 "Index offset should be 0 (from SST dump)"
192 );
193 assert_eq!(
194 reader.get_footer().index_handle.size,
195 0,
196 "Index size should be 0 (from SST dump)"
197 );
198 assert!(reader.get_footer().base_context_checksum.is_some());
200
201 Ok(())
202 }
203
204 #[test]
205 fn test_format_v6_xxh64_snappy() -> Result<()> {
206 let path = fixture_path(6, "xxhash64", "snappy");
207 let reader = SstReader::open(&path)?;
208
209 assert_eq!(reader.get_footer().checksum_type, ChecksumType::Hash64);
210 assert_eq!(reader.get_footer().format_version, 6);
211
212 assert_eq!(
215 reader.get_footer().metaindex_handle.offset,
216 1471,
217 "Metaindex offset should be 1471 (from SST dump)"
218 );
219 assert_eq!(
220 reader.get_footer().metaindex_handle.size,
221 103,
222 "Metaindex size should be 103 (from SST dump)"
223 );
224 assert_eq!(
226 reader.get_footer().index_handle.offset,
227 0,
228 "Index offset should be 0 (from SST dump)"
229 );
230 assert_eq!(
231 reader.get_footer().index_handle.size,
232 0,
233 "Index size should be 0 (from SST dump)"
234 );
235 assert!(reader.get_footer().base_context_checksum.is_some());
237
238 Ok(())
239 }
240
241 #[test]
242 fn test_format_v7_xxhash_snappy() -> Result<()> {
243 let path = fixture_path(7, "xxhash", "snappy");
244 let reader = SstReader::open(&path)?;
245
246 assert_eq!(reader.get_footer().checksum_type, ChecksumType::Hash);
247 assert_eq!(reader.get_footer().format_version, 7);
248
249 assert_eq!(
251 reader.get_footer().metaindex_handle.offset,
252 1477,
253 "Metaindex offset should be 1477 (from SST dump)"
254 );
255 assert_eq!(
256 reader.get_footer().metaindex_handle.size,
257 103,
258 "Metaindex size should be 103 (from SST dump)"
259 );
260 assert_eq!(
262 reader.get_footer().index_handle.offset,
263 0,
264 "Index offset should be 0 (from SST dump)"
265 );
266 assert_eq!(
267 reader.get_footer().index_handle.size,
268 0,
269 "Index size should be 0 (from SST dump)"
270 );
271 assert!(reader.get_footer().base_context_checksum.is_some());
273
274 Ok(())
275 }
276
277 #[test]
278 fn test_format_v7_xxh64_snappy() -> Result<()> {
279 let path = fixture_path(7, "xxhash64", "snappy");
280 let reader = SstReader::open(&path)?;
281
282 assert_eq!(reader.get_footer().checksum_type, ChecksumType::Hash64);
283 assert_eq!(reader.get_footer().format_version, 7);
284
285 assert_eq!(
288 reader.get_footer().metaindex_handle.offset,
289 1478,
290 "Metaindex offset should be 1478 (from SST dump)"
291 );
292 assert_eq!(
293 reader.get_footer().metaindex_handle.size,
294 103,
295 "Metaindex size should be 103 (from SST dump)"
296 );
297 assert_eq!(
299 reader.get_footer().index_handle.offset,
300 0,
301 "Index offset should be 0 (from SST dump)"
302 );
303 assert_eq!(
304 reader.get_footer().index_handle.size,
305 0,
306 "Index size should be 0 (from SST dump)"
307 );
308 assert!(reader.get_footer().base_context_checksum.is_some());
310
311 Ok(())
312 }
313
314 }