diff --git a/src/sstable.rs b/src/sstable.rs index 3890a81..d865d05 100644 --- a/src/sstable.rs +++ b/src/sstable.rs @@ -181,13 +181,17 @@ impl SsTable { if !self.zone_map.contains(key) || !self.bloom.may_contain(key) { return Ok(None); } - let offset = self.seek_offset(key); + let offset = self.seek_offset(key) as usize; if let Some(root) = storage.local_path() { let path = root.join(&self.path); let file = File::open(path)?; // memory-map the file for efficient slicing let mmap = unsafe { MmapOptions::new().map(&file)? }; - if let Some(enc) = Self::scan_from(&mmap[offset as usize..], key) { + // The sparse index comes from a separately stored meta file; an + // offset beyond the data file means the index is stale or + // corrupt, so fall back to scanning the whole table. + let slice = mmap.get(offset..).unwrap_or(&mmap[..]); + if let Some(enc) = Self::scan_from(slice, key) { let val = STANDARD .decode(enc) .map_err(std::io::Error::other)?; @@ -197,7 +201,8 @@ impl SsTable { } // fallback to reading entire file from storage let raw = storage.get(&self.path).await?; - if let Some(enc) = Self::scan_from(&raw[offset as usize..], key) { + let slice = raw.get(offset..).unwrap_or(&raw[..]); + if let Some(enc) = Self::scan_from(slice, key) { let val = STANDARD .decode(enc) .map_err(std::io::Error::other)?; diff --git a/tests/sstable_test.rs b/tests/sstable_test.rs index b3fa8b1..a8d753e 100644 --- a/tests/sstable_test.rs +++ b/tests/sstable_test.rs @@ -23,3 +23,51 @@ async fn sstable_roundtrip() { .collect(); assert_eq!(keys, vec!["a", "b", "c"]); } + +#[tokio::test] +async fn out_of_bounds_index_offset_does_not_panic() { + let dir = tempfile::tempdir().unwrap(); + let storage = LocalStorage::new(dir.path()); + let entries = vec![ + ("a".to_string(), b"1".to_vec()), + ("b".to_string(), b"2".to_vec()), + ]; + let mut table = SsTable::create("table", &entries, &storage).await.unwrap(); + // Simulate a corrupt/stale sparse index pointing past the end of the + // data file. Lookups must not panic and must still find existing keys + // by falling back to a full scan. + table.index = vec![("a".to_string(), u64::MAX), ("b".to_string(), 1 << 40)]; + assert_eq!(table.get("a", &storage).await.unwrap(), Some(b"1".to_vec())); + assert_eq!(table.get("b", &storage).await.unwrap(), Some(b"2".to_vec())); + assert_eq!(table.get("zz", &storage).await.unwrap(), None); +} + +#[tokio::test] +async fn out_of_bounds_index_offset_remote_storage_does_not_panic() { + struct NoLocal(LocalStorage); + + #[async_trait::async_trait] + impl Storage for NoLocal { + async fn put(&self, path: &str, data: Vec) -> Result<(), cass::storage::StorageError> { + self.0.put(path, data).await + } + async fn get(&self, path: &str) -> Result, cass::storage::StorageError> { + self.0.get(path).await + } + async fn append(&self, path: &str, data: &[u8]) -> Result<(), cass::storage::StorageError> { + self.0.append(path, data).await + } + async fn list(&self, prefix: &str) -> Result, cass::storage::StorageError> { + self.0.list(prefix).await + } + } + + let dir = tempfile::tempdir().unwrap(); + // Hide the local path so `get` exercises the remote-storage fallback. + let storage = NoLocal(LocalStorage::new(dir.path())); + let entries = vec![("a".to_string(), b"1".to_vec())]; + let mut table = SsTable::create("table", &entries, &storage).await.unwrap(); + table.index = vec![("a".to_string(), u64::MAX)]; + assert_eq!(table.get("a", &storage).await.unwrap(), Some(b"1".to_vec())); + assert_eq!(table.get("zz", &storage).await.unwrap(), None); +}