From 98ec8de623c4c17afe59545acf4b3a17d5ad2d0d Mon Sep 17 00:00:00 2001 From: nferhat Date: Sun, 15 Mar 2026 14:19:35 +0100 Subject: [PATCH 1/5] feat(vault): Add `vault_resize` method For now there's no functionality, it will be broken down into two different functions. --- rust/src/api/evfs/mod.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/rust/src/api/evfs/mod.rs b/rust/src/api/evfs/mod.rs index 0625bc7..d5f7e50 100644 --- a/rust/src/api/evfs/mod.rs +++ b/rust/src/api/evfs/mod.rs @@ -426,6 +426,25 @@ pub fn vault_delete( Ok(()) } +/// Resize vault data region capacity. +/// +/// - Grow: extend file, CSPRNG-fill new space, relocate shadow index + WAL +/// - Shrink: validate segments fit, relocate shadow + WAL, truncate file +/// - Returns VaultFull if shrink would lose data +#[cfg(feature = "compression")] +pub fn vault_resize(handle: &mut VaultHandle, new_capacity: u64) -> Result<(), CryptoError> { + + let old_capacity = handle.index.capacity; + + if old_capacity == new_capacity { + return Ok(()); + } else if old_capacity > new_capacity { + // TODO: Shrink algorithm. + } else { + // TODO: Grow algorithm. + } +} + /// List all segment names in the vault. pub fn vault_list(handle: &VaultHandle) -> Vec { handle.index.entries.iter().map(|e| e.name.clone()).collect() From 230206c3dcb0c779a3105bf0d2cb7019285c8f54 Mon Sep 17 00:00:00 2001 From: nferhat Date: Sun, 15 Mar 2026 14:36:57 +0100 Subject: [PATCH 2/5] evfs(vault): Add grow algorithm implementation --- rust/src/api/evfs/mod.rs | 47 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/rust/src/api/evfs/mod.rs b/rust/src/api/evfs/mod.rs index d5f7e50..6b9de17 100644 --- a/rust/src/api/evfs/mod.rs +++ b/rust/src/api/evfs/mod.rs @@ -433,18 +433,59 @@ pub fn vault_delete( /// - Returns VaultFull if shrink would lose data #[cfg(feature = "compression")] pub fn vault_resize(handle: &mut VaultHandle, new_capacity: u64) -> Result<(), CryptoError> { - let old_capacity = handle.index.capacity; - if old_capacity == new_capacity { return Ok(()); } else if old_capacity > new_capacity { // TODO: Shrink algorithm. + unimplemented!("shrink algo") } else { - // TODO: Grow algorithm. + vault_resize_grow_impl(handle, old_capacity, new_capacity) } } +#[cfg(feature = "compression")] +fn vault_resize_grow_impl( + handle: &mut VaultHandle, + old_capacity: u64, + new_capacity: u64, +) -> Result<(), CryptoError> { + let old_encrypted_index = read_encrypted_index(&mut handle.file, PRIMARY_INDEX_OFFSET)?; + handle.wal.begin(WalOp::UpdateIndex, &old_encrypted_index)?; + + let old_shadow_off = format::shadow_index_offset(old_capacity)?; + let shadow_bytes = read_encrypted_index(&mut handle.file, old_shadow_off)?; + + // Extend the file to the new total size + let new_total = format::total_vault_size(new_capacity)?; + handle.file.set_len(new_total)?; + // And fill the old_cap..new_cap region with CSPRNG data. + let fill_offset = DATA_REGION_OFFSET + old_capacity; + let fill_size = new_capacity - old_capacity; + segment::secure_erase_region(&mut handle.file, fill_offset, fill_size)?; + + // 5. Write shadow index at new position. + let new_shadow_off = format::shadow_index_offset(new_capacity)?; + handle.file.seek(SeekFrom::Start(new_shadow_off))?; + handle.file.write_all(&shadow_bytes)?; + handle.file.sync_all()?; + + // Update handle data. + handle.wal = WriteAheadLog::open(&handle.path)?; + handle.index.capacity = new_capacity; + flush_index( + &mut handle.file, + &handle.index, + &handle.keys, + handle.algorithm, + new_capacity, + )?; + + handle.wal.commit()?; + handle.wal.checkpoint()?; + + Ok(()) +} /// List all segment names in the vault. pub fn vault_list(handle: &VaultHandle) -> Vec { handle.index.entries.iter().map(|e| e.name.clone()).collect() From 40c42efac00f2d04cb85c056f25a14913e12c488 Mon Sep 17 00:00:00 2001 From: nferhat Date: Sun, 15 Mar 2026 14:47:55 +0100 Subject: [PATCH 3/5] evfs(vault): Add vault shrinking implementation --- rust/src/api/evfs/mod.rs | 80 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/rust/src/api/evfs/mod.rs b/rust/src/api/evfs/mod.rs index 6b9de17..165947a 100644 --- a/rust/src/api/evfs/mod.rs +++ b/rust/src/api/evfs/mod.rs @@ -437,8 +437,7 @@ pub fn vault_resize(handle: &mut VaultHandle, new_capacity: u64) -> Result<(), C if old_capacity == new_capacity { return Ok(()); } else if old_capacity > new_capacity { - // TODO: Shrink algorithm. - unimplemented!("shrink algo") + vault_resize_shrink_impl(handle, old_capacity, new_capacity) } else { vault_resize_grow_impl(handle, old_capacity, new_capacity) } @@ -486,6 +485,83 @@ fn vault_resize_grow_impl( Ok(()) } + +#[cfg(feature = "compression")] +fn vault_resize_shrink_impl( + handle: &mut VaultHandle, + old_capacity: u64, + new_capacity: u64, +) -> Result<(), CryptoError> { + // Validate: every live segment must fit within the new capacity. + let max_used = handle + .index + .entries + .iter() + .map(|e| e.offset.saturating_add(e.size)) + .max() + .unwrap_or(0); + if max_used > new_capacity { + return Err(CryptoError::VaultFull { + needed: max_used, + available: new_capacity, + }); + } + + // WAL begin — journal the current encrypted index for crash recovery. + let old_encrypted_index = read_encrypted_index(&mut handle.file, PRIMARY_INDEX_OFFSET)?; + handle.wal.begin(WalOp::UpdateIndex, &old_encrypted_index)?; + + // Write shadow index at new (closer) position. + let new_shadow_off = format::shadow_index_offset(new_capacity)?; + let old_shadow_off = format::shadow_index_offset(old_capacity)?; + let shadow_bytes = read_encrypted_index(&mut handle.file, old_shadow_off)?; + handle.file.seek(SeekFrom::Start(new_shadow_off))?; + handle.file.write_all(&shadow_bytes)?; + handle.file.sync_all()?; + + // Update handle + handle.wal = WriteAheadLog::open(&handle.path)?; + handle.index.capacity = new_capacity; + // Clamp next_free_offset to new_capacity (it cannot point beyond). + if handle.index.next_free_offset > new_capacity { + handle.index.next_free_offset = new_capacity; + } + + // Remove free regions that fall entirely beyond the new boundary, + // and truncate any that partially overlap. + handle.index.free_regions.retain_mut(|r| { + if r.offset >= new_capacity { + // Entirely beyond — drop. + return false; + } + let end = r.offset + r.size; + if end > new_capacity { + // Partially beyond — truncate. + r.size = new_capacity - r.offset; + } + true + }); + + // Flush index (primary + shadow at new position). + flush_index( + &mut handle.file, + &handle.index, + &handle.keys, + handle.algorithm, + new_capacity, + )?; + + // Truncate file to new total size. + let new_total = format::total_vault_size(new_capacity)?; + handle.file.set_len(new_total)?; + handle.file.sync_all()?; + + handle.wal.commit()?; + handle.wal.checkpoint()?; + + Ok(()) +} + /// List all segment names in the vault. pub fn vault_list(handle: &VaultHandle) -> Vec { handle.index.entries.iter().map(|e| e.name.clone()).collect() From d338785a428c61562764cdad2c0c8c26d084851f Mon Sep 17 00:00:00 2001 From: nferhat Date: Sun, 15 Mar 2026 14:51:48 +0100 Subject: [PATCH 4/5] chore: Lint fix --- rust/src/api/evfs/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/api/evfs/mod.rs b/rust/src/api/evfs/mod.rs index 165947a..8bc6b48 100644 --- a/rust/src/api/evfs/mod.rs +++ b/rust/src/api/evfs/mod.rs @@ -435,7 +435,7 @@ pub fn vault_delete( pub fn vault_resize(handle: &mut VaultHandle, new_capacity: u64) -> Result<(), CryptoError> { let old_capacity = handle.index.capacity; if old_capacity == new_capacity { - return Ok(()); + Ok(()) } else if old_capacity > new_capacity { vault_resize_shrink_impl(handle, old_capacity, new_capacity) } else { From 3cf1a8d539dd559fd75de539cb715e5c1cf4d5e6 Mon Sep 17 00:00:00 2001 From: nferhat Date: Sun, 15 Mar 2026 15:00:15 +0100 Subject: [PATCH 5/5] evfs(vault): Add testing for resize feature --- rust/src/api/evfs/mod.rs | 174 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/rust/src/api/evfs/mod.rs b/rust/src/api/evfs/mod.rs index 8bc6b48..0e2a9f9 100644 --- a/rust/src/api/evfs/mod.rs +++ b/rust/src/api/evfs/mod.rs @@ -1334,4 +1334,178 @@ mod tests { vault_close(handle).expect("close"); } + + // -- Resize ------------------------------------------------------------- + const SIZE_MB: u64 = 0x100000; + + #[test] + fn test_resize_grow_then_write_in_new_space() { + let dir = tempfile::tempdir().expect("tempdir"); + let mut handle = create_test_vault(&dir, SIZE_MB); + + // Fill most of the original 1MB capacity + let filler = vec![0xAA; 900_000]; + vault_write(&mut handle, "filler.bin".into(), filler.clone(), None).expect("write filler"); + + // Grow to 2MB + vault_resize(&mut handle, 2 * SIZE_MB).expect("grow to 2MB"); + + // Write a segment that requires space in the new region + let big = vec![0xBB; 900_000]; + vault_write(&mut handle, "big.bin".into(), big.clone(), None).expect("write in new space"); + + // Read both back + let filler_read = vault_read(&mut handle, "filler.bin".into()).expect("read filler"); + assert_eq!(filler_read, filler); + let big_read = vault_read(&mut handle, "big.bin".into()).expect("read big"); + assert_eq!(big_read, big); + + vault_close(handle).expect("close"); + } + + #[test] + fn test_resize_shrink_after_consolidation() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = vault_path(&dir); + + // Create a 2MB vault and write ~500KB of data. + { + let mut handle = + vault_create(path.clone(), test_key(), "aes-256-gcm".into(), 2 * SIZE_MB) + .expect("create 2MB"); + let data = vec![0xCC; 500_000]; + vault_write(&mut handle, "doc.bin".into(), data, None).expect("write 500KB"); + vault_close(handle).expect("close"); + } + + // Reopen, shrink to 1MB. Data is at the beginning so it should fit. + { + let mut handle = vault_open(path.clone(), test_key()).expect("reopen"); + vault_resize(&mut handle, SIZE_MB).expect("shrink to 1MB"); + + // Verify all data is still readable. + let data = vault_read(&mut handle, "doc.bin".into()).expect("read after shrink"); + assert_eq!(data, vec![0xCC; 500_000]); + + // Verify capacity reflects the new size. + let cap = vault_capacity(&handle); + assert_eq!(cap.total_bytes, SIZE_MB); + + vault_close(handle).expect("close"); + } + + // Reopen again to confirm persistence across close/open. + { + let mut handle = vault_open(path, test_key()).expect("reopen again"); + let data = vault_read(&mut handle, "doc.bin".into()).expect("read persisted"); + assert_eq!(data, vec![0xCC; 500_000]); + vault_close(handle).expect("close"); + } + } + + #[test] + fn test_resize_shrink_below_used_space_fails() { + let dir = tempfile::tempdir().expect("tempdir"); + let mut handle = create_test_vault(&dir, SIZE_MB); + + // Write enough data so that shrinking below used space is impossible. + vault_write(&mut handle, "a.bin".into(), vec![0xDD; 600_000], None).expect("write"); + + // Attempt to shrink to 256KB — should fail with VaultFull. + let result = vault_resize(&mut handle, 256 * 1024); + assert!( + matches!(result, Err(CryptoError::VaultFull { .. })), + "expected VaultFull, got: {result:?}" + ); + + // Vault should still be usable after failed shrink. + let data = vault_read(&mut handle, "a.bin".into()).expect("read after failed shrink"); + assert_eq!(data, vec![0xDD; 600_000]); + + vault_close(handle).expect("close"); + } + + #[test] + fn test_resize_grow_updates_capacity() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = vault_path(&dir); + + { + let mut handle = vault_create(path.clone(), test_key(), "aes-256-gcm".into(), SIZE_MB) + .expect("create"); + + let cap_before = vault_capacity(&handle); + assert_eq!(cap_before.total_bytes, SIZE_MB); + + vault_resize(&mut handle, 2 * SIZE_MB).expect("grow"); + + let cap_after = vault_capacity(&handle); + assert_eq!(cap_after.total_bytes, 2 * SIZE_MB); + // Unallocated should have grown by ~1MB. + assert!(cap_after.unallocated_bytes > cap_before.unallocated_bytes); + + vault_close(handle).expect("close"); + } + + // Verify persistence: reopen and check capacity. + { + let handle = vault_open(path, test_key()).expect("reopen"); + let cap = vault_capacity(&handle); + assert_eq!(cap.total_bytes, 2 * SIZE_MB); + vault_close(handle).expect("close"); + } + } + + #[test] + fn test_resize_grow_crash_recovery() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = vault_path(&dir); + + // Create vault with one segment. + { + let mut handle = vault_create(path.clone(), test_key(), "aes-256-gcm".into(), SIZE_MB) + .expect("create"); + vault_write(&mut handle, "a.txt".into(), b"before grow".to_vec(), None) + .expect("write A"); + vault_close(handle).expect("close"); + } + + // Save the "good" encrypted index (1MB capacity, contains A). + let good_encrypted = { + let mut f = File::open(&path).expect("open"); + read_encrypted_index(&mut f, PRIMARY_INDEX_OFFSET).expect("read index") + }; + + // Perform a real grow to 2MB. + { + let mut handle = vault_open(path.clone(), test_key()).expect("open"); + vault_resize(&mut handle, 2 * SIZE_MB).expect("grow"); + vault_write(&mut handle, "b.txt".into(), b"after grow".to_vec(), None) + .expect("write B"); + vault_close(handle).expect("close"); + } + + // Simulate crash: write an uncommitted WAL entry that restores the + // pre-grow index (1MB capacity, only A). This simulates a crash + // that occurred mid-grow before commit. + { + let mut wal = WriteAheadLog::open(&path).expect("wal"); + wal.begin(WalOp::UpdateIndex, &good_encrypted) + .expect("begin"); + // Don't commit — simulates crash. + } + + // Reopen — WAL recovery should roll back to the A-only / 1MB index. + let mut handle = vault_open(path, test_key()).expect("open after recovery"); + + // A should be readable. + let data = vault_read(&mut handle, "a.txt".into()).expect("read A"); + assert_eq!(data, b"before grow"); + + // B should NOT be in the recovered index. + let result = vault_read(&mut handle, "b.txt".into()); + assert!(matches!(result, Err(CryptoError::SegmentNotFound(_)))); + + vault_close(handle).expect("close"); + } }