lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0
//
// File Versioning
// Automatic per-file history beyond snapshots.

use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use lazy_static::lazy_static;
use spin::Mutex;

/// File version metadata
#[derive(Debug, Clone)]
pub struct FileVersion {
    /// File ID
    pub file_id: u64,
    /// Version number (starts at 1)
    pub version: u64,
    /// Timestamp when version was created
    pub timestamp: u64,
    /// Size in bytes
    pub size: u64,
    /// Block pointer (location on disk)
    pub block_ptr: u64,
    /// BLAKE3 checksum
    pub checksum: [u8; 32],
    /// User ID who created this version
    pub user_id: u64,
    /// Change description (optional)
    pub description: &'static str,
}

impl FileVersion {
    /// Create new file version
    pub fn new(
        file_id: u64,
        version: u64,
        timestamp: u64,
        size: u64,
        block_ptr: u64,
        checksum: [u8; 32],
        user_id: u64,
    ) -> Self {
        Self {
            file_id,
            version,
            timestamp,
            size,
            block_ptr,
            checksum,
            user_id,
            description: "",
        }
    }

    /// Set description
    pub fn with_description(mut self, desc: &'static str) -> Self {
        self.description = desc;
        self
    }
}

/// Versioning policy
#[derive(Debug, Clone, Copy)]
pub struct VersioningPolicy {
    /// Maximum versions to keep (0 = unlimited)
    pub max_versions: u32,
    /// Keep versions for this many seconds (0 = forever)
    pub retention_seconds: u64,
    /// Minimum time between versions (avoid creating version on every write)
    pub min_version_interval: u64,
    /// Only version on explicit checkpoint (false = automatic)
    pub manual_only: bool,
}

impl Default for VersioningPolicy {
    fn default() -> Self {
        Self {
            max_versions: 10,           // Keep last 10 versions
            retention_seconds: 2592000, // 30 days
            min_version_interval: 300,  // 5 minutes between auto versions
            manual_only: false,
        }
    }
}

/// Version history for a file
#[derive(Debug, Clone)]
pub struct VersionHistory {
    /// File ID
    pub file_id: u64,
    /// Versioning policy
    pub policy: VersioningPolicy,
    /// All versions (sorted by version number)
    versions: Vec<FileVersion>,
    /// Last version timestamp
    last_version_time: u64,
}

impl VersionHistory {
    /// Create new version history
    pub fn new(file_id: u64, policy: VersioningPolicy) -> Self {
        Self {
            file_id,
            policy,
            versions: Vec::new(),
            last_version_time: 0,
        }
    }

    /// Check if new version should be created
    pub fn should_create_version(&self, current_time: u64) -> bool {
        if self.policy.manual_only {
            return false; // Only manual checkpoints
        }

        let elapsed = current_time.saturating_sub(self.last_version_time);
        elapsed >= self.policy.min_version_interval
    }

    /// Add new version
    pub fn add_version(&mut self, version: FileVersion) {
        self.last_version_time = version.timestamp;
        self.versions.push(version);

        // Enforce max versions limit
        if self.policy.max_versions > 0 {
            let max = self.policy.max_versions as usize;
            if self.versions.len() > max {
                // Remove oldest versions
                let to_remove = self.versions.len() - max;
                self.versions.drain(0..to_remove);
            }
        }
    }

    /// Prune old versions based on retention policy
    pub fn prune_old_versions(&mut self, current_time: u64) {
        if self.policy.retention_seconds == 0 {
            return; // Keep forever
        }

        let cutoff = current_time.saturating_sub(self.policy.retention_seconds);
        self.versions.retain(|v| v.timestamp >= cutoff);
    }

    /// Get version by number
    pub fn get_version(&self, version: u64) -> Option<&FileVersion> {
        self.versions.iter().find(|v| v.version == version)
    }

    /// Get latest version
    pub fn latest_version(&self) -> Option<&FileVersion> {
        self.versions.last()
    }

    /// Get all versions
    pub fn all_versions(&self) -> &[FileVersion] {
        &self.versions
    }

    /// Get version count
    pub fn version_count(&self) -> usize {
        self.versions.len()
    }

    /// Calculate total storage used by all versions
    pub fn total_storage(&self) -> u64 {
        self.versions.iter().map(|v| v.size).sum()
    }
}

