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
//
// Quota Enforcement
// Enforce dataset quotas and track space usage.

use crate::FsError;
use crate::mgmt::dataset::DatasetPhys;

/// Quota enforcement engine
pub struct QuotaEngine;

impl QuotaEngine {
    /// Check if a write would exceed the dataset quota
    ///
    /// # Arguments
    /// * `dataset` - Dataset physical metadata
    /// * `additional_bytes` - Bytes to be written
    ///
    /// # Returns
    /// * `Ok(())` - Write is allowed (under quota)
    /// * `Err(FsError::DiskFull)` - Write would exceed quota
    ///
    /// # Algorithm
    /// 1. Check if quota is set (quota == 0 means unlimited)
    /// 2. Calculate new usage: current + additional
    /// 3. Compare against quota
    /// 4. Return error if would exceed
    pub fn check_quota(dataset: &DatasetPhys, additional_bytes: u64) -> Result<(), FsError> {
        // Quota of 0 means unlimited
        if dataset.quota == 0 {
            return Ok(());
        }

        let new_usage = dataset.used_bytes.saturating_add(additional_bytes);

        if new_usage > dataset.quota {
            return Err(FsError::DiskFull {
                needed_bytes: additional_bytes,
            });
        }

        Ok(())
    }

    /// Update dataset usage after a successful write
    ///
    /// # Arguments
    /// * `dataset` - Dataset physical metadata (mutable)
    /// * `bytes_written` - Number of bytes successfully written
    ///
    /// # Note
    /// This should be called after the write has been committed to disk.
    /// It updates the `used_bytes` counter for quota tracking.
    pub fn update_usage(dataset: &mut DatasetPhys, bytes_written: u64) {
        dataset.used_bytes = dataset.used_bytes.saturating_add(bytes_written);
    }

    /// Decrease dataset usage after a file deletion or truncation
    ///
    /// # Arguments
    /// * `dataset` - Dataset physical metadata (mutable)
    /// * `bytes_freed` - Number of bytes freed
    pub fn decrease_usage(dataset: &mut DatasetPhys, bytes_freed: u64) {
        dataset.used_bytes = dataset.used_bytes.saturating_sub(bytes_freed);
    }

    /// Get remaining quota space
    ///
    /// # Arguments
    /// * `dataset` - Dataset physical metadata
    ///
    /// # Returns
    /// * `Some(bytes)` - Remaining quota space
    /// * `None` - Unlimited quota
    pub fn remaining_quota(dataset: &DatasetPhys) -> Option<u64> {
        if dataset.quota == 0 {
            return None; // Unlimited
        }

        Some(dataset.quota.saturating_sub(dataset.used_bytes))
    }

    /// Check if dataset is over quota
    ///
    /// # Arguments
    /// * `dataset` - Dataset physical metadata
    ///
    /// # Returns
    /// * `true` - Dataset is over quota (should prevent new writes)
    /// * `false` - Dataset is under quota or has unlimited quota
    pub fn is_over_quota(dataset: &DatasetPhys) -> bool {
        if dataset.quota == 0 {
            return false; // Unlimited
        }

        dataset.used_bytes > dataset.quota
    }

    /// Get quota utilization percentage
    ///
    /// # Arguments
    /// * `dataset` - Dataset physical metadata
    ///
    /// # Returns
    /// * `Some(percentage)` - Utilization percentage (0.0 - 100.0)
    /// * `None` - Unlimited quota
    pub fn quota_utilization(dataset: &DatasetPhys) -> Option<f64> {
        if dataset.quota == 0 {
            return None; // Unlimited
        }

        let utilization = (dataset.used_bytes as f64 / dataset.quota as f64) * 100.0;
        Some(utilization.min(100.0))
    }
}

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

    #[test]
    fn test_quota_unlimited() {
        let dataset = DatasetPhys {
            quota: 0, // Unlimited
            used_bytes: 1000,
            ..DatasetPhys::new(1)
        };

        // Unlimited quota should always allow writes
        assert!(QuotaEngine::check_quota(&dataset, 1_000_000_000).is_ok());
        assert!(!QuotaEngine::is_over_quota(&dataset));
        assert_eq!(QuotaEngine::remaining_quota(&dataset), None);
        assert_eq!(QuotaEngine::quota_utilization(&dataset), None);
    }

    #[test]
    fn test_quota_under_limit() {
        let dataset = DatasetPhys {
            quota: 1000,
            used_bytes: 500,
            ..DatasetPhys::new(1)
        };

        // Should allow write that stays under quota
        assert!(QuotaEngine::check_quota(&dataset, 400).is_ok());
        assert!(!QuotaEngine::is_over_quota(&dataset));
        assert_eq!(QuotaEngine::remaining_quota(&dataset), Some(500));
        assert_eq!(QuotaEngine::quota_utilization(&dataset), Some(50.0));
    }

    #[test]
    fn test_quota_at_limit() {
        let dataset = DatasetPhys {
            quota: 1000,
            used_bytes: 500,
            ..DatasetPhys::new(1)
        };

        // Should allow write that reaches quota exactly
        assert!(QuotaEngine::check_quota(&dataset, 500).is_ok());
    }

    #[test]
    fn test_quota_exceeded() {
        let dataset = DatasetPhys {
            quota: 1000,
            used_bytes: 500,
            ..DatasetPhys::new(1)
        };

        // Should reject write that would exceed quota
        assert!(matches!(
            QuotaEngine::check_quota(&dataset, 501),
            Err(FsError::DiskFull { .. })
        ));
    }

    #[test]
    fn test_quota_already_over() {
        let dataset = DatasetPhys {
            quota: 1000,
            used_bytes: 1500, // Already over quota
            ..DatasetPhys::new(1)
        };

        // Should reject any write when already over quota
        assert!(matches!(
            QuotaEngine::check_quota(&dataset, 1),
            Err(FsError::DiskFull { .. })
        ));
        assert!(QuotaEngine::is_over_quota(&dataset));
    }

    #[test]
    fn test_update_usage() {
        let mut dataset = DatasetPhys {
            quota: 1000,
            used_bytes: 100,
            ..DatasetPhys::new(1)
        };

        QuotaEngine::update_usage(&mut dataset, 200);
        assert_eq!(dataset.used_bytes, 300);

        QuotaEngine::update_usage(&mut dataset, 50);
        assert_eq!(dataset.used_bytes, 350);
    }

    #[test]
    fn test_decrease_usage() {
        let mut dataset = DatasetPhys {
            quota: 1000,
            used_bytes: 500,
            ..DatasetPhys::new(1)
        };

        QuotaEngine::decrease_usage(&mut dataset, 200);
        assert_eq!(dataset.used_bytes, 300);

        // Should not underflow
        QuotaEngine::decrease_usage(&mut dataset, 1000);
        assert_eq!(dataset.used_bytes, 0);
    }

    #[test]
    fn test_utilization_calculation() {
        let dataset = DatasetPhys {
            quota: 1000,
            used_bytes: 750,
            ..DatasetPhys::new(1)
        };

        assert_eq!(QuotaEngine::quota_utilization(&dataset), Some(75.0));
    }
}