lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0

//! Sparse file type definitions

use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;

use crate::storage::dmu::get_block_size;

/// Sparse file errors
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SparseError {
    /// File not found
    FileNotFound(u64),
    /// Dataset not found
    DatasetNotFound(String),
    /// Invalid range
    InvalidRange {
        /// Offset
        offset: u64,
        /// Length
        length: u64,
        /// File size
        file_size: u64,
    },
    /// Operation not supported
    NotSupported,
    /// I/O error
    IoError(String),
    /// No space left
    NoSpace,
    /// Permission denied
    PermissionDenied,
}

impl fmt::Display for SparseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::FileNotFound(id) => write!(f, "File not found: {}", id),
            Self::DatasetNotFound(ds) => write!(f, "Dataset not found: {}", ds),
            Self::InvalidRange {
                offset,
                length,
                file_size,
            } => {
                write!(
                    f,
                    "Invalid range: offset={}, length={}, file_size={}",
                    offset, length, file_size
                )
            }
            Self::NotSupported => write!(f, "Operation not supported"),
            Self::IoError(msg) => write!(f, "I/O error: {}", msg),
            Self::NoSpace => write!(f, "No space left on device"),
            Self::PermissionDenied => write!(f, "Permission denied"),
        }
    }
}

/// Information about a sparse file
#[derive(Debug, Clone)]
pub struct SparseInfo {
    /// Object ID
    pub object_id: u64,
    /// Logical file size
    pub logical_size: u64,
    /// Physical size on disk
    pub physical_size: u64,
    /// Whether the file is sparse
    pub is_sparse: bool,
    /// Number of holes
    pub hole_count: u64,
    /// Total hole size
    pub total_hole_size: u64,
    /// Number of data regions
    pub data_region_count: u64,
    /// Block size for sparse operations
    pub block_size: u32,
}

impl Default for SparseInfo {
    fn default() -> Self {
        Self {
            object_id: 0,
            logical_size: 0,
            physical_size: 0,
            is_sparse: false,
            hole_count: 0,
            total_hole_size: 0,
            data_region_count: 0,
            block_size: get_block_size() as u32,
        }
    }
}

/// A hole (unallocated region) in a sparse file
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HoleRegion {
    /// Start offset
    pub offset: u64,
    /// Length in bytes
    pub length: u64,
}

impl HoleRegion {
    /// Create a new hole region
    pub fn new(offset: u64, length: u64) -> Self {
        Self { offset, length }
    }

    /// End offset (exclusive)
    pub fn end(&self) -> u64 {
        self.offset + self.length
    }

    /// Check if this hole contains an offset
    pub fn contains(&self, offset: u64) -> bool {
        offset >= self.offset && offset < self.end()
    }

    /// Check if this hole overlaps with a range
    pub fn overlaps(&self, offset: u64, length: u64) -> bool {
        let range_end = offset + length;
        self.offset < range_end && self.end() > offset
    }
}

/// A data region in a sparse file
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DataRegion {
    /// Start offset
    pub offset: u64,
    /// Length in bytes
    pub length: u64,
    /// Physical block address
    pub physical_block: u64,
}

impl DataRegion {
    /// Create a new data region
    pub fn new(offset: u64, length: u64, physical_block: u64) -> Self {
        Self {
            offset,
            length,
            physical_block,
        }
    }

    /// End offset (exclusive)
    pub fn end(&self) -> u64 {
        self.offset + self.length
    }
}

/// Result of sparsifying a file
#[derive(Debug, Clone, Default)]
pub struct SparsifyResult {
    /// Space saved in bytes
    pub space_saved: u64,
    /// Number of holes created
    pub holes_created: u64,
    /// Time taken in microseconds
    pub time_us: u64,
    /// Whether the file was modified
    pub modified: bool,
}

/// Result of densifying a file
#[derive(Debug, Clone, Default)]
pub struct DensifyResult {
    /// Additional space used
    pub space_used: u64,
    /// Number of holes filled
    pub holes_filled: u64,
    /// Time taken in microseconds
    pub time_us: u64,
}

/// Space savings information
#[derive(Debug, Clone, Default)]
pub struct SpaceSavings {
    /// Logical size
    pub logical_size: u64,
    /// Physical size
    pub physical_size: u64,
    /// Space saved
    pub space_saved: u64,
    /// Savings percentage
    pub savings_percent: f32,
}

/// Sparse file extent (for extent-based operations)
#[derive(Debug, Clone)]
pub struct SparseExtent {
    /// Type of extent
    pub extent_type: ExtentType,
    /// Logical offset
    pub logical_offset: u64,
    /// Physical offset (0 for holes)
    pub physical_offset: u64,
    /// Length
    pub length: u64,
    /// Flags
    pub flags: ExtentFlags,
}

/// Type of extent
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtentType {
    /// Data extent
    Data,
    /// Hole (unallocated)
    Hole,
    /// Preallocated but unwritten
    Preallocated,
    /// Shared with other files (dedup/reflink)
    Shared,
}

/// Extent flags
#[derive(Debug, Clone, Copy, Default)]
pub struct ExtentFlags {
    /// Extent is shared
    pub shared: bool,
    /// Extent is compressed
    pub compressed: bool,
    /// Extent is encrypted
    pub encrypted: bool,
    /// Extent is inline (in metadata)
    pub inline: bool,
    /// Last extent in file
    pub last: bool,
}

/// Hole detection policy
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HoleDetectionPolicy {
    /// Only explicit holes (SEEK_HOLE)
    #[default]
    Explicit,
    /// Detect zero-filled regions
    DetectZeros,
    /// Aggressive hole detection
    Aggressive,
}

/// Sparse copy options
#[derive(Debug, Clone, Default)]
pub struct SparseCopyOptions {
    /// Preserve holes
    pub preserve_holes: bool,
    /// Create new holes for zero regions
    pub create_holes: bool,
    /// Minimum hole size to preserve
    pub min_hole_size: u64,
    /// Use reflink if available
    pub use_reflink: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_hole_region() {
        let hole = HoleRegion::new(1000, 500);
        assert_eq!(hole.offset, 1000);
        assert_eq!(hole.length, 500);
        assert_eq!(hole.end(), 1500);
        assert!(hole.contains(1000));
        assert!(hole.contains(1499));
        assert!(!hole.contains(1500));
    }

    #[test]
    fn test_hole_overlaps() {
        let hole = HoleRegion::new(1000, 500);

        // Overlapping ranges
        assert!(hole.overlaps(900, 200)); // Before and into
        assert!(hole.overlaps(1100, 100)); // Inside
        assert!(hole.overlaps(1400, 200)); // Spanning end
        assert!(hole.overlaps(800, 1000)); // Encompasses

        // Non-overlapping ranges
        assert!(!hole.overlaps(0, 1000)); // Before
        assert!(!hole.overlaps(1500, 100)); // After
    }

    #[test]
    fn test_sparse_info_default() {
        let info = SparseInfo::default();
        // Block size now comes from DMU (default 128KB = 131072)
        assert_eq!(
            info.block_size,
            crate::storage::dmu::get_block_size() as u32
        );
        assert!(!info.is_sparse);
    }
}