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    pub fn write_file(&mut self, path: &str, offset: u64, data: &[u8]) -> FsResult<()> {
359        let (dir_parts, leaf) = Self::split_path(path)?;
360        let sb = self
361            .superblock
362            .as_ref()
363            .ok_or(FsError::NotInitialized)?
364            .clone();
365        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
366        let (ancestors, target_inode, dir_page) =
367            self.resolve_dir_chain(&dir_parts, &root_inode)?;
368
369        let entry = dir_page
370            .entries
371            .iter()
372            .find(|e| e.name == leaf)
373            .ok_or_else(|| FsError::FileNotFound(leaf.to_string()))?;
374
375        if entry.kind != InodeKind::File {
376            return Err(FsError::NotAFile(leaf.to_string()));
377        }
378
379        let file_inode: Inode = self.read_obj(entry.inode_ref.block_id)?;
380        let mut extent_map: ExtentMap = self.read_obj(file_inode.extent_map_ref.block_id)?;
381
382        // V1: support write-at-offset by building a full buffer.
383        // Read existing data (if any) and splice in the new data.
384        let mut buf = self.read_all_chunks(&extent_map)?;
385
386        let end = offset as usize + data.len();
387        if end > buf.len() {
388            buf.resize(end, 0);
389        }
390        buf[offset as usize..end].copy_from_slice(data);
391
392        let total_size = buf.len();
393
394        // Re-chunk the data. Each chunk must fit in max_chunk_payload.
395        let chunk_size = max_chunk_payload(self.store.block_size());
396        if chunk_size == 0 {
397            return Err(FsError::DataTooLarge(total_size));
398        }
399
400        let mut new_entries = Vec::new();
401        for (i, chunk_data) in buf.chunks(chunk_size).enumerate() {
402            let data_block = self.allocator.allocate()?;
403            write_encrypted_raw(
404                self.store.as_ref(),
405                self.crypto.as_ref(),
406                &self.codec,
407                data_block,
408                ObjectKind::FileDataChunk,
409                chunk_data,
410            )?;
411            new_entries.push(ExtentEntry {
412                chunk_index: i as u64,
413                data_ref: ObjectRef::new(data_block),
414                plaintext_len: chunk_data.len() as u32,
415            });
416        }
417
418        // TODO: free old data blocks (deferred GC).
419        extent_map.entries = new_entries;
420
421        // Write updated extent map (CoW).
422        let new_em_block = self.allocator.allocate()?;
423        self.write_obj(new_em_block, ObjectKind::ExtentMap, &extent_map)?;
424
425        // Write updated file inode.
426        let mut new_file_inode = file_inode.clone();
427        new_file_inode.size = total_size as u64;
428        new_file_inode.extent_map_ref = ObjectRef::new(new_em_block);
429        new_file_inode.modified_at = now_secs();
430        let new_inode_block = self.allocator.allocate()?;
431        self.write_obj(new_inode_block, ObjectKind::Inode, &new_file_inode)?;
432
433        // Update directory entry to point to new inode block.
434        let mut new_dir_page = dir_page.clone();
435        for e in &mut new_dir_page.entries {
436            if e.name == leaf {
437                e.inode_ref = ObjectRef::new(new_inode_block);
438            }
439        }
440
441        self.commit_cow_chain(&sb, &ancestors, &target_inode, &new_dir_page)?;
442        Ok(())
443    }
444
445    /// Read file data at the given path. Returns the requested slice.
446    pub fn read_file(&self, path: &str, offset: u64, len: usize) -> FsResult<Vec<u8>> {
447        let (dir_parts, leaf) = Self::split_path(path)?;
448        let sb = self.superblock.as_ref().ok_or(FsError::NotInitialized)?;
449        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
450        let (_, _, dir_page) = self.resolve_dir_chain(&dir_parts, &root_inode)?;
451
452        let entry = dir_page
453            .entries
454            .iter()
455            .find(|e| e.name == leaf)
456            .ok_or_else(|| FsError::FileNotFound(leaf.to_string()))?;
457
458        if entry.kind != InodeKind::File {
459            return Err(FsError::NotAFile(leaf.to_string()));
460        }
461
462        let file_inode: Inode = self.read_obj(entry.inode_ref.block_id)?;
463        let extent_map: ExtentMap = self.read_obj(file_inode.extent_map_ref.block_id)?;
464
465        let full_data = self.read_all_chunks(&extent_map)?;
466
467        let start = offset as usize;
468        if start >= full_data.len() {
469            return Ok(Vec::new());
470        }
471        let end = std::cmp::min(start + len, full_data.len());
472        Ok(full_data[start..end].to_vec())
473    }
474
475    /// List entries in a directory at the given path.
476    ///
477    /// Pass `""` or `"/"` to list the root directory.
478    pub fn list_directory(&self, path: &str) -> FsResult<Vec<DirListEntry>> {
479        let sb = self.superblock.as_ref().ok_or(FsError::NotInitialized)?;
480        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
481
482        let components = Self::split_dir_path(path);
483        let (_, _, dir_page) = self.resolve_dir_chain(&components, &root_inode)?;
484
485        let mut result = Vec::new();
486        for entry in &dir_page.entries {
487            let inode: Inode = self.read_obj(entry.inode_ref.block_id)?;
488            result.push(DirListEntry {
489                name: entry.name.clone(),
490                kind: entry.kind,
491                size: inode.size,
492                inode_id: entry.inode_id,
493            });
494        }
495        Ok(result)
496    }
497
498    /// Create a subdirectory at the given path.
499    ///
500    /// Parent directories must already exist; only the leaf is created.
501    pub fn create_directory(&mut self, path: &str) -> FsResult<()> {
502        let (dir_parts, leaf) = Self::split_path(path)?;
503        self.validate_name(leaf)?;
504        let sb = self
505            .superblock
506            .as_ref()
507            .ok_or(FsError::NotInitialized)?
508            .clone();
509        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
510        let (ancestors, target_inode, mut dir_page) =
511            self.resolve_dir_chain(&dir_parts, &root_inode)?;
512
513        if dir_page.entries.iter().any(|e| e.name == leaf) {
514            return Err(FsError::DirectoryAlreadyExists(leaf.to_string()));
515        }
516
517        // Create empty directory page for the new subdirectory.
518        let sub_dp = DirectoryPage::new();
519        let sub_dp_block = self.allocator.allocate()?;
520        self.write_obj(sub_dp_block, ObjectKind::DirectoryPage, &sub_dp)?;
521
522        let inode_id = self.alloc_inode_id();
523        let ts = now_secs();
524        let dir_inode = Inode {
525            id: inode_id,
526            kind: InodeKind::Directory,
527            size: 0,
528            directory_page_ref: ObjectRef::new(sub_dp_block),
529            extent_map_ref: ObjectRef::null(),
530            created_at: ts,
531            modified_at: ts,
532        };
533        let inode_block = self.allocator.allocate()?;
534        self.write_obj(inode_block, ObjectKind::Inode, &dir_inode)?;
535
536        dir_page.entries.push(DirectoryEntry {
537            name: leaf.to_string(),
538            inode_ref: ObjectRef::new(inode_block),
539            inode_id,
540            kind: InodeKind::Directory,
541        });
542
543        self.commit_cow_chain(&sb, &ancestors, &target_inode, &dir_page)?;
544        Ok(())
545    }
546
547    /// Remove a file or empty directory at the given path.
548    pub fn remove_file(&mut self, path: &str) -> FsResult<()> {
549        let (dir_parts, leaf) = Self::split_path(path)?;
550        let sb = self
551            .superblock
552            .as_ref()
553            .ok_or(FsError::NotInitialized)?
554            .clone();
555        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
556        let (ancestors, target_inode, mut dir_page) =
557            self.resolve_dir_chain(&dir_parts, &root_inode)?;
558
559        let idx = dir_page
560            .entries
561            .iter()
562            .position(|e| e.name == leaf)
563            .ok_or_else(|| FsError::FileNotFound(leaf.to_string()))?;
564
565        let entry = &dir_page.entries[idx];
566        if entry.kind == InodeKind::Directory {
567            let dir_inode: Inode = self.read_obj(entry.inode_ref.block_id)?;
568            let sub_page: DirectoryPage = self.read_obj(dir_inode.directory_page_ref.block_id)?;
569            if !sub_page.entries.is_empty() {
570                return Err(FsError::DirectoryNotEmpty(leaf.to_string()));
571            }
572        }
573
574        dir_page.entries.remove(idx);
575        self.commit_cow_chain(&sb, &ancestors, &target_inode, &dir_page)?;
576        Ok(())
577    }
578
579    /// Rename a file or directory.  Both `old_path` and `new_path` must share
580    /// the same parent directory (move across directories is not supported yet).
581    pub fn rename(&mut self, old_path: &str, new_path: &str) -> FsResult<()> {
582        let (old_dir, old_leaf) = Self::split_path(old_path)?;
583        let (new_dir, new_leaf) = Self::split_path(new_path)?;
584        self.validate_name(new_leaf)?;
585
586        if old_dir != new_dir {
587            return Err(FsError::Internal(
588                "rename across directories is not supported".into(),
589            ));
590        }
591
592        let sb = self
593            .superblock
594            .as_ref()
595            .ok_or(FsError::NotInitialized)?
596            .clone();
597        let root_inode: Inode = self.read_obj(sb.root_inode_ref.block_id)?;
598        let (ancestors, target_inode, mut dir_page) =
599            self.resolve_dir_chain(&old_dir, &root_inode)?;
600
601        if dir_page.entries.iter().any(|e| e.name == new_leaf) {
602            return Err(FsError::FileAlreadyExists(new_leaf.to_string()));
603        }
604
605        let entry = dir_page
606            .entries
607            .iter_mut()
608            .find(|e| e.name == old_leaf)
609            .ok_or_else(|| FsError::FileNotFound(old_leaf.to_string()))?;
610
611        entry.name = new_leaf.to_string();
612
613        self.commit_cow_chain(&sb, &ancestors, &target_inode, &dir_page)?;
614        Ok(())
615    }
616
617    /// Sync / flush. Calls through to the block store sync.
618    pub fn sync(&self) -> FsResult<()> {
619        self.store.sync()
620    }
621
622    // ── Internal helpers ──
623
624    fn alloc_inode_id(&mut self) -> InodeId {
625        let id = self.next_inode_id;
626        self.next_inode_id += 1;
627        id
628    }
629
630    fn validate_name(&self, name: &str) -> FsResult<()> {
631        if name.is_empty() || name.contains('/') || name.contains('\0') {
632            return Err(FsError::Internal("invalid name".into()));
633        }
634        if name.len() > MAX_NAME_LEN {
635            return Err(FsError::NameTooLong(name.len(), MAX_NAME_LEN));
636        }
637        Ok(())
638    }
639
640    fn read_obj<T: serde::de::DeserializeOwned>(&self, block_id: u64) -> FsResult<T> {
641        read_encrypted_object(
642            self.store.as_ref(),
643            self.crypto.as_ref(),
644            &self.codec,
645            block_id,
646        )
647    }
648
649    fn write_obj<T: serde::Serialize>(
650        &self,
651        block_id: u64,
652        kind: ObjectKind,
653        obj: &T,
654    ) -> FsResult<()> {
655        write_encrypted_object(
656            self.store.as_ref(),
657            self.crypto.as_ref(),
658            &self.codec,
659            block_id,
660            kind,
661            obj,
662        )
663    }
664
665    fn read_all_chunks(&self, extent_map: &ExtentMap) -> FsResult<Vec<u8>> {
666        let mut entries = extent_map.entries.clone();
667        entries.sort_by_key(|e| e.chunk_index);
668
669        let mut buf = Vec::new();
670        for entry in &entries {
671            let chunk = read_encrypted_raw(
672                self.store.as_ref(),
673                self.crypto.as_ref(),
674                &self.codec,
675                entry.data_ref.block_id,
676            )?;
677            // Only take plaintext_len bytes (chunk may have been decrypted from padded block).
678            let len = entry.plaintext_len as usize;
679            if len <= chunk.len() {
680                buf.extend_from_slice(&chunk[..len]);
681            } else {
682                buf.extend_from_slice(&chunk);
683            }
684        }
685        Ok(buf)
686    }
687
688    fn read_storage_header(&self) -> FsResult<StorageHeader> {
689        let block = self.store.read_block(BLOCK_STORAGE_HEADER)?;
690        if block.len() < 4 {
691            return Err(FsError::InvalidSuperblock);
692        }
693        let len = u32::from_le_bytes([block[0], block[1], block[2], block[3]]) as usize;
694        if len == 0 || 4 + len > block.len() {
695            return Err(FsError::InvalidSuperblock);
696        }
697        self.codec
698            .deserialize_object::<StorageHeader>(&block[4..4 + len])
699    }
700
701    fn commit_superblock(&mut self, sb: Superblock) -> FsResult<()> {
702        self.txn.commit(
703            self.store.as_ref(),
704            self.crypto.as_ref(),
705            &self.codec,
706            &self.allocator,
707            &sb,
708        )?;
709        self.superblock = Some(sb);
710        Ok(())
711    }
712
713    /// Walk the metadata tree from the superblock and mark all referenced blocks
714    /// as allocated in the allocator. Used during open/mount.
715    fn rebuild_allocator(&mut self, sb: &Superblock) -> FsResult<()> {
716        // Mark superblock block.
717        // The superblock_ref's block was allocated by the transaction manager.
718        // We also need to mark root pointer blocks, but those are reserved (0,1,2).
719
720        // We need to find which block the superblock is stored in.
721        // The root pointer tells us.
722        let (rp, _) = TransactionManager::recover_latest(self.store.as_ref(), &self.codec)?
723            .ok_or(FsError::InvalidRootPointer)?;
724        self.allocator.mark_allocated(rp.superblock_ref.block_id)?;
725
726        // Walk root inode.
727        self.mark_inode_tree(sb.root_inode_ref.block_id)?;
728
729        // Set next_inode_id to be higher than any seen inode.
730        // (We updated it during the walk.)
731
732        Ok(())
733    }
734
735    fn mark_inode_tree(&mut self, inode_block: u64) -> FsResult<()> {
736        self.allocator.mark_allocated(inode_block)?;
737        let inode: Inode = self.read_obj(inode_block)?;
738
739        if inode.id >= self.next_inode_id {
740            self.next_inode_id = inode.id + 1;
741        }
742
743        match inode.kind {
744            InodeKind::Directory => {
745                if !inode.directory_page_ref.is_null() {
746                    self.allocator
747                        .mark_allocated(inode.directory_page_ref.block_id)?;
748                    let dir_page: DirectoryPage =
749                        self.read_obj(inode.directory_page_ref.block_id)?;
750                    for entry in &dir_page.entries {
751                        self.mark_inode_tree(entry.inode_ref.block_id)?;
752                    }
753                }
754            }
755            InodeKind::File => {
756                if !inode.extent_map_ref.is_null() {
757                    self.allocator
758                        .mark_allocated(inode.extent_map_ref.block_id)?;
759                    let extent_map: ExtentMap = self.read_obj(inode.extent_map_ref.block_id)?;
760                    for entry in &extent_map.entries {
761                        self.allocator.mark_allocated(entry.data_ref.block_id)?;
762                    }
763                }
764            }
765        }
766        Ok(())
767    }
768}
769
770/// Return type for directory listings (used by FFI and public API).
771#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
772pub struct DirListEntry {
773    pub name: String,
774    pub kind: InodeKind,
775    pub size: u64,
776    pub inode_id: InodeId,
777}