Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/sstable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand All @@ -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)?;
Expand Down
48 changes: 48 additions & 0 deletions tests/sstable_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>) -> Result<(), cass::storage::StorageError> {
self.0.put(path, data).await
}
async fn get(&self, path: &str) -> Result<Vec<u8>, 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<Vec<String>, 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);
}
Loading