fstool 0.3.0

Build disk images and filesystems (ext2/3/4, MBR, GPT) from a directory tree and TOML spec, in the spirit of genext2fs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! FAT32 in-place read/write file handle.
//!
//! Backs [`crate::fs::Filesystem::open_file_rw`]. Every `Write::write` is
//! eager: the relevant cluster(s) are read, patched, written back, and the
//! directory entry's `file_size` is updated when bytes land past the
//! previous EOF. There is no in-memory buffer of file data on the handle —
//! crash safety reduces to "the FAT and the 8.3 entry were persisted by
//! the most recent `sync()` (or `Drop`)".
//!
//! ## Lifetime
//!
//! The handle holds `&'a mut Fat32` and `&'a mut dyn BlockDevice` for its
//! full lifetime, matching the trait signature.

use std::io::{self, Read, Seek, SeekFrom, Write};

use super::{Fat32, SECTOR, dir, table};
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::{FileHandle, FileReadHandle};

/// A FAT32 file handle: eager byte-granular reads + writes, with on-write
/// directory-entry updates.
pub struct FatFileHandle<'a> {
    fs: &'a mut Fat32,
    dev: &'a mut dyn BlockDevice,
    /// Cluster chain backing the file's data. Empty for zero-length files.
    chain: Vec<u32>,
    /// Logical file size in bytes.
    file_size: u64,
    /// Read/write cursor.
    pos: u64,
    /// Cluster chain of the parent directory.
    dir_chain: Vec<u32>,
    /// Byte offset of the 8.3 directory entry within the parent's flat
    /// directory buffer (= `cluster_index * cluster_bytes + in_cluster_off`).
    entry_pos: usize,
    /// Attribute byte for the entry — preserved across rewrites.
    entry_attr: u8,
    /// Raw 8.3 name field — preserved across rewrites.
    entry_name_83: [u8; 11],
    /// True once writes (or set_len) have made the in-memory FAT / entry
    /// diverge from disk. Cleared on `sync()` and `Drop`.
    dirty: bool,
}

impl<'a> FatFileHandle<'a> {
    /// Build a handle for the file whose 8.3 entry lives in `parent_chain`
    /// at byte offset `entry_pos`. Decodes the entry to recover the cluster
    /// chain and length; subsequent writes go through `self`.
    pub(super) fn open_existing(
        fs: &'a mut Fat32,
        dev: &'a mut dyn BlockDevice,
        parent_chain: Vec<u32>,
        entry_pos: usize,
        entry: dir::DirEntry,
    ) -> Result<Self> {
        let chain = if entry.first_cluster < 2 {
            Vec::new()
        } else {
            fs.chain_of(entry.first_cluster)?
        };
        Ok(Self {
            fs,
            dev,
            chain,
            file_size: u64::from(entry.file_size),
            pos: 0,
            dir_chain: parent_chain,
            entry_pos,
            entry_attr: entry.attr,
            entry_name_83: entry.name_83,
            dirty: false,
        })
    }

    /// Cluster size in bytes.
    fn cb(&self) -> u64 {
        self.fs.boot_sector().sectors_per_cluster as u64 * SECTOR as u64
    }

    /// Absolute byte offset of a data cluster's first sector.
    fn cluster_offset(&self, cluster: u32) -> u64 {
        let boot = self.fs.boot_sector();
        let sector = boot.data_start_sector() + (cluster - 2) * boot.sectors_per_cluster as u32;
        sector as u64 * SECTOR as u64
    }

    /// Read `buf.len()` bytes starting at the current cursor — up to EOF.
    /// Returns the number of bytes actually read.
    fn read_inner(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        if self.pos >= self.file_size || buf.is_empty() {
            return Ok(0);
        }
        let cb = self.cb();
        let remaining_in_file = self.file_size - self.pos;
        let want = (buf.len() as u64).min(remaining_in_file);
        let mut written: u64 = 0;
        while written < want {
            let pos = self.pos + written;
            let cluster_idx = (pos / cb) as usize;
            let in_cluster = pos % cb;
            let chunk = (cb - in_cluster).min(want - written);
            if cluster_idx >= self.chain.len() {
                break;
            }
            let cluster = self.chain[cluster_idx];
            let off = self.cluster_offset(cluster) + in_cluster;
            let dst_start = written as usize;
            let dst_end = dst_start + chunk as usize;
            self.dev
                .read_at(off, &mut buf[dst_start..dst_end])
                .map_err(io::Error::other)?;
            written += chunk;
        }
        self.pos += written;
        Ok(written as usize)
    }

