mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat(blob): add FilePool read cache and fix BucketCache TOCTOU
This commit is contained in:
@@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex, RwLock};
|
|||||||
|
|
||||||
use crate::bucket::{self, BucketFile, IndexRecord};
|
use crate::bucket::{self, BucketFile, IndexRecord};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
use crate::file_pool::FilePool;
|
||||||
use crate::meta::{AccountMeta, SegmentStats};
|
use crate::meta::{AccountMeta, SegmentStats};
|
||||||
use crate::segment::{self, SegmentReader, SegmentWriter};
|
use crate::segment::{self, SegmentReader, SegmentWriter};
|
||||||
use crate::types::Codec;
|
use crate::types::Codec;
|
||||||
@@ -16,6 +17,7 @@ pub struct AccountHandle {
|
|||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
inner: RwLock<AccountInner>,
|
inner: RwLock<AccountInner>,
|
||||||
pub(crate) write_mutex: Mutex<()>,
|
pub(crate) write_mutex: Mutex<()>,
|
||||||
|
file_pool: FilePool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountHandle {
|
impl AccountHandle {
|
||||||
@@ -39,6 +41,7 @@ impl AccountHandle {
|
|||||||
dir,
|
dir,
|
||||||
inner: RwLock::new(inner),
|
inner: RwLock::new(inner),
|
||||||
write_mutex: Mutex::new(()),
|
write_mutex: Mutex::new(()),
|
||||||
|
file_pool: FilePool::new(8),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +57,7 @@ impl AccountHandle {
|
|||||||
dir,
|
dir,
|
||||||
inner: RwLock::new(inner),
|
inner: RwLock::new(inner),
|
||||||
write_mutex: Mutex::new(()),
|
write_mutex: Mutex::new(()),
|
||||||
|
file_pool: FilePool::new(8),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +70,16 @@ impl AccountHandle {
|
|||||||
pub fn write(&self) -> std::sync::RwLockWriteGuard<'_, AccountInner> {
|
pub fn write(&self) -> std::sync::RwLockWriteGuard<'_, AccountInner> {
|
||||||
self.inner.write().unwrap()
|
self.inner.write().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a cached file handle for a segment.
|
||||||
|
pub fn get_segment_file(&self, seg_id: u32, path: &Path) -> Result<Arc<Mutex<std::fs::File>>> {
|
||||||
|
self.file_pool.get(seg_id, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invalidate cached file handles for a segment (after GC).
|
||||||
|
pub fn invalidate_file_cache(&self, seg_id: u32) {
|
||||||
|
self.file_pool.invalidate(seg_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── AccountInner ───────────────────────────────────────────────────────────
|
// ── AccountInner ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
+99
-69
@@ -1,17 +1,22 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::bucket::{BucketFile, BucketIndex, IndexRecord};
|
use crate::bucket::{BucketFile, BucketIndex, IndexRecord};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
|
||||||
/// Cache key: (account_name, bucket_id)
|
|
||||||
type CacheKey = (String, u16);
|
type CacheKey = (String, u16);
|
||||||
|
|
||||||
/// LRU bucket cache. Thread-safe.
|
/// Thread-safe LRU bucket cache with single-lock interior.
|
||||||
|
/// Eliminates the TOCTOU race in the old two-Mutex design.
|
||||||
pub struct BucketCache {
|
pub struct BucketCache {
|
||||||
|
inner: Mutex<CacheInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CacheInner {
|
||||||
max_entries: usize,
|
max_entries: usize,
|
||||||
entries: Mutex<Vec<CacheEntry>>,
|
entries: Vec<CacheEntry>,
|
||||||
index: Mutex<HashMap<CacheKey, usize>>,
|
index: HashMap<CacheKey, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CacheEntry {
|
struct CacheEntry {
|
||||||
@@ -22,27 +27,28 @@ struct CacheEntry {
|
|||||||
impl BucketCache {
|
impl BucketCache {
|
||||||
pub fn new(max_entries: usize) -> Self {
|
pub fn new(max_entries: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
max_entries: max_entries.max(1),
|
inner: Mutex::new(CacheInner {
|
||||||
entries: Mutex::new(Vec::new()),
|
max_entries: max_entries.max(1),
|
||||||
index: Mutex::new(HashMap::new()),
|
entries: Vec::new(),
|
||||||
|
index: HashMap::new(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get or load a bucket index. Returns the sorted, deduplicated records for the bucket.
|
/// Get or load a bucket index. Eliminates TOCTOU via double-checked locking.
|
||||||
pub fn get_or_load(
|
pub fn get_or_load(
|
||||||
&self,
|
&self,
|
||||||
account: &str,
|
account: &str,
|
||||||
bucket_id: u16,
|
bucket_id: u16,
|
||||||
account_dir: &std::path::Path,
|
account_dir: &Path,
|
||||||
) -> Result<Vec<IndexRecord>> {
|
) -> Result<Vec<IndexRecord>> {
|
||||||
let key: CacheKey = (account.to_string(), bucket_id);
|
let key: CacheKey = (account.to_string(), bucket_id);
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
{
|
{
|
||||||
let index = self.index.lock().unwrap();
|
let inner = self.inner.lock().unwrap();
|
||||||
if let Some(&pos) = index.get(&key) {
|
if let Some(&pos) = inner.index.get(&key) {
|
||||||
let entries = self.entries.lock().unwrap();
|
return Ok(inner.entries[pos].index.records.clone());
|
||||||
return Ok(entries[pos].index.records.clone());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,84 +57,74 @@ impl BucketCache {
|
|||||||
let bucket_index = bucket_file.load_index()?;
|
let bucket_index = bucket_file.load_index()?;
|
||||||
let records = bucket_index.records.clone();
|
let records = bucket_index.records.clone();
|
||||||
|
|
||||||
// Insert into cache
|
// Insert with double-check (another thread might have beaten us)
|
||||||
self.insert(key, bucket_index);
|
{
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
if let Some(&pos) = inner.index.get(&key) {
|
||||||
|
return Ok(inner.entries[pos].index.records.clone());
|
||||||
|
}
|
||||||
|
// Evict if full
|
||||||
|
if inner.entries.len() >= inner.max_entries {
|
||||||
|
if let Some(evicted) = inner.entries.pop() {
|
||||||
|
inner.index.remove(&evicted.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Insert at front
|
||||||
|
inner.entries.insert(0, CacheEntry {
|
||||||
|
key: key.clone(),
|
||||||
|
index: bucket_index,
|
||||||
|
});
|
||||||
|
// Rebuild index
|
||||||
|
inner.index.clear();
|
||||||
|
for i in 0..inner.entries.len() {
|
||||||
|
let key = inner.entries[i].key.clone();
|
||||||
|
inner.index.insert(key, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert(&self, key: CacheKey, index: BucketIndex) {
|
/// Insert or update a single record in a cached bucket.
|
||||||
let mut idx_map = self.index.lock().unwrap();
|
|
||||||
let mut entries = self.entries.lock().unwrap();
|
|
||||||
|
|
||||||
// If already exists, update and move to front
|
|
||||||
if let Some(&pos) = idx_map.get(&key) {
|
|
||||||
entries[pos].index = index;
|
|
||||||
let entry = entries.remove(pos);
|
|
||||||
entries.insert(0, entry);
|
|
||||||
// Rebuild index
|
|
||||||
idx_map.clear();
|
|
||||||
for (i, e) in entries.iter().enumerate() {
|
|
||||||
idx_map.insert(e.key.clone(), i);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evict if full
|
|
||||||
if entries.len() >= self.max_entries {
|
|
||||||
if let Some(evicted) = entries.pop() {
|
|
||||||
idx_map.remove(&evicted.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert at front (most recently used)
|
|
||||||
entries.insert(0, CacheEntry { key: key.clone(), index });
|
|
||||||
// Rebuild index (positions shifted)
|
|
||||||
idx_map.clear();
|
|
||||||
for (i, e) in entries.iter().enumerate() {
|
|
||||||
idx_map.insert(e.key.clone(), i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert or update a single record in a cached bucket. If bucket not cached, no-op.
|
|
||||||
pub fn update_record(&self, account: &str, bucket_id: u16, record: IndexRecord) {
|
pub fn update_record(&self, account: &str, bucket_id: u16, record: IndexRecord) {
|
||||||
let key: CacheKey = (account.to_string(), bucket_id);
|
let key: CacheKey = (account.to_string(), bucket_id);
|
||||||
let mut idx_map = self.index.lock().unwrap();
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
if let Some(&pos) = idx_map.get(&key) {
|
if let Some(&pos) = inner.index.get(&key) {
|
||||||
let mut entries = self.entries.lock().unwrap();
|
inner.entries[pos].index.insert(record);
|
||||||
entries[pos].index.insert(record);
|
|
||||||
// Move to front
|
// Move to front
|
||||||
let entry = entries.remove(pos);
|
let entry = inner.entries.remove(pos);
|
||||||
entries.insert(0, entry);
|
inner.entries.insert(0, entry);
|
||||||
// Rebuild index
|
// Rebuild index
|
||||||
idx_map.clear();
|
inner.index.clear();
|
||||||
for (i, e) in entries.iter().enumerate() {
|
for i in 0..inner.entries.len() {
|
||||||
idx_map.insert(e.key.clone(), i);
|
let entry_key = inner.entries[i].key.clone();
|
||||||
|
inner.index.insert(entry_key, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invalidate a cached bucket.
|
/// Invalidate a cached bucket (after GC rewrites bucket files).
|
||||||
pub fn invalidate(&self, account: &str, bucket_id: u16) {
|
pub fn invalidate(&self, account: &str, bucket_id: u16) {
|
||||||
let key: CacheKey = (account.to_string(), bucket_id);
|
let key: CacheKey = (account.to_string(), bucket_id);
|
||||||
let mut idx_map = self.index.lock().unwrap();
|
let mut inner = self.inner.lock().unwrap();
|
||||||
if let Some(&pos) = idx_map.get(&key) {
|
|
||||||
let mut entries = self.entries.lock().unwrap();
|
if let Some(&pos) = inner.index.get(&key) {
|
||||||
entries.remove(pos);
|
inner.entries.remove(pos);
|
||||||
idx_map.clear();
|
inner.index.clear();
|
||||||
for (i, e) in entries.iter().enumerate() {
|
for i in 0..inner.entries.len() {
|
||||||
idx_map.insert(e.key.clone(), i);
|
let entry_key = inner.entries[i].key.clone();
|
||||||
|
inner.index.insert(entry_key, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.entries.lock().unwrap().len()
|
self.inner.lock().unwrap().entries.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.entries.lock().unwrap().is_empty()
|
self.inner.lock().unwrap().entries.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +159,6 @@ mod tests {
|
|||||||
|
|
||||||
let cache = BucketCache::new(10);
|
let cache = BucketCache::new(10);
|
||||||
let _ = cache.get_or_load("test", 0, dir.path()).unwrap();
|
let _ = cache.get_or_load("test", 0, dir.path()).unwrap();
|
||||||
// Second call should hit cache
|
|
||||||
let records = cache
|
let records = cache
|
||||||
.get_or_load("test", 0, dir.path())
|
.get_or_load("test", 0, dir.path())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -186,4 +181,39 @@ mod tests {
|
|||||||
|
|
||||||
assert!(cache.len() <= 2);
|
assert!(cache.len() <= 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_concurrent_get_or_load_no_deadlock() {
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
crate::bucket::BucketFile::ensure_dir(dir.path()).unwrap();
|
||||||
|
let bf = BucketFile::open(dir.path(), 0);
|
||||||
|
for i in 0..10u8 {
|
||||||
|
bf.append(&IndexRecord::new([i; 32], 1, i as u64 * 100, 50, 0))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache = Arc::new(BucketCache::new(10));
|
||||||
|
let dir_path = dir.path().to_path_buf();
|
||||||
|
|
||||||
|
let mut handles = vec![];
|
||||||
|
for _ in 0..4 {
|
||||||
|
let cache = cache.clone();
|
||||||
|
let dir_path = dir_path.clone();
|
||||||
|
handles.push(thread::spawn(move || {
|
||||||
|
for _ in 0..100 {
|
||||||
|
let records = cache
|
||||||
|
.get_or_load("test", 0, &dir_path)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(records.len(), 10);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,8 +203,9 @@ impl Engine {
|
|||||||
return Err(Error::SegmentNotFound(record.segment_id));
|
return Err(Error::SegmentNotFound(record.segment_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = SegmentReader::open(seg_path, record.segment_id)?;
|
let reader = SegmentReader::open(seg_path.clone(), record.segment_id)?;
|
||||||
let (entry, _) = reader.read_entry_at(record.offset)?;
|
let file = handle.get_segment_file(record.segment_id, &seg_path)?;
|
||||||
|
let (entry, _) = reader.read_entry_at_file(record.offset, &file)?;
|
||||||
|
|
||||||
let value = compress::decompress(&entry.data, entry.codec, entry.raw_size as usize)?;
|
let value = compress::decompress(&entry.data, entry.codec, entry.raw_size as usize)?;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
/// Simple LRU pool of open file handles, keyed by segment_id.
|
||||||
|
/// Uses Arc<Mutex<File>> to allow safe concurrent reads from the same segment.
|
||||||
|
pub struct FilePool {
|
||||||
|
max_entries: usize,
|
||||||
|
entries: Mutex<VecDeque<(u32, Arc<Mutex<File>>)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilePool {
|
||||||
|
pub fn new(max_entries: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
max_entries: max_entries.max(1),
|
||||||
|
entries: Mutex::new(VecDeque::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an open File for the given segment. Reuses cached handle if available.
|
||||||
|
pub fn get(&self, seg_id: u32, path: &Path) -> Result<Arc<Mutex<File>>> {
|
||||||
|
let mut entries = self.entries.lock().unwrap();
|
||||||
|
|
||||||
|
// Check for existing entry
|
||||||
|
for (i, (id, _)) in entries.iter().enumerate() {
|
||||||
|
if *id == seg_id {
|
||||||
|
let (_, file) = entries.remove(i).unwrap();
|
||||||
|
entries.push_front((seg_id, file.clone()));
|
||||||
|
return Ok(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open new file
|
||||||
|
let file = Arc::new(Mutex::new(File::open(path)?));
|
||||||
|
|
||||||
|
// Evict oldest if full
|
||||||
|
if entries.len() >= self.max_entries {
|
||||||
|
entries.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push_front((seg_id, file.clone()));
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a cached file handle (e.g. after GC rewrites a segment).
|
||||||
|
pub fn invalidate(&self, seg_id: u32) {
|
||||||
|
let mut entries = self.entries.lock().unwrap();
|
||||||
|
entries.retain(|(id, _)| *id != seg_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ pub mod checksum;
|
|||||||
pub mod compress;
|
pub mod compress;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod file_pool;
|
||||||
pub mod gc;
|
pub mod gc;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
pub mod recovery;
|
pub mod recovery;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::fs::{self, File, OpenOptions};
|
use std::fs::{self, File, OpenOptions};
|
||||||
use std::io::{Read, Seek, SeekFrom, Write};
|
use std::io::{Read, Seek, SeekFrom, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::checksum;
|
use crate::checksum;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
@@ -269,6 +270,94 @@ impl SegmentReader {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a single entry at the given offset using a pre-opened File (via Mutex).
|
||||||
|
/// This avoids the per-read File::open cost for hot segments.
|
||||||
|
pub fn read_entry_at_file(&self, offset: u64, file: &Mutex<File>) -> Result<(Entry, u64)> {
|
||||||
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
|
|
||||||
|
let mut file = file.lock().unwrap();
|
||||||
|
|
||||||
|
file.seek(SeekFrom::Start(offset))?;
|
||||||
|
|
||||||
|
// Read magic
|
||||||
|
let mut magic_buf = [0u8; 4];
|
||||||
|
file.read_exact(&mut magic_buf)?;
|
||||||
|
let magic = u32::from_le_bytes(magic_buf);
|
||||||
|
if magic != ENTRY_MAGIC {
|
||||||
|
return Err(Error::CorruptEntry {
|
||||||
|
path: self.path.clone(),
|
||||||
|
offset,
|
||||||
|
reason: format!("bad magic: 0x{:08X}", magic),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read CRC32
|
||||||
|
let mut crc_buf = [0u8; 4];
|
||||||
|
file.read_exact(&mut crc_buf)?;
|
||||||
|
let stored_crc = u32::from_le_bytes(crc_buf);
|
||||||
|
|
||||||
|
// Read flags, codec
|
||||||
|
let mut flags_buf = [0u8; 1];
|
||||||
|
file.read_exact(&mut flags_buf)?;
|
||||||
|
let flags = flags_buf[0];
|
||||||
|
|
||||||
|
let mut codec_buf = [0u8; 1];
|
||||||
|
file.read_exact(&mut codec_buf)?;
|
||||||
|
let codec = Codec::from_u8(codec_buf[0]).ok_or_else(|| Error::CorruptEntry {
|
||||||
|
path: self.path.clone(),
|
||||||
|
offset,
|
||||||
|
reason: format!("unknown codec: {}", codec_buf[0]),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Read key, raw_size, data_size
|
||||||
|
let mut key = [0u8; 32];
|
||||||
|
file.read_exact(&mut key)?;
|
||||||
|
|
||||||
|
let mut raw_size_buf = [0u8; 4];
|
||||||
|
file.read_exact(&mut raw_size_buf)?;
|
||||||
|
let raw_size = u32::from_le_bytes(raw_size_buf);
|
||||||
|
|
||||||
|
let mut data_size_buf = [0u8; 4];
|
||||||
|
file.read_exact(&mut data_size_buf)?;
|
||||||
|
let data_size = u32::from_le_bytes(data_size_buf);
|
||||||
|
|
||||||
|
// Read data
|
||||||
|
let mut data = vec![0u8; data_size as usize];
|
||||||
|
file.read_exact(&mut data)?;
|
||||||
|
|
||||||
|
// Verify CRC32
|
||||||
|
let computed_crc = {
|
||||||
|
let mut hasher = crate::checksum::CrcWriter::new();
|
||||||
|
hasher.update(&[flags]);
|
||||||
|
hasher.update(&[codec as u8]);
|
||||||
|
hasher.update(&key);
|
||||||
|
hasher.update(&raw_size.to_le_bytes());
|
||||||
|
hasher.update(&data_size.to_le_bytes());
|
||||||
|
hasher.update(&data);
|
||||||
|
hasher.finalize()
|
||||||
|
};
|
||||||
|
|
||||||
|
if stored_crc != computed_crc {
|
||||||
|
return Err(Error::CrcMismatch {
|
||||||
|
path: self.path.clone(),
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_offset = offset + ENTRY_HEADER_SIZE as u64 + data_size as u64;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
Entry {
|
||||||
|
flags,
|
||||||
|
codec,
|
||||||
|
key,
|
||||||
|
raw_size,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
next_offset,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// Read data portion of an entry (for pread-style reads when you already know offset + data_size).
|
/// Read data portion of an entry (for pread-style reads when you already know offset + data_size).
|
||||||
pub fn read_data(&self, offset: u64, data_size: u32) -> Result<Vec<u8>> {
|
pub fn read_data(&self, offset: u64, data_size: u32) -> Result<Vec<u8>> {
|
||||||
let mut file = File::open(&self.path)?;
|
let mut file = File::open(&self.path)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user