Skip to main content

doublecrypt_core/
fs.rs

1use std::sync::Arc;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use rand::RngCore;
5
6use crate::allocator::{BitmapAllocator, SlotAllocator};
7use crate::block_store::BlockStore;
8use crate::codec::{
9    read_encrypted_object, read_encrypted_raw, write_encrypted_object, write_encrypted_raw,
10    ObjectCodec, PostcardCodec,
11};
12use crate::crypto::CryptoEngine;
13use crate::error::{FsError, FsResult};
14use crate::model::*;
15use crate::transaction::TransactionManager;
16
17/// The main filesystem core. Owns the block store, crypto, codec, allocator,
18/// and transaction manager. Provides high-level filesystem operations.
19///
20/// All path-accepting methods use `/`-separated paths.  An empty string or
21/// `"/"` refers to the root directory.  Parent directories must already exist;
22/// only `create_file` and `create_directory` create the leaf entry.
23pub struct FilesystemCore {
24    store: Arc<dyn BlockStore>,
25    crypto: Arc<dyn CryptoEngine>,
26    codec: PostcardCodec,
27    allocator: BitmapAllocator,
28    txn: TransactionManager,
29    /// Cached current superblock.
30    superblock: Option<Superblock>,
31    /// Next inode ID to allocate.
32    next_inode_id: InodeId,
33}
34
35/// Tracks one ancestor directory during path resolution, used by
36/// `commit_cow_chain` to propagate CoW writes back to the root.
37struct AncestorEntry {
38    inode: Inode,
39    dir_page: DirectoryPage,
40    child_index: usize,
41}
42
43/// Maximum payload size for a single file data chunk.
44/// Computed conservatively: block_size minus overhead for envelope framing.
45/// We'll compute this dynamically based on block size.
46fn max_chunk_payload(block_size: usize) -> usize {
47    // Rough overhead: 4 bytes length prefix, ~60 bytes envelope metadata,
48    // 16 bytes Poly1305 tag, some postcard framing. Be conservative.
49    if block_size > 200 {
50        block_size - 200
51    } else {
52        0
53    }
54}
55
56fn now_secs() -> u64 {
57    SystemTime::now()
58        .duration_since(UNIX_EPOCH)
59        .unwrap_or_default()
60        .as_secs()
61}
62
63impl FilesystemCore {
64    /// Create a new FilesystemCore backed by the given store and crypto engine.
65    pub fn new(store: Arc<dyn BlockStore>, crypto: Arc<dyn CryptoEngine>) -> Self {
66        let total_blocks = store.total_blocks();
67        Self {
68            store,
69            crypto,
70            codec: PostcardCodec,
71            allocator: BitmapAllocator::new(total_blocks),
72            txn: TransactionManager::new(),
73            superblock: None,
74            next_inode_id: 1,
75        }
76    }
77
78    // ── Initialization ──
79
80    /// Initialize a brand-new filesystem on the block store.
81    /// Writes the storage header, creates the root directory, and commits.
82    pub fn init_filesystem(&mut self) -> FsResult<()> {
83        let block_size = self.store.block_size() as u32;
84        let total_blocks = self.store.total_blocks();
85
86        // Write storage header to block 0 (unencrypted).
87        let header = StorageHeader::new(block_size, total_blocks);
88        let header_bytes = self.codec.serialize_object(&header)?;
89        let bs = self.store.block_size();
90        let mut block = vec![0u8; bs];
91        rand::thread_rng().fill_bytes(&mut block);
92        let len = header_bytes.len() as u32;
93        block[..4].copy_from_slice(&len.to_le_bytes());
94        block[4..4 + header_bytes.len()].copy_from_slice(&header_bytes);
95        self.store.write_block(BLOCK_STORAGE_HEADER, &block)?;
96
97        // Create root directory inode.
98        let root_inode_id = self.alloc_inode_id();
99        let dir_page = DirectoryPage::new();
100        let dir_page_block = self.allocator.allocate()?;
101        write_encrypted_object(
102            self.store.as_ref(),
103            self.crypto.as_ref(),
104            &self.codec,
105            dir_page_block,
106            ObjectKind::DirectoryPage,
107            &dir_page,
108        )?;
109
110        let ts = now_secs();
111        let root_inode = Inode {
112            id: root_inode_id,
113            kind: InodeKind::Directory,
114            size: 0,
115            directory_page_ref: ObjectRef::new(dir_page_block),
116            extent_map_ref: ObjectRef::null(),
117            created_at: ts,
118            modified_at: ts,
119        };
120        let root_inode_block = self.allocator.allocate()?;
121        write_encrypted_object(
122            self.store.as_ref(),
123            self.crypto.as_ref(),
124            &self.codec,
125            root_inode_block,
126            ObjectKind::Inode,
127            &root_inode,
128        )?;
129
130        // Create superblock.
131        let sb = Superblock {
132            generation: 1,
133            root_inode_ref: ObjectRef::new(root_inode_block),
134        };
135        self.superblock = Some(sb.clone());
136
137        // Commit.
138        self.txn.commit(
139            self.store.as_ref(),
140            self.crypto.as_ref(),
141            &self.codec,
142            &self.allocator,
143            &sb,
144        )?;
145
146        Ok(())
147    }
148
149    /// Open / mount an existing filesystem by recovering the latest root pointer.
150    pub fn open(&mut self) -> FsResult<()> {
151        // Verify storage header.
152        let header = self.read_storage_header()?;
153        if !header.is_valid() {
154            return Err(FsError::InvalidSuperblock);
155        }
156
157        // Recover latest root pointer.
158        let (rp, was_b) = TransactionManager::recover_latest(self.store.as_ref(), &self.codec)?
159            .ok_or(FsError::InvalidRootPointer)?;
160
161        // Read superblock.
162        let sb: Superblock = read_encrypted_object(
163            self.store.as_ref(),
164            self.crypto.as_ref(),
165            &self.codec,
166            rp.superblock_ref.block_id,
167        )?;
168
169        // Verify checksum.
170        let sb_bytes = self.codec.serialize_object(&sb)?;
171        let checksum = blake3::hash(&sb_bytes);
172        if *checksum.as_bytes() != rp.checksum {
173            return Err(FsError::InvalidSuperblock);
174        }
175
176        self.txn = TransactionManager::from_recovered(rp.generation, was_b);
177        self.superblock = Some(sb.clone());
178
179        // Rebuild allocator knowledge by walking the metadata tree.
180        self.rebuild_allocator(&sb)?;
181
182        Ok(())
183    }
184
185    // ── File operations ──
186
187    // ── Path helpers ──────────────────────────────────────────
188
189    /// Split a path into its directory components and the leaf name.
190    /// Returns `(["a","b"], "c")` for `"a/b/c"`, or `([], "c")` for `"c"`.
191    fn split_path(path: &str) -> FsResult<(Vec<&str>, &str)> {
192        let trimmed = path.trim_matches('/');
193        if trimmed.is_empty() {
194            return Err(FsError::Internal("empty path".into()));
195        }
196        let parts: Vec<&str> = trimmed.split('/').collect();
197        let (dirs, leaf) = parts.split_at(parts.len() - 1);
198        Ok((dirs.to_vec(), leaf[0]))
199    }
200
201    /// Parse a directory path (may be empty / "/" for root) into components.
202    fn split_dir_path(path: &str) -> Vec<&str> {
203        let trimmed = path.trim_matches('/');
204        if trimmed.is_empty() {
205            return Vec::new();
206        }
207        trimmed.split('/').collect()
208    }
209
210    /// Resolve a sequence of directory components starting from the root inode,
211    /// returning the ancestor chain needed for CoW commit propagation.
212    ///
213    /// Returns `(ancestors, target_inode, target_dir_page)` where `ancestors`
214    /// is a list of `(Inode, DirectoryPage, entry_index_in_parent)` from root
215    /// down to (but not including) the final resolved directory.
216    fn resolve_dir_chain(
217        &self,
218        components: &[&str],
219        root_inode: &Inode,
220    ) -> FsResult<(Vec<AncestorEntry>, Inode, DirectoryPage)> {
221        let mut ancestors: Vec<AncestorEntry> = Vec::new();
222        let mut current_inode = root_inode.clone();
223        let mut current_dir_page: DirectoryPage =
224            self.read_obj(current_inode.directory_page_ref.block_id)?;
225
226        for component in components {
227            let idx = current_dir_page
228                .entries
229                .iter()
230                .position(|e| e.name == *component)
231                .ok_or_else(|| FsError::DirectoryNotFound(component.to_string()))?;
232
233            let entry = &current_dir_page.entries[idx];
234            if entry.kind != InodeKind::Directory {
235                return Err(FsError::NotADirectory(component.to_string()));
236            }
237
238            let child_inode: Inode = self.read_obj(entry.inode_ref.block_id)?;
239            let child_dir_page: DirectoryPage =
240                self.read_obj(child_inode.directory_page_ref.block_id)?;
241
242            ancestors.push(AncestorEntry {
243                inode: current_inode,
244                dir_page: current_dir_page,
245                child_index: idx,
246            });
247
248            current_inode = child_inode;
249            current_dir_page = child_dir_page;
250        }
251
252        Ok((ancestors, current_inode, current_dir_page))
253    }
254
255    /// After mutating a directory's page, propagate CoW changes up through
256    /// the ancestor chain to the root, then commit a new superblock.
257    ///
258    /// `new_dir_page` is the already-modified DirectoryPage of the target dir.
259    /// `target_inode` is the inode of the directory that owns `new_dir_page`.
260    /// `ancestors` is the chain from root down to (but not including) target.
261    fn commit_cow_chain(
262        &mut self,
263        sb: &Superblock,
264        ancestors: &[AncestorEntry],
265        target_inode: &Inode,
266        new_dir_page: &DirectoryPage,
267    ) -> FsResult<()> {
268        // Write the modified directory page.
269        let mut new_dp_block = self.allocator.allocate()?;
270        self.write_obj(new_dp_block, ObjectKind::DirectoryPage, new_dir_page)?;
271
272        // Write the modified directory inode.
273        let mut new_inode = target_inode.clone();
274        new_inode.directory_page_ref = ObjectRef::new(new_dp_block);
275        new_inode.modified_at = now_secs();
276        let mut new_inode_block = self.allocator.allocate()?;
277        self.write_obj(new_inode_block, ObjectKind::Inode, &new_inode)?;
278
279        // Propagate upward through ancestors (bottom to top).
280        for ancestor in ancestors.iter().rev() {
281            let mut parent_dp = ancestor.dir_page.clone();
282            parent_dp.entries[ancestor.child_index].inode_ref = ObjectRef::new(new_inode_block);
283
284            new_dp_block = self.allocator.allocate()?;
285            self.write_obj(new_dp_block, ObjectKind::DirectoryPage, &parent_dp)?;
286
287            let mut parent_inode = ancestor.inode.clone();
288            parent_inode.directory_page_ref = ObjectRef::new(new_dp_block);
289            parent_inode.modified_at = now_secs();
290            new_inode_block = self.allocator.allocate()?;
291            self.write_obj(new_inode_block, ObjectKind::Inode, &parent_inode)?;
292        }
293
294        // new_inode_block is now the new root inode block.
295        let new_sb = Superblock {
296            generation: sb.generation + 1,
297            root_inode_ref: ObjectRef::new(new_inode_block),
298        };
299        self.commit_superblock(new_sb)?;
300        Ok(())
301    }
302
303    // ── Public operations ─────────────────────────────────────
304
305    /// Create a new empty file at the given path.
306    ///
307    /// Parent directories must already exist.  The leaf name is created in
308    /// the innermost directory.
309    pub fn create_file(&mut self, path: &str) -> FsResult<()> {
310        let (dir_parts, leaf) = Self::split_path(path)?;
311        self.validate_name(leaf)?;
312        let sb = self
313            .superblock
314            .as_ref()
315            .ok_or(FsError::NotInitialized)?
316            .clone();
317
318        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
319        let (ancestors, target_inode, mut dir_page) =
320            self.resolve_dir_chain(&dir_parts, &root_inode)?;
321
322        if dir_page.entries.iter().any(|e| e.name == leaf) {
323            return Err(FsError::FileAlreadyExists(leaf.to_string()));
324        }
325
326        // Create empty extent map.
327        let extent_map = ExtentMap::new();
328        let em_block = self.allocator.allocate()?;
329        self.write_obj(em_block, ObjectKind::ExtentMap, &extent_map)?;
330
331        // Create file inode.
332        let inode_id = self.alloc_inode_id();
333        let ts = now_secs();
334        let file_inode = Inode {
335            id: inode_id,
336            kind: InodeKind::File,
337            size: 0,
338            directory_page_ref: ObjectRef::null(),
339            extent_map_ref: ObjectRef::new(em_block),
340            created_at: ts,
341            modified_at: ts,
342        };
343        let inode_block = self.allocator.allocate()?;
344        self.write_obj(inode_block, ObjectKind::Inode, &file_inode)?;
345
346        dir_page.entries.push(DirectoryEntry {
347            name: leaf.to_string(),
348            inode_ref: ObjectRef::new(inode_block),
349            inode_id,
350            kind: InodeKind::File,
351        });
352
353        self.commit_cow_chain(&sb, &ancestors, &target_inode, &dir_page)?;
354        Ok(())
355    }
356
357    /// Write data to a file at the given path.
358    ///
359    /// Only the chunks that overlap with `[offset, offset+data.len())` are
360    /// read and rewritten.  For sequential appends this means O(new_data)
361    /// work instead of O(file_size).
362    pub fn write_file(&mut self, path: &str, offset: u64, data: &[u8]) -> FsResult<()> {
363        let (dir_parts, leaf) = Self::split_path(path)?;
364        let sb = self
365            .superblock
366            .as_ref()
367            .ok_or(FsError::NotInitialized)?
368            .clone();
369        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
370        let (ancestors, target_inode, dir_page) =
371            self.resolve_dir_chain(&dir_parts, &root_inode)?;
372
373        let entry = dir_page
374            .entries
375            .iter()
376            .find(|e| e.name == leaf)
377            .ok_or_else(|| FsError::FileNotFound(leaf.to_string()))?;
378
379        if entry.kind != InodeKind::File {
380            return Err(FsError::NotAFile(leaf.to_string()));
381        }
382
383        let file_inode: Inode = self.read_obj(entry.inode_ref.block_id)?;
384        let mut extent_map: ExtentMap = self.read_obj(file_inode.extent_map_ref.block_id)?;
385
386        let chunk_size = max_chunk_payload(self.store.block_size());
387        if chunk_size == 0 {
388            return Err(FsError::DataTooLarge(data.len()));
389        }
390
391        if data.is_empty() {
392            return Ok(());
393        }
394
395        let old_size = file_inode.size as usize;
396        let write_start = offset as usize;
397        let write_end = write_start + data.len();
398        let new_size = std::cmp::max(old_size, write_end);
399
400        // Sort existing entries so we can binary-search by chunk_index.
401        extent_map.entries.sort_by_key(|e| e.chunk_index);
402        // Remember the sorted portion length — new entries are appended
403        // after this and are NOT included in binary searches.
404        let sorted_len = extent_map.entries.len();
405
406        // Determine which chunk indices need writing.
407        // For appends past the current end we also need to extend or
408        // zero-fill from the last partial chunk onward.
409        let first_chunk = if write_start >= old_size {
410            old_size / chunk_size
411        } else {
412            write_start / chunk_size
413        };
414        let last_chunk = (new_size - 1) / chunk_size;
415
416        for chunk_idx in first_chunk..=last_chunk {
417            let chunk_file_start = chunk_idx * chunk_size;
418            let chunk_file_end = std::cmp::min(chunk_file_start + chunk_size, new_size);
419            let chunk_len = chunk_file_end - chunk_file_start;
420
421            let mut chunk_buf = vec![0u8; chunk_len];
422
423            // Copy existing data for this chunk if it already exists on disk.
424            let chunk_idx_u64 = chunk_idx as u64;
425            if chunk_file_start < old_size {
426                if let Ok(pos) = extent_map.entries[..sorted_len]
427                    .binary_search_by_key(&chunk_idx_u64, |e| e.chunk_index)
428                {
429                    let existing = &extent_map.entries[pos];
430                    let raw = read_encrypted_raw(
431                        self.store.as_ref(),
432                        self.crypto.as_ref(),
433                        &self.codec,
434                        existing.data_ref.block_id,
435                    )?;
436                    let copy_len = std::cmp::min(existing.plaintext_len as usize, chunk_len);
437                    let src_len = std::cmp::min(copy_len, raw.len());
438                    chunk_buf[..src_len].copy_from_slice(&raw[..src_len]);
439                }
440            }
441
442            // Overlay the write data onto the chunk.
443            let overlap_start = std::cmp::max(chunk_file_start, write_start);
444            let overlap_end = std::cmp::min(chunk_file_end, write_end);
445            if overlap_start < overlap_end {
446                let data_off = overlap_start - write_start;
447                let chunk_off = overlap_start - chunk_file_start;
448                let len = overlap_end - overlap_start;
449                chunk_buf[chunk_off..chunk_off + len]
450                    .copy_from_slice(&data[data_off..data_off + len]);
451            }
452
453            // Write the (possibly new) chunk.
454            let data_block = self.allocator.allocate()?;
455            write_encrypted_raw(
456                self.store.as_ref(),
457                self.crypto.as_ref(),
458                &self.codec,
459                data_block,
460                ObjectKind::FileDataChunk,
461                &chunk_buf,
462            )?;
463
464            // Update existing extent entry or append a new one.
465            if let Ok(pos) = extent_map.entries[..sorted_len]
466                .binary_search_by_key(&chunk_idx_u64, |e| e.chunk_index)
467            {
468                extent_map.entries[pos].data_ref = ObjectRef::new(data_block);
469                extent_map.entries[pos].plaintext_len = chunk_buf.len() as u32;
470            } else {
471                extent_map.entries.push(ExtentEntry {
472                    chunk_index: chunk_idx_u64,
473                    data_ref: ObjectRef::new(data_block),
474                    plaintext_len: chunk_buf.len() as u32,
475                });
476            }
477        }
478
479        // Re-sort after potential appends.
480        extent_map.entries.sort_by_key(|e| e.chunk_index);
481
482        // Write updated extent map (CoW).
483        let new_em_block = self.allocator.allocate()?;
484        self.write_obj(new_em_block, ObjectKind::ExtentMap, &extent_map)?;
485
486        // Write updated file inode.
487        let mut new_file_inode = file_inode.clone();
488        new_file_inode.size = new_size as u64;
489        new_file_inode.extent_map_ref = ObjectRef::new(new_em_block);
490        new_file_inode.modified_at = now_secs();
491        let new_inode_block = self.allocator.allocate()?;
492        self.write_obj(new_inode_block, ObjectKind::Inode, &new_file_inode)?;
493
494        // Update directory entry to point to new inode block.
495        let mut new_dir_page = dir_page.clone();
496        for e in &mut new_dir_page.entries {
497            if e.name == leaf {
498                e.inode_ref = ObjectRef::new(new_inode_block);
499            }
500        }
501
502        self.commit_cow_chain(&sb, &ancestors, &target_inode, &new_dir_page)?;
503        Ok(())
504    }
505
506    /// Read file data at the given path. Returns the requested slice.
507    pub fn read_file(&self, path: &str, offset: u64, len: usize) -> FsResult<Vec<u8>> {
508        let (dir_parts, leaf) = Self::split_path(path)?;
509        let sb = self.superblock.as_ref().ok_or(FsError::NotInitialized)?;
510        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
511        let (_, _, dir_page) = self.resolve_dir_chain(&dir_parts, &root_inode)?;
512
513        let entry = dir_page
514            .entries
515            .iter()
516            .find(|e| e.name == leaf)
517            .ok_or_else(|| FsError::FileNotFound(leaf.to_string()))?;
518
519        if entry.kind != InodeKind::File {
520            return Err(FsError::NotAFile(leaf.to_string()));
521        }
522
523        let file_inode: Inode = self.read_obj(entry.inode_ref.block_id)?;
524        let extent_map: ExtentMap = self.read_obj(file_inode.extent_map_ref.block_id)?;
525
526        let full_data = self.read_all_chunks(&extent_map)?;
527
528        let start = offset as usize;
529        if start >= full_data.len() {
530            return Ok(Vec::new());
531        }
532        let end = std::cmp::min(start + len, full_data.len());
533        Ok(full_data[start..end].to_vec())
534    }
535
536    /// List entries in a directory at the given path.
537    ///
538    /// Pass `""` or `"/"` to list the root directory.
539    pub fn list_directory(&self, path: &str) -> FsResult<Vec<DirListEntry>> {
540        let sb = self.superblock.as_ref().ok_or(FsError::NotInitialized)?;
541        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
542
543        let components = Self::split_dir_path(path);
544        let (_, _, dir_page) = self.resolve_dir_chain(&components, &root_inode)?;
545
546        let mut result = Vec::new();
547        for entry in &dir_page.entries {
548            let inode: Inode = self.read_obj(entry.inode_ref.block_id)?;
549            result.push(DirListEntry {
550                name: entry.name.clone(),
551                kind: entry.kind,
552                size: inode.size,
553                inode_id: entry.inode_id,
554            });
555        }
556        Ok(result)
557    }
558
559    /// Create a subdirectory at the given path.
560    ///
561    /// Parent directories must already exist; only the leaf is created.
562    pub fn create_directory(&mut self, path: &str) -> FsResult<()> {
563        let (dir_parts, leaf) = Self::split_path(path)?;
564        self.validate_name(leaf)?;
565        let sb = self
566            .superblock
567            .as_ref()
568            .ok_or(FsError::NotInitialized)?
569            .clone();
570        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
571        let (ancestors, target_inode, mut dir_page) =
572            self.resolve_dir_chain(&dir_parts, &root_inode)?;
573
574        if dir_page.entries.iter().any(|e| e.name == leaf) {
575            return Err(FsError::DirectoryAlreadyExists(leaf.to_string()));
576        }
577
578        // Create empty directory page for the new subdirectory.
579        let sub_dp = DirectoryPage::new();
580        let sub_dp_block = self.allocator.allocate()?;
581        self.write_obj(sub_dp_block, ObjectKind::DirectoryPage, &sub_dp)?;
582
583        let inode_id = self.alloc_inode_id();
584        let ts = now_secs();
585        let dir_inode = Inode {
586            id: inode_id,
587            kind: InodeKind::Directory,
588            size: 0,
589            directory_page_ref: ObjectRef::new(sub_dp_block),
590            extent_map_ref: ObjectRef::null(),
591            created_at: ts,
592            modified_at: ts,
593        };
594        let inode_block = self.allocator.allocate()?;
595        self.write_obj(inode_block, ObjectKind::Inode, &dir_inode)?;
596
597        dir_page.entries.push(DirectoryEntry {
598            name: leaf.to_string(),
599            inode_ref: ObjectRef::new(inode_block),
600            inode_id,
601            kind: InodeKind::Directory,
602        });
603
604        self.commit_cow_chain(&sb, &ancestors, &target_inode, &dir_page)?;
605        Ok(())
606    }
607
608    /// Remove a file or empty directory at the given path.
609    pub fn remove_file(&mut self, path: &str) -> FsResult<()> {
610        let (dir_parts, leaf) = Self::split_path(path)?;
611        let sb = self
612            .superblock
613            .as_ref()
614            .ok_or(FsError::NotInitialized)?
615            .clone();
616        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
617        let (ancestors, target_inode, mut dir_page) =
618            self.resolve_dir_chain(&dir_parts, &root_inode)?;
619
620        let idx = dir_page
621            .entries
622            .iter()
623            .position(|e| e.name == leaf)
624            .ok_or_else(|| FsError::FileNotFound(leaf.to_string()))?;
625
626        let entry = &dir_page.entries[idx];
627        if entry.kind == InodeKind::Directory {
628            let dir_inode: Inode = self.read_obj(entry.inode_ref.block_id)?;
629            let sub_page: DirectoryPage = self.read_obj(dir_inode.directory_page_ref.block_id)?;
630            if !sub_page.entries.is_empty() {
631                return Err(FsError::DirectoryNotEmpty(leaf.to_string()));
632            }
633        }
634
635        dir_page.entries.remove(idx);
636        self.commit_cow_chain(&sb, &ancestors, &target_inode, &dir_page)?;
637        Ok(())
638    }
639
640    /// Rename a file or directory.  Both `old_path` and `new_path` must share
641    /// the same parent directory (move across directories is not supported yet).
642    pub fn rename(&mut self, old_path: &str, new_path: &str) -> FsResult<()> {
643        let (old_dir, old_leaf) = Self::split_path(old_path)?;
644        let (new_dir, new_leaf) = Self::split_path(new_path)?;
645        self.validate_name(new_leaf)?;
646
647        if old_dir != new_dir {
648            return Err(FsError::Internal(
649                "rename across directories is not supported".into(),
650            ));
651        }
652
653        let sb = self
654            .superblock
655            .as_ref()
656            .ok_or(FsError::NotInitialized)?
657            .clone();
658        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
659        let (ancestors, target_inode, mut dir_page) =
660            self.resolve_dir_chain(&old_dir, &root_inode)?;
661
662        if dir_page.entries.iter().any(|e| e.name == new_leaf) {
663            return Err(FsError::FileAlreadyExists(new_leaf.to_string()));
664        }
665
666        let entry = dir_page
667            .entries
668            .iter_mut()
669            .find(|e| e.name == old_leaf)
670            .ok_or_else(|| FsError::FileNotFound(old_leaf.to_string()))?;
671
672        entry.name = new_leaf.to_string();
673
674        self.commit_cow_chain(&sb, &ancestors, &target_inode, &dir_page)?;
675        Ok(())
676    }
677
678    /// Sync / flush. Calls through to the block store sync.
679    pub fn sync(&self) -> FsResult<()> {
680        self.store.sync()
681    }
682
683    // ── Internal helpers ──
684
685    fn alloc_inode_id(&mut self) -> InodeId {
686        let id = self.next_inode_id;
687        self.next_inode_id += 1;
688        id
689    }
690
691    fn validate_name(&self, name: &str) -> FsResult<()> {
692        if name.is_empty() || name.contains('/') || name.contains('\0') {
693            return Err(FsError::Internal("invalid name".into()));
694        }
695        if name.len() > MAX_NAME_LEN {
696            return Err(FsError::NameTooLong(name.len(), MAX_NAME_LEN));
697        }
698        Ok(())
699    }
700
701    fn read_obj<T: serde::de::DeserializeOwned>(&self, block_id: u64) -> FsResult<T> {
702        read_encrypted_object(
703            self.store.as_ref(),
704            self.crypto.as_ref(),
705            &self.codec,
706            block_id,
707        )
708    }
709
710    fn write_obj<T: serde::Serialize>(
711        &self,
712        block_id: u64,
713        kind: ObjectKind,
714        obj: &T,
715    ) -> FsResult<()> {
716        write_encrypted_object(
717            self.store.as_ref(),
718            self.crypto.as_ref(),
719            &self.codec,
720            block_id,
721            kind,
722            obj,
723        )
724    }
725
726    fn read_all_chunks(&self, extent_map: &ExtentMap) -> FsResult<Vec<u8>> {
727        let mut entries = extent_map.entries.clone();
728        entries.sort_by_key(|e| e.chunk_index);
729
730        let mut buf = Vec::new();
731        for entry in &entries {
732            let chunk = read_encrypted_raw(
733                self.store.as_ref(),
734                self.crypto.as_ref(),
735                &self.codec,
736                entry.data_ref.block_id,
737            )?;
738            // Only take plaintext_len bytes (chunk may have been decrypted from padded block).
739            let len = entry.plaintext_len as usize;
740            if len <= chunk.len() {
741                buf.extend_from_slice(&chunk[..len]);
742            } else {
743                buf.extend_from_slice(&chunk);
744            }
745        }
746        Ok(buf)
747    }
748
749    fn read_storage_header(&self) -> FsResult<StorageHeader> {
750        let block = self.store.read_block(BLOCK_STORAGE_HEADER)?;
751        if block.len() < 4 {
752            return Err(FsError::InvalidSuperblock);
753        }
754        let len = u32::from_le_bytes([block[0], block[1], block[2], block[3]]) as usize;
755        if len == 0 || 4 + len > block.len() {
756            return Err(FsError::InvalidSuperblock);
757        }
758        self.codec
759            .deserialize_object::<StorageHeader>(&block[4..4 + len])
760    }
761
762    fn commit_superblock(&mut self, sb: Superblock) -> FsResult<()> {
763        self.txn.commit(
764            self.store.as_ref(),
765            self.crypto.as_ref(),
766            &self.codec,
767            &self.allocator,
768            &sb,
769        )?;
770        self.superblock = Some(sb);
771        Ok(())
772    }
773
774    /// Walk the metadata tree from the superblock and mark all referenced blocks
775    /// as allocated in the allocator. Used during open/mount.
776    fn rebuild_allocator(&mut self, sb: &Superblock) -> FsResult<()> {
777        // Mark superblock block.
778        // The superblock_ref's block was allocated by the transaction manager.
779        // We also need to mark root pointer blocks, but those are reserved (0,1,2).
780
781        // We need to find which block the superblock is stored in.
782        // The root pointer tells us.
783        let (rp, _) = TransactionManager::recover_latest(self.store.as_ref(), &self.codec)?
784            .ok_or(FsError::InvalidRootPointer)?;
785        self.allocator.mark_allocated(rp.superblock_ref.block_id)?;
786
787        // Walk root inode.
788        self.mark_inode_tree(sb.root_inode_ref.block_id)?;
789
790        // Set next_inode_id to be higher than any seen inode.
791        // (We updated it during the walk.)
792
793        Ok(())
794    }
795
796    fn mark_inode_tree(&mut self, inode_block: u64) -> FsResult<()> {
797        self.allocator.mark_allocated(inode_block)?;
798        let inode: Inode = self.read_obj(inode_block)?;
799
800        if inode.id >= self.next_inode_id {
801            self.next_inode_id = inode.id + 1;
802        }
803
804        match inode.kind {
805            InodeKind::Directory => {
806                if !inode.directory_page_ref.is_null() {
807                    self.allocator
808                        .mark_allocated(inode.directory_page_ref.block_id)?;
809                    let dir_page: DirectoryPage =
810                        self.read_obj(inode.directory_page_ref.block_id)?;
811                    for entry in &dir_page.entries {
812                        self.mark_inode_tree(entry.inode_ref.block_id)?;
813                    }
814                }
815            }
816            InodeKind::File => {
817                if !inode.extent_map_ref.is_null() {
818                    self.allocator
819                        .mark_allocated(inode.extent_map_ref.block_id)?;
820                    let extent_map: ExtentMap = self.read_obj(inode.extent_map_ref.block_id)?;
821                    for entry in &extent_map.entries {
822                        self.allocator.mark_allocated(entry.data_ref.block_id)?;
823                    }
824                }
825            }
826        }
827        Ok(())
828    }
829}
830
831/// Return type for directory listings (used by FFI and public API).
832#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
833pub struct DirListEntry {
834    pub name: String,
835    pub kind: InodeKind,
836    pub size: u64,
837    pub inode_id: InodeId,
838}