fstool 0.4.1

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
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
//! Fresh-image F2FS formatter.
//!
//! Produces an empty F2FS volume on a [`BlockDevice`]. The layout is
//! laid out exactly as the read driver expects (see the head comment in
//! [`super::superblock`] and [`super::checkpoint`]); the writer then
//! mutates that layout incrementally through [`super::write`] and stamps
//! a fresh checkpoint pack at [`super::F2fs::flush`] time.
//!
//! ## What "fresh-image" buys us
//!
//! - No wear-levelling: blocks are allocated sequentially from
//!   `main_blkaddr` upward.
//! - No checkpoint replay / roll-forward: a single CP pack (CP0) is
//!   stamped at end of life with the live state; CP1 is left zero and
//!   only validates after the writer asks for a second flush.
//! - No GC / cleaning: a "section" is never reused, so the SIT bitmap
//!   only ever gains valid bits.
//!
//! ## On-disk regions (all in 4 KiB block units)
//!
//! ```text
//!   block 0..2   : superblock (primary at byte 1024, backup at 1024 + 4096)
//!   block 2..    : checkpoint area, `segment_count_ckpt * blocks_per_seg` blocks
//!                  → CP0 head + summary, CP1 head + summary
//!         ..     : SIT area  `segment_count_sit * blocks_per_seg`
//!         ..     : NAT area  `segment_count_nat * blocks_per_seg`
//!         ..     : SSA area  `segment_count_ssa * blocks_per_seg`
//!         ..     : main area `segment_count_main * blocks_per_seg`
//! ```
//!
//! The two SIT / NAT halves serve the shadow-copy scheme but the v1
//! formatter writes both halves to the same content so the reader's
//! "pick `cur_nat_pack`" logic is a no-op.
//!
//! Reference: kernel docs §"Filesystem Architecture" and FAST '15 §2.

use crate::Result;
use crate::block::BlockDevice;

use super::constants::{
    F2FS_BLK_CSUM_OFFSET, F2FS_BLKSIZE, NAT_ENTRY_PER_BLOCK, NAT_ENTRY_SIZE, S_IFDIR,
};
use super::superblock::{F2FS_MAGIC, F2FS_ROOT_INO_DEFAULT, SB_OFFSET_BACKUP, SB_OFFSET_PRIMARY};

/// User-facing format knobs. Anything not in here is derived from the
/// device size and a couple of geometry defaults.
#[derive(Debug, Clone)]
pub struct FormatOpts {
    /// 16-byte UUID stamped into the superblock. Defaults to all zero
    /// (caller decides whether to randomise).
    pub uuid: [u8; 16],
    /// Up to 512 UTF-16LE code units; truncated when longer.
    pub volume_label: String,
    /// log2 of blocks per segment. F2FS canonical value is `9`
    /// (→ 512 blocks → 2 MiB segments). The formatter accepts smaller
    /// values for tiny test images.
    pub log_blocks_per_seg: u32,
    /// Segments per section. Set to 1 unless you know what you're doing.
    pub segs_per_sec: u32,
    /// Sections per zone. Same caveat as above.
    pub secs_per_zone: u32,
    /// File mode for the root directory (mode bits, no type — type is
    /// always `S_IFDIR`).
    pub root_mode: u16,
    /// Owner of the root directory.
    pub root_uid: u32,
    pub root_gid: u32,
    /// Stamp this Unix-epoch timestamp on root and on the CP version.
    pub mtime: u32,
}

impl Default for FormatOpts {
    fn default() -> Self {
        Self {
            uuid: [0; 16],
            volume_label: String::new(),
            // log2(512) = 9 — standard F2FS segment size. Smaller is
            // allowed for tiny test images (the formatter accepts 2..=9).
            log_blocks_per_seg: 9,
            segs_per_sec: 1,
            secs_per_zone: 1,
            root_mode: 0o755,
            root_uid: 0,
            root_gid: 0,
            mtime: 0,
        }
    }
}