    /// Extend the file's cluster chain so it covers at least `needed_clusters`.
    /// New clusters are appended; the FAT links are updated in memory.
    fn ensure_chain_clusters(&mut self, needed: u32) -> Result<()> {
        if self.chain.len() as u32 >= needed {
            return Ok(());
        }
        let extra = needed - self.chain.len() as u32;
        let new_clusters = self.fs.alloc_free_clusters(extra)?;
        if let Some(&last) = self.chain.last() {
            // The previous tail is no longer EOC — link it to the new head.
            self.fs.fat_mut().set(last, new_clusters[0]);
        }
        // Zero the newly-allocated clusters so reads of holes see zeros.
        let cb = self.cb();
        let zero = vec![0u8; cb as usize];
        for &c in &new_clusters {
            self.dev.write_at(self.cluster_offset(c), &zero)?;
        }
        self.chain.extend_from_slice(&new_clusters);
        Ok(())
    }

    /// Truncate the cluster chain to at most `keep` clusters, freeing the rest.
    /// Updates the new tail (if any) to EOC.
    fn truncate_chain(&mut self, keep: u32) -> Result<()> {
        if self.chain.len() as u32 <= keep {
            return Ok(());
        }
        let drained: Vec<u32> = self.chain.drain(keep as usize..).collect();
        for c in &drained {
            self.fs.fat_mut().set(*c, table::FREE);
        }
        if let Some(&last) = self.chain.last() {
            self.fs.fat_mut().set(last, table::EOC);
        }
        // Allow the allocator to revisit these.
        if let Some(&first_freed) = drained.first() {
            self.fs.hint_next_free(first_freed);
        }
        Ok(())
    }

    /// Write `data` into the cluster chain starting at byte offset `off`. The
    /// chain must already be large enough to cover `off + data.len()` bytes.
    fn write_into_chain(&mut self, off: u64, data: &[u8]) -> io::Result<()> {
        let cb = self.cb();
        let mut written: u64 = 0;
        let total = data.len() as u64;
        while written < total {
            let pos = off + written;
            let cluster_idx = (pos / cb) as usize;
            let in_cluster = pos % cb;
            let chunk = (cb - in_cluster).min(total - written);
            let cluster = self.chain[cluster_idx];
            let dst = self.cluster_offset(cluster) + in_cluster;
            let src_start = written as usize;
            let src_end = src_start + chunk as usize;
            self.dev
                .write_at(dst, &data[src_start..src_end])
                .map_err(io::Error::other)?;
            written += chunk;
        }
        Ok(())
    }

    /// Persist the in-memory directory entry: re-encode the 8.3 record with
    /// the current `first_cluster` and `file_size`, then write it back to
    /// disk at its original slot.
    fn flush_dir_entry(&mut self) -> Result<()> {
        let first_cluster = self.chain.first().copied().unwrap_or(0);
        let entry = dir::DirEntry {
            name_83: self.entry_name_83,
            attr: self.entry_attr,
            first_cluster,
            file_size: self.file_size as u32,
        };
        let enc = entry.encode();
        let cb = self.cb() as usize;
        let cluster_idx = self.entry_pos / cb;
        let in_cluster = self.entry_pos % cb;
        let cluster = self.dir_chain[cluster_idx];
        let off = self.cluster_offset(cluster) + in_cluster as u64;
        self.dev.write_at(off, &enc)?;
        Ok(())
    }

    /// Write zeros into the file across the byte range `[start, end)`. The
    /// chain must already cover that range. Used to fill the gap when the
    /// user `set_len`-grows past EOF.
    fn zero_range(&mut self, start: u64, end: u64) -> io::Result<()> {
        if end <= start {
            return Ok(());
        }
        let cb = self.cb();
        let zero = vec![0u8; cb as usize];
        let mut pos = start;
        while pos < end {
            let cluster_idx = (pos / cb) as usize;
            let in_cluster = pos % cb;
            let chunk = (cb - in_cluster).min(end - pos);
            let cluster = self.chain[cluster_idx];
            let dst = self.cluster_offset(cluster) + in_cluster;
            self.dev
                .write_at(dst, &zero[..chunk as usize])
                .map_err(io::Error::other)?;
            pos += chunk;
        }
        Ok(())
    }

    /// Internal write path: extends the chain as needed, writes the bytes,
    /// updates `file_size`, and marks the handle dirty.
    fn write_inner(&mut self, buf: &[u8]) -> io::Result<usize> {
        if buf.is_empty() {
            return Ok(0);
        }
        let cb = self.cb();
        let new_end = self.pos + buf.len() as u64;
        let needed_clusters = new_end.div_ceil(cb) as u32;
        // If we're writing past EOF and EOF isn't on a cluster boundary, the
        // remainder of the EOF cluster is already zeroed (we always zero new
        // clusters on allocation). If EOF *is* mid-cluster and we leave a
        // gap (seek + write), the gap clusters and the gap bytes inside the
        // current EOF cluster also need to be zeroed — handled below.
        let gap_start = self.file_size;
        let gap_end = self.pos.min(new_end);
        self.ensure_chain_clusters(needed_clusters)
            .map_err(|e| io::Error::other(e.to_string()))?;
        // Zero the gap (between old EOF and the cursor) so unwritten bytes
        // read back as zero. Newly-allocated clusters are already zeroed,
        // but bytes inside the old EOF cluster between old_size and
        // start-of-new-cluster aren't.
        if gap_end > gap_start {
            self.zero_range(gap_start, gap_end)?;
        }
        self.write_into_chain(self.pos, buf)?;
        self.pos += buf.len() as u64;
        if self.pos > self.file_size {
            self.file_size = self.pos;
        }
        self.dirty = true;
        Ok(buf.len())
    }

