use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::types::*;
use crate::fscore::structs::Dva;
use crate::storage::dmu::get_block_size;
use crate::storage::zpl::ZPL;
use crate::util::alloc::ALLOCATOR;
#[inline]
fn block_size() -> u64 {
get_block_size()
}
#[inline]
fn min_hole_size() -> u64 {
block_size()
}
pub fn get_sparse_info(dataset: &str, object_id: u64) -> Result<SparseInfo, SparseError> {
let _ = dataset;
let bs = block_size();
let zpl = ZPL.lock();
let znode = zpl
.get_znode(object_id)
.ok_or(SparseError::FileNotFound(object_id))?;
let logical_size = znode.phys.size;
let (hole_count, total_hole_size, data_region_count) = if let Some(ref data) = znode.data_cache
{
let holes = detect_zero_blocks(data, bs as usize);
let hole_size: u64 = holes.iter().map(|h| h.length).sum();
let num_blocks = logical_size.div_ceil(bs);
let data_regions = num_blocks.saturating_sub(holes.len() as u64);
(holes.len() as u64, hole_size, data_regions)
} else {
if logical_size == 0 {
(0, 0, 0)
} else {
let num_blocks = logical_size.div_ceil(bs);
(0, 0, num_blocks)
}
};
let physical_size = logical_size.saturating_sub(total_hole_size);
Ok(SparseInfo {
object_id,
logical_size,
physical_size,
is_sparse: hole_count > 0 || total_hole_size > 0,
hole_count,
total_hole_size,
data_region_count,
block_size: bs as u32,
})
}
pub fn get_holes(dataset: &str, object_id: u64) -> Result<Vec<HoleRegion>, SparseError> {
let _ = dataset;
let bs = block_size();
let zpl = ZPL.lock();
let znode = zpl
.get_znode(object_id)
.ok_or(SparseError::FileNotFound(object_id))?;
if let Some(ref data) = znode.data_cache {
let mut holes = detect_zero_blocks(data, bs as usize);
merge_holes(&mut holes);
Ok(holes)
} else {
Ok(Vec::new())
}
}
pub fn get_data_regions(dataset: &str, object_id: u64) -> Result<Vec<DataRegion>, SparseError> {
let _ = dataset;
let bs = block_size();
let zpl = ZPL.lock();
let znode = zpl
.get_znode(object_id)
.ok_or(SparseError::FileNotFound(object_id))?;
let logical_size = znode.phys.size;
if logical_size == 0 {
return Ok(Vec::new());
}
let holes = if let Some(ref data) = znode.data_cache {
let mut h = detect_zero_blocks(data, bs as usize);
merge_holes(&mut h);
h
} else {
Vec::new()
};
let mut data_regions = Vec::new();
let mut current_offset = 0u64;
for hole in &holes {
if current_offset < hole.offset {
data_regions.push(DataRegion {
offset: current_offset,
length: hole.offset - current_offset,
physical_block: current_offset / bs, });
}
current_offset = hole.end();
}
if current_offset < logical_size {
data_regions.push(DataRegion {
offset: current_offset,
length: logical_size - current_offset,
physical_block: current_offset / bs,
});
}
if data_regions.is_empty() && logical_size > 0 {
let base_block = if znode.master_node.offset != 0 {
znode.master_node.offset / bs
} else {
0
};
data_regions.push(DataRegion {
offset: 0,
length: logical_size,
physical_block: base_block,
});
}
Ok(data_regions)
}
pub fn punch_hole_impl(
dataset: &str,
object_id: u64,
offset: u64,
length: u64,
) -> Result<(), SparseError> {
let _ = dataset;
if length == 0 {
return Ok(());
}
let bs = block_size();
let aligned_offset = align_down(offset, bs);
let aligned_end = align_up(offset + length, bs);
let aligned_length = aligned_end - aligned_offset;
if aligned_length == 0 {
return Ok(());
}
let mut zpl = ZPL.lock();
let znode = zpl
.get_znode_mut(object_id)
.ok_or(SparseError::FileNotFound(object_id))?;
let file_size = znode.phys.size;
if aligned_offset >= file_size {
return Ok(()); }
let actual_length = core::cmp::min(aligned_length, file_size - aligned_offset);
if let Some(ref mut data) = znode.data_cache {
let start = aligned_offset as usize;
let end = core::cmp::min(start + actual_length as usize, data.len());
if start < data.len() && end > start {
data[start..end].fill(0);
}
}
znode.dirty = true;
crate::lcpfs_println!(
"[ SPARSE ] Punched hole in object {} at offset {} length {}",
object_id,
aligned_offset,
actual_length
);
Ok(())
}
pub fn zero_range_impl(
dataset: &str,
object_id: u64,
offset: u64,
length: u64,
) -> Result<(), SparseError> {
if length >= min_hole_size() {
punch_hole_impl(dataset, object_id, offset, length)
} else {
let mut zpl = ZPL.lock();
let znode = zpl
.get_znode_mut(object_id)
.ok_or(SparseError::FileNotFound(object_id))?;
if let Some(ref mut data) = znode.data_cache {
let start = offset as usize;
let end = core::cmp::min(start + length as usize, data.len());
if start < data.len() && end > start {
data[start..end].fill(0);
znode.dirty = true;
}
}
Ok(())
}
}
pub fn sparsify_file(dataset: &str, object_id: u64) -> Result<SparsifyResult, SparseError> {
let _ = dataset;
let start_time = crate::time::monotonic();
let bs = block_size();
let zpl = ZPL.lock();
let znode = zpl
.get_znode(object_id)
.ok_or(SparseError::FileNotFound(object_id))?;
let logical_size = znode.phys.size;
if logical_size == 0 {
return Ok(SparsifyResult::default());
}
let (holes_created, space_saved) = if let Some(ref data) = znode.data_cache {
let holes = detect_zero_blocks(data, bs as usize);
let total_hole_size: u64 = holes.iter().map(|h| h.length).sum();
(holes.len() as u64, total_hole_size)
} else {
(0, 0)
};
let end_time = crate::time::monotonic();
let time_us = (end_time - start_time) * 1000;
Ok(SparsifyResult {
space_saved,
holes_created,
time_us,
modified: holes_created > 0,
})
}
pub fn densify_file(dataset: &str, object_id: u64) -> Result<DensifyResult, SparseError> {
let _ = dataset;
let start_time = crate::time::monotonic();
let holes = get_holes(dataset, object_id)?;
let holes_filled = holes.len() as u64;
let space_used: u64 = holes.iter().map(|h| h.length).sum();
let mut zpl = ZPL.lock();
if let Some(znode) = zpl.get_znode_mut(object_id) {
if znode.data_cache.is_none() && znode.phys.size > 0 {
znode.data_cache = Some(alloc::vec![0u8; znode.phys.size as usize]);
}
znode.dirty = true;
}
let end_time = crate::time::monotonic();
let time_us = (end_time - start_time) * 1000;
Ok(DensifyResult {
space_used,
holes_filled,
time_us,
})
}
pub fn seek_data_impl(dataset: &str, object_id: u64, offset: u64) -> Result<u64, SparseError> {
let _ = dataset;
let holes = get_holes(dataset, object_id)?;
let info = get_sparse_info(dataset, object_id)?;
if holes.is_empty() {
return if offset < info.logical_size {
Ok(offset)
} else {
Ok(info.logical_size)
};
}
for hole in &holes {
if hole.contains(offset) {
return Ok(hole.end());
}
}
if offset < info.logical_size {
Ok(offset)
} else {
Ok(info.logical_size)
}
}
pub fn seek_hole_impl(dataset: &str, object_id: u64, offset: u64) -> Result<u64, SparseError> {
let _ = dataset;
let holes = get_holes(dataset, object_id)?;
let info = get_sparse_info(dataset, object_id)?;
for hole in &holes {
if hole.offset >= offset {
return Ok(hole.offset);
}
if hole.contains(offset) {
return Ok(offset);
}
}
Ok(info.logical_size)
}
pub fn preallocate_impl(
dataset: &str,
object_id: u64,
offset: u64,
length: u64,
) -> Result<(), SparseError> {
let _ = dataset;
if length == 0 {
return Ok(());
}
let mut zpl = ZPL.lock();
let znode = zpl
.get_znode_mut(object_id)
.ok_or(SparseError::FileNotFound(object_id))?;
let required_len = (offset + length) as usize;
if let Some(ref mut data) = znode.data_cache {
if data.len() < required_len {
data.resize(required_len, 0);
}
} else {
znode.data_cache = Some(alloc::vec![0u8; required_len]);
}
crate::lcpfs_println!(
"[ SPARSE ] Preallocated {} bytes at offset {} for object {}",
length,
offset,
object_id
);
Ok(())
}
pub fn calculate_space_savings(dataset: &str, object_id: u64) -> Result<SpaceSavings, SparseError> {
let info = get_sparse_info(dataset, object_id)?;
let space_saved = info.logical_size.saturating_sub(info.physical_size);
let savings_percent = if info.logical_size > 0 {
(space_saved as f32 / info.logical_size as f32) * 100.0
} else {
0.0
};
Ok(SpaceSavings {
logical_size: info.logical_size,
physical_size: info.physical_size,
space_saved,
savings_percent,
})
}
pub fn copy_sparse_impl(
src_dataset: &str,
src_id: u64,
dst_dataset: &str,
dst_path: &str,
) -> Result<u64, SparseError> {
let _ = (src_dataset, dst_dataset, dst_path);
let (src_size, src_data) = {
let zpl = ZPL.lock();
let znode = zpl
.get_znode(src_id)
.ok_or(SparseError::FileNotFound(src_id))?;
let data = znode.data_cache.clone().unwrap_or_default();
(znode.phys.size, data)
};
if src_size == 0 {
return Err(SparseError::IoError("Source file is empty".into()));
}
Err(SparseError::NotSupported)
}
fn align_down(offset: u64, block_size: u64) -> u64 {
offset & !(block_size - 1)
}
fn align_up(offset: u64, block_size: u64) -> u64 {
(offset + block_size - 1) & !(block_size - 1)
}
pub fn detect_zero_blocks(data: &[u8], block_size: usize) -> Vec<HoleRegion> {
let mut holes: Vec<HoleRegion> = Vec::new();
let mut i = 0;
while i < data.len() {
let block_end = (i + block_size).min(data.len());
let block = &data[i..block_end];
if is_zero_block(block) {
if let Some(last) = holes.last_mut() {
if last.end() == i as u64 {
last.length += block.len() as u64;
} else {
holes.push(HoleRegion::new(i as u64, block.len() as u64));
}
} else {
holes.push(HoleRegion::new(i as u64, block.len() as u64));
}
}
i = block_end;
}
holes
.into_iter()
.filter(|h| h.length >= block_size as u64)
.collect()
}
fn is_zero_block(block: &[u8]) -> bool {
let mut i = 0;
while i + 8 <= block.len() {
let chunk = u64::from_ne_bytes(block[i..i + 8].try_into().unwrap_or([0; 8]));
if chunk != 0 {
return false;
}
i += 8;
}
for byte in &block[i..] {
if *byte != 0 {
return false;
}
}
true
}
pub fn merge_holes(holes: &mut Vec<HoleRegion>) {
if holes.len() < 2 {
return;
}
holes.sort_by_key(|h| h.offset);
let mut i = 0;
while i < holes.len() - 1 {
if holes[i].end() == holes[i + 1].offset {
holes[i].length += holes[i + 1].length;
holes.remove(i + 1);
} else {
i += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn test_align_down() {
assert_eq!(align_down(0, 4096), 0);
assert_eq!(align_down(4095, 4096), 0);
assert_eq!(align_down(4096, 4096), 4096);
assert_eq!(align_down(5000, 4096), 4096);
}
#[test]
fn test_align_up() {
assert_eq!(align_up(0, 4096), 0);
assert_eq!(align_up(1, 4096), 4096);
assert_eq!(align_up(4096, 4096), 4096);
assert_eq!(align_up(4097, 4096), 8192);
}
#[test]
fn test_is_zero_block() {
let zeros = [0u8; 4096];
assert!(is_zero_block(&zeros));
let mut nonzero = [0u8; 4096];
nonzero[2048] = 1;
assert!(!is_zero_block(&nonzero));
}
#[test]
fn test_detect_zero_blocks() {
let mut data = vec![0u8; 16384];
data[0..4096].fill(1);
data[8192..12288].fill(2);
let holes = detect_zero_blocks(&data, 4096);
assert_eq!(holes.len(), 2);
assert_eq!(holes[0].offset, 4096);
assert_eq!(holes[0].length, 4096);
assert_eq!(holes[1].offset, 12288);
assert_eq!(holes[1].length, 4096);
}
#[test]
fn test_merge_holes() {
let mut holes = vec![
HoleRegion::new(0, 4096),
HoleRegion::new(4096, 4096),
HoleRegion::new(12288, 4096),
];
merge_holes(&mut holes);
assert_eq!(holes.len(), 2);
assert_eq!(holes[0].offset, 0);
assert_eq!(holes[0].length, 8192);
assert_eq!(holes[1].offset, 12288);
assert_eq!(holes[1].length, 4096);
}
}