impl FormatOpts {
    /// Apply a generic option-bag (CLI `-O key=val` / TOML
    /// `[filesystem.options]`) on top of these opts. Unknown keys are
    /// left in the map for the caller to flag.
    pub fn apply_options(&mut self, map: &mut crate::format_opts::OptionMap) -> crate::Result<()> {
        if let Some(s) = map.take_str("volume_label") {
            self.volume_label = s;
        }
        if let Some(n) = map.take_u32("log_blocks_per_seg")? {
            self.log_blocks_per_seg = n;
        }
        if let Some(n) = map.take_u32("segs_per_sec")? {
            self.segs_per_sec = n;
        }
        if let Some(n) = map.take_u32("secs_per_zone")? {
            self.secs_per_zone = n;
        }
        if let Some(n) = map.take_u16("root_mode")? {
            self.root_mode = n;
        }
        if let Some(n) = map.take_u32("root_uid")? {
            self.root_uid = n;
        }
        if let Some(n) = map.take_u32("root_gid")? {
            self.root_gid = n;
        }
        if let Some(t) = map.take_u32("mtime")? {
            self.mtime = t;
        }
        Ok(())
    }
}

/// Resolved geometry of the volume we're about to write. Every field is
/// in 4 KiB blocks unless noted.
#[derive(Debug, Clone, Copy)]
pub struct Geometry {
    pub total_blocks: u64,
    pub blocks_per_seg: u32,
    pub log_blocks_per_seg: u32,
    pub segs_per_sec: u32,
    pub secs_per_zone: u32,
    pub segment_count_ckpt: u32,
    pub segment_count_sit: u32,
    pub segment_count_nat: u32,
    pub segment_count_ssa: u32,
    pub segment_count_main: u32,
    pub cp_blkaddr: u32,
    pub sit_blkaddr: u32,
    pub nat_blkaddr: u32,
    pub ssa_blkaddr: u32,
    pub main_blkaddr: u32,
}

impl Geometry {
    /// Total segment count across every region.
    pub fn segment_count(&self) -> u32 {
        self.segment_count_ckpt
            + self.segment_count_sit
            + self.segment_count_nat
            + self.segment_count_ssa
            + self.segment_count_main
    }

    /// Block addresses of the two CP packs.
    pub fn cp_pack_blkaddrs(&self) -> [u32; 2] {
        [self.cp_blkaddr, self.cp_blkaddr + self.blocks_per_seg]
    }

    /// Total number of NAT entries the on-disk pages can hold (per pack,
    /// since each pack holds half the NAT region).
    pub fn max_nat_entries(&self) -> u32 {
        let pages_per_pack = (self.segment_count_nat * self.blocks_per_seg) / 2;
        pages_per_pack * NAT_ENTRY_PER_BLOCK as u32
    }
}

