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 scanning and recalculation.
//!
//! This module provides filesystem scanning to recalculate usage.

use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use super::store::{reset_usage, set_usage};
use super::types::{QuotaError, QuotaKey, QuotaResult, QuotaType, QuotaUsage};

// ═══════════════════════════════════════════════════════════════════════════════
// SCAN RESULT
// ═══════════════════════════════════════════════════════════════════════════════

/// Result of a quota scan.
#[derive(Debug, Clone, Default)]
pub struct ScanResult {
    /// Dataset scanned.
    pub dataset: String,
    /// Number of files scanned.
    pub files_scanned: u64,
    /// Number of directories scanned.
    pub dirs_scanned: u64,
    /// Total bytes scanned.
    pub total_bytes: u64,
    /// Number of users found.
    pub users_found: u32,
    /// Number of groups found.
    pub groups_found: u32,
    /// Scan duration in nanoseconds.
    pub duration_ns: u64,
    /// Errors encountered.
    pub errors: Vec<String>,
}

impl ScanResult {
    /// Create a new scan result.
    pub fn new(dataset: &str) -> Self {
        Self {
            dataset: dataset.to_string(),
            ..Default::default()
        }
    }

    /// Add an error message.
    pub fn add_error(&mut self, msg: String) {
        self.errors.push(msg);
    }

