1mod format;
2
3use std::io::{self, BufWriter, SeekFrom, Write};
4use std::path::Path;
5
6use crate::crc32c;
7use crate::filetree::{DirectoryNode, FileTree, TreeNode};
8use format::{
9 EXT4_BG_INODE_ZEROED, EXT4_BLOCK_SIZE, EXT4_BLOCKS_PER_GROUP, EXT4_DESC_SIZE, EXT4_EH_MAGIC,
10 EXT4_EXTENTS_FL, EXT4_FEATURE_COMPAT_DIR_INDEX, EXT4_FEATURE_COMPAT_EXT_ATTR,
11 EXT4_FEATURE_COMPAT_HAS_JOURNAL, EXT4_FEATURE_INCOMPAT_64BIT, EXT4_FEATURE_INCOMPAT_EXTENTS,
12 EXT4_FEATURE_INCOMPAT_FILETYPE, EXT4_FEATURE_RO_COMPAT_DIR_NLINK,
13 EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE, EXT4_FEATURE_RO_COMPAT_HUGE_FILE,
14 EXT4_FEATURE_RO_COMPAT_LARGE_FILE, EXT4_FEATURE_RO_COMPAT_METADATA_CSUM,
15 EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER, EXT4_FIRST_INO, EXT4_INODE_SIZE, EXT4_INODES_PER_GROUP,
16 EXT4_JOURNAL_INO, EXT4_LOG_BLOCK_SIZE, EXT4_MIN_EXTRA_ISIZE, EXT4_ROOT_INO, EXT4_SUPER_MAGIC,
17 JBD2_MAGIC, JBD2_SUPERBLOCK_V2, S_IFCHR, S_IFDIR, S_IFLNK, S_IFREG,
18};
19
20const DEFAULT_SIZE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
26
27const DEFAULT_JOURNAL_BLOCKS: u32 = 16384;
29
30const MAX_GROUPS: u32 = 32;
32
33const RESERVED_GDT_BLOCKS: u32 = 0;
35
36const EXT4_FT_DIR: u8 = 2;
38
39#[allow(dead_code)]
41const EXT4_FT_REG_FILE: u8 = 1;
42
43const EXT4_FT_CHRDEV: u8 = 3;
45
46const EXT4_FT_SYMLINK: u8 = 7;
48
49const JBD2_SUPERBLOCK_SIZE: usize = 1024;
51
52pub struct Ext4FormatOptions {
58 pub size_bytes: u64,
61
62 pub journal_blocks: u32,
65}
66
67#[derive(Debug)]
69pub enum Ext4Error {
70 Io(io::Error),
72
73 TooSmall,
76
77 Layout(String),
79}
80
81struct Layout {
83 num_blocks: u64,
84 num_groups: u32,
85 uuid: [u8; 16],
86 gdt_blocks: u32,
87 inode_table_block: u32,
89 inode_table_blocks: u32,
91 first_data_block: u32,
93 journal_start_block: u32,
95 journal_blocks: u32,
97 csum_seed: u32,
99 feature_compat: u32,
101 feature_incompat: u32,
103 feature_ro_compat: u32,
105}
106
107struct FsStats {
108 group_free_blocks: Vec<u32>,
109 group_free_inodes: Vec<u32>,
110 group_used_dirs: Vec<u32>,
111 total_free_blocks: u64,
112 total_free_inodes: u64,
113 total_used_blocks: u64,
114}
115
116enum NodeKind {
117 Directory { children: u16, data: Vec<u8> },
118 RegularFile { data: Vec<u8> },
119 Symlink { target: Vec<u8>, inline: bool },
120 CharDevice { major: u32, minor: u32 },
121}
122
123struct NodePlan {
124 inode: u32,
125 path: String,
126 permissions: u16,
127 uid: u16,
128 gid: u16,
129 kind: NodeKind,
130 block_start: Option<u32>,
131 block_count: u32,
132}
133
134struct DraftDirectory {
135 children: u16,
136 data: Vec<u8>,
137}
138
139struct DirEntrySpec {
140 inode: u32,
141 file_type: u8,
142 name: Vec<u8>,
143}
144
145struct DataAllocator {
146 regions: Vec<(u32, u32)>,
147}
148
149impl Default for Ext4FormatOptions {
154 fn default() -> Self {
155 Self {
156 size_bytes: DEFAULT_SIZE_BYTES,
157 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
158 }
159 }
160}
161
162impl std::fmt::Display for Ext4Error {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 match self {
165 Ext4Error::Io(e) => write!(f, "ext4 I/O error: {e}"),
166 Ext4Error::TooSmall => write!(f, "image size is too small for ext4 formatting"),
167 Ext4Error::Layout(e) => write!(f, "ext4 layout error: {e}"),
168 }
169 }
170}
171
172impl std::error::Error for Ext4Error {
173 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
174 match self {
175 Ext4Error::Io(e) => Some(e),
176 Ext4Error::TooSmall | Ext4Error::Layout(_) => None,
177 }
178 }
179}
180
181impl From<io::Error> for Ext4Error {
182 fn from(e: io::Error) -> Self {
183 Ext4Error::Io(e)
184 }
185}
186
187impl Layout {
188 #[cfg(test)]
189 fn compute(opts: &Ext4FormatOptions) -> Result<Self, Ext4Error> {
190 Self::compute_with_root_blocks(opts, 1)
191 }
192
193 fn compute_with_root_blocks(
194 opts: &Ext4FormatOptions,
195 root_dir_blocks: u32,
196 ) -> Result<Self, Ext4Error> {
197 let block_size = EXT4_BLOCK_SIZE as u64;
198 let num_blocks = opts.size_bytes / block_size;
199 let num_groups_raw = num_blocks.div_ceil(EXT4_BLOCKS_PER_GROUP as u64);
200 let num_groups = num_groups_raw.min(MAX_GROUPS as u64) as u32;
201
202 let inode_table_blocks =
205 (EXT4_INODES_PER_GROUP as u64 * EXT4_INODE_SIZE as u64 / block_size) as u32;
206 let gdt_blocks = (num_groups as u64 * EXT4_DESC_SIZE as u64).div_ceil(block_size) as u32;
207
208 let overhead_blocks = 1 + gdt_blocks + RESERVED_GDT_BLOCKS; let block_bitmap_block = overhead_blocks;
220 let inode_bitmap_block = block_bitmap_block + 1;
221 let inode_table_block = inode_bitmap_block + 1;
222 let first_data_block = inode_table_block + inode_table_blocks;
223 let journal_start_block = first_data_block + root_dir_blocks;
224
225 let min_blocks = journal_start_block as u64 + opts.journal_blocks as u64 + 1; if num_blocks < min_blocks {
227 return Err(Ext4Error::TooSmall);
228 }
229
230 let uuid = Self::generate_uuid();
232
233 let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, &uuid);
234
235 let feature_compat = EXT4_FEATURE_COMPAT_HAS_JOURNAL
236 | EXT4_FEATURE_COMPAT_EXT_ATTR
237 | EXT4_FEATURE_COMPAT_DIR_INDEX;
238
239 let feature_incompat = EXT4_FEATURE_INCOMPAT_FILETYPE
240 | EXT4_FEATURE_INCOMPAT_EXTENTS
241 | EXT4_FEATURE_INCOMPAT_64BIT;
242
243 let feature_ro_compat = EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER
244 | EXT4_FEATURE_RO_COMPAT_LARGE_FILE
245 | EXT4_FEATURE_RO_COMPAT_HUGE_FILE
246 | EXT4_FEATURE_RO_COMPAT_DIR_NLINK
247 | EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE
248 | EXT4_FEATURE_RO_COMPAT_METADATA_CSUM;
249
250 Ok(Layout {
251 num_blocks,
252 num_groups,
253 uuid,
254 gdt_blocks,
255 inode_table_block,
256 inode_table_blocks,
257 first_data_block,
258 journal_start_block,
259 journal_blocks: opts.journal_blocks,
260 csum_seed,
261 feature_compat,
262 feature_incompat,
263 feature_ro_compat,
264 })
265 }
266
267 fn generate_uuid() -> [u8; 16] {
268 let mut uuid = [0u8; 16];
270 if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
271 use std::io::Read;
272 let _ = f.read_exact(&mut uuid);
273 } else {
274 let now = std::time::SystemTime::now()
276 .duration_since(std::time::UNIX_EPOCH)
277 .unwrap_or_default();
278 let nanos = now.as_nanos();
279 uuid[..8].copy_from_slice(&(nanos as u64).to_le_bytes());
280 uuid[8..16].copy_from_slice(&((nanos >> 64) as u64).to_le_bytes());
281 }
282 uuid[6] = (uuid[6] & 0x0F) | 0x40;
284 uuid[7] = (uuid[7] & 0x3F) | 0x80;
285 uuid
286 }
287
288 fn group_start_block(&self, group: u32) -> u32 {
289 group * EXT4_BLOCKS_PER_GROUP
290 }
291
292 fn blocks_in_group(&self, group: u32) -> u32 {
293 let group_start = self.group_start_block(group) as u64;
294 std::cmp::min(
295 EXT4_BLOCKS_PER_GROUP as u64,
296 self.num_blocks.saturating_sub(group_start),
297 ) as u32
298 }
299
300 fn group_has_backup_super(&self, group: u32) -> bool {
301 group == 0 || sparse_super_group(group)
302 }
303
304 fn group_leading_overhead_blocks(&self, group: u32) -> u32 {
305 if self.group_has_backup_super(group) {
306 1 + self.gdt_blocks + RESERVED_GDT_BLOCKS
307 } else {
308 0
309 }
310 }
311
312 fn group_block_bitmap_block(&self, group: u32) -> u32 {
313 self.group_start_block(group) + self.group_leading_overhead_blocks(group)
314 }
315
316 fn group_inode_bitmap_block(&self, group: u32) -> u32 {
317 self.group_block_bitmap_block(group) + 1
318 }
319
320 fn group_inode_table_block(&self, group: u32) -> u32 {
321 self.group_inode_bitmap_block(group) + 1
322 }
323
324 fn group_data_start_block(&self, group: u32) -> u32 {
325 let mut start = self.group_start_block(group) + self.group_metadata_blocks(group);
326 if group == 0 {
327 start = self.journal_start_block + self.journal_blocks;
328 }
329 start
330 }
331
332 fn group_metadata_blocks(&self, group: u32) -> u32 {
333 self.group_leading_overhead_blocks(group) + 2 + self.inode_table_blocks
334 }
335
336 fn group_used_blocks(&self, group: u32) -> u32 {
337 let mut used = self.group_metadata_blocks(group);
338 if group == 0 {
339 used += 1 + self.journal_blocks; }
341 used.min(self.blocks_in_group(group))
342 }
343
344 fn group_free_blocks(&self, group: u32) -> u32 {
345 self.blocks_in_group(group)
346 .saturating_sub(self.group_used_blocks(group))
347 }
348
349 fn group_free_inodes(&self, group: u32) -> u32 {
350 if group == 0 {
351 EXT4_INODES_PER_GROUP - (EXT4_FIRST_INO - 1)
352 } else {
353 EXT4_INODES_PER_GROUP
354 }
355 }
356
357 #[cfg(test)]
358 fn group_used_dirs(&self, group: u32) -> u32 {
359 if group == 0 { 1 } else { 0 }
360 }
361
362 fn total_free_blocks(&self) -> u64 {
363 (0..self.num_groups)
364 .map(|group| self.group_free_blocks(group) as u64)
365 .sum()
366 }
367
368 fn total_free_inodes(&self) -> u64 {
369 (0..self.num_groups)
370 .map(|group| self.group_free_inodes(group) as u64)
371 .sum()
372 }
373
374 fn total_used_blocks(&self) -> u64 {
375 (0..self.num_groups)
376 .map(|group| self.group_used_blocks(group) as u64)
377 .sum()
378 }
379}
380
381pub fn format_ext4(path: &Path, options: &Ext4FormatOptions) -> Result<(), Ext4Error> {
391 let tree = FileTree::new();
392 format_ext4_with_tree(path, options, tree)
393}
394
395pub fn format_ext4_with_tree(
396 path: &Path,
397 options: &Ext4FormatOptions,
398 tree: FileTree,
399) -> Result<(), Ext4Error> {
400 let mut next_inode = EXT4_FIRST_INO;
401 let mut plans = Vec::new();
402 let root_mode = tree.root.metadata.mode;
403 let root_draft = draft_directory(
404 "/",
405 tree.root,
406 EXT4_ROOT_INO,
407 EXT4_ROOT_INO,
408 &mut next_inode,
409 &mut plans,
410 )?;
411 let root_dir_blocks = blocks_for_len(root_draft.data.len());
412 let layout = Layout::compute_with_root_blocks(options, root_dir_blocks.max(1))?;
413 let mut allocator = DataAllocator::new(&layout);
414
415 for plan in &mut plans {
416 allocate_node_data(&mut allocator, plan)?;
417 }
418
419 let mut all_plans = Vec::with_capacity(plans.len() + 1);
420 all_plans.push(NodePlan {
421 inode: EXT4_ROOT_INO,
422 path: "/".to_string(),
423 permissions: normalize_dir_permissions(root_mode),
424 uid: 0,
425 gid: 0,
426 kind: NodeKind::Directory {
427 children: root_draft.children,
428 data: root_draft.data,
429 },
430 block_start: Some(layout.first_data_block),
431 block_count: root_dir_blocks.max(1),
432 });
433 all_plans.extend(plans);
434 all_plans.sort_by_key(|plan| plan.inode);
435
436 let block_bitmaps = build_block_bitmaps_for_plan(&layout, &all_plans);
437 let inode_bitmaps = build_inode_bitmaps_for_plan(&layout, &all_plans);
438 let stats = compute_fs_stats(&layout, &block_bitmaps, &all_plans);
439
440 let raw_file = std::fs::File::create(path)?;
441 raw_file.set_len(options.size_bytes)?;
442 let mut file = BufWriter::new(raw_file);
443
444 write_bitmaps(&mut file, &layout, &block_bitmaps, &inode_bitmaps)?;
445 write_tree_data(&mut file, &layout, &all_plans)?;
446 write_inode_table_with_plan(&mut file, &layout, &all_plans)?;
447 write_journal(&mut file, &layout)?;
448
449 let sb_bytes = build_superblock_with_stats(&layout, &stats)?;
450 write_superblock_at(&mut file, 0, &sb_bytes)?;
451
452 let gdt_bytes = build_gdt_with_stats(&layout, &stats, &block_bitmaps, &inode_bitmaps)?;
453 write_gdt_at(&mut file, 0, &gdt_bytes)?;
454
455 for g in 1..layout.num_groups {
456 if sparse_super_group(g) {
457 let group_start_block = g as u64 * EXT4_BLOCKS_PER_GROUP as u64;
458 write_superblock_at(&mut file, group_start_block, &sb_bytes)?;
459 write_gdt_at(&mut file, group_start_block, &gdt_bytes)?;
460 }
461 }
462
463 file.flush()?;
464 Ok(())
468}
469
470fn draft_directory(
471 path: &str,
472 dir: DirectoryNode,
473 inode: u32,
474 parent_inode: u32,
475 next_inode: &mut u32,
476 plans: &mut Vec<NodePlan>,
477) -> Result<DraftDirectory, Ext4Error> {
478 if !dir.xattrs.is_empty() {
479 return Err(Ext4Error::Layout(format!(
480 "ext4 patch baking does not yet support xattrs on '{path}'"
481 )));
482 }
483
484 let mut children = Vec::new();
485 let mut child_dir_count = 0u16;
486
487 for (name, node) in dir.entries {
488 let name_bytes = name.as_os_str().as_encoded_bytes().to_vec();
489 let child_path = child_path(path, &name_bytes);
490 let child_inode = *next_inode;
491 if child_inode >= EXT4_INODES_PER_GROUP {
492 return Err(Ext4Error::Layout(
493 "too many upper-layer inodes for group 0 inode table".to_string(),
494 ));
495 }
496 *next_inode += 1;
497
498 match node {
499 TreeNode::Directory(child_dir) => {
500 child_dir_count = child_dir_count.saturating_add(1);
501 let dir_mode = child_dir.metadata.mode;
502 let child_draft = draft_directory(
503 &child_path,
504 child_dir,
505 child_inode,
506 inode,
507 next_inode,
508 plans,
509 )?;
510 let block_count = blocks_for_len(child_draft.data.len());
511 plans.push(NodePlan {
512 inode: child_inode,
513 path: child_path.clone(),
514 permissions: normalize_dir_permissions(dir_mode),
515 uid: 0,
516 gid: 0,
517 kind: NodeKind::Directory {
518 children: child_draft.children,
519 data: child_draft.data,
520 },
521 block_start: None,
522 block_count,
523 });
524 children.push(DirEntrySpec {
525 inode: child_inode,
526 file_type: EXT4_FT_DIR,
527 name: name_bytes,
528 });
529 }
530 TreeNode::RegularFile(file) => {
531 if !file.xattrs.is_empty() {
532 return Err(Ext4Error::Layout(format!(
533 "ext4 patch baking does not yet support xattrs on '{child_path}'"
534 )));
535 }
536 plans.push(NodePlan {
537 inode: child_inode,
538 path: child_path.clone(),
539 permissions: normalize_file_permissions(file.metadata.mode),
540 uid: 0,
541 gid: 0,
542 block_count: blocks_for_len(file.data.len()),
543 kind: NodeKind::RegularFile {
544 data: file.data.read_all().map_err(Ext4Error::Io)?,
545 },
546 block_start: None,
547 });
548 children.push(DirEntrySpec {
549 inode: child_inode,
550 file_type: EXT4_FT_REG_FILE,
551 name: name_bytes,
552 });
553 }
554 TreeNode::Symlink(symlink) => {
555 let target_len = symlink.target.len();
556 let inline = target_len <= 59;
557 let block_count = if inline {
558 0
559 } else {
560 blocks_for_len(target_len)
561 };
562 plans.push(NodePlan {
563 inode: child_inode,
564 path: child_path.clone(),
565 permissions: 0o777,
566 uid: 0,
567 gid: 0,
568 kind: NodeKind::Symlink {
569 target: symlink.target,
570 inline,
571 },
572 block_start: None,
573 block_count,
574 });
575 children.push(DirEntrySpec {
576 inode: child_inode,
577 file_type: EXT4_FT_SYMLINK,
578 name: name_bytes,
579 });
580 }
581 TreeNode::CharDevice(device) => {
582 plans.push(NodePlan {
583 inode: child_inode,
584 path: child_path.clone(),
585 permissions: 0,
586 uid: 0,
587 gid: 0,
588 kind: NodeKind::CharDevice {
589 major: device.major,
590 minor: device.minor,
591 },
592 block_start: None,
593 block_count: 0,
594 });
595 children.push(DirEntrySpec {
596 inode: child_inode,
597 file_type: EXT4_FT_CHRDEV,
598 name: name_bytes,
599 });
600 }
601 _ => {
602 return Err(Ext4Error::Layout(format!(
603 "unsupported upper-layer node at '{child_path}'"
604 )));
605 }
606 }
607 }
608
609 let data = build_directory_data(inode, parent_inode, &children, path)?;
610 Ok(DraftDirectory {
611 children: child_dir_count,
612 data,
613 })
614}
615
616fn child_path(parent: &str, name: &[u8]) -> String {
617 let name = String::from_utf8_lossy(name);
618 if parent == "/" {
619 format!("/{name}")
620 } else {
621 format!("{parent}/{name}")
622 }
623}
624
625fn normalize_file_permissions(mode: u16) -> u16 {
626 let perms = mode & 0o7777;
627 if perms == 0 { 0o644 } else { perms }
628}
629
630fn normalize_dir_permissions(mode: u16) -> u16 {
631 let perms = mode & 0o7777;
632 if perms == 0 { 0o755 } else { perms }
633}
634
635fn blocks_for_len(len: usize) -> u32 {
636 if len == 0 {
637 0
638 } else {
639 (len as u64).div_ceil(EXT4_BLOCK_SIZE as u64) as u32
640 }
641}
642
643fn build_directory_data(
644 dir_inode: u32,
645 parent_inode: u32,
646 children: &[DirEntrySpec],
647 path: &str,
648) -> Result<Vec<u8>, Ext4Error> {
649 let mut entries = Vec::with_capacity(children.len() + 2);
650 entries.push(DirEntrySpec {
651 inode: dir_inode,
652 file_type: EXT4_FT_DIR,
653 name: b".".to_vec(),
654 });
655 entries.push(DirEntrySpec {
656 inode: parent_inode,
657 file_type: EXT4_FT_DIR,
658 name: b"..".to_vec(),
659 });
660 entries.extend(children.iter().map(|entry| DirEntrySpec {
661 inode: entry.inode,
662 file_type: entry.file_type,
663 name: entry.name.clone(),
664 }));
665
666 let mut blocks = Vec::new();
667 let mut index = 0usize;
668 while index < entries.len() {
669 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
670 let mut pos = 0usize;
671 let data_limit = EXT4_BLOCK_SIZE as usize - 12;
672 let block_start = index;
673
674 while index < entries.len() {
675 let min_len = dir_entry_len(entries[index].name.len());
676 let needed = if pos == 0 { min_len } else { pos + min_len };
677 if needed > data_limit {
678 if pos == 0 {
679 return Err(Ext4Error::Layout(format!(
680 "directory entry too large for '{path}'"
681 )));
682 }
683 break;
684 }
685 pos += min_len;
686 index += 1;
687 }
688
689 let mut write_pos = 0usize;
690 for (entry_index, entry) in entries[block_start..index].iter().enumerate() {
691 let is_last = entry_index + 1 == index - block_start;
692 let rec_len = if is_last {
693 (data_limit - write_pos) as u16
694 } else {
695 dir_entry_len(entry.name.len()) as u16
696 };
697 put_le32(&mut block, write_pos, entry.inode);
698 put_le16(&mut block, write_pos + 4, rec_len);
699 block[write_pos + 6] = entry.name.len() as u8;
700 block[write_pos + 7] = entry.file_type;
701 block[write_pos + 8..write_pos + 8 + entry.name.len()].copy_from_slice(&entry.name);
702 write_pos += rec_len as usize;
703 }
704
705 let tail = data_limit;
706 put_le32(&mut block, tail, 0);
707 put_le16(&mut block, tail + 4, 12);
708 block[tail + 6] = 0;
709 block[tail + 7] = 0xDE;
710 blocks.extend_from_slice(&block);
711 }
712
713 if blocks.is_empty() {
714 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
715 put_le32(&mut block, 0, dir_inode);
716 put_le16(&mut block, 4, 12);
717 block[6] = 1;
718 block[7] = EXT4_FT_DIR;
719 block[8] = b'.';
720 put_le32(&mut block, 12, parent_inode);
721 put_le16(&mut block, 16, (EXT4_BLOCK_SIZE - 24) as u16);
722 block[18] = 2;
723 block[19] = EXT4_FT_DIR;
724 block[20] = b'.';
725 block[21] = b'.';
726 let tail = EXT4_BLOCK_SIZE as usize - 12;
727 put_le32(&mut block, tail, 0);
728 put_le16(&mut block, tail + 4, 12);
729 block[tail + 7] = 0xDE;
730 blocks = block;
731 }
732
733 Ok(blocks)
734}
735
736fn dir_entry_len(name_len: usize) -> usize {
737 (8 + name_len + 3) & !3
738}
739
740fn allocate_node_data(allocator: &mut DataAllocator, plan: &mut NodePlan) -> Result<(), Ext4Error> {
741 if plan.block_count == 0 {
742 plan.block_start = None;
743 return Ok(());
744 }
745
746 plan.block_start = allocator.allocate(plan.block_count, &plan.path)?;
747 Ok(())
748}
749
750impl DataAllocator {
751 fn new(layout: &Layout) -> Self {
752 let mut regions = Vec::new();
753 for group in 0..layout.num_groups {
754 let group_start = layout.group_start_block(group);
755 let group_end = group_start + layout.blocks_in_group(group);
756 let start = layout.group_data_start_block(group);
757 if start < group_end {
758 regions.push((start, group_end - start));
759 }
760 }
761 Self { regions }
762 }
763
764 fn allocate(&mut self, blocks: u32, path: &str) -> Result<Option<u32>, Ext4Error> {
765 if blocks == 0 {
766 return Ok(None);
767 }
768
769 for region in &mut self.regions {
770 if region.1 >= blocks {
771 let start = region.0;
772 region.0 += blocks;
773 region.1 -= blocks;
774 return Ok(Some(start));
775 }
776 }
777
778 Err(Ext4Error::Layout(format!(
779 "not enough space in upper.ext4 for '{path}'"
780 )))
781 }
782}
783
784fn build_block_bitmaps_for_plan(layout: &Layout, plans: &[NodePlan]) -> Vec<Vec<u8>> {
785 let mut used_extents = Vec::new();
786 used_extents.push((
787 layout.first_data_block,
788 layout.journal_start_block - layout.first_data_block,
789 ));
790 used_extents.push((layout.journal_start_block, layout.journal_blocks));
791
792 for plan in plans {
793 if let Some(start) = plan.block_start
794 && plan.block_count > 0
795 {
796 used_extents.push((start, plan.block_count));
797 }
798 }
799
800 (0..layout.num_groups)
801 .map(|group| {
802 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
803 let group_start = layout.group_start_block(group);
804 let group_end = group_start + layout.blocks_in_group(group);
805
806 for bit in 0..layout.group_metadata_blocks(group) {
807 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
808 }
809
810 for (start, len) in &used_extents {
811 let extent_start = *start;
812 let extent_end = extent_start + *len;
813 let overlap_start = extent_start.max(group_start);
814 let overlap_end = extent_end.min(group_end);
815 if overlap_start < overlap_end {
816 for block in overlap_start..overlap_end {
817 let bit = block - group_start;
818 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
819 }
820 }
821 }
822
823 let blocks_in_group = layout.blocks_in_group(group);
824 for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
825 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
826 }
827
828 bitmap
829 })
830 .collect()
831}
832
833fn build_inode_bitmaps_for_plan(layout: &Layout, plans: &[NodePlan]) -> Vec<Vec<u8>> {
834 let max_used_inode = plans
835 .iter()
836 .map(|plan| plan.inode)
837 .max()
838 .unwrap_or(EXT4_JOURNAL_INO)
839 .max(EXT4_FIRST_INO - 1);
840
841 (0..layout.num_groups)
842 .map(|group| {
843 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
844 if group == 0 {
845 for bit in 0..max_used_inode {
846 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
847 }
848 }
849 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
850 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
851 }
852 bitmap
853 })
854 .collect()
855}
856
857fn compute_fs_stats(layout: &Layout, block_bitmaps: &[Vec<u8>], plans: &[NodePlan]) -> FsStats {
858 let max_used_inode = plans
859 .iter()
860 .map(|plan| plan.inode)
861 .max()
862 .unwrap_or(EXT4_JOURNAL_INO)
863 .max(EXT4_FIRST_INO - 1);
864 let dir_count = plans
865 .iter()
866 .filter(|plan| matches!(plan.kind, NodeKind::Directory { .. }))
867 .count() as u32;
868
869 let mut group_free_blocks = Vec::with_capacity(layout.num_groups as usize);
870 let mut total_free_blocks = 0u64;
871 let mut total_used_blocks = 0u64;
872 for (group, bitmap) in block_bitmaps
873 .iter()
874 .enumerate()
875 .take(layout.num_groups as usize)
876 {
877 let blocks_in_group = layout.blocks_in_group(group as u32) as usize;
878 let used = count_used_bits(bitmap, blocks_in_group);
879 let free = blocks_in_group.saturating_sub(used) as u32;
880 group_free_blocks.push(free);
881 total_free_blocks += free as u64;
882 total_used_blocks += used as u64;
883 }
884
885 let mut group_free_inodes = vec![EXT4_INODES_PER_GROUP; layout.num_groups as usize];
886 group_free_inodes[0] = EXT4_INODES_PER_GROUP - max_used_inode;
887 let total_free_inodes = group_free_inodes.iter().map(|count| *count as u64).sum();
888
889 let mut group_used_dirs = vec![0u32; layout.num_groups as usize];
890 group_used_dirs[0] = dir_count;
891
892 FsStats {
893 group_free_blocks,
894 group_free_inodes,
895 group_used_dirs,
896 total_free_blocks,
897 total_free_inodes,
898 total_used_blocks,
899 }
900}
901
902fn count_used_bits(bitmap: &[u8], bits: usize) -> usize {
903 let full_bytes = bits / 8;
904 let mut used: usize = bitmap[..full_bytes]
905 .iter()
906 .map(|b| b.count_ones() as usize)
907 .sum();
908
909 let remaining = bits % 8;
911 if remaining > 0 {
912 let mask = (1u8 << remaining) - 1;
913 used += (bitmap[full_bytes] & mask).count_ones() as usize;
914 }
915 used
916}
917
918fn write_tree_data(
919 file: &mut (impl std::io::Write + std::io::Seek),
920 layout: &Layout,
921 plans: &[NodePlan],
922) -> Result<(), Ext4Error> {
923 for plan in plans {
924 match &plan.kind {
925 NodeKind::Directory { data, .. } => {
926 let start = plan.block_start.unwrap_or(layout.first_data_block);
927 let mut bytes = data.clone();
928 update_dir_block_checksums(layout.csum_seed, plan.inode, &mut bytes);
929 write_extent_bytes(file, start, &bytes)?;
930 }
931 NodeKind::RegularFile { data } => {
932 if let Some(start) = plan.block_start {
933 write_extent_bytes(file, start, data)?;
934 }
935 }
936 NodeKind::Symlink { target, inline } => {
937 if !inline && let Some(start) = plan.block_start {
938 write_extent_bytes(file, start, target)?;
939 }
940 }
941 NodeKind::CharDevice { .. } => {}
942 }
943 }
944
945 Ok(())
946}
947
948fn write_extent_bytes(
949 file: &mut (impl std::io::Write + std::io::Seek),
950 start_block: u32,
951 data: &[u8],
952) -> Result<(), Ext4Error> {
953 let offset = start_block as u64 * EXT4_BLOCK_SIZE as u64;
954 file.seek(SeekFrom::Start(offset))?;
955 file.write_all(data)?;
956
957 let pad = (EXT4_BLOCK_SIZE as usize - (data.len() % EXT4_BLOCK_SIZE as usize))
958 % EXT4_BLOCK_SIZE as usize;
959 if pad > 0 {
960 static ZEROS: [u8; 4096] = [0u8; 4096];
961 file.write_all(&ZEROS[..pad])?;
962 }
963
964 Ok(())
965}
966
967fn update_dir_block_checksums(csum_seed: u32, inode: u32, data: &mut [u8]) {
968 for chunk in data.chunks_exact_mut(EXT4_BLOCK_SIZE as usize) {
969 let tail = EXT4_BLOCK_SIZE as usize - 12;
970 let checksum = dir_block_checksum(csum_seed, inode, 0, &chunk[..tail]);
971 put_le32(chunk, tail + 8, checksum);
972 }
973}
974
975fn write_inode_table_with_plan(
976 file: &mut (impl std::io::Write + std::io::Seek),
977 layout: &Layout,
978 plans: &[NodePlan],
979) -> Result<(), Ext4Error> {
980 let table_offset = layout.inode_table_block as u64 * EXT4_BLOCK_SIZE as u64;
981
982 let root_inode = build_inode_from_plan(layout, &plans[0])?;
983 let root_offset = table_offset + (EXT4_ROOT_INO as u64 - 1) * EXT4_INODE_SIZE as u64;
984 file.seek(SeekFrom::Start(root_offset))?;
985 file.write_all(&root_inode)?;
986
987 let journal_inode = build_journal_inode(layout);
988 let journal_offset = table_offset + (EXT4_JOURNAL_INO as u64 - 1) * EXT4_INODE_SIZE as u64;
989 file.seek(SeekFrom::Start(journal_offset))?;
990 file.write_all(&journal_inode)?;
991
992 for plan in plans.iter().filter(|plan| plan.inode >= EXT4_FIRST_INO) {
993 let inode_bytes = build_inode_from_plan(layout, plan)?;
994 let inode_offset = table_offset + (plan.inode as u64 - 1) * EXT4_INODE_SIZE as u64;
995 file.seek(SeekFrom::Start(inode_offset))?;
996 file.write_all(&inode_bytes)?;
997 }
998
999 Ok(())
1000}
1001
1002fn build_inode_from_plan(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
1003 let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1004 let (mode, size, links_count, extents) = match &plan.kind {
1005 NodeKind::Directory { children, data } => (
1006 S_IFDIR | normalize_dir_permissions(plan.permissions),
1007 data.len() as u64,
1008 2 + *children,
1009 true,
1010 ),
1011 NodeKind::RegularFile { data } => (
1012 S_IFREG | normalize_file_permissions(plan.permissions),
1013 data.len() as u64,
1014 1,
1015 true,
1016 ),
1017 NodeKind::Symlink { target, inline } => (S_IFLNK | 0o777, target.len() as u64, 1, !inline),
1018 NodeKind::CharDevice { .. } => (S_IFCHR | plan.permissions, 0, 1, false),
1019 };
1020
1021 put_le16(&mut inode, 0x00, mode);
1022 put_le16(&mut inode, 0x02, plan.uid);
1023 put_le32(&mut inode, 0x04, size as u32);
1024 put_le16(&mut inode, 0x18, plan.gid);
1025 put_le16(&mut inode, 0x1A, links_count);
1026 put_le32(&mut inode, 0x1C, plan.block_count * (EXT4_BLOCK_SIZE / 512));
1027 if extents {
1028 put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1029 }
1030
1031 match &plan.kind {
1032 NodeKind::Directory { .. } | NodeKind::RegularFile { .. } => {
1033 if let Some(start) = plan.block_start {
1034 write_extent_tree(&mut inode, 0x28, start, plan.block_count as u16);
1035 } else {
1036 write_empty_extent_tree(&mut inode, 0x28);
1037 }
1038 }
1039 NodeKind::Symlink { target, inline } => {
1040 if *inline {
1041 inode[0x28..0x28 + target.len()].copy_from_slice(target);
1042 } else if let Some(start) = plan.block_start {
1043 write_extent_tree(&mut inode, 0x28, start, plan.block_count as u16);
1044 }
1045 }
1046 NodeKind::CharDevice { major, minor } => {
1047 put_le32(&mut inode, 0x28, (*minor & 0xFF) | (major << 8));
1048 }
1049 }
1050
1051 put_le32(&mut inode, 0x64, 0);
1052 put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1053 put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1054
1055 let csum = inode_checksum(layout.csum_seed, plan.inode, 0, &inode);
1056 put_le16(&mut inode, 0x7C, csum as u16);
1057 put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1058
1059 Ok(inode)
1060}
1061
1062fn write_empty_extent_tree(buf: &mut [u8], offset: usize) {
1063 put_le16(buf, offset, EXT4_EH_MAGIC);
1064 put_le16(buf, offset + 2, 0);
1065 put_le16(buf, offset + 4, 4);
1066 put_le16(buf, offset + 6, 0);
1067 put_le32(buf, offset + 8, 0);
1068}
1069
1070fn build_superblock_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1071 let mut block = build_superblock(layout)?;
1072 let sb = &mut block[1024..2048];
1073 put_le32(sb, 0x0C, stats.total_free_blocks as u32);
1074 put_le32(sb, 0x10, stats.total_free_inodes as u32);
1075 put_le32(sb, 0x158, (stats.total_free_blocks >> 32) as u32);
1076 put_le32(sb, 0x194, stats.total_used_blocks as u32);
1077 put_le32(sb, 0x3FC, 0);
1078 let checksum = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
1079 put_le32(sb, 0x3FC, checksum);
1080 Ok(block)
1081}
1082
1083fn build_gdt_with_stats(
1084 layout: &Layout,
1085 stats: &FsStats,
1086 block_bitmaps: &[Vec<u8>],
1087 inode_bitmaps: &[Vec<u8>],
1088) -> Result<Vec<u8>, Ext4Error> {
1089 let desc_size = EXT4_DESC_SIZE as usize;
1090 let mut gdt = vec![0u8; layout.num_groups as usize * desc_size];
1091
1092 for g in 0..layout.num_groups {
1093 let off = g as usize * desc_size;
1094 let desc = &mut gdt[off..off + desc_size];
1095 let bb = layout.group_block_bitmap_block(g);
1096 let ib = layout.group_inode_bitmap_block(g);
1097 let it = layout.group_inode_table_block(g);
1098 let bb_csum = bitmap_checksum(
1099 layout.csum_seed,
1100 &block_bitmaps[g as usize],
1101 EXT4_BLOCK_SIZE as usize,
1102 );
1103 let ib_csum = bitmap_checksum(
1104 layout.csum_seed,
1105 &inode_bitmaps[g as usize],
1106 (EXT4_INODES_PER_GROUP / 8) as usize,
1107 );
1108
1109 put_le32(desc, 0x00, bb);
1110 put_le32(desc, 0x04, ib);
1111 put_le32(desc, 0x08, it);
1112 put_le16(desc, 0x0C, stats.group_free_blocks[g as usize] as u16);
1113 put_le16(desc, 0x0E, stats.group_free_inodes[g as usize] as u16);
1114 put_le16(desc, 0x10, stats.group_used_dirs[g as usize] as u16);
1115 put_le16(desc, 0x12, EXT4_BG_INODE_ZEROED);
1116 put_le16(desc, 0x18, bb_csum as u16);
1117 put_le16(desc, 0x1A, ib_csum as u16);
1118 put_le16(desc, 0x1C, stats.group_free_inodes[g as usize] as u16);
1119 put_le16(desc, 0x38, (bb_csum >> 16) as u16);
1120 put_le16(desc, 0x3A, (ib_csum >> 16) as u16);
1121 put_le16(desc, 0x1E, 0);
1122 let checksum = gdt_checksum(layout.csum_seed, g, desc);
1123 put_le16(desc, 0x1E, checksum);
1124 }
1125
1126 Ok(gdt)
1127}
1128#[cfg(test)]
1129fn build_block_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1130 (0..layout.num_groups)
1131 .map(|group| build_block_bitmap(layout, group))
1132 .collect()
1133}
1134
1135#[cfg(test)]
1136fn build_inode_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1137 (0..layout.num_groups)
1138 .map(|group| build_inode_bitmap(layout, group))
1139 .collect()
1140}
1141
1142#[cfg(test)]
1143fn build_block_bitmap(layout: &Layout, group: u32) -> Vec<u8> {
1144 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1145
1146 let used = layout.group_used_blocks(group);
1149 for bit in 0..used {
1150 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1151 }
1152
1153 let blocks_in_group = layout.blocks_in_group(group);
1155 for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
1156 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1157 }
1158
1159 bitmap
1160}
1161
1162#[cfg(test)]
1163fn build_inode_bitmap(_layout: &Layout, group: u32) -> Vec<u8> {
1164 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1165
1166 if group == 0 {
1167 for bit in 0..(EXT4_FIRST_INO - 1) {
1169 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1170 }
1171 }
1172
1173 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
1176 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1177 }
1178
1179 bitmap
1180}
1181
1182fn write_bitmaps(
1183 file: &mut (impl std::io::Write + std::io::Seek),
1184 layout: &Layout,
1185 block_bitmaps: &[Vec<u8>],
1186 inode_bitmaps: &[Vec<u8>],
1187) -> Result<(), Ext4Error> {
1188 for group in 0..layout.num_groups as usize {
1189 let block_offset =
1190 layout.group_block_bitmap_block(group as u32) as u64 * EXT4_BLOCK_SIZE as u64;
1191 file.seek(SeekFrom::Start(block_offset))?;
1192 file.write_all(&block_bitmaps[group])?;
1193
1194 let inode_offset =
1195 layout.group_inode_bitmap_block(group as u32) as u64 * EXT4_BLOCK_SIZE as u64;
1196 file.seek(SeekFrom::Start(inode_offset))?;
1197 file.write_all(&inode_bitmaps[group])?;
1198 }
1199
1200 Ok(())
1201}
1202
1203fn build_journal_inode(layout: &Layout) -> Vec<u8> {
1205 let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1206
1207 let mode = S_IFREG | 0o600;
1208 let size = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1209
1210 put_le16(&mut inode, 0x00, mode);
1212 put_le32(&mut inode, 0x04, size as u32);
1214 put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1216 put_le16(&mut inode, 0x1A, 1);
1218 let sectors = (layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64) / 512;
1220 put_le32(&mut inode, 0x1C, sectors as u32);
1221 put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1223
1224 write_extent_tree(
1226 &mut inode,
1227 0x28,
1228 layout.journal_start_block,
1229 layout.journal_blocks as u16,
1230 );
1231
1232 put_le32(&mut inode, 0x64, 0);
1234
1235 put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1238
1239 let csum = inode_checksum(layout.csum_seed, EXT4_JOURNAL_INO, 0, &inode);
1241 put_le16(&mut inode, 0x7C, csum as u16);
1243 put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1245
1246 inode
1247}
1248
1249fn write_extent_tree(buf: &mut [u8], offset: usize, start_block: u32, block_count: u16) {
1253 put_le16(buf, offset, EXT4_EH_MAGIC); put_le16(buf, offset + 2, 1); put_le16(buf, offset + 4, 4); put_le16(buf, offset + 6, 0); put_le32(buf, offset + 8, 0); let ext_off = offset + 12;
1262 put_le32(buf, ext_off, 0); put_le16(buf, ext_off + 4, block_count); put_le16(buf, ext_off + 6, 0); put_le32(buf, ext_off + 8, start_block); }
1267
1268fn write_journal(
1270 file: &mut (impl std::io::Write + std::io::Seek),
1271 layout: &Layout,
1272) -> Result<(), Ext4Error> {
1273 let mut jsb = vec![0u8; EXT4_BLOCK_SIZE as usize];
1274
1275 put_be32(&mut jsb, 0, JBD2_MAGIC); put_be32(&mut jsb, 4, JBD2_SUPERBLOCK_V2); put_be32(&mut jsb, 8, 0); put_be32(&mut jsb, 12, EXT4_BLOCK_SIZE); put_be32(&mut jsb, 16, layout.journal_blocks); put_be32(&mut jsb, 20, 1); put_be32(&mut jsb, 24, 1); put_be32(&mut jsb, 28, 0); put_be32(&mut jsb, 32, 0);
1290 put_be32(&mut jsb, 36, 0);
1292 put_be32(&mut jsb, 40, 0x13);
1294 put_be32(&mut jsb, 44, 0);
1296
1297 jsb[48..64].copy_from_slice(&layout.uuid);
1299
1300 put_be32(&mut jsb, 64, 1);
1302
1303 put_be32(&mut jsb, 68, 0);
1305
1306 put_be32(&mut jsb, 72, 0);
1308 put_be32(&mut jsb, 76, 0);
1309
1310 jsb[0x50] = 4; let jsb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb[..JBD2_SUPERBLOCK_SIZE]);
1350 put_be32(&mut jsb, 0xFC, jsb_csum);
1351
1352 let offset = layout.journal_start_block as u64 * EXT4_BLOCK_SIZE as u64;
1353 file.seek(SeekFrom::Start(offset))?;
1354 file.write_all(&jsb)?;
1355
1356 Ok(())
1357}
1358
1359fn build_superblock(layout: &Layout) -> Result<Vec<u8>, Ext4Error> {
1361 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1364 let sb = &mut block[1024..2048]; let total_blocks = layout.num_blocks;
1367 let total_inodes = layout.num_groups as u64 * EXT4_INODES_PER_GROUP as u64;
1368
1369 let free_blocks = layout.total_free_blocks();
1370 let free_inodes = layout.total_free_inodes();
1371
1372 put_le32(sb, 0x00, total_inodes as u32);
1374 put_le32(sb, 0x04, total_blocks as u32);
1376 put_le32(sb, 0x08, 0);
1378 put_le32(sb, 0x0C, free_blocks as u32);
1380 put_le32(sb, 0x10, free_inodes as u32);
1382 put_le32(sb, 0x14, 0);
1384 put_le32(sb, 0x18, EXT4_LOG_BLOCK_SIZE);
1386 put_le32(sb, 0x1C, EXT4_LOG_BLOCK_SIZE);
1388 put_le32(sb, 0x20, EXT4_BLOCKS_PER_GROUP);
1390 put_le32(sb, 0x24, EXT4_BLOCKS_PER_GROUP);
1392 put_le32(sb, 0x28, EXT4_INODES_PER_GROUP);
1394
1395 put_le16(sb, 0x34, 0);
1400 put_le16(sb, 0x36, 0xFFFF);
1402 put_le16(sb, 0x38, EXT4_SUPER_MAGIC);
1404 put_le16(sb, 0x3A, 1);
1406 put_le16(sb, 0x3C, 1);
1408 put_le16(sb, 0x3E, 0);
1410
1411 put_le32(sb, 0x48, 0);
1416 put_le32(sb, 0x4C, 1);
1418
1419 put_le16(sb, 0x50, 0);
1421 put_le16(sb, 0x52, 0);
1423
1424 put_le32(sb, 0x54, EXT4_FIRST_INO);
1427 put_le16(sb, 0x58, EXT4_INODE_SIZE);
1429 put_le16(sb, 0x5A, 0);
1431
1432 put_le32(sb, 0x5C, layout.feature_compat);
1434 put_le32(sb, 0x60, layout.feature_incompat);
1436 put_le32(sb, 0x64, layout.feature_ro_compat);
1438
1439 sb[0x68..0x78].copy_from_slice(&layout.uuid);
1441
1442 put_le32(sb, 0xC8, 0);
1448
1449 sb[0xCC] = 0;
1451 sb[0xCD] = 0;
1452
1453 put_le16(sb, 0xCE, RESERVED_GDT_BLOCKS as u16);
1455
1456 put_le32(sb, 0xE0, EXT4_JOURNAL_INO);
1460 put_le32(sb, 0xE4, 0);
1462 put_le32(sb, 0xE8, 0);
1464
1465 sb[0xEC..0xFC].copy_from_slice(&layout.uuid); sb[0xFC] = 1;
1470 sb[0xFD] = 1;
1472
1473 put_le16(sb, 0xFE, EXT4_DESC_SIZE);
1475
1476 put_le32(sb, 0x100, 0x000C);
1478
1479 put_le32(sb, 0x104, 0);
1481
1482 {
1487 let mut extent_buf = [0u8; 60];
1488 write_extent_tree(
1489 &mut extent_buf,
1490 0,
1491 layout.journal_start_block,
1492 layout.journal_blocks as u16,
1493 );
1494 sb[0x10C..0x10C + 60].copy_from_slice(&extent_buf);
1496 let jsize = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1498 put_le32(sb, 0x10C + 60, jsize as u32);
1499 put_le32(sb, 0x10C + 64, (jsize >> 32) as u32);
1501 }
1502
1503 put_le32(sb, 0x150, (total_blocks >> 32) as u32);
1506 put_le32(sb, 0x154, 0);
1508 put_le32(sb, 0x158, (free_blocks >> 32) as u32);
1510
1511 put_le16(sb, 0x15C, EXT4_MIN_EXTRA_ISIZE);
1513 put_le16(sb, 0x15E, EXT4_MIN_EXTRA_ISIZE);
1515
1516 put_le32(sb, 0x160, 0);
1518
1519 sb[0x174] = 0;
1521
1522 sb[0x175] = 1;
1524
1525 put_le32(sb, 0x194, layout.total_used_blocks() as u32);
1530
1531 put_le32(sb, 0x270, 0);
1536
1537 put_le16(sb, 0x27C, 0);
1539
1540 let sb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
1542 put_le32(sb, 0x3FC, sb_csum);
1543
1544 Ok(block)
1545}
1546
1547#[cfg(test)]
1550fn build_gdt(
1551 layout: &Layout,
1552 block_bitmaps: &[Vec<u8>],
1553 inode_bitmaps: &[Vec<u8>],
1554) -> Result<Vec<u8>, Ext4Error> {
1555 let desc_size = EXT4_DESC_SIZE as usize;
1556 let mut gdt = vec![0u8; layout.num_groups as usize * desc_size];
1557
1558 for g in 0..layout.num_groups {
1559 let off = g as usize * desc_size;
1560 let desc = &mut gdt[off..off + desc_size];
1561 let bb = layout.group_block_bitmap_block(g);
1562 let ib = layout.group_inode_bitmap_block(g);
1563 let it = layout.group_inode_table_block(g);
1564 let bb_csum = bitmap_checksum(
1565 layout.csum_seed,
1566 &block_bitmaps[g as usize],
1567 EXT4_BLOCK_SIZE as usize,
1568 );
1569 let ib_csum = bitmap_checksum(
1570 layout.csum_seed,
1571 &inode_bitmaps[g as usize],
1572 (EXT4_INODES_PER_GROUP / 8) as usize,
1573 );
1574
1575 put_le32(desc, 0x00, bb);
1576 put_le32(desc, 0x04, ib);
1577 put_le32(desc, 0x08, it);
1578 put_le16(desc, 0x0C, layout.group_free_blocks(g) as u16);
1579 put_le16(desc, 0x0E, layout.group_free_inodes(g) as u16);
1580 put_le16(desc, 0x10, layout.group_used_dirs(g) as u16);
1581 put_le16(desc, 0x12, EXT4_BG_INODE_ZEROED);
1582 put_le32(desc, 0x14, 0);
1583 put_le16(desc, 0x18, bb_csum as u16);
1584 put_le16(desc, 0x1A, ib_csum as u16);
1585 put_le16(desc, 0x1C, layout.group_free_inodes(g) as u16);
1586 put_le32(desc, 0x20, 0);
1587 put_le32(desc, 0x24, 0);
1588 put_le32(desc, 0x28, 0);
1589 put_le16(desc, 0x2C, 0);
1590 put_le16(desc, 0x2E, 0);
1591 put_le16(desc, 0x30, 0);
1592 put_le16(desc, 0x32, 0);
1593 put_le32(desc, 0x34, 0);
1594 put_le16(desc, 0x38, (bb_csum >> 16) as u16);
1595 put_le16(desc, 0x3A, (ib_csum >> 16) as u16);
1596
1597 put_le16(desc, 0x1E, 0);
1601 let gdt_csum = gdt_checksum(layout.csum_seed, g, desc);
1602 put_le16(desc, 0x1E, gdt_csum);
1603 }
1604
1605 Ok(gdt)
1606}
1607
1608fn write_superblock_at(
1610 file: &mut (impl std::io::Write + std::io::Seek),
1611 group_start_block: u64,
1612 sb_block: &[u8],
1613) -> Result<(), Ext4Error> {
1614 let offset = group_start_block * EXT4_BLOCK_SIZE as u64;
1619 file.seek(SeekFrom::Start(offset))?;
1620 file.write_all(sb_block)?;
1621 Ok(())
1622}
1623
1624fn write_gdt_at(
1626 file: &mut (impl std::io::Write + std::io::Seek),
1627 group_start_block: u64,
1628 gdt: &[u8],
1629) -> Result<(), Ext4Error> {
1630 let offset = (group_start_block + 1) * EXT4_BLOCK_SIZE as u64;
1631 file.seek(SeekFrom::Start(offset))?;
1632 file.write_all(gdt)?;
1633 Ok(())
1634}
1635
1636fn gdt_checksum(csum_seed: u32, group: u32, desc: &[u8]) -> u16 {
1642 let mut crc = crc32c::crc32c_raw(csum_seed, &group.to_le_bytes());
1643 crc = crc32c::crc32c_raw(crc, desc);
1644 (crc & 0xFFFF) as u16
1645}
1646
1647fn inode_checksum(csum_seed: u32, inum: u32, generation: u32, inode_bytes: &[u8]) -> u32 {
1649 let mut crc = crc32c::crc32c_raw(csum_seed, &inum.to_le_bytes());
1650 crc = crc32c::crc32c_raw(crc, &generation.to_le_bytes());
1651 crc = crc32c::crc32c_raw(crc, &inode_bytes[..0x7C]);
1652 crc = crc32c::crc32c_raw(crc, &[0u8; 2]);
1653 crc = crc32c::crc32c_raw(crc, &inode_bytes[0x7E..0x82]);
1654 crc = crc32c::crc32c_raw(crc, &[0u8; 2]);
1655 crc = crc32c::crc32c_raw(crc, &inode_bytes[0x84..]);
1656 crc
1657}
1658
1659fn bitmap_checksum(csum_seed: u32, bitmap: &[u8], checksum_len: usize) -> u32 {
1667 crc32c::crc32c_raw(csum_seed, &bitmap[..checksum_len])
1668}
1669
1670fn dir_block_checksum(csum_seed: u32, inum: u32, generation: u32, data: &[u8]) -> u32 {
1672 let mut crc = crc32c::crc32c_raw(csum_seed, &inum.to_le_bytes());
1673 crc = crc32c::crc32c_raw(crc, &generation.to_le_bytes());
1674 crc = crc32c::crc32c_raw(crc, data);
1675 crc
1676}
1677
1678fn put_le16(buf: &mut [u8], off: usize, val: u16) {
1683 buf[off..off + 2].copy_from_slice(&val.to_le_bytes());
1684}
1685
1686fn put_le32(buf: &mut [u8], off: usize, val: u32) {
1687 buf[off..off + 4].copy_from_slice(&val.to_le_bytes());
1688}
1689
1690fn put_be32(buf: &mut [u8], off: usize, val: u32) {
1691 buf[off..off + 4].copy_from_slice(&val.to_be_bytes());
1692}
1693
1694pub use format::sparse_super_group;
1699
1700#[cfg(test)]
1705mod tests {
1706 use super::*;
1707
1708 #[test]
1709 fn test_format_creates_file_of_correct_size() {
1710 let dir = tempfile::tempdir().unwrap();
1711 let path = dir.path().join("test.ext4");
1712
1713 let size: u64 = 256 * 1024 * 1024; let opts = Ext4FormatOptions {
1715 size_bytes: size,
1716 journal_blocks: 4096, };
1718
1719 format_ext4(&path, &opts).unwrap();
1720
1721 let meta = std::fs::metadata(&path).unwrap();
1722 assert_eq!(meta.len(), size);
1723 }
1724
1725 #[test]
1726 fn test_format_too_small() {
1727 let dir = tempfile::tempdir().unwrap();
1728 let path = dir.path().join("tiny.ext4");
1729
1730 let opts = Ext4FormatOptions {
1731 size_bytes: 4096, journal_blocks: 16384,
1733 };
1734
1735 let result = format_ext4(&path, &opts);
1736 assert!(matches!(result, Err(Ext4Error::TooSmall)));
1737 }
1738
1739 #[test]
1740 fn test_format_default_options() {
1741 let dir = tempfile::tempdir().unwrap();
1742 let path = dir.path().join("default.ext4");
1743
1744 let opts = Ext4FormatOptions::default();
1745 format_ext4(&path, &opts).unwrap();
1746
1747 let meta = std::fs::metadata(&path).unwrap();
1748 assert_eq!(meta.len(), DEFAULT_SIZE_BYTES);
1749 }
1750
1751 #[test]
1752 fn test_superblock_magic() {
1753 let dir = tempfile::tempdir().unwrap();
1754 let path = dir.path().join("magic.ext4");
1755
1756 let opts = Ext4FormatOptions {
1757 size_bytes: 256 * 1024 * 1024,
1758 journal_blocks: 4096,
1759 };
1760 format_ext4(&path, &opts).unwrap();
1761
1762 let data = std::fs::read(&path).unwrap();
1764 let magic = u16::from_le_bytes([data[1024 + 0x38], data[1024 + 0x39]]);
1765 assert_eq!(magic, EXT4_SUPER_MAGIC);
1766 }
1767
1768 #[test]
1769 fn test_journal_magic() {
1770 let dir = tempfile::tempdir().unwrap();
1771 let path = dir.path().join("journal.ext4");
1772
1773 let opts = Ext4FormatOptions {
1774 size_bytes: 256 * 1024 * 1024,
1775 journal_blocks: 4096,
1776 };
1777 format_ext4(&path, &opts).unwrap();
1778
1779 let layout = Layout::compute(&opts).unwrap();
1780 let data = std::fs::read(&path).unwrap();
1781
1782 let jsb_offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
1784 let magic = u32::from_be_bytes([
1785 data[jsb_offset],
1786 data[jsb_offset + 1],
1787 data[jsb_offset + 2],
1788 data[jsb_offset + 3],
1789 ]);
1790 assert_eq!(magic, JBD2_MAGIC);
1791 }
1792
1793 #[test]
1794 fn test_root_dir_inode_exists() {
1795 let dir = tempfile::tempdir().unwrap();
1796 let path = dir.path().join("rootdir.ext4");
1797
1798 let opts = Ext4FormatOptions {
1799 size_bytes: 256 * 1024 * 1024,
1800 journal_blocks: 4096,
1801 };
1802 format_ext4(&path, &opts).unwrap();
1803
1804 let layout = Layout::compute(&opts).unwrap();
1805 let data = std::fs::read(&path).unwrap();
1806
1807 let inode_offset = layout.inode_table_block as usize * EXT4_BLOCK_SIZE as usize
1809 + (EXT4_INODE_SIZE as usize);
1810 let mode = u16::from_le_bytes([data[inode_offset], data[inode_offset + 1]]);
1811 assert_eq!(mode, S_IFDIR | 0o755);
1812 }
1813
1814 #[test]
1815 fn test_backup_group_bitmap_starts_after_backup_metadata() {
1816 let opts = Ext4FormatOptions {
1817 size_bytes: 256 * 1024 * 1024,
1818 journal_blocks: 4096,
1819 };
1820 let layout = Layout::compute(&opts).unwrap();
1821 let block_bitmaps = build_block_bitmaps(&layout);
1822 let inode_bitmaps = build_inode_bitmaps(&layout);
1823 let gdt = build_gdt(&layout, &block_bitmaps, &inode_bitmaps).unwrap();
1824
1825 let desc = &gdt[EXT4_DESC_SIZE as usize..(2 * EXT4_DESC_SIZE as usize)];
1826 let block_bitmap = u32::from_le_bytes([desc[0], desc[1], desc[2], desc[3]]);
1827 let group_start = layout.group_start_block(1);
1828
1829 assert_eq!(block_bitmap, layout.group_block_bitmap_block(1));
1830 assert!(block_bitmap > group_start + layout.gdt_blocks - 1);
1831 }
1832
1833 #[test]
1834 fn test_inode_bitmap_padding_is_marked_used() {
1835 let layout = Layout::compute(&Ext4FormatOptions {
1836 size_bytes: 256 * 1024 * 1024,
1837 journal_blocks: 4096,
1838 })
1839 .unwrap();
1840
1841 let bitmap = build_inode_bitmap(&layout, 0);
1842 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
1843 assert_ne!(bitmap[(bit / 8) as usize] & (1 << (bit % 8)), 0);
1844 }
1845 }
1846
1847 #[test]
1848 fn test_journal_superblock_checksum_matches_contents() {
1849 let dir = tempfile::tempdir().unwrap();
1850 let path = dir.path().join("journal-csum.ext4");
1851 let opts = Ext4FormatOptions {
1852 size_bytes: 256 * 1024 * 1024,
1853 journal_blocks: 4096,
1854 };
1855
1856 format_ext4(&path, &opts).unwrap();
1857
1858 let layout = Layout::compute(&opts).unwrap();
1859 let data = std::fs::read(&path).unwrap();
1860 let offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
1861 let mut jsb = data[offset..offset + JBD2_SUPERBLOCK_SIZE].to_vec();
1862 let stored = u32::from_be_bytes([jsb[0xFC], jsb[0xFD], jsb[0xFE], jsb[0xFF]]);
1863
1864 jsb[0xFC..0x100].fill(0);
1865 let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb);
1866
1867 assert_eq!(stored, expected);
1868 }
1869
1870 #[test]
1871 fn test_root_dir_checksum_matches_contents() {
1872 let dir = tempfile::tempdir().unwrap();
1873 let path = dir.path().join("rootdir-csum.ext4");
1874 let opts = Ext4FormatOptions {
1875 size_bytes: 256 * 1024 * 1024,
1876 journal_blocks: 4096,
1877 };
1878
1879 format_ext4(&path, &opts).unwrap();
1880
1881 let layout = Layout::compute(&opts).unwrap();
1882 let data = std::fs::read(&path).unwrap();
1883 let sb = &data[1024..2048];
1884 let uuid = &sb[0x68..0x78];
1885 let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, uuid);
1886 let block_offset = layout.first_data_block as usize * EXT4_BLOCK_SIZE as usize;
1887 let tail_offset = block_offset + EXT4_BLOCK_SIZE as usize - 12;
1888 let stored = u32::from_le_bytes([
1889 data[tail_offset + 8],
1890 data[tail_offset + 9],
1891 data[tail_offset + 10],
1892 data[tail_offset + 11],
1893 ]);
1894 let expected = dir_block_checksum(
1895 csum_seed,
1896 EXT4_ROOT_INO,
1897 0,
1898 &data[block_offset..tail_offset],
1899 );
1900
1901 assert_eq!(stored, expected);
1902 }
1903}