/// Plan a geometry for a device of `total_blocks` 4 KiB blocks.
///
/// The plan is biased toward "smallest legal layout that still works":
/// 2 CP segs (two packs), 1 SIT seg, 2 NAT segs (two packs), 1 SSA seg,
/// the rest to the main area.
pub fn plan_geometry(total_blocks: u64, opts: &FormatOpts) -> Result<Geometry> {
    if total_blocks < 64 {
        return Err(crate::Error::InvalidArgument(format!(
            "f2fs: device too small ({total_blocks} blocks; need ≥ 64)"
        )));
    }
    if !(1..=9).contains(&opts.log_blocks_per_seg) {
        return Err(crate::Error::InvalidArgument(format!(
            "f2fs: log_blocks_per_seg must be in 1..=9, got {}",
            opts.log_blocks_per_seg
        )));
    }
    let blocks_per_seg: u32 = 1u32 << opts.log_blocks_per_seg;

    // Two superblock blocks at the start.
    let sb_blocks = 2u32;

    // Meta region: CP / SIT / NAT / SSA in *segments*. The SIT and NAT
    // regions are shadow-paired (two halves for ping-pong updates), so
    // their segment counts must be EVEN. fsck.f2fs's
    // `sanity_check_ckpt` derives the bitmap sizes via
    // `((segment_count_X / 2) << log_blocks_per_seg) / 8` — anything
    // less than 2 yields a zero-byte bitmap, mismatches the CP's
    // recorded size, and fsck rejects with `Wrong bitmap size`.
    let segment_count_ckpt = 2u32;
    let segment_count_sit = 2u32;
    let segment_count_nat = 2u32;
    let segment_count_ssa = 1u32;

    let meta_blocks = sb_blocks
        + (segment_count_ckpt + segment_count_sit + segment_count_nat + segment_count_ssa)
            * blocks_per_seg;
    if (meta_blocks as u64) >= total_blocks {
        return Err(crate::Error::InvalidArgument(format!(
            "f2fs: device has {total_blocks} blocks, need > {meta_blocks} for metadata"
        )));
    }
    // Main area: rounded down to a whole-segment count.
    let main_blocks = total_blocks as u32 - meta_blocks;
    let segment_count_main = main_blocks / blocks_per_seg;
    if segment_count_main == 0 {
        return Err(crate::Error::InvalidArgument(
            "f2fs: not enough room for any main-area segment".into(),
        ));
    }

    let cp_blkaddr = sb_blocks;
    let sit_blkaddr = cp_blkaddr + segment_count_ckpt * blocks_per_seg;
    let nat_blkaddr = sit_blkaddr + segment_count_sit * blocks_per_seg;
    let ssa_blkaddr = nat_blkaddr + segment_count_nat * blocks_per_seg;
    let main_blkaddr = ssa_blkaddr + segment_count_ssa * blocks_per_seg;

    Ok(Geometry {
        total_blocks,
        blocks_per_seg,
        log_blocks_per_seg: opts.log_blocks_per_seg,
        segs_per_sec: opts.segs_per_sec.max(1),
        secs_per_zone: opts.secs_per_zone.max(1),
        segment_count_ckpt,
        segment_count_sit,
        segment_count_nat,
        segment_count_ssa,
        segment_count_main,
        cp_blkaddr,
        sit_blkaddr,
        nat_blkaddr,
        ssa_blkaddr,
        main_blkaddr,
    })
}

/// Stamp two primary + backup superblock copies derived from `geom` and
/// `opts`.
pub(crate) fn write_superblocks(
    dev: &mut dyn BlockDevice,
    geom: &Geometry,
    opts: &FormatOpts,
) -> Result<()> {
    let mut buf = vec![0u8; 0x400];
    buf[0x00..0x04].copy_from_slice(&F2FS_MAGIC.to_le_bytes());
    buf[0x04..0x06].copy_from_slice(&1u16.to_le_bytes()); // major_ver
    buf[0x06..0x08].copy_from_slice(&15u16.to_le_bytes()); // minor_ver
    // Field offsets per kernel `f2fs_fs.h`. The whole tail of the
    // superblock used to be 4 bytes too late; fsck.f2fs / mkfs.f2fs
    // now accept what we emit.
    buf[0x08..0x0C].copy_from_slice(&9u32.to_le_bytes()); // log_sectorsize (512)
    buf[0x0C..0x10].copy_from_slice(&3u32.to_le_bytes()); // log_sectors_per_block (8 sec / 4 KiB)
    buf[0x10..0x14].copy_from_slice(&12u32.to_le_bytes()); // log_blocksize (4096)
    buf[0x14..0x18].copy_from_slice(&geom.log_blocks_per_seg.to_le_bytes());
    buf[0x18..0x1C].copy_from_slice(&geom.segs_per_sec.to_le_bytes());
    buf[0x1C..0x20].copy_from_slice(&geom.secs_per_zone.to_le_bytes());
    // 0x20 = checksum_offset (unused by our reader)
    buf[0x24..0x2C].copy_from_slice(&geom.total_blocks.to_le_bytes());
    let section_count = geom.segment_count_main / geom.segs_per_sec.max(1);
    buf[0x2C..0x30].copy_from_slice(&section_count.to_le_bytes());
    buf[0x30..0x34].copy_from_slice(&geom.segment_count().to_le_bytes());
    buf[0x34..0x38].copy_from_slice(&geom.segment_count_ckpt.to_le_bytes());
    buf[0x38..0x3C].copy_from_slice(&geom.segment_count_sit.to_le_bytes());
    buf[0x3C..0x40].copy_from_slice(&geom.segment_count_nat.to_le_bytes());
    buf[0x40..0x44].copy_from_slice(&geom.segment_count_ssa.to_le_bytes());
    buf[0x44..0x48].copy_from_slice(&geom.segment_count_main.to_le_bytes());
    // F2FS requires `segment0_blkaddr == cp_blkaddr` — segment 0 starts
    // where the first checkpoint pack lives (right after the two SB
    // copies). fsck.f2fs reports `Mismatch segment0(N) cp_blkaddr(M)`
    // when they disagree.
    buf[0x48..0x4C].copy_from_slice(&geom.cp_blkaddr.to_le_bytes()); // segment0_blkaddr
    buf[0x4C..0x50].copy_from_slice(&geom.cp_blkaddr.to_le_bytes());
    buf[0x50..0x54].copy_from_slice(&geom.sit_blkaddr.to_le_bytes());
    buf[0x54..0x58].copy_from_slice(&geom.nat_blkaddr.to_le_bytes());
    buf[0x58..0x5C].copy_from_slice(&geom.ssa_blkaddr.to_le_bytes());
    buf[0x5C..0x60].copy_from_slice(&geom.main_blkaddr.to_le_bytes());
    buf[0x60..0x64].copy_from_slice(&F2FS_ROOT_INO_DEFAULT.to_le_bytes());
    buf[0x64..0x68].copy_from_slice(&1u32.to_le_bytes()); // node_ino
    buf[0x68..0x6C].copy_from_slice(&2u32.to_le_bytes()); // meta_ino

    // 16-byte UUID at 0x6C.
    buf[0x6C..0x7C].copy_from_slice(&opts.uuid);

    // 512-codepoint UTF-16LE volume label at 0x7C.
    let mut units = opts
        .volume_label
        .encode_utf16()
        .take(511)
        .collect::<Vec<u16>>();
    units.push(0);
    for (i, c) in units.iter().enumerate() {
        let o = 0x7C + i * 2;
        if o + 2 > buf.len() {
            break;
        }
        buf[o..o + 2].copy_from_slice(&c.to_le_bytes());
    }

    // cp_payload (0 for our small / single-pack layout).
    buf[0x3F8..0x3FC].copy_from_slice(&0u32.to_le_bytes());

    dev.write_at(SB_OFFSET_PRIMARY, &buf)?;
    dev.write_at(SB_OFFSET_BACKUP, &buf)?;
    Ok(())
}

