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

//! User and Group Quota Management for LCPFS.
//!
//! This module provides per-user and per-group storage limits with
//! soft limits (with grace period) and hard limits.
//!
//! # Features
//!
//! - **Soft Limits**: Warnings with configurable grace period
//! - **Hard Limits**: Immediate denial when exceeded
//! - **Usage Tracking**: Real-time tracking of bytes and inodes
//! - **Quota Reports**: Summary reports of all quotas
//! - **Rescan**: Recalculate usage from filesystem
//!
//! # Example
//!
//! ```ignore
//! use lcpfs::quota::{QuotaKey, QuotaLimits, set_quota, check_write};
//!
//! // Set a 1GB soft limit, 2GB hard limit for user 1000
//! set_quota("pool/data", QuotaKey::user(1000),
//!     QuotaLimits::bytes(1_000_000_000, 2_000_000_000))?;
//!
//! // Check if a write is allowed
//! let result = check_write("pool/data", 1000, 100, 1_000_000, timestamp)?;
//! if !result.is_allowed() {
//!     // Quota exceeded
//! }
//! ```
//!
//! # Quota Types
//!
//! - **User**: Limits per user ID
//! - **Group**: Limits per group ID
//! - **Project**: Limits per project ID (for directory trees)
//!
//! # Soft vs Hard Limits
//!
//! - **Soft Limit**: Can be exceeded for a grace period (default 7 days)
//! - **Hard Limit**: Cannot be exceeded (immediate EDQUOT error)
//!
//! When over the soft limit:
//! 1. Writes are allowed during grace period
//! 2. Warnings are issued
//! 3. After grace expires, soft limit becomes hard limit
//!
//! # Integration with Write Path
//!
//! ```ignore
//! // Before write:
//! let check = check_write(dataset, uid, gid, write_size, now)?;
//! if !check.is_allowed() {
//!     return Err(EDQUOT);
//! }
//!
//! // After successful write:
//! update_usage(dataset, uid, gid, write_size as i64, 0, now)?;
//! ```

pub mod scan;
pub mod store;
pub mod types;

// Re-exports
pub use scan::{
    FileInfo, MemoryScanner, QuotaScanner, ScanResult, rescan, rescan_group, rescan_user,
};
pub use store::{
    check_create, check_write, clear_quotas, generate_report, get_quota, get_usage, has_quotas,
    list_datasets, list_quotas, list_quotas_by_type, remove_quota, reset_usage, set_quota,
    set_usage, update_usage,
};
pub use types::{
    DEFAULT_GRACE_PERIOD, NO_LIMIT, QUOTA_BLOCK_SIZE, Quota, QuotaCheckResult, QuotaError,
    QuotaKey, QuotaLimits, QuotaReport, QuotaReportEntry, QuotaResult, QuotaStatus, QuotaType,
    QuotaUsage,
};

// ═══════════════════════════════════════════════════════════════════════════════
// CONVENIENCE FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════════

/// Set a user quota with byte limits.
pub fn set_user_quota(
    dataset: &str,
    uid: u32,
    soft_bytes: u64,
    hard_bytes: u64,
) -> QuotaResult<()> {
    set_quota(
        dataset,
        QuotaKey::user(uid),
        QuotaLimits::bytes(soft_bytes, hard_bytes),
    )
}

/// Set a group quota with byte limits.
pub fn set_group_quota(
    dataset: &str,
    gid: u32,
    soft_bytes: u64,
    hard_bytes: u64,
) -> QuotaResult<()> {
    set_quota(
        dataset,
        QuotaKey::group(gid),
        QuotaLimits::bytes(soft_bytes, hard_bytes),
    )
}

/// Get user quota.
pub fn get_user_quota(dataset: &str, uid: u32) -> QuotaResult<Quota> {
    get_quota(dataset, QuotaKey::user(uid))
}

/// Get group quota.
pub fn get_group_quota(dataset: &str, gid: u32) -> QuotaResult<Quota> {
    get_quota(dataset, QuotaKey::group(gid))
}

