diff --git a/crates/core/src/io/mod.rs b/crates/core/src/io/mod.rs index 77c88bd..1ddbcc4 100644 --- a/crates/core/src/io/mod.rs +++ b/crates/core/src/io/mod.rs @@ -18,4 +18,5 @@ pub mod cache_util; pub mod index_cache; pub mod line_sampler; pub mod progressive_reader; +pub mod read_cache; pub mod wrap; diff --git a/crates/core/src/io/read_cache.rs b/crates/core/src/io/read_cache.rs index 2df7c01..e3ae08a 100644 --- a/crates/core/src/io/read_cache.rs +++ b/crates/core/src/io/read_cache.rs @@ -56,9 +56,15 @@ impl LruReadCache { /// on a hit, or fills a cache slot on a miss. Cross-block reads go through /// the spill buffer and are not cached. pub fn get(&mut self, file: &File, offset: u64, len: usize) -> io::Result<&[u8]> { + if len == 0 { + return Ok(&[]); + } + let aligned_key = offset & !(BLOCK_ALIGN as u64 - 1); - let request_end = offset.saturating_add(len as u64); - let block_end = aligned_key + BLOCK_ALIGN as u64; + let request_end = offset.checked_add(len as u64).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "read range overflows u64") + })?; + let block_end = aligned_key.saturating_add(BLOCK_ALIGN as u64); if request_end > block_end { self.spill_buf.resize(len, 0); @@ -74,7 +80,8 @@ impl LruReadCache { } let hit_idx = self.slots.iter().position(|slot| { - slot.block_offset == aligned_key && request_end <= slot.block_offset + slot.len as u64 + let slot_end = slot.block_offset.saturating_add(slot.len as u64); + slot.len > 0 && slot.block_offset == aligned_key && request_end <= slot_end }); if let Some(idx) = hit_idx { @@ -96,8 +103,8 @@ impl LruReadCache { let slot = &mut self.slots[evict_idx]; let bytes_read = file.read_at(&mut slot.buf, aligned_key)?; - // Note: get(file, 0, 0) on an empty file now returns Err (old code returned Ok(&[])). - // No callers pass len == 0, so this is a safe semantic change. + // Non-empty reads that return 0 are EOF. Zero-length reads are handled above + // as a successful no-op. if bytes_read == 0 { return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "read 0 bytes")); } @@ -107,7 +114,8 @@ impl LruReadCache { slot.last_access = self.tick; self.tick += 1; - if request_end > aligned_key + bytes_read as u64 { + let bytes_end = aligned_key.saturating_add(bytes_read as u64); + if request_end > bytes_end { return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "short read")); } @@ -118,7 +126,9 @@ impl LruReadCache { /// Invalidate all cache slots and the spill buffer. pub fn clear(&mut self) { for slot in &mut self.slots { + slot.block_offset = 0; slot.len = 0; + slot.last_access = 0; } self.spill_len = 0; } @@ -314,9 +324,11 @@ mod tests { cache.clear(); - // All slots should have len == 0. + // All slots should be fully reset. for slot in &cache.slots { + assert_eq!(slot.block_offset, 0); assert_eq!(slot.len, 0); + assert_eq!(slot.last_access, 0); } assert_eq!(cache.spill_len, 0); @@ -432,4 +444,52 @@ mod tests { assert_eq!(&line2[..4090], &[b'B'; 4090]); assert_eq!(line2[4090], b'\n'); } + + #[test] + fn zero_len_read_is_noop_on_fresh_cache() { + let f = make_file(b""); + let file = File::open(f.path()).unwrap(); + let mut cache = ReadCache::new(); + + let result = cache.get(&file, 0, 0).unwrap(); + assert!(result.is_empty()); + assert_eq!(cache.tick, 0); + assert!(cache.slots.iter().all(|s| s.len == 0)); + } + + #[test] + fn zero_len_read_is_noop_on_populated_cache() { + let f = make_file(b"abc"); + let file = File::open(f.path()).unwrap(); + let mut cache = ReadCache::new(); + + cache.get(&file, 0, 1).unwrap(); + let tick_before = cache.tick; + + let result = cache.get(&file, 0, 0).unwrap(); + assert!(result.is_empty()); + assert_eq!(cache.tick, tick_before); + } + + #[test] + fn zero_len_read_at_max_offset_is_ok() { + let f = make_file(b""); + let file = File::open(f.path()).unwrap(); + let mut cache = ReadCache::new(); + + let result = cache.get(&file, u64::MAX, 0).unwrap(); + assert!(result.is_empty()); + assert_eq!(cache.tick, 0); + assert!(cache.slots.iter().all(|s| s.len == 0)); + } + + #[test] + fn nonzero_read_range_overflow_returns_invalid_input() { + let f = make_file(b"abc"); + let file = File::open(f.path()).unwrap(); + let mut cache = ReadCache::new(); + + let err = cache.get(&file, u64::MAX, 1).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } } \ No newline at end of file