/// Zero every metadata block once at format time. The main area is left
/// untouched (it's already zero on a freshly-`zero_range`d device) so
/// large images don't pay an O(N) zeroing pass twice.
pub(crate) fn wipe_metadata_region(dev: &mut dyn BlockDevice, geom: &Geometry) -> Result<()> {
    let bs = F2FS_BLKSIZE as u64;
    let start = (geom.cp_blkaddr as u64) * bs;
    let end = (geom.main_blkaddr as u64) * bs;
    dev.zero_range(start, end - start)?;
    Ok(())
}

/// Encode a single 4 KiB NAT page that holds `entries` starting at the
/// page's slot 0.
///
/// NAT pages have NO trailing CRC: `struct f2fs_nat_block` is exactly
/// `entries[NAT_ENTRY_PER_BLOCK]` packed, and mkfs.f2fs leaves the
/// trailing bytes (after the last full 9-byte slot) as zero. Writing a
/// 4-byte CRC at offset 4092 would alias into slot 454's block_addr
/// field (slot 454 spans bytes 4086..4095), making fsck see garbage
/// addresses for an entry we never wrote.
pub(crate) fn encode_nat_page(entries: &[(u8, u32, u32)]) -> Vec<u8> {
    let mut page = vec![0u8; F2FS_BLKSIZE];
    for (i, (version, ino, block_addr)) in entries.iter().enumerate() {
        let o = i * NAT_ENTRY_SIZE;
        if o + NAT_ENTRY_SIZE > F2FS_BLKSIZE {
            break;
        }
        page[o] = *version;
        page[o + 1..o + 5].copy_from_slice(&ino.to_le_bytes());
        page[o + 5..o + 9].copy_from_slice(&block_addr.to_le_bytes());
    }
    page
}