    /// Check if scan completed without errors.
    pub fn is_ok(&self) -> bool {
        self.errors.is_empty()
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// FILE INFO (for scanning)
// ═══════════════════════════════════════════════════════════════════════════════

/// File information for quota scanning.
#[derive(Debug, Clone)]
pub struct FileInfo {
    /// File path.
    pub path: String,
    /// Owner UID.
    pub uid: u32,
    /// Owner GID.
    pub gid: u32,
    /// File size in bytes.
    pub size: u64,
    /// Is a directory.
    pub is_dir: bool,
    /// Project ID (if applicable).
    pub project_id: Option<u32>,
}

impl FileInfo {
    /// Create new file info.
    pub fn new(path: &str, uid: u32, gid: u32, size: u64, is_dir: bool) -> Self {
        Self {
            path: path.to_string(),
            uid,
            gid,
            size,
            is_dir,
            project_id: None,
        }
    }

    /// Set project ID.
    pub fn with_project(mut self, project_id: u32) -> Self {
        self.project_id = Some(project_id);
        self
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// SCANNER TRAIT
// ═══════════════════════════════════════════════════════════════════════════════

/// Trait for filesystem scanning.
pub trait QuotaScanner: Send + Sync {
    /// Scan the filesystem and return file info.
    fn scan(&self, dataset: &str) -> QuotaResult<Vec<FileInfo>>;
}

// ═══════════════════════════════════════════════════════════════════════════════
// IN-MEMORY SCANNER (for testing)
// ═══════════════════════════════════════════════════════════════════════════════

/// In-memory scanner for testing.
#[derive(Debug, Default)]
pub struct MemoryScanner {
    /// Files by dataset.
    files: BTreeMap<String, Vec<FileInfo>>,
}

impl MemoryScanner {
    /// Create a new memory scanner.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add files for a dataset.
    pub fn add_files(&mut self, dataset: &str, files: Vec<FileInfo>) {
        self.files.insert(dataset.to_string(), files);
    }

    /// Clear all files.
    pub fn clear(&mut self) {
        self.files.clear();
    }
}

impl QuotaScanner for MemoryScanner {
    fn scan(&self, dataset: &str) -> QuotaResult<Vec<FileInfo>> {
        Ok(self.files.get(dataset).cloned().unwrap_or_default())
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// RESCAN FUNCTION
// ═══════════════════════════════════════════════════════════════════════════════

/// Rescan a dataset and recalculate all usage.
pub fn rescan<S: QuotaScanner>(dataset: &str, scanner: &S) -> QuotaResult<ScanResult> {
    let start = 0u64; // In real kernel, get current time
    let mut result = ScanResult::new(dataset);

    // Reset current usage
    reset_usage(dataset)?;

    // Scan filesystem
    let files = scanner.scan(dataset)?;

    // Accumulate usage by user, group, project
    let mut user_usage: BTreeMap<u32, QuotaUsage> = BTreeMap::new();
    let mut group_usage: BTreeMap<u32, QuotaUsage> = BTreeMap::new();
    let mut project_usage: BTreeMap<u32, QuotaUsage> = BTreeMap::new();

    for file in &files {
        if file.is_dir {
            result.dirs_scanned += 1;
        } else {
            result.files_scanned += 1;
        }
        result.total_bytes += file.size;

        // User usage
        let user = user_usage.entry(file.uid).or_default();
        user.add_bytes(file.size);
        user.add_inode();

        // Group usage
        let group = group_usage.entry(file.gid).or_default();
        group.add_bytes(file.size);
        group.add_inode();

        // Project usage
        if let Some(pid) = file.project_id {
            let project = project_usage.entry(pid).or_default();
            project.add_bytes(file.size);
            project.add_inode();
        }
    }

    result.users_found = user_usage.len() as u32;
    result.groups_found = group_usage.len() as u32;

    // Update usage in store
    for (uid, usage) in user_usage {
        if let Err(e) = set_usage(dataset, QuotaKey::user(uid), usage) {
            result.add_error(alloc::format!("failed to set user {} usage: {}", uid, e));
        }
    }

    for (gid, usage) in group_usage {
        if let Err(e) = set_usage(dataset, QuotaKey::group(gid), usage) {
            result.add_error(alloc::format!("failed to set group {} usage: {}", gid, e));
        }
    }

    for (pid, usage) in project_usage {
        if let Err(e) = set_usage(dataset, QuotaKey::project(pid), usage) {
            result.add_error(alloc::format!("failed to set project {} usage: {}", pid, e));
        }
    }

    result.duration_ns = 0u64.saturating_sub(start); // In real kernel, calculate elapsed

    Ok(result)
}

/// Rescan usage for a specific user.
pub fn rescan_user<S: QuotaScanner>(
    dataset: &str,
    uid: u32,
    scanner: &S,
) -> QuotaResult<QuotaUsage> {
    let files = scanner.scan(dataset)?;

    let mut usage = QuotaUsage::zero();
    for file in files {
        if file.uid == uid {
            usage.add_bytes(file.size);
            usage.add_inode();
        }
    }

    set_usage(dataset, QuotaKey::user(uid), usage)?;
    Ok(usage)
}

/// Rescan usage for a specific group.
pub fn rescan_group<S: QuotaScanner>(
    dataset: &str,
    gid: u32,
    scanner: &S,
) -> QuotaResult<QuotaUsage> {
    let files = scanner.scan(dataset)?;

    let mut usage = QuotaUsage::zero();
    for file in files {
        if file.gid == gid {
            usage.add_bytes(file.size);
            usage.add_inode();
        }
    }

    set_usage(dataset, QuotaKey::group(gid), usage)?;
    Ok(usage)
}

// ═══════════════════════════════════════════════════════════════════════════════
// TESTS
// ═══════════════════════════════════════════════════════════════════════════════

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

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

    #[test]
    fn test_file_info() {
        let file = FileInfo::new("/test/file.txt", 1000, 100, 4096, false);
        assert_eq!(file.uid, 1000);
        assert_eq!(file.gid, 100);
        assert_eq!(file.size, 4096);
        assert!(!file.is_dir);
    }

    #[test]
    fn test_file_info_with_project() {
        let file = FileInfo::new("/test/file.txt", 1000, 100, 4096, false).with_project(42);
        assert_eq!(file.project_id, Some(42));
    }

    #[test]
    fn test_memory_scanner() {
        let mut scanner = MemoryScanner::new();
        scanner.add_files(
            "test",
            vec![
                FileInfo::new("/a", 1, 1, 100, false),
                FileInfo::new("/b", 1, 1, 200, false),
            ],
        );

        let files = scanner.scan("test").unwrap();
        assert_eq!(files.len(), 2);

        let empty = scanner.scan("nonexistent").unwrap();
        assert!(empty.is_empty());
    }

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

        let mut scanner = MemoryScanner::new();
        scanner.add_files(
            dataset,
            vec![
                FileInfo::new("/home/user1/file1", 1000, 100, 1000, false),
                FileInfo::new("/home/user1/file2", 1000, 100, 2000, false),
                FileInfo::new("/home/user2/file1", 1001, 100, 500, false),
                FileInfo::new("/shared", 1000, 200, 1500, true),
            ],
        );

        let result = rescan(dataset, &scanner).unwrap();

        assert_eq!(result.files_scanned, 3);
        assert_eq!(result.dirs_scanned, 1);
        assert_eq!(result.total_bytes, 5000);
        assert_eq!(result.users_found, 2);
        assert!(result.is_ok());

        clean_test_dataset(dataset);
    }

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

        let mut scanner = MemoryScanner::new();
        scanner.add_files(
            dataset,
            vec![
                FileInfo::new("/user1/a", 1000, 100, 1000, false),
                FileInfo::new("/user1/b", 1000, 100, 2000, false),
                FileInfo::new("/user2/a", 1001, 100, 500, false),
            ],
        );

        let usage = rescan_user(dataset, 1000, &scanner).unwrap();

        assert_eq!(usage.bytes_used, 3000);
        assert_eq!(usage.inodes_used, 2);

        clean_test_dataset(dataset);
    }

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

        let mut scanner = MemoryScanner::new();
        scanner.add_files(
            dataset,
            vec![
                FileInfo::new("/a", 1000, 100, 1000, false),
                FileInfo::new("/b", 1001, 100, 2000, false),
                FileInfo::new("/c", 1002, 200, 500, false),
            ],
        );

        let usage = rescan_group(dataset, 100, &scanner).unwrap();

        assert_eq!(usage.bytes_used, 3000);
        assert_eq!(usage.inodes_used, 2);

        clean_test_dataset(dataset);
    }

    #[test]
    fn test_scan_result() {
        let mut result = ScanResult::new("test");
        result.files_scanned = 100;
        result.total_bytes = 1_000_000;

        assert!(result.is_ok());

        result.add_error("test error".into());
        assert!(!result.is_ok());
    }
}