grafos_fs/
lib.rs

1//! Distributed filesystem abstraction backed by leased block storage.
2//!
3//! `grafos-fs` provides a practical filesystem subset (open, read, write,
4//! seek, readdir) on top of [`BlockLease`] storage obtained through the
5//! fabricBIOS data plane. Files larger than a configurable stripe threshold
6//! are automatically striped across multiple block leases for parallel I/O.
7//!
8//! # On-block format
9//!
10//! ```text
11//! Block 0:           Superblock (magic "GFFS", version 1, layout offsets)
12//! Blocks 1..B:       Free block bitmap (1 bit per block, packed)
13//! Blocks B..I:       Inode table (fixed-size inode records)
14//! Blocks I..N:       Data blocks
15//! ```
16//!
17//! # Quick start
18//!
19//! ```rust
20//! use grafos_fs::{FabricFs, OpenFlags};
21//! use grafos_std::block::BlockBuilder;
22//!
23//! # grafos_std::host::reset_mock();
24//! # grafos_std::host::mock_set_fbbu_num_blocks(2048);
25//! let lease = BlockBuilder::new().min_blocks(2048).acquire()?;
26//! let mut fs = FabricFs::format(vec![lease])?;
27//!
28//! let mut fh = fs.create("/hello.txt")?;
29//! fs.write(&mut fh, b"hello fabric")?;
30//! fs.close(fh)?;
31//!
32//! let fh = fs.open("/hello.txt", OpenFlags::Read)?;
33//! let mut buf = [0u8; 12];
34//! let n = fs.read(&fh, &mut buf)?;
35//! assert_eq!(&buf[..n], b"hello fabric");
36//! # Ok::<(), grafos_std::FabricError>(())
37//! ```
38
39extern crate alloc;
40
41use alloc::collections::BTreeMap;
42use alloc::string::String;
43use alloc::vec;
44use alloc::vec::Vec;
45use core::cmp;
46
47use grafos_std::block::{BlockLease, BLOCK_SIZE};
48use grafos_std::error::{FabricError, Result};
49use serde::{Deserialize, Serialize};
50
51const SUPERBLOCK_MAGIC: [u8; 4] = *b"GFFS";
52const SUPERBLOCK_VERSION: u32 = 1;
53const INODE_SIZE: usize = 256;
54const INODES_PER_BLOCK: usize = BLOCK_SIZE / INODE_SIZE;
55const MAX_DIRECT_BLOCKS: usize = 12;
56const ROOT_INODE_ID: u64 = 0;
57const MAX_NAME_LEN: usize = 255;
58
59// Default striping parameters
60const DEFAULT_STRIPE_THRESHOLD: u64 = 1024 * 1024; // 1 MB
61const DEFAULT_STRIPE_SIZE: u32 = 256; // blocks per stripe unit (128 KB)
62
63/// File type constants.
64const FILE_TYPE_FILE: u8 = 1;
65const FILE_TYPE_DIR: u8 = 2;
66
67// --- On-block structures ---
68
69/// Superblock stored at block 0 of lease 0.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71struct Superblock {
72    magic: [u8; 4],
73    version: u32,
74    block_size: u32,
75    total_blocks: u64,
76    inode_table_start: u64,
77    data_start: u64,
78    free_bitmap_start: u64,
79    next_inode_id: u64,
80    inode_table_blocks: u64,
81}
82
83/// On-disk inode record.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85struct Inode {
86    inode_id: u64,
87    file_type: u8,
88    size: u64,
89    block_count: u32,
90    direct_blocks: [u64; MAX_DIRECT_BLOCKS],
91    indirect_block: u64,
92    created_at: u64,
93    modified_at: u64,
94    name_hash: u64,
95}
96
97impl Inode {
98    fn new(inode_id: u64, file_type: u8) -> Self {
99        Inode {
100            inode_id,
101            file_type,
102            size: 0,
103            block_count: 0,
104            direct_blocks: [0; MAX_DIRECT_BLOCKS],
105            indirect_block: 0,
106            created_at: 0,
107            modified_at: 0,
108            name_hash: 0,
109        }
110    }
111}
112
113/// Directory entry stored in directory data blocks.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct DirEntry {
116    /// Inode identifier for this directory entry.
117    pub inode_id: u64,
118    /// UTF-8 file or directory name.
119    pub name: String,
120    /// Type tag (`FILE_TYPE_FILE` or `FILE_TYPE_DIR`).
121    pub file_type: u8,
122}
123
124/// File metadata returned by stat().
125#[derive(Debug, Clone)]
126pub struct FileStat {
127    /// File size in bytes.
128    pub size: u64,
129    /// Type tag (`FILE_TYPE_FILE` or `FILE_TYPE_DIR`).
130    pub file_type: u8,
131    /// Number of allocated data blocks.
132    pub block_count: u32,
133    /// Creation timestamp (unix seconds in current implementation semantics).
134    pub created_at: u64,
135    /// Last modification timestamp (unix seconds in current implementation semantics).
136    pub modified_at: u64,
137}
138
139/// Flags for opening files.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum OpenFlags {
142    /// Open for reads only.
143    Read,
144    /// Open for writes only.
145    Write,
146    /// Open for both reads and writes.
147    ReadWrite,
148}
149
150/// Position for seek operations.
151#[derive(Debug, Clone, Copy)]
152pub enum SeekFrom {
153    /// Seek from start of file.
154    Start(u64),
155    /// Seek relative to current cursor position.
156    Current(i64),
157    /// Seek relative to end of file.
158    End(i64),
159}
160
161/// Handle to an open file.
162#[derive(Debug)]
163pub struct FileHandle {
164    inode_id: u64,
165    position: u64,
166    flags: OpenFlags,
167}
168
169/// Distributed filesystem backed by leased block storage.
170///
171/// Stores metadata (inodes, directory entries) and data on one or more
172/// [`BlockLease`]s. Files larger than `stripe_threshold` are striped
173/// round-robin across all leases.
174pub struct FabricFs {
175    leases: Vec<BlockLease>,
176    superblock: Superblock,
177    // In-memory caches
178    inode_cache: BTreeMap<u64, Inode>,
179    dir_cache: BTreeMap<u64, Vec<DirEntry>>,
180    dirty_inodes: BTreeMap<u64, bool>,
181    dirty_dirs: BTreeMap<u64, bool>,
182    // Striping config
183    stripe_threshold: u64,
184    stripe_size: u32,
185    // Bitmap cache for primary lease
186    bitmap_cache: Vec<u8>,
187    bitmap_dirty: bool,
188}
189
190impl FabricFs {
191    /// Format leases with a fresh filesystem and return a mounted instance.
192    ///
193    /// Writes a superblock to block 0, initializes the free bitmap and inode
194    /// table, and creates the root directory inode.
195    pub fn format(leases: Vec<BlockLease>) -> Result<Self> {
196        if leases.is_empty() {
197            return Err(FabricError::IoError(-1));
198        }
199
200        let total_blocks = leases[0].block().num_blocks();
201        if total_blocks < 16 {
202            return Err(FabricError::CapacityExceeded);
203        }
204
205        // Layout: block 0 = superblock, blocks 1..B = bitmap, B..I = inode table, I..N = data
206        let bitmap_blocks = total_blocks.div_ceil(BLOCK_SIZE as u64 * 8);
207        let free_bitmap_start = 1u64;
208        let inode_table_start = free_bitmap_start + bitmap_blocks;
209        let inode_table_blocks = cmp::max(4, total_blocks / 64);
210        let data_start = inode_table_start + inode_table_blocks;
211
212        if data_start >= total_blocks {
213            return Err(FabricError::CapacityExceeded);
214        }
215
216        let superblock = Superblock {
217            magic: SUPERBLOCK_MAGIC,
218            version: SUPERBLOCK_VERSION,
219            block_size: BLOCK_SIZE as u32,
220            total_blocks,
221            inode_table_start,
222            data_start,
223            free_bitmap_start,
224            next_inode_id: 1,
225            inode_table_blocks,
226        };
227
228        // Write superblock
229        let sb_bytes = Self::serialize_superblock(&superblock);
230        leases[0].block().write_block(0, &sb_bytes)?;
231
232        // Initialize bitmap — mark metadata blocks as used
233        let bitmap_size = (total_blocks as usize).div_ceil(8);
234        let mut bitmap = vec![0u8; bitmap_size];
235        for blk in 0..data_start as usize {
236            bitmap[blk / 8] |= 1 << (blk % 8);
237        }
238
239        // Write bitmap to disk
240        let bitmap_block_count = bitmap_blocks as usize;
241        for i in 0..bitmap_block_count {
242            let mut block = [0u8; BLOCK_SIZE];
243            let start = i * BLOCK_SIZE;
244            let end = cmp::min(start + BLOCK_SIZE, bitmap.len());
245            if start < end {
246                block[..end - start].copy_from_slice(&bitmap[start..end]);
247            }
248            leases[0]
249                .block()
250                .write_block(free_bitmap_start + i as u64, &block)?;
251        }
252
253        // Initialize inode table (zero)
254        let zero_block = [0u8; BLOCK_SIZE];
255        for i in 0..inode_table_blocks {
256            leases[0]
257                .block()
258                .write_block(inode_table_start + i, &zero_block)?;
259        }
260
261        // Create root directory inode (inode 0)
262        let root_inode = Inode::new(ROOT_INODE_ID, FILE_TYPE_DIR);
263
264        let mut fs = FabricFs {
265            leases,
266            superblock,
267            inode_cache: BTreeMap::new(),
268            dir_cache: BTreeMap::new(),
269            dirty_inodes: BTreeMap::new(),
270            dirty_dirs: BTreeMap::new(),
271            stripe_threshold: DEFAULT_STRIPE_THRESHOLD,
272            stripe_size: DEFAULT_STRIPE_SIZE,
273            bitmap_cache: bitmap,
274            bitmap_dirty: false,
275        };
276
277        fs.inode_cache.insert(ROOT_INODE_ID, root_inode);
278        fs.dirty_inodes.insert(ROOT_INODE_ID, true);
279        fs.dir_cache.insert(ROOT_INODE_ID, Vec::new());
280        fs.dirty_dirs.insert(ROOT_INODE_ID, true);
281        fs.sync()?;
282
283        Ok(fs)
284    }
285
286    /// Mount an existing filesystem from leases.
287    ///
288    /// Reads the superblock, validates the magic and version, and populates
289    /// the inode and directory caches for the root directory.
290    pub fn mount(leases: Vec<BlockLease>) -> Result<Self> {
291        if leases.is_empty() {
292            return Err(FabricError::IoError(-1));
293        }
294
295        let sb_block = leases[0].block().read_block(0)?;
296        let superblock = Self::deserialize_superblock(&sb_block)?;
297
298        if superblock.magic != SUPERBLOCK_MAGIC {
299            return Err(FabricError::IoError(-2));
300        }
301        if superblock.version != SUPERBLOCK_VERSION {
302            return Err(FabricError::IoError(-3));
303        }
304
305        // Read bitmap
306        let bitmap_blocks = superblock.total_blocks.div_ceil(BLOCK_SIZE as u64 * 8);
307        let bitmap_size = (superblock.total_blocks as usize).div_ceil(8);
308        let mut bitmap = vec![0u8; bitmap_size];
309        for i in 0..bitmap_blocks as usize {
310            let block = leases[0]
311                .block()
312                .read_block(superblock.free_bitmap_start + i as u64)?;
313            let start = i * BLOCK_SIZE;
314            let end = cmp::min(start + BLOCK_SIZE, bitmap_size);
315            if start < end {
316                bitmap[start..end].copy_from_slice(&block[..end - start]);
317            }
318        }
319
320        let mut fs = FabricFs {
321            leases,
322            superblock,
323            inode_cache: BTreeMap::new(),
324            dir_cache: BTreeMap::new(),
325            dirty_inodes: BTreeMap::new(),
326            dirty_dirs: BTreeMap::new(),
327            stripe_threshold: DEFAULT_STRIPE_THRESHOLD,
328            stripe_size: DEFAULT_STRIPE_SIZE,
329            bitmap_cache: bitmap,
330            bitmap_dirty: false,
331        };
332
333        // Read root inode
334        let root_inode = fs.read_inode_from_disk(ROOT_INODE_ID)?;
335        fs.inode_cache.insert(ROOT_INODE_ID, root_inode);
336
337        // Read root directory entries
338        let entries = fs.read_dir_entries(ROOT_INODE_ID)?;
339        // Pre-load inodes referenced by root directory entries
340        for entry in &entries {
341            if !fs.inode_cache.contains_key(&entry.inode_id) {
342                if let Ok(inode) = fs.read_inode_from_disk(entry.inode_id) {
343                    fs.inode_cache.insert(entry.inode_id, inode);
344                }
345            }
346        }
347        fs.dir_cache.insert(ROOT_INODE_ID, entries);
348
349        Ok(fs)
350    }
351
352    /// Create a file at the given path and return a writable handle.
353    pub fn create(&mut self, path: &str) -> Result<FileHandle> {
354        let (parent_inode_id, name) = self.resolve_parent(path)?;
355        if name.is_empty() || name.len() > MAX_NAME_LEN {
356            return Err(FabricError::IoError(-1));
357        }
358
359        // Check if file already exists
360        let existing_inode = self
361            .dir_cache
362            .get(&parent_inode_id)
363            .and_then(|entries| entries.iter().find(|e| e.name == name))
364            .map(|e| e.inode_id);
365
366        if let Some(inode_id) = existing_inode {
367            // Truncate existing file
368            self.truncate_inode(inode_id)?;
369            return Ok(FileHandle {
370                inode_id,
371                position: 0,
372                flags: OpenFlags::Write,
373            });
374        }
375
376        // Allocate new inode
377        let inode_id = self.superblock.next_inode_id;
378        self.superblock.next_inode_id += 1;
379
380        let mut inode = Inode::new(inode_id, FILE_TYPE_FILE);
381        inode.name_hash = fnv_hash(name.as_bytes());
382
383        self.inode_cache.insert(inode_id, inode);
384        self.dirty_inodes.insert(inode_id, true);
385
386        // Add dir entry
387        let dir_entry = DirEntry {
388            inode_id,
389            name,
390            file_type: FILE_TYPE_FILE,
391        };
392        let entries = self.dir_cache.entry(parent_inode_id).or_default();
393        entries.push(dir_entry);
394        self.dirty_dirs.insert(parent_inode_id, true);
395
396        self.write_superblock()?;
397
398        Ok(FileHandle {
399            inode_id,
400            position: 0,
401            flags: OpenFlags::Write,
402        })
403    }
404
405    /// Open a file at the given path with the specified flags.
406    pub fn open(&mut self, path: &str, flags: OpenFlags) -> Result<FileHandle> {
407        let inode_id = self.resolve_path(path)?;
408        let inode = self.get_inode(inode_id)?;
409        if inode.file_type != FILE_TYPE_FILE {
410            return Err(FabricError::IoError(-1));
411        }
412
413        if flags == OpenFlags::Write {
414            self.truncate_inode(inode_id)?;
415        }
416
417        Ok(FileHandle {
418            inode_id,
419            position: 0,
420            flags,
421        })
422    }
423
424    /// Read from an open file handle into `buf`.
425    ///
426    /// Returns the number of bytes read. Returns 0 at EOF.
427    pub fn read(&self, handle: &FileHandle, buf: &mut [u8]) -> Result<usize> {
428        let inode = self
429            .inode_cache
430            .get(&handle.inode_id)
431            .ok_or(FabricError::IoError(-1))?;
432
433        if handle.position >= inode.size {
434            return Ok(0);
435        }
436
437        let remaining = (inode.size - handle.position) as usize;
438        let to_read = cmp::min(buf.len(), remaining);
439        let mut bytes_read = 0;
440        let mut pos = handle.position;
441
442        while bytes_read < to_read {
443            let block_index = (pos / BLOCK_SIZE as u64) as usize;
444            let offset_in_block = (pos % BLOCK_SIZE as u64) as usize;
445            let bytes_in_block = cmp::min(BLOCK_SIZE - offset_in_block, to_read - bytes_read);
446
447            let block_addr = self.get_data_block_addr(inode, block_index)?;
448            if block_addr == 0 {
449                for b in &mut buf[bytes_read..bytes_read + bytes_in_block] {
450                    *b = 0;
451                }
452            } else {
453                let (lease_idx, lba) = decode_block_addr(block_addr);
454                let block_data = self.leases[lease_idx].block().read_block(lba)?;
455                buf[bytes_read..bytes_read + bytes_in_block].copy_from_slice(
456                    &block_data[offset_in_block..offset_in_block + bytes_in_block],
457                );
458            }
459
460            bytes_read += bytes_in_block;
461            pos += bytes_in_block as u64;
462        }
463
464        Ok(bytes_read)
465    }
466
467    /// Write data to an open file handle.
468    ///
469    /// Returns the number of bytes written.
470    pub fn write(&mut self, handle: &mut FileHandle, data: &[u8]) -> Result<usize> {
471        if handle.flags == OpenFlags::Read {
472            return Err(FabricError::IoError(-1));
473        }
474
475        let mut bytes_written = 0;
476        let mut pos = handle.position;
477
478        while bytes_written < data.len() {
479            let block_index = (pos / BLOCK_SIZE as u64) as usize;
480            let offset_in_block = (pos % BLOCK_SIZE as u64) as usize;
481            let bytes_in_block = cmp::min(BLOCK_SIZE - offset_in_block, data.len() - bytes_written);
482
483            let block_addr = self.ensure_data_block(handle.inode_id, block_index)?;
484            let (lease_idx, lba) = decode_block_addr(block_addr);
485
486            let mut block_data = if offset_in_block != 0 || bytes_in_block != BLOCK_SIZE {
487                self.leases[lease_idx].block().read_block(lba)?
488            } else {
489                [0u8; BLOCK_SIZE]
490            };
491
492            block_data[offset_in_block..offset_in_block + bytes_in_block]
493                .copy_from_slice(&data[bytes_written..bytes_written + bytes_in_block]);
494
495            self.leases[lease_idx]
496                .block()
497                .write_block(lba, &block_data)?;
498
499            bytes_written += bytes_in_block;
500            pos += bytes_in_block as u64;
501        }
502
503        handle.position = pos;
504
505        // Update inode size
506        let inode = self
507            .inode_cache
508            .get_mut(&handle.inode_id)
509            .ok_or(FabricError::IoError(-1))?;
510        if pos > inode.size {
511            inode.size = pos;
512        }
513        self.dirty_inodes.insert(handle.inode_id, true);
514
515        Ok(bytes_written)
516    }
517
518    /// Seek to a position in an open file.
519    pub fn seek(&mut self, handle: &mut FileHandle, pos: SeekFrom) -> Result<u64> {
520        let inode = self
521            .inode_cache
522            .get(&handle.inode_id)
523            .ok_or(FabricError::IoError(-1))?;
524
525        let new_pos = match pos {
526            SeekFrom::Start(offset) => offset,
527            SeekFrom::Current(offset) => {
528                if offset < 0 {
529                    handle
530                        .position
531                        .checked_sub((-offset) as u64)
532                        .ok_or(FabricError::IoError(-1))?
533                } else {
534                    handle.position + offset as u64
535                }
536            }
537            SeekFrom::End(offset) => {
538                if offset < 0 {
539                    inode
540                        .size
541                        .checked_sub((-offset) as u64)
542                        .ok_or(FabricError::IoError(-1))?
543                } else {
544                    inode.size + offset as u64
545                }
546            }
547        };
548
549        handle.position = new_pos;
550        Ok(new_pos)
551    }
552
553    /// Close an open file handle.
554    pub fn close(&mut self, _handle: FileHandle) -> Result<()> {
555        self.sync()
556    }
557
558    /// Remove a file at the given path.
559    pub fn remove(&mut self, path: &str) -> Result<()> {
560        let (parent_inode_id, name) = self.resolve_parent(path)?;
561
562        let entries = self
563            .dir_cache
564            .get(&parent_inode_id)
565            .cloned()
566            .unwrap_or_default();
567
568        let mut found_inode_id = None;
569        let mut found_idx = None;
570        for (i, entry) in entries.iter().enumerate() {
571            if entry.name == name {
572                if entry.file_type == FILE_TYPE_DIR {
573                    let dir_entries = self.dir_cache.get(&entry.inode_id);
574                    if let Some(de) = dir_entries {
575                        if !de.is_empty() {
576                            return Err(FabricError::IoError(-1));
577                        }
578                    }
579                }
580                found_inode_id = Some(entry.inode_id);
581                found_idx = Some(i);
582                break;
583            }
584        }
585
586        let inode_id = found_inode_id.ok_or(FabricError::IoError(-1))?;
587        let idx = found_idx.unwrap();
588
589        // Collect blocks to free
590        let blocks_to_free: Vec<u64> = if let Some(inode) = self.inode_cache.get(&inode_id) {
591            let mut blocks = Vec::new();
592            for i in 0..inode.block_count as usize {
593                if i < MAX_DIRECT_BLOCKS && inode.direct_blocks[i] != 0 {
594                    blocks.push(inode.direct_blocks[i]);
595                }
596            }
597            if inode.indirect_block != 0 {
598                blocks.push(inode.indirect_block);
599            }
600            blocks
601        } else {
602            Vec::new()
603        };
604
605        // Free the blocks
606        for block_addr in blocks_to_free {
607            self.free_block(block_addr);
608        }
609
610        // Remove from parent directory
611        let entries = self.dir_cache.get_mut(&parent_inode_id).unwrap();
612        entries.remove(idx);
613        self.dirty_dirs.insert(parent_inode_id, true);
614
615        // Remove inode from cache
616        self.inode_cache.remove(&inode_id);
617        self.dir_cache.remove(&inode_id);
618
619        self.zero_inode_on_disk(inode_id)?;
620        self.sync()?;
621        Ok(())
622    }
623
624    /// List entries in the directory at the given path.
625    pub fn readdir(&mut self, path: &str) -> Result<Vec<DirEntry>> {
626        let inode_id = self.resolve_path(path)?;
627        let inode = self.get_inode(inode_id)?;
628        if inode.file_type != FILE_TYPE_DIR {
629            return Err(FabricError::IoError(-1));
630        }
631
632        if !self.dir_cache.contains_key(&inode_id) {
633            let entries = self.read_dir_entries(inode_id)?;
634            self.dir_cache.insert(inode_id, entries);
635        }
636
637        Ok(self.dir_cache.get(&inode_id).cloned().unwrap_or_default())
638    }
639
640    /// Get file metadata for the given path.
641    pub fn stat(&mut self, path: &str) -> Result<FileStat> {
642        let inode_id = self.resolve_path(path)?;
643        let inode = self.get_inode(inode_id)?;
644        Ok(FileStat {
645            size: inode.size,
646            file_type: inode.file_type,
647            block_count: inode.block_count,
648            created_at: inode.created_at,
649            modified_at: inode.modified_at,
650        })
651    }
652
653    /// Create a directory at the given path.
654    pub fn mkdir(&mut self, path: &str) -> Result<()> {
655        let (parent_inode_id, name) = self.resolve_parent(path)?;
656        if name.is_empty() || name.len() > MAX_NAME_LEN {
657            return Err(FabricError::IoError(-1));
658        }
659
660        // Check for duplicates
661        let exists = self
662            .dir_cache
663            .get(&parent_inode_id)
664            .map(|entries| entries.iter().any(|e| e.name == name))
665            .unwrap_or(false);
666        if exists {
667            return Err(FabricError::IoError(-1));
668        }
669
670        let inode_id = self.superblock.next_inode_id;
671        self.superblock.next_inode_id += 1;
672
673        let mut inode = Inode::new(inode_id, FILE_TYPE_DIR);
674        inode.name_hash = fnv_hash(name.as_bytes());
675
676        self.inode_cache.insert(inode_id, inode);
677        self.dirty_inodes.insert(inode_id, true);
678        self.dir_cache.insert(inode_id, Vec::new());
679        self.dirty_dirs.insert(inode_id, true);
680
681        let dir_entry = DirEntry {
682            inode_id,
683            name,
684            file_type: FILE_TYPE_DIR,
685        };
686        let entries = self.dir_cache.entry(parent_inode_id).or_default();
687        entries.push(dir_entry);
688        self.dirty_dirs.insert(parent_inode_id, true);
689
690        self.write_superblock()?;
691        self.sync()?;
692        Ok(())
693    }
694
695    /// Flush all dirty metadata and data to block storage.
696    pub fn sync(&mut self) -> Result<()> {
697        // Flush dirty directory entries first — this updates inode sizes
698        let dirty_dirs: Vec<u64> = self.dirty_dirs.keys().cloned().collect();
699        for inode_id in dirty_dirs {
700            if let Some(entries) = self.dir_cache.get(&inode_id).cloned() {
701                self.write_dir_entries(inode_id, &entries)?;
702            }
703            self.dirty_dirs.remove(&inode_id);
704        }
705
706        // Flush dirty inodes (now with up-to-date sizes from dir writes)
707        let dirty_inodes: Vec<u64> = self.dirty_inodes.keys().cloned().collect();
708        for inode_id in dirty_inodes {
709            if let Some(inode) = self.inode_cache.get(&inode_id).cloned() {
710                self.write_inode(&inode)?;
711            }
712            self.dirty_inodes.remove(&inode_id);
713        }
714
715        // Flush bitmap
716        if self.bitmap_dirty {
717            self.flush_bitmap()?;
718            self.bitmap_dirty = false;
719        }
720
721        Ok(())
722    }
723
724    /// Sync and release all leases.
725    pub fn unmount(mut self) -> Result<Vec<BlockLease>> {
726        self.sync()?;
727        Ok(self.leases)
728    }
729
730    /// Set the stripe threshold in bytes.
731    pub fn set_stripe_threshold(&mut self, threshold: u64) {
732        self.stripe_threshold = threshold;
733    }
734
735    /// Set the stripe size in blocks.
736    pub fn set_stripe_size(&mut self, size: u32) {
737        self.stripe_size = size;
738    }
739
740    // --- Internal helpers ---
741
742    fn truncate_inode(&mut self, inode_id: u64) -> Result<()> {
743        // Collect blocks to free
744        let blocks_to_free: Vec<u64> = {
745            let inode = self
746                .inode_cache
747                .get(&inode_id)
748                .ok_or(FabricError::IoError(-1))?;
749            let mut blocks = Vec::new();
750            for i in 0..inode.block_count as usize {
751                if i < MAX_DIRECT_BLOCKS && inode.direct_blocks[i] != 0 {
752                    blocks.push(inode.direct_blocks[i]);
753                }
754            }
755            blocks
756        };
757
758        for block_addr in blocks_to_free {
759            self.free_block(block_addr);
760        }
761
762        let inode = self
763            .inode_cache
764            .get_mut(&inode_id)
765            .ok_or(FabricError::IoError(-1))?;
766        for i in 0..MAX_DIRECT_BLOCKS {
767            inode.direct_blocks[i] = 0;
768        }
769        inode.size = 0;
770        inode.block_count = 0;
771        self.dirty_inodes.insert(inode_id, true);
772        Ok(())
773    }
774
775    fn serialize_superblock(sb: &Superblock) -> [u8; BLOCK_SIZE] {
776        let mut block = [0u8; BLOCK_SIZE];
777        block[0..4].copy_from_slice(&sb.magic);
778        block[4..8].copy_from_slice(&sb.version.to_le_bytes());
779        block[8..12].copy_from_slice(&sb.block_size.to_le_bytes());
780        block[12..20].copy_from_slice(&sb.total_blocks.to_le_bytes());
781        block[20..28].copy_from_slice(&sb.inode_table_start.to_le_bytes());
782        block[28..36].copy_from_slice(&sb.data_start.to_le_bytes());
783        block[36..44].copy_from_slice(&sb.free_bitmap_start.to_le_bytes());
784        block[44..52].copy_from_slice(&sb.next_inode_id.to_le_bytes());
785        block[52..60].copy_from_slice(&sb.inode_table_blocks.to_le_bytes());
786        block
787    }
788
789    fn deserialize_superblock(block: &[u8; BLOCK_SIZE]) -> Result<Superblock> {
790        let mut magic = [0u8; 4];
791        magic.copy_from_slice(&block[0..4]);
792        Ok(Superblock {
793            magic,
794            version: u32::from_le_bytes([block[4], block[5], block[6], block[7]]),
795            block_size: u32::from_le_bytes([block[8], block[9], block[10], block[11]]),
796            total_blocks: u64::from_le_bytes(block[12..20].try_into().unwrap()),
797            inode_table_start: u64::from_le_bytes(block[20..28].try_into().unwrap()),
798            data_start: u64::from_le_bytes(block[28..36].try_into().unwrap()),
799            free_bitmap_start: u64::from_le_bytes(block[36..44].try_into().unwrap()),
800            next_inode_id: u64::from_le_bytes(block[44..52].try_into().unwrap()),
801            inode_table_blocks: u64::from_le_bytes(block[52..60].try_into().unwrap()),
802        })
803    }
804
805    fn write_superblock(&self) -> Result<()> {
806        let sb_bytes = Self::serialize_superblock(&self.superblock);
807        self.leases[0].block().write_block(0, &sb_bytes)
808    }
809
810    fn read_inode_from_disk(&self, inode_id: u64) -> Result<Inode> {
811        let max_inodes = self.superblock.inode_table_blocks * INODES_PER_BLOCK as u64;
812        if inode_id >= max_inodes {
813            return Err(FabricError::IoError(-1));
814        }
815
816        let block_offset = inode_id as usize / INODES_PER_BLOCK;
817        let slot_in_block = inode_id as usize % INODES_PER_BLOCK;
818        let block_lba = self.superblock.inode_table_start + block_offset as u64;
819
820        let block_data = self.leases[0].block().read_block(block_lba)?;
821        let start = slot_in_block * INODE_SIZE;
822        let inode_bytes = &block_data[start..start + INODE_SIZE];
823
824        Self::deserialize_inode(inode_bytes)
825    }
826
827    fn write_inode(&self, inode: &Inode) -> Result<()> {
828        let block_offset = inode.inode_id as usize / INODES_PER_BLOCK;
829        let slot_in_block = inode.inode_id as usize % INODES_PER_BLOCK;
830        let block_lba = self.superblock.inode_table_start + block_offset as u64;
831
832        let mut block_data = self.leases[0].block().read_block(block_lba)?;
833        let start = slot_in_block * INODE_SIZE;
834        let inode_bytes = Self::serialize_inode(inode);
835        block_data[start..start + INODE_SIZE].copy_from_slice(&inode_bytes);
836
837        self.leases[0].block().write_block(block_lba, &block_data)
838    }
839
840    fn zero_inode_on_disk(&self, inode_id: u64) -> Result<()> {
841        let block_offset = inode_id as usize / INODES_PER_BLOCK;
842        let slot_in_block = inode_id as usize % INODES_PER_BLOCK;
843        let block_lba = self.superblock.inode_table_start + block_offset as u64;
844
845        let mut block_data = self.leases[0].block().read_block(block_lba)?;
846        let start = slot_in_block * INODE_SIZE;
847        for b in &mut block_data[start..start + INODE_SIZE] {
848            *b = 0;
849        }
850        self.leases[0].block().write_block(block_lba, &block_data)
851    }
852
853    fn serialize_inode(inode: &Inode) -> [u8; INODE_SIZE] {
854        let mut buf = [0u8; INODE_SIZE];
855        buf[0..8].copy_from_slice(&inode.inode_id.to_le_bytes());
856        buf[8] = inode.file_type;
857        buf[9..17].copy_from_slice(&inode.size.to_le_bytes());
858        buf[17..21].copy_from_slice(&inode.block_count.to_le_bytes());
859        for (i, &blk) in inode.direct_blocks.iter().enumerate() {
860            let off = 21 + i * 8;
861            buf[off..off + 8].copy_from_slice(&blk.to_le_bytes());
862        }
863        let off = 21 + MAX_DIRECT_BLOCKS * 8; // 21 + 96 = 117
864        buf[off..off + 8].copy_from_slice(&inode.indirect_block.to_le_bytes());
865        buf[off + 8..off + 16].copy_from_slice(&inode.created_at.to_le_bytes());
866        buf[off + 16..off + 24].copy_from_slice(&inode.modified_at.to_le_bytes());
867        buf[off + 24..off + 32].copy_from_slice(&inode.name_hash.to_le_bytes());
868        buf
869    }
870
871    fn deserialize_inode(buf: &[u8]) -> Result<Inode> {
872        if buf.len() < INODE_SIZE {
873            return Err(FabricError::IoError(-1));
874        }
875        let inode_id = u64::from_le_bytes(buf[0..8].try_into().unwrap());
876        let file_type = buf[8];
877        let size = u64::from_le_bytes(buf[9..17].try_into().unwrap());
878        let block_count = u32::from_le_bytes(buf[17..21].try_into().unwrap());
879        let mut direct_blocks = [0u64; MAX_DIRECT_BLOCKS];
880        for (i, block) in direct_blocks.iter_mut().enumerate() {
881            let off = 21 + i * 8;
882            *block = u64::from_le_bytes(buf[off..off + 8].try_into().unwrap());
883        }
884        let off = 21 + MAX_DIRECT_BLOCKS * 8;
885        let indirect_block = u64::from_le_bytes(buf[off..off + 8].try_into().unwrap());
886        let created_at = u64::from_le_bytes(buf[off + 8..off + 16].try_into().unwrap());
887        let modified_at = u64::from_le_bytes(buf[off + 16..off + 24].try_into().unwrap());
888        let name_hash = u64::from_le_bytes(buf[off + 24..off + 32].try_into().unwrap());
889
890        Ok(Inode {
891            inode_id,
892            file_type,
893            size,
894            block_count,
895            direct_blocks,
896            indirect_block,
897            created_at,
898            modified_at,
899            name_hash,
900        })
901    }
902
903    fn get_inode(&mut self, inode_id: u64) -> Result<Inode> {
904        if let Some(inode) = self.inode_cache.get(&inode_id) {
905            return Ok(inode.clone());
906        }
907        let inode = self.read_inode_from_disk(inode_id)?;
908        self.inode_cache.insert(inode_id, inode.clone());
909        Ok(inode)
910    }
911
912    fn read_dir_entries(&self, inode_id: u64) -> Result<Vec<DirEntry>> {
913        let inode = self
914            .inode_cache
915            .get(&inode_id)
916            .cloned()
917            .ok_or(FabricError::IoError(-1))?;
918
919        if inode.size == 0 {
920            return Ok(Vec::new());
921        }
922
923        let num_blocks = (inode.size as usize).div_ceil(BLOCK_SIZE);
924        let mut data = Vec::with_capacity(inode.size as usize);
925        for i in 0..num_blocks {
926            let block_addr = self.get_data_block_addr(&inode, i)?;
927            if block_addr == 0 {
928                data.extend_from_slice(&[0u8; BLOCK_SIZE]);
929            } else {
930                let (lease_idx, lba) = decode_block_addr(block_addr);
931                let block = self.leases[lease_idx].block().read_block(lba)?;
932                data.extend_from_slice(&block);
933            }
934        }
935        data.truncate(inode.size as usize);
936
937        postcard::from_bytes(&data).map_err(|_| FabricError::IoError(-1))
938    }
939
940    fn write_dir_entries(&mut self, inode_id: u64, entries: &[DirEntry]) -> Result<()> {
941        let data = postcard::to_allocvec(entries).map_err(|_| FabricError::IoError(-1))?;
942        let num_blocks = data.len().div_ceil(BLOCK_SIZE);
943
944        // Ensure enough blocks allocated
945        for i in 0..num_blocks {
946            self.ensure_data_block(inode_id, i)?;
947        }
948
949        // Read the inode to get block addresses
950        let inode = self.inode_cache.get(&inode_id).unwrap().clone();
951
952        // Write data to blocks
953        for i in 0..num_blocks {
954            let start = i * BLOCK_SIZE;
955            let end = cmp::min(start + BLOCK_SIZE, data.len());
956            let mut block = [0u8; BLOCK_SIZE];
957            block[..end - start].copy_from_slice(&data[start..end]);
958
959            let block_addr = self.get_data_block_addr(&inode, i)?;
960            let (lease_idx, lba) = decode_block_addr(block_addr);
961            self.leases[lease_idx].block().write_block(lba, &block)?;
962        }
963
964        // Update inode size and mark dirty so sync writes it to disk
965        let inode = self.inode_cache.get_mut(&inode_id).unwrap();
966        inode.size = data.len() as u64;
967        self.dirty_inodes.insert(inode_id, true);
968
969        Ok(())
970    }
971
972    fn get_data_block_addr(&self, inode: &Inode, block_index: usize) -> Result<u64> {
973        if block_index < MAX_DIRECT_BLOCKS {
974            Ok(inode.direct_blocks[block_index])
975        } else {
976            if inode.indirect_block == 0 {
977                return Ok(0);
978            }
979            let indirect_index = block_index - MAX_DIRECT_BLOCKS;
980            let entries_per_block = BLOCK_SIZE / 8;
981            if indirect_index >= entries_per_block {
982                return Err(FabricError::CapacityExceeded);
983            }
984            let (lease_idx, lba) = decode_block_addr(inode.indirect_block);
985            let block = self.leases[lease_idx].block().read_block(lba)?;
986            let off = indirect_index * 8;
987            Ok(u64::from_le_bytes(block[off..off + 8].try_into().unwrap()))
988        }
989    }
990
991    fn ensure_data_block(&mut self, inode_id: u64, block_index: usize) -> Result<u64> {
992        // Check if already allocated
993        let existing = {
994            let inode = self.inode_cache.get(&inode_id).unwrap();
995            self.get_data_block_addr(inode, block_index)?
996        };
997        if existing != 0 {
998            return Ok(existing);
999        }
1000
1001        // Allocate a new block
1002        let new_block = self.alloc_block_for_file(block_index)?;
1003
1004        if block_index < MAX_DIRECT_BLOCKS {
1005            let inode = self.inode_cache.get_mut(&inode_id).unwrap();
1006            inode.direct_blocks[block_index] = new_block;
1007            inode.block_count += 1;
1008        } else {
1009            // Handle indirect block
1010            let indirect_index = block_index - MAX_DIRECT_BLOCKS;
1011            let entries_per_block = BLOCK_SIZE / 8;
1012            if indirect_index >= entries_per_block {
1013                return Err(FabricError::CapacityExceeded);
1014            }
1015
1016            // Ensure indirect block exists
1017            let indirect_addr = {
1018                let inode = self.inode_cache.get(&inode_id).unwrap();
1019                inode.indirect_block
1020            };
1021            let indirect_addr = if indirect_addr == 0 {
1022                let new_indirect = self.alloc_block_primary()?;
1023                let (li, lba) = decode_block_addr(new_indirect);
1024                let zero = [0u8; BLOCK_SIZE];
1025                self.leases[li].block().write_block(lba, &zero)?;
1026                let inode = self.inode_cache.get_mut(&inode_id).unwrap();
1027                inode.indirect_block = new_indirect;
1028                new_indirect
1029            } else {
1030                indirect_addr
1031            };
1032
1033            // Write entry in indirect block
1034            let (li, lba) = decode_block_addr(indirect_addr);
1035            let mut block = self.leases[li].block().read_block(lba)?;
1036            let off = indirect_index * 8;
1037            block[off..off + 8].copy_from_slice(&new_block.to_le_bytes());
1038            self.leases[li].block().write_block(lba, &block)?;
1039
1040            let inode = self.inode_cache.get_mut(&inode_id).unwrap();
1041            inode.block_count += 1;
1042        }
1043
1044        self.dirty_inodes.insert(inode_id, true);
1045        Ok(new_block)
1046    }
1047
1048    fn alloc_block_for_file(&mut self, block_index: usize) -> Result<u64> {
1049        if self.leases.len() <= 1 {
1050            return self.alloc_block_primary();
1051        }
1052
1053        let stripe_unit = self.stripe_size as usize;
1054        let stripe_num = block_index / stripe_unit;
1055        let lease_idx = stripe_num % self.leases.len();
1056
1057        if lease_idx == 0 {
1058            self.alloc_block_primary()
1059        } else {
1060            let lba = self.find_free_secondary_block(lease_idx)?;
1061            let num_blocks = self.leases[lease_idx].block().num_blocks();
1062            if lba >= num_blocks {
1063                return Err(FabricError::CapacityExceeded);
1064            }
1065            Ok(encode_block_addr(lease_idx, lba))
1066        }
1067    }
1068
1069    fn find_free_secondary_block(&self, lease_idx: usize) -> Result<u64> {
1070        let mut used_lbas: Vec<u64> = Vec::new();
1071        for inode in self.inode_cache.values() {
1072            for i in 0..inode.block_count as usize {
1073                if i < MAX_DIRECT_BLOCKS {
1074                    let addr = inode.direct_blocks[i];
1075                    if addr != 0 {
1076                        let (li, lba) = decode_block_addr(addr);
1077                        if li == lease_idx {
1078                            used_lbas.push(lba);
1079                        }
1080                    }
1081                }
1082            }
1083        }
1084        used_lbas.sort_unstable();
1085
1086        let num_blocks = self.leases[lease_idx].block().num_blocks();
1087        let mut candidate = 0u64;
1088        for &used in &used_lbas {
1089            if candidate == used {
1090                candidate += 1;
1091            }
1092        }
1093        if candidate >= num_blocks {
1094            return Err(FabricError::CapacityExceeded);
1095        }
1096        Ok(candidate)
1097    }
1098
1099    fn alloc_block_primary(&mut self) -> Result<u64> {
1100        let start = self.superblock.data_start as usize;
1101        let total = self.superblock.total_blocks as usize;
1102
1103        for blk in start..total {
1104            let byte_idx = blk / 8;
1105            let bit_idx = blk % 8;
1106            if byte_idx < self.bitmap_cache.len()
1107                && (self.bitmap_cache[byte_idx] & (1 << bit_idx)) == 0
1108            {
1109                self.bitmap_cache[byte_idx] |= 1 << bit_idx;
1110                self.bitmap_dirty = true;
1111                return Ok(encode_block_addr(0, blk as u64));
1112            }
1113        }
1114
1115        Err(FabricError::CapacityExceeded)
1116    }
1117
1118    fn free_block(&mut self, block_addr: u64) {
1119        let (lease_idx, lba) = decode_block_addr(block_addr);
1120        if lease_idx == 0 {
1121            let blk = lba as usize;
1122            let byte_idx = blk / 8;
1123            let bit_idx = blk % 8;
1124            if byte_idx < self.bitmap_cache.len() {
1125                self.bitmap_cache[byte_idx] &= !(1 << bit_idx);
1126                self.bitmap_dirty = true;
1127            }
1128        }
1129    }
1130
1131    fn flush_bitmap(&self) -> Result<()> {
1132        let bitmap_blocks = self.superblock.total_blocks.div_ceil(BLOCK_SIZE as u64 * 8);
1133        for i in 0..bitmap_blocks as usize {
1134            let mut block = [0u8; BLOCK_SIZE];
1135            let start = i * BLOCK_SIZE;
1136            let end = cmp::min(start + BLOCK_SIZE, self.bitmap_cache.len());
1137            if start < end {
1138                block[..end - start].copy_from_slice(&self.bitmap_cache[start..end]);
1139            }
1140            self.leases[0]
1141                .block()
1142                .write_block(self.superblock.free_bitmap_start + i as u64, &block)?;
1143        }
1144        Ok(())
1145    }
1146
1147    fn resolve_path(&mut self, path: &str) -> Result<u64> {
1148        if path == "/" {
1149            return Ok(ROOT_INODE_ID);
1150        }
1151
1152        let path = path.trim_start_matches('/');
1153        let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
1154
1155        let mut current_inode_id = ROOT_INODE_ID;
1156        for component in &components {
1157            if !self.dir_cache.contains_key(&current_inode_id) {
1158                let entries = self.read_dir_entries(current_inode_id)?;
1159                self.dir_cache.insert(current_inode_id, entries);
1160            }
1161
1162            let entries = self.dir_cache.get(&current_inode_id).unwrap();
1163            let mut found = false;
1164            for entry in entries {
1165                if entry.name == *component {
1166                    current_inode_id = entry.inode_id;
1167                    if !self.inode_cache.contains_key(&current_inode_id) {
1168                        let inode = self.read_inode_from_disk(current_inode_id)?;
1169                        self.inode_cache.insert(current_inode_id, inode);
1170                    }
1171                    found = true;
1172                    break;
1173                }
1174            }
1175            if !found {
1176                return Err(FabricError::IoError(-1));
1177            }
1178        }
1179
1180        Ok(current_inode_id)
1181    }
1182
1183    /// Resolve a path to its parent inode ID and the final component name.
1184    fn resolve_parent(&mut self, path: &str) -> Result<(u64, String)> {
1185        let path = path.trim_start_matches('/');
1186        if path.is_empty() {
1187            return Err(FabricError::IoError(-1));
1188        }
1189
1190        let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
1191        if components.is_empty() {
1192            return Err(FabricError::IoError(-1));
1193        }
1194
1195        let name = String::from(*components.last().unwrap());
1196
1197        if components.len() == 1 {
1198            // File/dir in root
1199            return Ok((ROOT_INODE_ID, name));
1200        }
1201
1202        // Resolve all components except the last to find the parent
1203        let parent_components = &components[..components.len() - 1];
1204        let mut current_inode_id = ROOT_INODE_ID;
1205        for component in parent_components {
1206            if !self.dir_cache.contains_key(&current_inode_id) {
1207                let entries = self.read_dir_entries(current_inode_id)?;
1208                self.dir_cache.insert(current_inode_id, entries);
1209            }
1210
1211            let entries = self.dir_cache.get(&current_inode_id).unwrap();
1212            let mut found = false;
1213            for entry in entries {
1214                if entry.name == *component {
1215                    current_inode_id = entry.inode_id;
1216                    if !self.inode_cache.contains_key(&current_inode_id) {
1217                        let inode = self.read_inode_from_disk(current_inode_id)?;
1218                        self.inode_cache.insert(current_inode_id, inode);
1219                    }
1220                    // Load the directory entries for this directory
1221                    if !self.dir_cache.contains_key(&current_inode_id) {
1222                        let dir_entries = self.read_dir_entries(current_inode_id)?;
1223                        self.dir_cache.insert(current_inode_id, dir_entries);
1224                    }
1225                    found = true;
1226                    break;
1227                }
1228            }
1229            if !found {
1230                return Err(FabricError::IoError(-1));
1231            }
1232        }
1233
1234        Ok((current_inode_id, name))
1235    }
1236}
1237
1238/// Encode a lease index and LBA into a block address.
1239/// Format: bits [63:48] = lease_idx, bits [47:0] = lba
1240fn encode_block_addr(lease_idx: usize, lba: u64) -> u64 {
1241    ((lease_idx as u64) << 48) | (lba & 0x0000_FFFF_FFFF_FFFF)
1242}
1243
1244/// Decode a block address into (lease_idx, lba).
1245fn decode_block_addr(addr: u64) -> (usize, u64) {
1246    let lease_idx = (addr >> 48) as usize;
1247    let lba = addr & 0x0000_FFFF_FFFF_FFFF;
1248    (lease_idx, lba)
1249}
1250
1251/// FNV-1a hash.
1252fn fnv_hash(data: &[u8]) -> u64 {
1253    let mut hash: u64 = 0xcbf29ce484222325;
1254    for &b in data {
1255        hash ^= b as u64;
1256        hash = hash.wrapping_mul(0x100000001b3);
1257    }
1258    hash
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263    use super::*;
1264    use grafos_std::block::BlockBuilder;
1265    use grafos_std::host;
1266
1267    fn setup_lease(num_blocks: u64) -> BlockLease {
1268        host::reset_mock();
1269        host::mock_set_fbbu_num_blocks(num_blocks);
1270        BlockBuilder::new()
1271            .min_blocks(num_blocks)
1272            .acquire()
1273            .expect("acquire")
1274    }
1275
1276    fn setup_leases(count: usize, num_blocks: u64) -> Vec<BlockLease> {
1277        host::reset_mock();
1278        host::mock_set_fbbu_num_blocks(num_blocks);
1279        let mut leases = Vec::new();
1280        for _ in 0..count {
1281            leases.push(BlockBuilder::new().acquire().expect("acquire"));
1282        }
1283        leases
1284    }
1285
1286    #[test]
1287    fn format_mount_roundtrip() {
1288        let lease = setup_lease(2048);
1289        let fs = FabricFs::format(vec![lease]).expect("format");
1290        let leases = fs.unmount().expect("unmount");
1291
1292        let fs = FabricFs::mount(leases).expect("mount");
1293        assert_eq!(fs.superblock.magic, *b"GFFS");
1294        assert_eq!(fs.superblock.version, 1);
1295        assert_eq!(fs.superblock.block_size, 512);
1296        assert_eq!(fs.superblock.total_blocks, 2048);
1297    }
1298
1299    #[test]
1300    fn create_write_read() {
1301        let lease = setup_lease(2048);
1302        let mut fs = FabricFs::format(vec![lease]).expect("format");
1303
1304        let mut fh = fs.create("/test.txt").expect("create");
1305        let data = b"hello fabricBIOS filesystem";
1306        fs.write(&mut fh, data).expect("write");
1307        fs.close(fh).expect("close");
1308
1309        let fh = fs.open("/test.txt", OpenFlags::Read).expect("open");
1310        let mut buf = [0u8; 64];
1311        let n = fs.read(&fh, &mut buf).expect("read");
1312        assert_eq!(n, data.len());
1313        assert_eq!(&buf[..n], data);
1314    }
1315
1316    #[test]
1317    fn nested_directory_and_file() {
1318        let lease = setup_lease(4096);
1319        let mut fs = FabricFs::format(vec![lease]).expect("format");
1320
1321        fs.mkdir("/a").expect("mkdir a");
1322        fs.mkdir("/a/b").expect("mkdir a/b");
1323        fs.mkdir("/a/b/c").expect("mkdir a/b/c");
1324
1325        let mut fh = fs.create("/a/b/c/file.txt").expect("create");
1326        fs.write(&mut fh, b"nested file").expect("write");
1327        fs.close(fh).expect("close");
1328
1329        let fh = fs.open("/a/b/c/file.txt", OpenFlags::Read).expect("open");
1330        let mut buf = [0u8; 64];
1331        let n = fs.read(&fh, &mut buf).expect("read");
1332        assert_eq!(&buf[..n], b"nested file");
1333    }
1334
1335    #[test]
1336    fn readdir_lists_files() {
1337        let lease = setup_lease(2048);
1338        let mut fs = FabricFs::format(vec![lease]).expect("format");
1339
1340        let mut fh = fs.create("/alpha.txt").expect("create");
1341        fs.write(&mut fh, b"a").expect("write");
1342        fs.close(fh).expect("close");
1343
1344        let mut fh = fs.create("/beta.txt").expect("create");
1345        fs.write(&mut fh, b"b").expect("write");
1346        fs.close(fh).expect("close");
1347
1348        fs.mkdir("/subdir").expect("mkdir");
1349
1350        let entries = fs.readdir("/").expect("readdir");
1351        assert_eq!(entries.len(), 3);
1352        let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
1353        assert!(names.contains(&"alpha.txt"));
1354        assert!(names.contains(&"beta.txt"));
1355        assert!(names.contains(&"subdir"));
1356    }
1357
1358    #[test]
1359    fn remove_file_frees_blocks() {
1360        let lease = setup_lease(2048);
1361        let mut fs = FabricFs::format(vec![lease]).expect("format");
1362
1363        let mut fh = fs.create("/delete_me.txt").expect("create");
1364        let data = vec![0xABu8; 1024];
1365        fs.write(&mut fh, &data).expect("write");
1366        fs.close(fh).expect("close");
1367
1368        let stat = fs.stat("/delete_me.txt").expect("stat");
1369        assert_eq!(stat.size, 1024);
1370
1371        fs.remove("/delete_me.txt").expect("remove");
1372        assert!(fs.open("/delete_me.txt", OpenFlags::Read).is_err());
1373    }
1374
1375    #[test]
1376    fn stat_returns_correct_size() {
1377        let lease = setup_lease(2048);
1378        let mut fs = FabricFs::format(vec![lease]).expect("format");
1379
1380        let mut fh = fs.create("/sized.txt").expect("create");
1381        fs.write(&mut fh, b"twelve bytes").expect("write");
1382        fs.close(fh).expect("close");
1383
1384        let stat = fs.stat("/sized.txt").expect("stat");
1385        assert_eq!(stat.size, 12);
1386        assert_eq!(stat.file_type, FILE_TYPE_FILE);
1387    }
1388
1389    #[test]
1390    fn seek_and_read() {
1391        let lease = setup_lease(2048);
1392        let mut fs = FabricFs::format(vec![lease]).expect("format");
1393
1394        let mut fh = fs.create("/seekable.txt").expect("create");
1395        fs.write(&mut fh, b"ABCDEFGHIJ").expect("write");
1396        fs.close(fh).expect("close");
1397
1398        let mut fh = fs.open("/seekable.txt", OpenFlags::Read).expect("open");
1399        fs.seek(&mut fh, SeekFrom::Start(5)).expect("seek");
1400        let mut buf = [0u8; 5];
1401        let n = fs.read(&fh, &mut buf).expect("read");
1402        assert_eq!(n, 5);
1403        assert_eq!(&buf, b"FGHIJ");
1404
1405        fs.seek(&mut fh, SeekFrom::Start(0)).expect("seek start");
1406        let mut buf2 = [0u8; 3];
1407        let n = fs.read(&fh, &mut buf2).expect("read");
1408        assert_eq!(n, 3);
1409        assert_eq!(&buf2, b"ABC");
1410    }
1411
1412    #[test]
1413    fn large_file_striped_across_leases() {
1414        let leases = setup_leases(3, 8192);
1415        let mut fs = FabricFs::format(leases).expect("format");
1416        fs.set_stripe_threshold(0);
1417
1418        let mut fh = fs.create("/striped.dat").expect("create");
1419        let data: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
1420        fs.write(&mut fh, &data).expect("write");
1421        fs.close(fh).expect("close");
1422
1423        let fh = fs.open("/striped.dat", OpenFlags::Read).expect("open");
1424        let mut buf = vec![0u8; 4096];
1425        let n = fs.read(&fh, &mut buf).expect("read");
1426        assert_eq!(n, 4096);
1427        assert_eq!(buf, data);
1428    }
1429
1430    #[test]
1431    fn format_create_many_small_files() {
1432        let lease = setup_lease(4096);
1433        let mut fs = FabricFs::format(vec![lease]).expect("format");
1434
1435        for i in 0..20u32 {
1436            let name = alloc::format!("/file_{}.txt", i);
1437            let mut fh = fs.create(&name).expect("create");
1438            let content = alloc::format!("content {}", i);
1439            fs.write(&mut fh, content.as_bytes()).expect("write");
1440            fs.close(fh).expect("close");
1441        }
1442
1443        let entries = fs.readdir("/").expect("readdir");
1444        assert_eq!(entries.len(), 20);
1445
1446        let fh = fs.open("/file_7.txt", OpenFlags::Read).expect("open");
1447        let mut buf = [0u8; 64];
1448        let n = fs.read(&fh, &mut buf).expect("read");
1449        assert_eq!(&buf[..n], b"content 7");
1450    }
1451
1452    #[test]
1453    fn unmount_releases_leases() {
1454        let lease = setup_lease(2048);
1455        let mut fs = FabricFs::format(vec![lease]).expect("format");
1456
1457        let mut fh = fs.create("/test.txt").expect("create");
1458        fs.write(&mut fh, b"data").expect("write");
1459        fs.close(fh).expect("close");
1460
1461        let leases = fs.unmount().expect("unmount");
1462        assert_eq!(leases.len(), 1);
1463    }
1464
1465    #[test]
1466    fn sync_flushes_dirty_metadata() {
1467        let lease = setup_lease(2048);
1468        let mut fs = FabricFs::format(vec![lease]).expect("format");
1469
1470        let mut fh = fs.create("/synced.txt").expect("create");
1471        fs.write(&mut fh, b"sync me").expect("write");
1472
1473        fs.sync().expect("sync");
1474
1475        let inode = fs.inode_cache.get(&fh.inode_id).unwrap();
1476        assert_eq!(inode.size, 7);
1477
1478        fs.close(fh).expect("close");
1479    }
1480
1481    #[test]
1482    fn mount_reads_existing_data() {
1483        let lease = setup_lease(4096);
1484        let mut fs = FabricFs::format(vec![lease]).expect("format");
1485
1486        let mut fh = fs.create("/persist.txt").expect("create");
1487        fs.write(&mut fh, b"persistent data").expect("write");
1488        fs.close(fh).expect("close");
1489
1490        let leases = fs.unmount().expect("unmount");
1491        let mut fs = FabricFs::mount(leases).expect("mount");
1492
1493        let fh = fs.open("/persist.txt", OpenFlags::Read).expect("open");
1494        let mut buf = [0u8; 64];
1495        let n = fs.read(&fh, &mut buf).expect("read");
1496        assert_eq!(&buf[..n], b"persistent data");
1497    }
1498
1499    #[test]
1500    fn write_spanning_multiple_blocks() {
1501        let lease = setup_lease(2048);
1502        let mut fs = FabricFs::format(vec![lease]).expect("format");
1503
1504        let mut fh = fs.create("/big.txt").expect("create");
1505        let data: Vec<u8> = (0..2048u32).map(|i| (i % 256) as u8).collect();
1506        fs.write(&mut fh, &data).expect("write");
1507        fs.close(fh).expect("close");
1508
1509        let fh = fs.open("/big.txt", OpenFlags::Read).expect("open");
1510        let mut buf = vec![0u8; 2048];
1511        let n = fs.read(&fh, &mut buf).expect("read");
1512        assert_eq!(n, 2048);
1513        assert_eq!(buf, data);
1514    }
1515
1516    #[test]
1517    fn seek_from_end() {
1518        let lease = setup_lease(2048);
1519        let mut fs = FabricFs::format(vec![lease]).expect("format");
1520
1521        let mut fh = fs.create("/endseek.txt").expect("create");
1522        fs.write(&mut fh, b"0123456789").expect("write");
1523        fs.close(fh).expect("close");
1524
1525        let mut fh = fs.open("/endseek.txt", OpenFlags::Read).expect("open");
1526        fs.seek(&mut fh, SeekFrom::End(-3)).expect("seek");
1527        let mut buf = [0u8; 3];
1528        let n = fs.read(&fh, &mut buf).expect("read");
1529        assert_eq!(n, 3);
1530        assert_eq!(&buf, b"789");
1531    }
1532
1533    #[test]
1534    fn seek_from_current() {
1535        let lease = setup_lease(2048);
1536        let mut fs = FabricFs::format(vec![lease]).expect("format");
1537
1538        let mut fh = fs.create("/curseek.txt").expect("create");
1539        fs.write(&mut fh, b"ABCDEFGHIJ").expect("write");
1540        fs.close(fh).expect("close");
1541
1542        let mut fh = fs.open("/curseek.txt", OpenFlags::Read).expect("open");
1543        fs.seek(&mut fh, SeekFrom::Start(2)).expect("seek start");
1544        fs.seek(&mut fh, SeekFrom::Current(3))
1545            .expect("seek current");
1546        let mut buf = [0u8; 2];
1547        let n = fs.read(&fh, &mut buf).expect("read");
1548        assert_eq!(n, 2);
1549        assert_eq!(&buf, b"FG");
1550    }
1551
1552    #[test]
1553    fn read_at_eof_returns_zero() {
1554        let lease = setup_lease(2048);
1555        let mut fs = FabricFs::format(vec![lease]).expect("format");
1556
1557        let mut fh = fs.create("/small.txt").expect("create");
1558        fs.write(&mut fh, b"hi").expect("write");
1559        fs.close(fh).expect("close");
1560
1561        let mut fh = fs.open("/small.txt", OpenFlags::Read).expect("open");
1562        fs.seek(&mut fh, SeekFrom::Start(2)).expect("seek");
1563        let mut buf = [0u8; 10];
1564        let n = fs.read(&fh, &mut buf).expect("read");
1565        assert_eq!(n, 0);
1566    }
1567
1568    #[test]
1569    fn open_nonexistent_file_fails() {
1570        let lease = setup_lease(2048);
1571        let mut fs = FabricFs::format(vec![lease]).expect("format");
1572        assert!(fs.open("/does_not_exist.txt", OpenFlags::Read).is_err());
1573    }
1574
1575    #[test]
1576    fn remove_from_subdirectory() {
1577        let lease = setup_lease(4096);
1578        let mut fs = FabricFs::format(vec![lease]).expect("format");
1579
1580        fs.mkdir("/dir").expect("mkdir");
1581        let mut fh = fs.create("/dir/file.txt").expect("create");
1582        fs.write(&mut fh, b"in subdir").expect("write");
1583        fs.close(fh).expect("close");
1584
1585        fs.remove("/dir/file.txt").expect("remove");
1586        assert!(fs.open("/dir/file.txt", OpenFlags::Read).is_err());
1587
1588        let entries = fs.readdir("/dir").expect("readdir");
1589        assert!(entries.is_empty());
1590    }
1591
1592    #[test]
1593    fn stat_directory() {
1594        let lease = setup_lease(2048);
1595        let mut fs = FabricFs::format(vec![lease]).expect("format");
1596
1597        fs.mkdir("/mydir").expect("mkdir");
1598        let stat = fs.stat("/mydir").expect("stat");
1599        assert_eq!(stat.file_type, FILE_TYPE_DIR);
1600    }
1601
1602    #[test]
1603    fn create_existing_file_truncates() {
1604        let lease = setup_lease(2048);
1605        let mut fs = FabricFs::format(vec![lease]).expect("format");
1606
1607        let mut fh = fs.create("/reuse.txt").expect("create");
1608        fs.write(&mut fh, b"original content").expect("write");
1609        fs.close(fh).expect("close");
1610
1611        let mut fh = fs.create("/reuse.txt").expect("create again");
1612        fs.write(&mut fh, b"new").expect("write");
1613        fs.close(fh).expect("close");
1614
1615        let fh = fs.open("/reuse.txt", OpenFlags::Read).expect("open");
1616        let mut buf = [0u8; 64];
1617        let n = fs.read(&fh, &mut buf).expect("read");
1618        assert_eq!(&buf[..n], b"new");
1619    }
1620}