/// Encode a single 4 KiB SIT page that tracks `n_valid_blocks_per_seg`
/// segments worth of bitmaps. v1 marks every block of every used segment
/// as valid (we never reclaim); unused segments stay zero.
///
/// Encode a 4 KiB SSA page. Each entry is a `f2fs_summary` (7 bytes) that
/// records `(nid, version, ofs_in_node)` for the block at that offset in
/// the parent segment. SSA blocks are `struct f2fs_summary_block` —
/// `entries[ENTRIES_IN_SUM] + journal + summary_footer`. A fresh image
/// emits an all-zero block; fsck does not verify the summary footer's
/// CRC for read-only walks.
pub(crate) fn encode_ssa_page() -> Vec<u8> {
    vec![0u8; F2FS_BLKSIZE]
}

/// Build a minimally-valid root inode block for the freshly formatted FS.
/// The block carries the directory mode/uid/gid/links + an inline-dentry
/// payload that's empty until the writer pushes children.
#[allow(dead_code)]
pub(crate) fn encode_root_inode_block(opts: &FormatOpts) -> Vec<u8> {
    use super::constants::{ADDRS_PER_INODE, F2FS_INLINE_DENTRY, NIDS_PER_INODE};
    use super::inode::I_ADDR_OFFSET;

    let mut buf = vec![0u8; F2FS_BLKSIZE];
    let mode = S_IFDIR | (opts.root_mode & 0x0FFF);
    buf[0x00..0x02].copy_from_slice(&mode.to_le_bytes());
    buf[0x03] = F2FS_INLINE_DENTRY;
    buf[0x04..0x08].copy_from_slice(&opts.root_uid.to_le_bytes());
    buf[0x08..0x0C].copy_from_slice(&opts.root_gid.to_le_bytes());
    buf[0x0C..0x10].copy_from_slice(&2u32.to_le_bytes()); // links: "." + parent ref
    buf[0x10..0x18].copy_from_slice(&(F2FS_BLKSIZE as u64).to_le_bytes());
    buf[0x18..0x20].copy_from_slice(&0u64.to_le_bytes()); // blocks (inline)
    buf[0x20..0x28].copy_from_slice(&(opts.mtime as u64).to_le_bytes());
    buf[0x28..0x30].copy_from_slice(&(opts.mtime as u64).to_le_bytes());
    buf[0x30..0x38].copy_from_slice(&(opts.mtime as u64).to_le_bytes());

    // i_addr + i_nid arrays are zero (no children, no node tree).
    let _ = (ADDRS_PER_INODE, NIDS_PER_INODE, I_ADDR_OFFSET);

    let crc = super::constants::f2fs_crc32(&buf[..F2FS_BLK_CSUM_OFFSET]);
    buf[F2FS_BLK_CSUM_OFFSET..F2FS_BLK_CSUM_OFFSET + 4].copy_from_slice(&crc.to_le_bytes());
    buf
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn geometry_rejects_tiny_devices() {
        let opts = FormatOpts {
            log_blocks_per_seg: 2,
            ..FormatOpts::default()
        };
        assert!(plan_geometry(8, &opts).is_err());
    }

    #[test]
    fn geometry_lays_out_meta_then_main() {
        let opts = FormatOpts {
            log_blocks_per_seg: 2,
            ..FormatOpts::default()
        };
        let g = plan_geometry(256, &opts).unwrap();
        // Sanity: cp < sit < nat < ssa < main.
        assert!(g.cp_blkaddr < g.sit_blkaddr);
        assert!(g.sit_blkaddr < g.nat_blkaddr);
        assert!(g.nat_blkaddr < g.ssa_blkaddr);
        assert!(g.ssa_blkaddr < g.main_blkaddr);
        // Sanity: SB blocks + every region fits inside the device.
        let used_blocks = 2u32
            + (g.segment_count_ckpt
                + g.segment_count_sit
                + g.segment_count_nat
                + g.segment_count_ssa
                + g.segment_count_main)
                * g.blocks_per_seg;
        assert!((used_blocks as u64) <= g.total_blocks);
        // Sanity: main_blkaddr matches the running sum.
        assert_eq!(
            g.main_blkaddr,
            used_blocks - g.segment_count_main * g.blocks_per_seg
        );
    }
}