    /// Internal set_len: grow or shrink the chain + size, mark dirty.
    fn set_len_inner(&mut self, new_len: u64) -> Result<()> {
        let cb = self.cb();
        let needed_clusters = new_len.div_ceil(cb) as u32;
        if new_len > self.file_size {
            // Grow.
            self.ensure_chain_clusters(needed_clusters)?;
            // Fill the gap between old EOF and new EOF with zeros.
            let old_len = self.file_size;
            self.file_size = new_len;
            self.zero_range(old_len, new_len)
                .map_err(crate::Error::Io)?;
        } else if new_len < self.file_size {
            // Shrink.
            self.truncate_chain(needed_clusters)?;
            self.file_size = new_len;
            // Clamp the cursor if it now points past EOF.
            if self.pos > self.file_size {
                self.pos = self.file_size;
            }
        }
        self.dirty = true;
        Ok(())
    }
}

impl<'a> Read for FatFileHandle<'a> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.read_inner(buf)
    }
}

impl<'a> Write for FatFileHandle<'a> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.write_inner(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        if !self.dirty {
            return Ok(());
        }
        self.flush_dir_entry()
            .map_err(|e| io::Error::other(e.to_string()))?;
        self.fs
            .flush(self.dev)
            .map_err(|e| io::Error::other(e.to_string()))?;
        self.dirty = false;
        Ok(())
    }
}

impl<'a> Seek for FatFileHandle<'a> {
    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
        let new_pos: i128 = match pos {
            SeekFrom::Start(n) => n as i128,
            SeekFrom::End(d) => self.file_size as i128 + d as i128,
            SeekFrom::Current(d) => self.pos as i128 + d as i128,
        };
        if new_pos < 0 {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "fat32: seek to negative offset",
            ));
        }
        // Cap at u32::MAX since FAT file sizes are 32-bit on disk.
        if new_pos > u32::MAX as i128 {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "fat32: seek past 4 GiB file-size limit",
            ));
        }
        self.pos = new_pos as u64;
        Ok(self.pos)
    }
}

impl<'a> FileHandle for FatFileHandle<'a> {
    fn len(&self) -> u64 {
        self.file_size
    }

    fn set_len(&mut self, new_len: u64) -> Result<()> {
        if new_len > u32::MAX as u64 {
            return Err(crate::Error::InvalidArgument(
                "fat32: files cannot exceed 4 GiB".into(),
            ));
        }
        self.set_len_inner(new_len)
    }

    fn sync(&mut self) -> Result<()> {
        if !self.dirty {
            return Ok(());
        }
        self.flush_dir_entry()?;
        self.fs.flush(self.dev)?;
        self.dirty = false;
        Ok(())
    }
}

impl<'a> Drop for FatFileHandle<'a> {
    fn drop(&mut self) {
        // Best-effort: persist on drop so the file is durable even when the
        // caller forgets to `sync`. Errors are swallowed because Drop can't
        // return them; tests should call sync() explicitly to surface I/O
        // failures.
        if self.dirty {
            let _ = self.flush_dir_entry();
            let _ = self.fs.flush(self.dev);
            self.dirty = false;
        }
    }
}

/// Read-only adapter over a [`FatFileHandle`]. Erases the `Write` half
/// of the underlying handle, exposing only `Read + Seek + len()` — the
/// shape required by [`crate::fs::FileReadHandle`].
///
/// Constructed by [`crate::fs::Filesystem::open_file_ro`]. Because the
/// adapter never calls into the inner handle's mutating paths and the
/// inner handle stays clean (`dirty = false`), its `Drop` is a no-op
/// — no spurious FAT / FSInfo writes happen at end-of-scope.
pub struct ReadOnlyFatHandle<'a> {
    inner: FatFileHandle<'a>,
}

impl<'a> ReadOnlyFatHandle<'a> {
    pub(super) fn new(inner: FatFileHandle<'a>) -> Self {
        Self { inner }
    }
}

impl<'a> Read for ReadOnlyFatHandle<'a> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.inner.read(buf)
    }
}

impl<'a> Seek for ReadOnlyFatHandle<'a> {
    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
        self.inner.seek(pos)
    }
}

impl<'a> FileReadHandle for ReadOnlyFatHandle<'a> {
    fn len(&self) -> u64 {
        FileHandle::len(&self.inner)
    }
}