diff --git a/markbase-core/src/vfs/checksum.rs b/markbase-core/src/vfs/checksum.rs new file mode 100644 index 0000000..2777380 --- /dev/null +++ b/markbase-core/src/vfs/checksum.rs @@ -0,0 +1,436 @@ +//! Block-level Checksum for Data Integrity +//! +//! Reference: ZFS/Btrfs checksum verification +//! - ZFS: Fletcher4/SHA256 per-block checksum +//! - Btrfs: CRC32C per-block checksum +//! +//! MarkBase uses SHA-256 (32 bytes) per 4KB block for integrity verification. + +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::io::{Read, Write, Seek, SeekFrom}; + +use sha2::{Sha256, Digest}; +use serde::{Serialize, Deserialize}; + +use super::{VfsBackend, VfsFile, VfsError, VfsStat}; + +pub const BLOCK_SIZE: usize = 4096; +pub const HASH_SIZE: usize = 32; // SHA-256 +pub const CHECKSUM_DIR: &str = ".checksums"; +pub const CHECKSUM_EXT: &str = ".checksums"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VfsBlockChecksum { + pub offset: u64, // Block offset (multiple of BLOCK_SIZE) + pub hash: Vec, // SHA-256 hash (32 bytes) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VfsChecksumFile { + pub block_size: usize, + pub algorithm: String, // "sha256" + pub blocks: Vec, + pub file_size: u64, // Original file size +} + +impl VfsChecksumFile { + pub fn new(file_size: u64) -> Self { + Self { + block_size: BLOCK_SIZE, + algorithm: "sha256".to_string(), + blocks: Vec::new(), + file_size, + } + } + + pub fn from_bytes(data: &[u8]) -> Result { + serde_json::from_slice(data) + .map_err(|e| VfsError::Io(format!("checksum parse failed: {}", e))) + } + + pub fn to_bytes(&self) -> Result, VfsError> { + serde_json::to_vec(self) + .map_err(|e| VfsError::Io(format!("checksum serialize failed: {}", e))) + } + + pub fn get_checksum(&self, offset: u64) -> Option<&[u8]> { + self.blocks.iter() + .find(|b| b.offset == offset) + .map(|b| b.hash.as_slice()) + } + + pub fn set_checksum(&mut self, offset: u64, hash: Vec) { + if let Some(block) = self.blocks.iter_mut().find(|b| b.offset == offset) { + block.hash = hash; + } else { + self.blocks.push(VfsBlockChecksum { offset, hash }); + self.blocks.sort_by_key(|b| b.offset); + } + } + + pub fn block_count(&self) -> usize { + (self.file_size as usize / BLOCK_SIZE) + + if self.file_size as usize % BLOCK_SIZE > 0 { 1 } else { 0 } + } +} + +pub fn compute_block_hash(data: &[u8]) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(data); + hasher.finalize().to_vec() +} + +pub fn verify_block_hash(data: &[u8], expected: &[u8]) -> bool { + let actual = compute_block_hash(data); + actual == expected +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChecksumMode { + Lazy, // Only verify on scrub (default) + OnRead, // Verify every read +} + +#[derive(Debug, Clone)] +pub struct ChecksumConfig { + pub mode: ChecksumMode, + pub cache_verified: bool, +} + +impl Default for ChecksumConfig { + fn default() -> Self { + Self { + mode: ChecksumMode::Lazy, + cache_verified: true, + } + } +} + +#[derive(Debug)] +pub struct ScrubResult { + pub path: PathBuf, + pub total_blocks: usize, + pub verified_blocks: usize, + pub corrupted_blocks: Vec, + pub repaired_blocks: Vec, + pub repair_failed: bool, +} + +impl ScrubResult { + pub fn is_clean(&self) -> bool { + self.corrupted_blocks.is_empty() + } + + pub fn repair_success_rate(&self) -> f64 { + if self.corrupted_blocks.is_empty() { + 1.0 + } else { + self.repaired_blocks.len() as f64 / self.corrupted_blocks.len() as f64 + } + } +} + +pub fn checksum_path_for_file(file_path: &PathBuf, root: &PathBuf) -> PathBuf { + let relative = file_path.strip_prefix(root) + .unwrap_or(file_path); + root.join(CHECKSUM_DIR) + .join(relative) + .with_extension(CHECKSUM_EXT) +} + +pub fn ensure_checksum_dir(root: &PathBuf, backend: &dyn VfsBackend) -> Result<(), VfsError> { + let checksum_dir = root.join(CHECKSUM_DIR); + if !backend.exists(&checksum_dir) { + backend.create_dir(&checksum_dir, 0o755)?; + } + Ok(()) +} + +/// Scrub a single file to verify integrity +/// +/// This reads the file and verifies each block checksum. +/// If repair=true and corrupted blocks are found, attempts to repair from RAID/Dedup. +pub fn scrub_file( + backend: &dyn VfsBackend, + file_path: &PathBuf, + root_path: &PathBuf, + repair: bool, +) -> Result { + let checksum_path = checksum_path_for_file(file_path, root_path); + + if !backend.exists(&checksum_path) { + return Ok(ScrubResult { + path: file_path.clone(), + total_blocks: 0, + verified_blocks: 0, + corrupted_blocks: vec![], + repaired_blocks: vec![], + repair_failed: false, + }); + } + + let checksum_file_data = { + let mut checksum_file = backend.open_file(&checksum_path, &super::open_flags::OpenFlags::new().read())?; + checksum_file.read_all()? + }; + let checksum_data = VfsChecksumFile::from_bytes(&checksum_file_data)?; + + let mut file_handle = backend.open_file(file_path, &super::open_flags::OpenFlags::new().read())?; + let stat = file_handle.stat()?; + let file_size = stat.size; + + let block_count = checksum_data.block_count(); + let mut verified_blocks = 0; + let mut corrupted_blocks: Vec = vec![]; + let mut repaired_blocks: Vec = vec![]; + + for block_idx in 0..block_count { + let offset = (block_idx as u64) * BLOCK_SIZE as u64; + let block_size = if offset + BLOCK_SIZE as u64 <= file_size { + BLOCK_SIZE + } else { + (file_size - offset) as usize + }; + + let mut buffer = vec![0u8; block_size]; + let bytes_read = file_handle.read_at(&mut buffer, offset)?; + + if bytes_read != block_size { + corrupted_blocks.push(offset); + continue; + } + + let expected_hash = checksum_data.get_checksum(offset); + if expected_hash.is_none() { + verified_blocks += 1; + continue; + } + + let is_valid = verify_block_hash(&buffer, expected_hash.unwrap()); + if is_valid { + verified_blocks += 1; + } else { + corrupted_blocks.push(offset); + + if repair { + if let Ok(_) = repair_block(backend, file_path, offset, &buffer) { + repaired_blocks.push(offset); + } + } + } + } + + let corrupted_count = corrupted_blocks.len(); + let repaired_count = repaired_blocks.len(); + + Ok(ScrubResult { + path: file_path.clone(), + total_blocks: block_count, + verified_blocks, + corrupted_blocks, + repaired_blocks, + repair_failed: repair && repaired_count < corrupted_count, + }) +} + +/// Scrub all files in a directory +/// +/// Recursively walks the directory and scrubs all files with checksums. +pub fn scrub_all( + backend: &dyn VfsBackend, + root_path: &PathBuf, + repair: bool, +) -> Result, VfsError> { + let mut results = vec![]; + + let checksum_dir = root_path.join(CHECKSUM_DIR); + if !backend.exists(&checksum_dir) { + return Ok(results); + } + + scrub_recursive(backend, root_path, root_path, repair, &mut results)?; + + Ok(results) +} + +fn scrub_recursive( + backend: &dyn VfsBackend, + current_path: &PathBuf, + root_path: &PathBuf, + repair: bool, + results: &mut Vec, +) -> Result<(), VfsError> { + let entries = backend.read_dir(current_path)?; + + for entry in entries { + let entry_path = current_path.join(&entry.name); + + if entry.stat.is_dir { + if entry.name != CHECKSUM_DIR { + scrub_recursive(backend, &entry_path, root_path, repair, results)?; + } + } else if !entry.name.ends_with(CHECKSUM_EXT) { + let result = scrub_file(backend, &entry_path, root_path, repair)?; + results.push(result); + } + } + + Ok(()) +} + +/// Attempt to repair a corrupted block +/// +/// This is a placeholder that returns error for now. +/// RAID/Dedup repair will be implemented in Phase 4/6. +fn repair_block( + backend: &dyn VfsBackend, + file_path: &PathBuf, + offset: u64, + corrupted_data: &[u8], +) -> Result, VfsError> { + Err(VfsError::Io("block repair not implemented (Phase 4/6)".to_string())) +} + +/// Create checksums for a file +/// +/// This reads the file and computes checksums for all blocks. +pub fn create_checksums_for_file( + backend: &dyn VfsBackend, + file_path: &PathBuf, + root_path: &PathBuf, +) -> Result<(), VfsError> { + ensure_checksum_dir(root_path, backend)?; + + let mut file_handle = backend.open_file(file_path, &super::open_flags::OpenFlags::new().read())?; + let stat = file_handle.stat()?; + let file_size = stat.size; + + let mut checksum_data = VfsChecksumFile::new(file_size); + + let block_count = checksum_data.block_count(); + + for block_idx in 0..block_count { + let offset = (block_idx as u64) * BLOCK_SIZE as u64; + let block_size = if offset + BLOCK_SIZE as u64 <= file_size { + BLOCK_SIZE + } else { + (file_size - offset) as usize + }; + + let mut buffer = vec![0u8; block_size]; + let bytes_read = file_handle.read_at(&mut buffer, offset)?; + + if bytes_read > 0 { + let hash = compute_block_hash(&buffer[..bytes_read]); + checksum_data.set_checksum(offset, hash); + } + } + + let checksum_path = checksum_path_for_file(file_path, root_path); + let checksum_bytes = checksum_data.to_bytes()?; + + let mut checksum_file = backend.open_file( + &checksum_path, + &super::open_flags::OpenFlags::new().write().create().truncate(), + )?; + checksum_file.write_all(&checksum_bytes)?; + checksum_file.flush()?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compute_block_hash() { + let data = b"test block data for hashing"; + let hash = compute_block_hash(data); + assert_eq!(hash.len(), HASH_SIZE); + + let hash2 = compute_block_hash(data); + assert_eq!(hash, hash2); + } + + #[test] + fn test_verify_block_hash() { + let data = b"test block data"; + let hash = compute_block_hash(data); + assert!(verify_block_hash(data, &hash)); + + let wrong_data = b"wrong block data"; + assert!(!verify_block_hash(wrong_data, &hash)); + } + + #[test] + fn test_checksum_file_roundtrip() { + let mut checksum_file = VfsChecksumFile::new(8192); + checksum_file.set_checksum(0, compute_block_hash(b"block0")); + checksum_file.set_checksum(4096, compute_block_hash(b"block1")); + + let bytes = checksum_file.to_bytes().unwrap(); + let decoded = VfsChecksumFile::from_bytes(&bytes).unwrap(); + + assert_eq!(decoded.block_size, BLOCK_SIZE); + assert_eq!(decoded.blocks.len(), 2); + assert_eq!(decoded.file_size, 8192); + } + + #[test] + fn test_checksum_file_get_set() { + let mut checksum_file = VfsChecksumFile::new(4096); + + let hash = compute_block_hash(b"test"); + checksum_file.set_checksum(0, hash.clone()); + + let retrieved = checksum_file.get_checksum(0); + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap(), hash.as_slice()); + + checksum_file.set_checksum(0, compute_block_hash(b"new")); + let updated = checksum_file.get_checksum(0).unwrap(); + assert_ne!(updated, hash.as_slice()); + } + + #[test] + fn test_block_count_calculation() { + let checksum_file = VfsChecksumFile::new(4096); + assert_eq!(checksum_file.block_count(), 1); + + let checksum_file = VfsChecksumFile::new(8192); + assert_eq!(checksum_file.block_count(), 2); + + let checksum_file = VfsChecksumFile::new(4097); + assert_eq!(checksum_file.block_count(), 2); + + let checksum_file = VfsChecksumFile::new(0); + assert_eq!(checksum_file.block_count(), 0); + } + + #[test] + fn test_scrub_result_metrics() { + let result = ScrubResult { + path: PathBuf::from("/test"), + total_blocks: 10, + verified_blocks: 10, + corrupted_blocks: vec![], + repaired_blocks: vec![], + repair_failed: false, + }; + assert!(result.is_clean()); + assert_eq!(result.repair_success_rate(), 1.0); + + let result2 = ScrubResult { + path: PathBuf::from("/test"), + total_blocks: 10, + verified_blocks: 8, + corrupted_blocks: vec![4096, 8192], + repaired_blocks: vec![4096], + repair_failed: false, + }; + assert!(!result2.is_clean()); + assert_eq!(result2.repair_success_rate(), 0.5); + } +} \ No newline at end of file diff --git a/markbase-core/src/vfs/checksum_file.rs b/markbase-core/src/vfs/checksum_file.rs new file mode 100644 index 0000000..3c158b4 --- /dev/null +++ b/markbase-core/src/vfs/checksum_file.rs @@ -0,0 +1,259 @@ +//! ChecksumFile Wrapper - Transparent checksum verification for VfsFile +//! +//! This wraps any VfsFile to provide: +//! - Automatic checksum calculation on write +//! - Optional verification on read (OnRead mode) +//! - Cache of verified blocks (Lazy mode) +//! - Scrub support for integrity checking + +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::io::{Seek, SeekFrom}; + +use super::{VfsBackend, VfsFile, VfsStat, VfsError}; +use super::checksum::{ + VfsChecksumFile, ChecksumConfig, ChecksumMode, + BLOCK_SIZE, compute_block_hash, verify_block_hash, + checksum_path_for_file, ensure_checksum_dir, +}; +use sha2::{Sha256, Digest}; + +pub struct ChecksumFile { + inner: Box, + file_path: PathBuf, + root_path: PathBuf, + backend: Box, + config: ChecksumConfig, + checksum_data: Option, + verified_cache: HashMap>, + modified_blocks: HashSet, + current_offset: u64, + file_size: u64, + loaded: bool, +} + +impl ChecksumFile { + pub fn new( + inner: Box, + file_path: PathBuf, + root_path: PathBuf, + backend: Box, + config: ChecksumConfig, + ) -> Self { + Self { + inner, + file_path, + root_path, + backend, + config, + checksum_data: None, + verified_cache: HashMap::new(), + modified_blocks: HashSet::new(), + current_offset: 0, + file_size: 0, + loaded: false, + } + } + + fn load_checksum_file(&mut self) -> Result<(), VfsError> { + if self.loaded { + return Ok(()); + } + + let checksum_path = checksum_path_for_file(&self.file_path, &self.root_path); + + if self.backend.exists(&checksum_path) { + let mut checksum_file = self.backend.open_file(&checksum_path, &super::open_flags::OpenFlags::new().read())?; + let data = checksum_file.read_all()?; + self.checksum_data = Some(VfsChecksumFile::from_bytes(&data)?); + } else { + let stat = self.inner.stat()?; + self.file_size = stat.size; + self.checksum_data = Some(VfsChecksumFile::new(self.file_size)); + } + + self.loaded = true; + Ok(()) + } + + fn save_checksum_file(&mut self) -> Result<(), VfsError> { + ensure_checksum_dir(&self.root_path, self.backend.as_ref())?; + + if let Some(checksum_data) = &self.checksum_data { + let checksum_path = checksum_path_for_file(&self.file_path, &self.root_path); + let data = checksum_data.to_bytes()?; + + let mut checksum_file = self.backend.open_file( + &checksum_path, + &super::open_flags::OpenFlags::new().write().create().truncate(), + )?; + checksum_file.write_all(&data)?; + checksum_file.flush()?; + } + + Ok(()) + } + + fn get_block_offset(offset: u64) -> u64 { + (offset / BLOCK_SIZE as u64) * BLOCK_SIZE as u64 + } + + fn verify_block_at_offset(&mut self, offset: u64, data: &[u8]) -> Result { + self.load_checksum_file()?; + + let block_offset = Self::get_block_offset(offset); + + if let Some(checksum_data) = &self.checksum_data { + if let Some(expected_hash) = checksum_data.get_checksum(block_offset) { + let is_valid = verify_block_hash(data, expected_hash); + + if self.config.cache_verified && is_valid { + self.verified_cache.insert(block_offset, expected_hash.to_vec()); + } + + return Ok(is_valid); + } + } + + Ok(true) + } + + fn update_checksum_for_block(&mut self, offset: u64, data: &[u8]) -> Result<(), VfsError> { + self.load_checksum_file()?; + + let block_offset = Self::get_block_offset(offset); + let hash = compute_block_hash(data); + + if let Some(checksum_data) = &mut self.checksum_data { + checksum_data.set_checksum(block_offset, hash); + } + + self.modified_blocks.insert(block_offset); + Ok(()) + } + + pub fn get_checksum_data(&self) -> Option<&VfsChecksumFile> { + self.checksum_data.as_ref() + } + + pub fn get_modified_blocks(&self) -> &HashSet { + &self.modified_blocks + } + + pub fn get_verified_cache(&self) -> &HashMap> { + &self.verified_cache + } +} + +impl VfsFile for ChecksumFile { + fn read(&mut self, buf: &mut [u8]) -> Result { + let bytes_read = self.inner.read(buf)?; + + if bytes_read > 0 && self.config.mode == ChecksumMode::OnRead { + self.verify_block_at_offset(self.current_offset, &buf[..bytes_read])?; + } + + self.current_offset += bytes_read as u64; + Ok(bytes_read) + } + + fn write(&mut self, buf: &[u8]) -> Result { + let bytes_written = self.inner.write(buf)?; + + if bytes_written > 0 { + self.update_checksum_for_block(self.current_offset, buf)?; + self.current_offset += bytes_written as u64; + + if self.current_offset > self.file_size { + self.file_size = self.current_offset; + if let Some(checksum_data) = &mut self.checksum_data { + checksum_data.file_size = self.file_size; + } + } + } + + Ok(bytes_written) + } + + fn seek(&mut self, pos: SeekFrom) -> Result { + self.current_offset = self.inner.seek(pos)?; + Ok(self.current_offset) + } + + fn flush(&mut self) -> Result<(), VfsError> { + self.inner.flush()?; + + if !self.modified_blocks.is_empty() { + self.save_checksum_file()?; + self.modified_blocks.clear(); + } + + Ok(()) + } + + fn stat(&mut self) -> Result { + let stat = self.inner.stat()?; + Ok(stat) + } + + fn set_len(&mut self, size: u64) -> Result<(), VfsError> { + self.inner.set_len(size)?; + self.file_size = size; + + if let Some(checksum_data) = &mut self.checksum_data { + checksum_data.file_size = size; + } + + Ok(()) + } + + fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result { + let bytes_read = self.inner.read_at(buf, offset)?; + + if bytes_read > 0 && self.config.mode == ChecksumMode::OnRead { + self.verify_block_at_offset(offset, &buf[..bytes_read])?; + } + + Ok(bytes_read) + } + + fn write_at(&mut self, buf: &[u8], offset: u64) -> Result { + let bytes_written = self.inner.write_at(buf, offset)?; + + if bytes_written > 0 { + self.update_checksum_for_block(offset, buf)?; + + let new_size = offset + bytes_written as u64; + if new_size > self.file_size { + self.file_size = new_size; + if let Some(checksum_data) = &mut self.checksum_data { + checksum_data.file_size = self.file_size; + } + } + } + + Ok(bytes_written) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn test_block_offset_calculation() { + assert_eq!(ChecksumFile::get_block_offset(0), 0); + assert_eq!(ChecksumFile::get_block_offset(4095), 0); + assert_eq!(ChecksumFile::get_block_offset(4096), 4096); + assert_eq!(ChecksumFile::get_block_offset(8191), 4096); + assert_eq!(ChecksumFile::get_block_offset(8192), 8192); + } + + #[test] + fn test_checksum_config_default() { + let config = ChecksumConfig::default(); + assert_eq!(config.mode, ChecksumMode::Lazy); + assert!(config.cache_verified); + } +} \ No newline at end of file diff --git a/markbase-core/src/vfs/mod.rs b/markbase-core/src/vfs/mod.rs index 3a44633..ca89de3 100644 --- a/markbase-core/src/vfs/mod.rs +++ b/markbase-core/src/vfs/mod.rs @@ -1,4 +1,6 @@ pub mod cache; +pub mod checksum; +pub mod checksum_file; pub mod compression; pub mod dedup; pub mod encrypted_fs; diff --git a/markbase-core/src/vfs/raid.rs b/markbase-core/src/vfs/raid.rs index 30229f6..2ee60a7 100644 --- a/markbase-core/src/vfs/raid.rs +++ b/markbase-core/src/vfs/raid.rs @@ -162,6 +162,137 @@ impl VfsRaidBackend { } Ok(()) } + + /// Repair a corrupted block from parity + /// + /// This reads the block from surviving disks and reconstructs using parity. + /// Works for RAID-Z1/2/3 (requires parity disks). + pub fn repair_block_from_parity( + &self, + path: &Path, + offset: u64, + corrupted_disk_index: usize, + ) -> Result, VfsError> { + if self.config.level == VfsRaidLevel::Single { + return Err(VfsError::Io("Cannot repair from single disk RAID".to_string())); + } + + if corrupted_disk_index >= self.backends.len() { + return Err(VfsError::Io(format!("Invalid disk index {}", corrupted_disk_index))); + } + + let block_size = self.stripe_size; + let mut data_blocks: Vec>> = vec![None; self.backends.len()]; + let mut parity_blocks: Vec> = vec![]; + + for (i, backend) in self.backends.iter().enumerate() { + if i == corrupted_disk_index { + continue; + } + + let mut file = backend.open_file(path, &super::open_flags::OpenFlags::new().read())?; + let mut buffer = vec![0u8; block_size]; + let bytes_read = file.read_at(&mut buffer, offset)?; + + if bytes_read > 0 { + if i < self.data_disks() { + data_blocks[i] = Some(buffer[..bytes_read].to_vec()); + } else { + parity_blocks.push(buffer[..bytes_read].to_vec()); + } + } + } + + match self.config.level { + VfsRaidLevel::RaidZ1 => { + if parity_blocks.len() < 1 { + return Err(VfsError::Io("Not enough parity for RaidZ1 repair".to_string())); + } + let reconstructed = Self::reconstruct_from_p( + &data_blocks, + &parity_blocks[0], + corrupted_disk_index, + self.data_disks(), + ); + Ok(reconstructed) + } + VfsRaidLevel::RaidZ2 => { + if parity_blocks.len() < 2 { + return Err(VfsError::Io("Not enough parity for RaidZ2 repair".to_string())); + } + let reconstructed = Self::reconstruct_from_pq( + &data_blocks, + &parity_blocks[0], + &parity_blocks[1], + corrupted_disk_index, + self.data_disks(), + ); + Ok(reconstructed) + } + VfsRaidLevel::RaidZ3 => { + if parity_blocks.len() < 3 { + return Err(VfsError::Io("Not enough parity for RaidZ3 repair".to_string())); + } + let reconstructed = Self::reconstruct_from_pqr( + &data_blocks, + &parity_blocks[0], + &parity_blocks[1], + &parity_blocks[2], + corrupted_disk_index, + self.data_disks(), + ); + Ok(reconstructed) + } + _ => Err(VfsError::Io("RAID level does not support block repair".to_string())), + } + } + + fn reconstruct_from_p( + data_blocks: &[Option>], + p_block: &[u8], + missing_index: usize, + data_disk_count: usize, + ) -> Vec { + let size = p_block.len(); + let mut reconstructed = vec![0u8; size]; + + for i in 0..data_disk_count { + if i != missing_index { + if let Some(data) = &data_blocks[i] { + for j in 0..size { + reconstructed[j] ^= data[j]; + } + } + } + } + + for j in 0..size { + reconstructed[j] ^= p_block[j]; + } + + reconstructed + } + + fn reconstruct_from_pq( + data_blocks: &[Option>], + p_block: &[u8], + q_block: &[u8], + missing_index: usize, + data_disk_count: usize, + ) -> Vec { + Self::reconstruct_from_p(data_blocks, p_block, missing_index, data_disk_count) + } + + fn reconstruct_from_pqr( + data_blocks: &[Option>], + p_block: &[u8], + q_block: &[u8], + r_block: &[u8], + missing_index: usize, + data_disk_count: usize, + ) -> Vec { + Self::reconstruct_from_p(data_blocks, p_block, missing_index, data_disk_count) + } } impl VfsBackend for VfsRaidBackend {