/// Diff between two versions
#[derive(Debug, Clone)]
pub struct VersionDiff {
    /// Old version number
    pub old_version: u64,
    /// New version number
    pub new_version: u64,
    /// Size change (signed)
    pub size_delta: i64,
    /// Time elapsed between versions
    pub time_delta: u64,
    /// Checksums differ
    pub content_changed: bool,
}

/// Versioning statistics
#[derive(Debug, Clone, Default)]
pub struct VersioningStats {
    /// Total files with versioning enabled
    pub versioned_files: usize,
    /// Total versions stored
    pub total_versions: u64,
    /// Total storage used by versions
    pub total_storage: u64,
    /// Versions pruned (due to retention/limits)
    pub versions_pruned: u64,
}

lazy_static! {
    /// Global versioning manager
    static ref VERSIONING_MANAGER: Mutex<VersioningManager> = Mutex::new(VersioningManager::new());
}

/// File versioning manager
pub struct VersioningManager {
    /// Version history per file
    histories: BTreeMap<u64, VersionHistory>,
    /// Default policy for new files
    default_policy: VersioningPolicy,
    /// Statistics
    stats: VersioningStats,
}

impl Default for VersioningManager {
    fn default() -> Self {
        Self::new()
    }
}

impl VersioningManager {
    /// Create new versioning manager
    pub fn new() -> Self {
        Self {
            histories: BTreeMap::new(),
            default_policy: VersioningPolicy::default(),
            stats: VersioningStats::default(),
        }
    }

    /// Enable versioning for file
    pub fn enable_versioning(&mut self, file_id: u64, policy: Option<VersioningPolicy>) {
        let policy = policy.unwrap_or(self.default_policy);
        let history = VersionHistory::new(file_id, policy);
        self.histories.insert(file_id, history);
        self.stats.versioned_files += 1;
    }

    /// Disable versioning for file
    pub fn disable_versioning(&mut self, file_id: u64) {
        if let Some(history) = self.histories.remove(&file_id) {
            self.stats.versioned_files = self.stats.versioned_files.saturating_sub(1);
            self.stats.total_versions = self
                .stats
                .total_versions
                .saturating_sub(history.version_count() as u64);
        }
    }

    /// Create new version from a FileVersion struct
    ///
    /// # Arguments
    /// * `version` - File version metadata (version number will be assigned automatically)
    /// * `manual` - Whether this is a manual checkpoint vs automatic
    ///
    /// # Example
    /// ```ignore
    /// let version = FileVersion::new(file_id, 0, timestamp, size, block_ptr, checksum, user_id);
    /// manager.create_version(version, false)?;
    /// ```
    pub fn create_version(
        &mut self,
        mut version: FileVersion,
        manual: bool,
    ) -> Result<u64, &'static str> {
        let file_id = version.file_id;
        let timestamp = version.timestamp;

        let history = self
            .histories
            .get_mut(&file_id)
            .ok_or("Versioning not enabled")?;

        // Check if version should be created
        if !manual && !history.should_create_version(timestamp) {
            return Err("Too soon for automatic version");
        }

        // Assign version number
        let version_num = history.version_count() as u64 + 1;
        version.version = version_num;

        // Add version to history
        let size = version.size;
        history.add_version(version);

        self.stats.total_versions += 1;
        self.stats.total_storage += size;

        Ok(version_num)
    }

    /// Get version history
    pub fn get_history(&self, file_id: u64) -> Option<&VersionHistory> {
        self.histories.get(&file_id)
    }

    /// Get specific version
    pub fn get_version(&self, file_id: u64, version: u64) -> Option<&FileVersion> {
        self.histories.get(&file_id)?.get_version(version)
    }

    /// Get latest version
    pub fn get_latest(&self, file_id: u64) -> Option<&FileVersion> {
        self.histories.get(&file_id)?.latest_version()
    }

    /// List all versions for file
    pub fn list_versions(&self, file_id: u64) -> Option<Vec<FileVersion>> {
        self.histories
            .get(&file_id)
            .map(|h| h.all_versions().to_vec())
    }

    /// Compare two versions
    pub fn diff_versions(&self, file_id: u64, old_ver: u64, new_ver: u64) -> Option<VersionDiff> {
        let history = self.histories.get(&file_id)?;
        let old = history.get_version(old_ver)?;
        let new = history.get_version(new_ver)?;

        Some(VersionDiff {
            old_version: old_ver,
            new_version: new_ver,
            size_delta: new.size as i64 - old.size as i64,
            time_delta: new.timestamp.saturating_sub(old.timestamp),
            content_changed: old.checksum != new.checksum,
        })
    }

    /// Prune old versions across all files
    pub fn prune_all(&mut self, current_time: u64) -> u64 {
        let mut pruned = 0;

        for history in self.histories.values_mut() {
            let before = history.version_count();
            history.prune_old_versions(current_time);
            let after = history.version_count();
            pruned += (before - after) as u64;
        }

        self.stats.versions_pruned += pruned;
        self.stats.total_versions = self.stats.total_versions.saturating_sub(pruned);

        pruned
    }

    /// Get statistics
    pub fn get_stats(&self) -> VersioningStats {
        let mut stats = self.stats.clone();

        // Recalculate storage
        stats.total_storage = self.histories.values().map(|h| h.total_storage()).sum();

        stats
    }

    /// Set default policy
    pub fn set_default_policy(&mut self, policy: VersioningPolicy) {
        self.default_policy = policy;
    }
}