/// Get user usage.
pub fn get_user_usage(dataset: &str, uid: u32) -> QuotaResult<QuotaUsage> {
    get_usage(dataset, QuotaKey::user(uid))
}

/// Get group usage.
pub fn get_group_usage(dataset: &str, gid: u32) -> QuotaResult<QuotaUsage> {
    get_usage(dataset, QuotaKey::group(gid))
}

// ═══════════════════════════════════════════════════════════════════════════════
// INTEGRATION TESTS
// ═══════════════════════════════════════════════════════════════════════════════

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

    fn clean_test_dataset(name: &str) {
        let _ = clear_quotas(name);
    }

    #[test]
    fn test_exports_accessible() {
        // Verify all re-exports are accessible
        let _ = QuotaType::User;
        let _ = QuotaKey::user(1000);
        let _ = QuotaLimits::unlimited();
        let _ = QuotaStatus::Ok;
    }

    #[test]
    fn test_convenience_functions() {
        let dataset = "test_conv";
        clean_test_dataset(dataset);

        set_user_quota(dataset, 1000, 1_000_000, 2_000_000).unwrap();
        let quota = get_user_quota(dataset, 1000).unwrap();
        assert_eq!(quota.limits.soft_bytes, 1_000_000);

        set_group_quota(dataset, 100, 5_000_000, 10_000_000).unwrap();
        let quota = get_group_quota(dataset, 100).unwrap();
        assert_eq!(quota.limits.hard_bytes, 10_000_000);

        clean_test_dataset(dataset);
    }

    #[test]
    fn test_full_quota_workflow() {
        let dataset = "test_workflow";
        clean_test_dataset(dataset);

        // 1. Set quotas
        set_user_quota(dataset, 1000, 1000, 2000).unwrap();
        set_group_quota(dataset, 100, 5000, 10000).unwrap();

        // 2. Check initial write
        let result = check_write(dataset, 1000, 100, 500, 0).unwrap();
        assert!(result.is_allowed());

        // 3. Update usage
        update_usage(dataset, 1000, 100, 500, 1, 0).unwrap();

        // 4. Check usage updated
        let usage = get_user_usage(dataset, 1000).unwrap();
        assert_eq!(usage.bytes_used, 500);

        // 5. Check another write
        let result = check_write(dataset, 1000, 100, 300, 0).unwrap();
        assert!(result.is_allowed()); // Still under soft limit

        // 6. Write goes over soft limit
        update_usage(dataset, 1000, 100, 600, 0, 0).unwrap();

        let result = check_write(dataset, 1000, 100, 100, 0).unwrap();
        assert!(result.is_allowed()); // Warning but allowed
        assert_eq!(result.status, QuotaStatus::SoftLimitWarning);

        // 7. Try to exceed hard limit
        update_usage(dataset, 1000, 100, 1000, 0, 0).unwrap();

        let result = check_write(dataset, 1000, 100, 500, 0).unwrap();
        assert!(!result.is_allowed());
        assert_eq!(result.status, QuotaStatus::HardLimitExceeded);

        clean_test_dataset(dataset);
    }

    #[test]
    fn test_rescan_workflow() {
        let dataset = "test_rescan_wf";
        clean_test_dataset(dataset);

        // Set up quotas
        set_user_quota(dataset, 1000, 10000, 20000).unwrap();

        // Create scanner with mock files
        let mut scanner = MemoryScanner::new();
        scanner.add_files(
            dataset,
            vec![
                FileInfo::new("/home/user/file1.txt", 1000, 100, 1000, false),
                FileInfo::new("/home/user/file2.txt", 1000, 100, 2000, false),
                FileInfo::new("/home/user/docs", 1000, 100, 0, true),
            ],
        );

        // Rescan
        let result = rescan(dataset, &scanner).unwrap();
        assert_eq!(result.files_scanned, 2);
        assert_eq!(result.dirs_scanned, 1);
        assert_eq!(result.total_bytes, 3000);

        // Check usage was updated
        let usage = get_user_usage(dataset, 1000).unwrap();
        assert_eq!(usage.bytes_used, 3000);
        assert_eq!(usage.inodes_used, 3);

        clean_test_dataset(dataset);
    }

    #[test]
    fn test_quota_report() {
        let dataset = "test_report";
        clean_test_dataset(dataset);

        set_user_quota(dataset, 1, 100, 200).unwrap();
        set_user_quota(dataset, 2, 100, 200).unwrap();
        set_group_quota(dataset, 1, 500, 1000).unwrap();

        // Set some usage
        set_usage(dataset, QuotaKey::user(1), QuotaUsage::new(50, 5)).unwrap();
        set_usage(dataset, QuotaKey::user(2), QuotaUsage::new(150, 10)).unwrap(); // Over soft

        let report = generate_report(dataset, 0).unwrap();
        assert_eq!(report.total_entries, 3);
        assert!(report.over_soft > 0);

        clean_test_dataset(dataset);
    }

    #[test]
    fn test_check_create() {
        let dataset = "test_create";
        clean_test_dataset(dataset);

        set_quota(dataset, QuotaKey::user(1000), QuotaLimits::inodes(10, 20)).unwrap();

        // Check create allowed
        let result = check_create(dataset, 1000, 100, 0).unwrap();
        assert!(result.is_allowed());

        // Set usage to hard limit
        set_usage(dataset, QuotaKey::user(1000), QuotaUsage::new(0, 20)).unwrap();

        // Check create denied
        let result = check_create(dataset, 1000, 100, 0).unwrap();
        assert!(!result.is_allowed());
        assert_eq!(result.status, QuotaStatus::HardLimitExceeded);

        clean_test_dataset(dataset);
    }

    #[test]
    fn test_list_functions() {
        let dataset = "test_list_fn";
        clean_test_dataset(dataset);

        assert!(!has_quotas(dataset));

        set_user_quota(dataset, 1, 100, 200).unwrap();
        set_user_quota(dataset, 2, 100, 200).unwrap();
        set_group_quota(dataset, 1, 500, 1000).unwrap();

        assert!(has_quotas(dataset));

        let all = list_quotas(dataset).unwrap();
        assert_eq!(all.len(), 3);

        let users = list_quotas_by_type(dataset, QuotaType::User).unwrap();
        assert_eq!(users.len(), 2);

        let groups = list_quotas_by_type(dataset, QuotaType::Group).unwrap();
        assert_eq!(groups.len(), 1);

        clean_test_dataset(dataset);
    }

    #[test]
    fn test_soft_limit_grace_period() {
        let dataset = "test_grace";
        clean_test_dataset(dataset);

        // Set quota with 10 second grace period
        let limits = QuotaLimits::bytes(100, 200).with_grace_period(10);
        set_quota(dataset, QuotaKey::user(1000), limits).unwrap();

        // Set usage over soft limit with exceeded timestamp
        let mut usage = QuotaUsage::new(150, 5);
        usage.soft_bytes_exceeded_at = 100; // Exceeded at time 100
        set_usage(dataset, QuotaKey::user(1000), usage).unwrap();

        // Check at time 105 (within grace)
        let result = check_write(dataset, 1000, 100, 10, 105).unwrap();
        assert!(result.is_allowed());
        assert_eq!(result.status, QuotaStatus::SoftLimitWarning);

        // Check at time 115 (grace expired)
        let result = check_write(dataset, 1000, 100, 10, 115).unwrap();
        assert!(!result.is_allowed());
        assert_eq!(result.status, QuotaStatus::SoftLimitExceeded);

        clean_test_dataset(dataset);
    }

    #[test]
    fn test_constants() {
        const { assert!(DEFAULT_GRACE_PERIOD > 0) };
        const { assert!(NO_LIMIT == 0) };
        const { assert!(QUOTA_BLOCK_SIZE > 0) };
    }
}