Skip to content

Commit 2129227

Browse files
authored
Merge pull request #88 from MicroClub-USTHB/76-evfs-maintenance
vault maintenance (defrag, resize, health, dynamic index)
2 parents 2eaaeb8 + d114ef8 commit 2129227

23 files changed

Lines changed: 4191 additions & 1224 deletions

File tree

example/integration_test/evfs_test.dart

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,235 @@ void main() {
551551

552552
await VaultService.close(handle: reopened2);
553553
});
554+
// -- Defragmentation ------------------------------------------------
555+
556+
test('defragment compacts free space', () async {
557+
final path = '${tempDir.path}/defrag.vault';
558+
final key = await generateAes256GcmKey();
559+
560+
final handle = await VaultService.create(
561+
path: path,
562+
key: key,
563+
algorithm: 'aes-256-gcm',
564+
capacityBytes: 2 * 1024 * 1024,
565+
);
566+
567+
// Write A, B, C
568+
final dataA = Uint8List.fromList(List.generate(1000, (i) => i % 256));
569+
final dataC = Uint8List.fromList(List.generate(500, (i) => (i * 3) % 256));
570+
await VaultService.write(handle: handle, name: 'a.txt', data: dataA);
571+
await VaultService.write(
572+
handle: handle,
573+
name: 'b.txt',
574+
data: Uint8List(2000),
575+
);
576+
await VaultService.write(handle: handle, name: 'c.txt', data: dataC);
577+
578+
// Delete B → creates a gap
579+
await VaultService.delete(handle: handle, name: 'b.txt');
580+
581+
final beforeHealth = await VaultService.health(handle: handle);
582+
expect(beforeHealth.freeRegionCount, greaterThan(0));
583+
expect(beforeHealth.fragmentationRatio, greaterThan(0.0));
584+
585+
// Defragment
586+
final result = await VaultService.defragment(handle: handle);
587+
expect(result.segmentsMoved, greaterThan(0));
588+
expect(result.bytesReclaimed, greaterThan(BigInt.zero));
589+
590+
// After defrag: no fragmentation
591+
final afterHealth = await VaultService.health(handle: handle);
592+
expect(afterHealth.freeRegionCount, 0);
593+
expect(afterHealth.fragmentationRatio, 0.0);
594+
595+
// Data still readable
596+
expect(await VaultService.read(handle: handle, name: 'a.txt'), dataA);
597+
expect(await VaultService.read(handle: handle, name: 'c.txt'), dataC);
598+
599+
await VaultService.close(handle: handle);
600+
});
601+
602+
test('defragment on empty vault is no-op', () async {
603+
final path = '${tempDir.path}/defrag_empty.vault';
604+
final key = await generateAes256GcmKey();
605+
606+
final handle = await VaultService.create(
607+
path: path,
608+
key: key,
609+
algorithm: 'aes-256-gcm',
610+
capacityBytes: 1024 * 1024,
611+
);
612+
613+
final result = await VaultService.defragment(handle: handle);
614+
expect(result.segmentsMoved, 0);
615+
expect(result.bytesReclaimed, BigInt.zero);
616+
617+
await VaultService.close(handle: handle);
618+
});
619+
620+
// -- Resize ---------------------------------------------------------
621+
622+
test('resize grow then write in new space', () async {
623+
final path = '${tempDir.path}/grow.vault';
624+
final key = await generateAes256GcmKey();
625+
626+
final handle = await VaultService.create(
627+
path: path,
628+
key: key,
629+
algorithm: 'aes-256-gcm',
630+
capacityBytes: 512 * 1024, // 512KB
631+
);
632+
633+
// Grow to 1MB
634+
await VaultService.resize(
635+
handle: handle,
636+
newCapacityBytes: 1024 * 1024,
637+
);
638+
639+
final health = await VaultService.health(handle: handle);
640+
expect(health.totalBytes, BigInt.from(1024 * 1024));
641+
642+
// Write data that wouldn't fit in original 512KB
643+
final bigData = Uint8List(600 * 1024); // 600KB
644+
await VaultService.write(handle: handle, name: 'big.bin', data: bigData);
645+
646+
final result = await VaultService.read(handle: handle, name: 'big.bin');
647+
expect(result, bigData);
648+
649+
await VaultService.close(handle: handle);
650+
});
651+
652+
test('resize shrink after defrag', () async {
653+
final path = '${tempDir.path}/shrink.vault';
654+
final key = await generateAes256GcmKey();
655+
656+
final handle = await VaultService.create(
657+
path: path,
658+
key: key,
659+
algorithm: 'aes-256-gcm',
660+
capacityBytes: 1024 * 1024, // 1MB
661+
);
662+
663+
// Write small data
664+
final data = Uint8List.fromList(List.generate(100, (i) => i % 256));
665+
await VaultService.write(handle: handle, name: 'small.bin', data: data);
666+
667+
// Defrag to compact
668+
await VaultService.defragment(handle: handle);
669+
670+
// Shrink to 512KB (data fits)
671+
await VaultService.resize(
672+
handle: handle,
673+
newCapacityBytes: 512 * 1024,
674+
);
675+
676+
final health = await VaultService.health(handle: handle);
677+
expect(health.totalBytes, BigInt.from(512 * 1024));
678+
679+
// Data still readable
680+
expect(await VaultService.read(handle: handle, name: 'small.bin'), data);
681+
682+
await VaultService.close(handle: handle);
683+
});
684+
685+
test('shrink below used space throws error', () async {
686+
final path = '${tempDir.path}/shrink_fail.vault';
687+
final key = await generateAes256GcmKey();
688+
689+
final handle = await VaultService.create(
690+
path: path,
691+
key: key,
692+
algorithm: 'aes-256-gcm',
693+
capacityBytes: 1024 * 1024,
694+
);
695+
696+
// Write enough data to occupy space
697+
final data = Uint8List(200 * 1024); // 200KB
698+
await VaultService.write(handle: handle, name: 'data.bin', data: data);
699+
700+
// Try to shrink to 1KB → should fail
701+
try {
702+
await VaultService.resize(handle: handle, newCapacityBytes: 1024);
703+
fail('Expected VaultFull error');
704+
} catch (e) {
705+
expect(e.toString(), contains('vaultFull'));
706+
}
707+
708+
await VaultService.close(handle: handle);
709+
});
710+
711+
// -- Health Check ---------------------------------------------------
712+
713+
test('health info on fresh vault', () async {
714+
final path = '${tempDir.path}/health.vault';
715+
final key = await generateAes256GcmKey();
716+
717+
final cap = 1024 * 1024;
718+
final handle = await VaultService.create(
719+
path: path,
720+
key: key,
721+
algorithm: 'aes-256-gcm',
722+
capacityBytes: cap,
723+
);
724+
725+
final h = await VaultService.health(handle: handle);
726+
expect(h.totalBytes, BigInt.from(cap));
727+
expect(h.usedBytes, BigInt.zero);
728+
expect(h.segmentCount, 0);
729+
expect(h.freeRegionCount, 0);
730+
expect(h.fragmentationRatio, 0.0);
731+
expect(h.isConsistent, true);
732+
expect(h.largestFreeBlock, BigInt.from(cap));
733+
734+
await VaultService.close(handle: handle);
735+
});
736+
737+
test('health after write and delete shows fragmentation', () async {
738+
final path = '${tempDir.path}/health_frag.vault';
739+
final key = await generateAes256GcmKey();
740+
741+
final handle = await VaultService.create(
742+
path: path,
743+
key: key,
744+
algorithm: 'aes-256-gcm',
745+
capacityBytes: 2 * 1024 * 1024,
746+
);
747+
748+
await VaultService.write(
749+
handle: handle,
750+
name: 'a.txt',
751+
data: Uint8List(1000),
752+
);
753+
await VaultService.write(
754+
handle: handle,
755+
name: 'b.txt',
756+
data: Uint8List(2000),
757+
);
758+
await VaultService.write(
759+
handle: handle,
760+
name: 'c.txt',
761+
data: Uint8List(500),
762+
);
763+
764+
// Delete middle segment
765+
await VaultService.delete(handle: handle, name: 'b.txt');
766+
767+
final h = await VaultService.health(handle: handle);
768+
expect(h.segmentCount, 2);
769+
expect(h.freeRegionCount, greaterThan(0));
770+
expect(h.fragmentationRatio, greaterThan(0.0));
771+
expect(h.isConsistent, true);
772+
773+
// Defrag → fragmentation goes to 0
774+
await VaultService.defragment(handle: handle);
775+
final after = await VaultService.health(handle: handle);
776+
expect(after.freeRegionCount, 0);
777+
expect(after.fragmentationRatio, 0.0);
778+
expect(after.isConsistent, true);
779+
780+
await VaultService.close(handle: handle);
781+
});
782+
554783
test('corrupted primary index falls back to shadow', () async {
555784
final path = '${tempDir.path}/shadow.vault';
556785
final key = await generateAes256GcmKey();

example/lib/main.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'package:m_security/m_security.dart';
77
import 'package:m_security/src/rust/api/encryption.dart' as rust_enc;
88
import 'package:m_security/src/rust/api/hashing.dart' as hashing;
99
import 'package:m_security/src/rust/api/compression.dart';
10-
import 'package:m_security/src/rust/api/evfs.dart' as rust_evfs;
10+
import 'package:m_security/src/rust/api/evfs/types.dart' as rust_evfs_types;
1111

1212
Future<void> main() async {
1313
WidgetsFlutterBinding.ensureInitialized();
@@ -613,7 +613,7 @@ class _VaultTabState extends State<_VaultTab> {
613613
bool _vaultOpen = false;
614614
String _compAlgo = 'Zstd';
615615

616-
rust_evfs.VaultHandle? _handle;
616+
rust_evfs_types.VaultHandle? _handle;
617617
Uint8List? _key;
618618
String? _vaultPath;
619619

lib/src/evfs/vault_service.dart

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:m_security/src/rust/api/evfs.dart' as rust_evfs;
2+
import 'package:m_security/src/rust/api/evfs/types.dart' as rust_types;
23
import 'package:m_security/src/rust/api/compression.dart';
34
import 'dart:typed_data';
45

@@ -12,7 +13,7 @@ class VaultService {
1213
/// Create a new vault file.
1314
///
1415
/// [algorithm] must be "aes-256-gcm" or "chacha20-poly1305".
15-
static Future<rust_evfs.VaultHandle> create({
16+
static Future<rust_types.VaultHandle> create({
1617
required String path,
1718
required Uint8List key,
1819
required String algorithm,
@@ -27,7 +28,7 @@ class VaultService {
2728
}
2829

2930
/// Open an existing vault (runs WAL recovery if needed).
30-
static Future<rust_evfs.VaultHandle> open({
31+
static Future<rust_types.VaultHandle> open({
3132
required String path,
3233
required Uint8List key,
3334
}) {
@@ -40,7 +41,7 @@ class VaultService {
4041
/// MIME-aware skip: if [name] has an already-compressed extension
4142
/// (e.g., ".jpg"), compression is bypassed automatically.
4243
static Future<void> write({
43-
required rust_evfs.VaultHandle handle,
44+
required rust_types.VaultHandle handle,
4445
required String name,
4546
required Uint8List data,
4647
CompressionConfig? compression,
@@ -55,34 +56,66 @@ class VaultService {
5556

5657
/// Read a named segment. Decompression is automatic.
5758
static Future<Uint8List> read({
58-
required rust_evfs.VaultHandle handle,
59+
required rust_types.VaultHandle handle,
5960
required String name,
6061
}) {
6162
return rust_evfs.vaultRead(handle: handle, name: name);
6263
}
6364

6465
/// Delete a named segment (securely erased from disk).
6566
static Future<void> delete({
66-
required rust_evfs.VaultHandle handle,
67+
required rust_types.VaultHandle handle,
6768
required String name,
6869
}) {
6970
return rust_evfs.vaultDelete(handle: handle, name: name);
7071
}
7172

7273
/// List all segment names.
73-
static Future<List<String>> list({required rust_evfs.VaultHandle handle}) {
74+
static Future<List<String>> list({required rust_types.VaultHandle handle}) {
7475
return rust_evfs.vaultList(handle: handle);
7576
}
7677

7778
/// Get vault capacity info.
78-
static Future<rust_evfs.VaultCapacityInfo> capacity({
79-
required rust_evfs.VaultHandle handle,
79+
static Future<rust_types.VaultCapacityInfo> capacity({
80+
required rust_types.VaultHandle handle,
8081
}) {
8182
return rust_evfs.vaultCapacity(handle: handle);
8283
}
8384

85+
/// Get vault health and diagnostic info (read-only, no I/O).
86+
static Future<rust_types.VaultHealthInfo> health({
87+
required rust_types.VaultHandle handle,
88+
}) {
89+
return rust_evfs.vaultHealth(handle: handle);
90+
}
91+
92+
/// Defragment the vault: compact segments, coalesce free space.
93+
///
94+
/// Each segment move is WAL-protected for crash safety.
95+
/// Returns a [DefragResult] with move count and bytes reclaimed.
96+
static Future<rust_types.DefragResult> defragment({
97+
required rust_types.VaultHandle handle,
98+
}) {
99+
return rust_evfs.vaultDefragment(handle: handle);
100+
}
101+
102+
/// Resize the vault data region capacity.
103+
///
104+
/// Grow: extends file with CSPRNG-filled space.
105+
/// Shrink: validates segments fit, then truncates.
106+
/// Throws if shrinking below used space.
107+
static Future<void> resize({
108+
required rust_types.VaultHandle handle,
109+
required int newCapacityBytes,
110+
}) {
111+
return rust_evfs.vaultResize(
112+
handle: handle,
113+
newCapacity: BigInt.from(newCapacityBytes),
114+
);
115+
}
116+
84117
/// Close the vault (release lock, zeroize keys).
85-
static Future<void> close({required rust_evfs.VaultHandle handle}) {
118+
static Future<void> close({required rust_types.VaultHandle handle}) {
86119
return rust_evfs.vaultClose(handle: handle);
87120
}
88121
}

0 commit comments

Comments
 (0)