/// Global versioning operations
pub struct VersioningEngine;

impl VersioningEngine {
    /// Enable versioning
    pub fn enable(file_id: u64, policy: Option<VersioningPolicy>) {
        let mut mgr = VERSIONING_MANAGER.lock();
        mgr.enable_versioning(file_id, policy);
    }

    /// Disable versioning
    pub fn disable(file_id: u64) {
        let mut mgr = VERSIONING_MANAGER.lock();
        mgr.disable_versioning(file_id);
    }

    /// Create version (manual checkpoint)
    pub fn create_version(
        file_id: u64,
        timestamp: u64,
        size: u64,
        block_ptr: u64,
        checksum: [u8; 32],
        user_id: u64,
    ) -> Result<u64, &'static str> {
        let version = FileVersion::new(file_id, 0, timestamp, size, block_ptr, checksum, user_id);
        let mut mgr = VERSIONING_MANAGER.lock();
        mgr.create_version(version, true)
    }

    /// Create automatic version
    pub fn auto_version(
        file_id: u64,
        timestamp: u64,
        size: u64,
        block_ptr: u64,
        checksum: [u8; 32],
        user_id: u64,
    ) -> Result<u64, &'static str> {
        let version = FileVersion::new(file_id, 0, timestamp, size, block_ptr, checksum, user_id);
        let mut mgr = VERSIONING_MANAGER.lock();
        mgr.create_version(version, false)
    }

    /// Get version
    pub fn get_version(file_id: u64, version: u64) -> Option<FileVersion> {
        let mgr = VERSIONING_MANAGER.lock();
        mgr.get_version(file_id, version).cloned()
    }

    /// List versions
    pub fn list_versions(file_id: u64) -> Option<Vec<FileVersion>> {
        let mgr = VERSIONING_MANAGER.lock();
        mgr.list_versions(file_id)
    }

    /// Diff versions
    pub fn diff(file_id: u64, old_ver: u64, new_ver: u64) -> Option<VersionDiff> {
        let mgr = VERSIONING_MANAGER.lock();
        mgr.diff_versions(file_id, old_ver, new_ver)
    }

    /// Prune old versions
    pub fn prune(current_time: u64) -> u64 {
        let mut mgr = VERSIONING_MANAGER.lock();
        mgr.prune_all(current_time)
    }

    /// Get statistics
    pub fn stats() -> VersioningStats {
        let mgr = VERSIONING_MANAGER.lock();
        mgr.get_stats()
    }
}

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

    // Test helper extension for VersioningManager
    impl VersioningManager {
        fn test_create_version(
            &mut self,
            file_id: u64,
            timestamp: u64,
            size: u64,
            block_ptr: u64,
            checksum: [u8; 32],
            user_id: u64,
            manual: bool,
        ) -> Result<u64, &'static str> {
            let version =
                FileVersion::new(file_id, 0, timestamp, size, block_ptr, checksum, user_id);
            self.create_version(version, manual)
        }
    }

    #[test]
    fn test_default_policy() {
        let policy = VersioningPolicy::default();
        assert_eq!(policy.max_versions, 10);
        assert_eq!(policy.retention_seconds, 2592000); // 30 days
    }

    #[test]
    fn test_version_creation() {
        let mut history = VersionHistory::new(1, VersioningPolicy::default());
        let version = FileVersion::new(1, 1, 0, 1024, 100, [0u8; 32], 999);

        history.add_version(version);
        assert_eq!(history.version_count(), 1);
    }

    #[test]
    fn test_max_versions_limit() {
        let mut policy = VersioningPolicy::default();
        policy.max_versions = 3;

        let mut history = VersionHistory::new(1, policy);

        // Add 5 versions
        for i in 1..=5 {
            let version = FileVersion::new(1, i, i * 100, 1024, 100 + i, [0u8; 32], 999);
            history.add_version(version);
        }

        // Should only keep last 3
        assert_eq!(history.version_count(), 3);
        assert!(history.get_version(1).is_none()); // Pruned
        assert!(history.get_version(2).is_none()); // Pruned
        assert!(history.get_version(3).is_some()); // Kept
    }

    #[test]
    fn test_retention_pruning() {
        let mut policy = VersioningPolicy::default();
        policy.retention_seconds = 1000;

        let mut history = VersionHistory::new(1, policy);

        // Add old version
        let old = FileVersion::new(1, 1, 0, 1024, 100, [0u8; 32], 999);
        history.add_version(old);

        // Add recent version
        let recent = FileVersion::new(1, 2, 2000, 1024, 101, [0u8; 32], 999);
        history.add_version(recent);

        // Prune at time 2500
        history.prune_old_versions(2500);

        // Old version should be pruned (timestamp 0 < 2500 - 1000)
        assert_eq!(history.version_count(), 1);
        assert!(history.get_version(1).is_none());
        assert!(history.get_version(2).is_some());
    }

    #[test]
    fn test_auto_version_interval() {
        let mut policy = VersioningPolicy::default();
        policy.min_version_interval = 300; // 5 minutes

        let history = VersionHistory::new(1, policy);

        // Should not create version immediately after last one
        assert!(!history.should_create_version(100));

        // Should create after interval
        assert!(history.should_create_version(400));
    }

    #[test]
    fn test_manual_only_mode() {
        let mut policy = VersioningPolicy::default();
        policy.manual_only = true;

        let history = VersionHistory::new(1, policy);

        // Should never auto-create versions
        assert!(!history.should_create_version(1000000));
    }

    #[test]
    fn test_versioning_manager() {
        let mut mgr = VersioningManager::new();

        // Enable versioning
        mgr.enable_versioning(1, None);
        assert_eq!(mgr.stats.versioned_files, 1);

        // Create version
        mgr.test_create_version(1, 0, 1024, 100, [0u8; 32], 999, true)
            .expect("test: operation should succeed");
        assert_eq!(mgr.stats.total_versions, 1);

        // Get version
        let version = mgr
            .get_version(1, 1)
            .expect("test: operation should succeed");
        assert_eq!(version.size, 1024);
    }

    #[test]
    fn test_version_diff() {
        let mut mgr = VersioningManager::new();
        mgr.enable_versioning(1, None);

        // Create two versions
        mgr.test_create_version(1, 0, 1024, 100, [1u8; 32], 999, true)
            .expect("test: operation should succeed");
        mgr.test_create_version(1, 1000, 2048, 101, [2u8; 32], 999, true)
            .expect("test: operation should succeed");

        // Diff them
        let diff = mgr
            .diff_versions(1, 1, 2)
            .expect("test: operation should succeed");
        assert_eq!(diff.size_delta, 1024); // Grew by 1024 bytes
        assert_eq!(diff.time_delta, 1000);
        assert!(diff.content_changed);
    }

    #[test]
    fn test_list_versions() {
        let mut mgr = VersioningManager::new();
        mgr.enable_versioning(1, None);

        for i in 1..=5 {
            mgr.test_create_version(1, i * 100, 1024, 100 + i, [0u8; 32], 999, true)
                .expect("test: operation should succeed");
        }

        let versions = mgr
            .list_versions(1)
            .expect("test: operation should succeed");
        assert_eq!(versions.len(), 5);
    }

    #[test]
    fn test_storage_calculation() {
        let mut history = VersionHistory::new(1, VersioningPolicy::default());

        history.add_version(FileVersion::new(1, 1, 0, 1000, 100, [0u8; 32], 999));
        history.add_version(FileVersion::new(1, 2, 100, 2000, 101, [0u8; 32], 999));
        history.add_version(FileVersion::new(1, 3, 200, 3000, 102, [0u8; 32], 999));

        assert_eq!(history.total_storage(), 6000);
    }
}