1use std::io::{self, BufWriter, SeekFrom, Write};
2use std::path::Path;
3
4use super::format::{
5 EXT4_BG_INODE_ZEROED, EXT4_BLOCK_SIZE, EXT4_BLOCKS_PER_GROUP, EXT4_DESC_SIZE, EXT4_EH_MAGIC,
6 EXT4_EXTENTS_FL, EXT4_FEATURE_COMPAT_DIR_INDEX, EXT4_FEATURE_COMPAT_EXT_ATTR,
7 EXT4_FEATURE_COMPAT_HAS_JOURNAL, EXT4_FEATURE_INCOMPAT_64BIT, EXT4_FEATURE_INCOMPAT_EXTENTS,
8 EXT4_FEATURE_INCOMPAT_FILETYPE, EXT4_FEATURE_RO_COMPAT_DIR_NLINK,
9 EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE, EXT4_FEATURE_RO_COMPAT_HUGE_FILE,
10 EXT4_FEATURE_RO_COMPAT_LARGE_FILE, EXT4_FEATURE_RO_COMPAT_METADATA_CSUM,
11 EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER, EXT4_FIRST_INO, EXT4_INODE_SIZE, EXT4_INODES_PER_GROUP,
12 EXT4_JOURNAL_INO, EXT4_LOG_BLOCK_SIZE, EXT4_MIN_EXTRA_ISIZE, EXT4_ROOT_INO, EXT4_SUPER_MAGIC,
13 JBD2_MAGIC, JBD2_SUPERBLOCK_V2, S_IFCHR, S_IFDIR, S_IFLNK, S_IFREG,
14};
15use crate::crc32c;
16use crate::tree::{DirectoryNode, FileTree, TreeNode};
17
18const DEFAULT_SIZE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
24
25const DEFAULT_JOURNAL_BLOCKS: u32 = 16384;
27
28const MAX_BLOCKS: u64 = u32::MAX as u64;
31
32const MAX_INITIALIZED_EXTENT_BLOCKS: u32 = 32768;
34
35const RESERVED_GDT_BLOCKS: u32 = 0;
37
38const EXT4_FT_DIR: u8 = 2;
40
41#[allow(dead_code)]
43const EXT4_FT_REG_FILE: u8 = 1;
44
45const EXT4_FT_CHRDEV: u8 = 3;
47
48const EXT4_FT_SYMLINK: u8 = 7;
50
51const JBD2_SUPERBLOCK_SIZE: usize = 1024;
53
54pub struct Ext4FormatOptions {
60 pub size_bytes: u64,
63
64 pub journal_blocks: u32,
67}
68
69#[derive(Debug)]
71pub enum Ext4Error {
72 Io(io::Error),
74
75 InvalidSize(String),
77
78 TooSmall,
81
82 TooLarge {
84 requested_blocks: u64,
86
87 max_blocks: u64,
89 },
90
91 Layout(String),
93}
94
95struct Layout {
97 num_blocks: u64,
98 num_groups: u32,
99 uuid: [u8; 16],
100 gdt_blocks: u32,
101 inode_table_block: u64,
103 inode_table_blocks: u32,
105 first_data_block: u64,
107 journal_start_block: u64,
109 journal_blocks: u32,
111 csum_seed: u32,
113 feature_compat: u32,
115 feature_incompat: u32,
117 feature_ro_compat: u32,
119}
120
121struct FsStats {
122 group_free_blocks: Vec<u32>,
123 group_free_inodes: Vec<u32>,
124 group_used_dirs: Vec<u32>,
125 block_bitmap_checksums: Vec<u32>,
126 inode_bitmap_checksums: Vec<u32>,
127 total_free_blocks: u64,
128 total_free_inodes: u64,
129 total_used_blocks: u64,
130}
131
132struct BitmapPlan {
133 block_extents: Vec<(u64, u32)>,
134 max_used_inode: u32,
135 dir_count: u32,
136}
137
138enum NodeKind {
139 Directory { children: u16, data: Vec<u8> },
140 RegularFile { data: Vec<u8> },
141 Symlink { target: Vec<u8>, inline: bool },
142 CharDevice { major: u32, minor: u32 },
143}
144
145struct NodePlan {
146 inode: u32,
147 path: String,
148 permissions: u16,
149 uid: u16,
150 gid: u16,
151 kind: NodeKind,
152 block_start: Option<u64>,
153 block_count: u32,
154}
155
156struct DraftDirectory {
157 children: u16,
158 data: Vec<u8>,
159}
160
161struct DirEntrySpec {
162 inode: u32,
163 file_type: u8,
164 name: Vec<u8>,
165}
166
167struct DataAllocator {
168 regions: Vec<(u64, u32)>,
169}
170
171impl Default for Ext4FormatOptions {
176 fn default() -> Self {
177 Self {
178 size_bytes: DEFAULT_SIZE_BYTES,
179 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
180 }
181 }
182}
183
184impl std::fmt::Display for Ext4Error {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 match self {
187 Ext4Error::Io(e) => write!(f, "ext4 I/O error: {e}"),
188 Ext4Error::InvalidSize(e) => write!(f, "invalid ext4 image size: {e}"),
189 Ext4Error::TooSmall => write!(f, "image size is too small for ext4 formatting"),
190 Ext4Error::TooLarge {
191 requested_blocks,
192 max_blocks,
193 } => write!(
194 f,
195 "image is too large for ext4 formatting: requested {requested_blocks} blocks, maximum is {max_blocks} blocks"
196 ),
197 Ext4Error::Layout(e) => write!(f, "ext4 layout error: {e}"),
198 }
199 }
200}
201
202impl std::error::Error for Ext4Error {
203 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
204 match self {
205 Ext4Error::Io(e) => Some(e),
206 Ext4Error::InvalidSize(_)
207 | Ext4Error::TooSmall
208 | Ext4Error::TooLarge { .. }
209 | Ext4Error::Layout(_) => None,
210 }
211 }
212}
213
214impl From<io::Error> for Ext4Error {
215 fn from(e: io::Error) -> Self {
216 Ext4Error::Io(e)
217 }
218}
219
220impl Layout {
221 #[cfg(test)]
222 fn compute(opts: &Ext4FormatOptions) -> Result<Self, Ext4Error> {
223 Self::compute_with_root_blocks(opts, 1)
224 }
225
226 fn compute_with_root_blocks(
227 opts: &Ext4FormatOptions,
228 root_dir_blocks: u32,
229 ) -> Result<Self, Ext4Error> {
230 let block_size = EXT4_BLOCK_SIZE as u64;
231 if !opts.size_bytes.is_multiple_of(block_size) {
232 return Err(Ext4Error::InvalidSize(format!(
233 "image size must be aligned to {block_size} bytes"
234 )));
235 }
236
237 let num_blocks = opts.size_bytes / block_size;
238 if num_blocks > MAX_BLOCKS {
239 return Err(Ext4Error::TooLarge {
240 requested_blocks: num_blocks,
241 max_blocks: MAX_BLOCKS,
242 });
243 }
244
245 if opts.journal_blocks == 0 {
246 return Err(Ext4Error::InvalidSize(
247 "journal must contain at least one block".to_string(),
248 ));
249 }
250 validate_extent_block_count(opts.journal_blocks, "journal")?;
251
252 let num_groups_raw = num_blocks.div_ceil(EXT4_BLOCKS_PER_GROUP as u64);
253 let num_groups = u32::try_from(num_groups_raw).map_err(|_| Ext4Error::TooLarge {
254 requested_blocks: num_blocks,
255 max_blocks: MAX_BLOCKS,
256 })?;
257
258 let inode_table_blocks =
261 (EXT4_INODES_PER_GROUP as u64 * EXT4_INODE_SIZE as u64 / block_size) as u32;
262 let gdt_blocks = (num_groups as u64 * EXT4_DESC_SIZE as u64).div_ceil(block_size) as u32;
263
264 let overhead_blocks = 1u64 + gdt_blocks as u64 + RESERVED_GDT_BLOCKS as u64;
275 let block_bitmap_block = overhead_blocks;
276 let inode_bitmap_block = block_bitmap_block + 1;
277 let inode_table_block = inode_bitmap_block + 1;
278 let first_data_block = inode_table_block + inode_table_blocks as u64;
279 let journal_start_block = first_data_block + root_dir_blocks as u64;
280
281 let min_blocks = journal_start_block + opts.journal_blocks as u64 + 1; if num_blocks < min_blocks {
283 return Err(Ext4Error::TooSmall);
284 }
285
286 let uuid = Self::generate_uuid();
288
289 let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, &uuid);
290
291 let feature_compat = EXT4_FEATURE_COMPAT_HAS_JOURNAL
292 | EXT4_FEATURE_COMPAT_EXT_ATTR
293 | EXT4_FEATURE_COMPAT_DIR_INDEX;
294
295 let feature_incompat = EXT4_FEATURE_INCOMPAT_FILETYPE
296 | EXT4_FEATURE_INCOMPAT_EXTENTS
297 | EXT4_FEATURE_INCOMPAT_64BIT;
298
299 let feature_ro_compat = EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER
300 | EXT4_FEATURE_RO_COMPAT_LARGE_FILE
301 | EXT4_FEATURE_RO_COMPAT_HUGE_FILE
302 | EXT4_FEATURE_RO_COMPAT_DIR_NLINK
303 | EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE
304 | EXT4_FEATURE_RO_COMPAT_METADATA_CSUM;
305
306 let layout = Layout {
307 num_blocks,
308 num_groups,
309 uuid,
310 gdt_blocks,
311 inode_table_block,
312 inode_table_blocks,
313 first_data_block,
314 journal_start_block,
315 journal_blocks: opts.journal_blocks,
316 csum_seed,
317 feature_compat,
318 feature_incompat,
319 feature_ro_compat,
320 };
321 layout.validate_group_metadata()?;
322
323 Ok(layout)
324 }
325
326 fn generate_uuid() -> [u8; 16] {
327 let mut uuid = [0u8; 16];
329 if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
330 use std::io::Read;
331 let _ = f.read_exact(&mut uuid);
332 } else {
333 let now = std::time::SystemTime::now()
335 .duration_since(std::time::UNIX_EPOCH)
336 .unwrap_or_default();
337 let nanos = now.as_nanos();
338 uuid[..8].copy_from_slice(&(nanos as u64).to_le_bytes());
339 uuid[8..16].copy_from_slice(&((nanos >> 64) as u64).to_le_bytes());
340 }
341 uuid[6] = (uuid[6] & 0x0F) | 0x40;
343 uuid[7] = (uuid[7] & 0x3F) | 0x80;
344 uuid
345 }
346
347 fn group_start_block(&self, group: u32) -> u64 {
348 group as u64 * EXT4_BLOCKS_PER_GROUP as u64
349 }
350
351 fn blocks_in_group(&self, group: u32) -> u32 {
352 let group_start = self.group_start_block(group);
353 std::cmp::min(
354 EXT4_BLOCKS_PER_GROUP as u64,
355 self.num_blocks.saturating_sub(group_start),
356 ) as u32
357 }
358
359 fn group_has_backup_super(&self, group: u32) -> bool {
360 group == 0 || sparse_super_group(group)
361 }
362
363 fn group_leading_overhead_blocks(&self, group: u32) -> u32 {
364 if self.group_has_backup_super(group) {
365 1 + self.gdt_blocks + RESERVED_GDT_BLOCKS
366 } else {
367 0
368 }
369 }
370
371 fn group_block_bitmap_block(&self, group: u32) -> u64 {
372 self.group_start_block(group) + self.group_leading_overhead_blocks(group) as u64
373 }
374
375 fn group_inode_bitmap_block(&self, group: u32) -> u64 {
376 self.group_block_bitmap_block(group) + 1
377 }
378
379 fn group_inode_table_block(&self, group: u32) -> u64 {
380 self.group_inode_bitmap_block(group) + 1
381 }
382
383 fn group_data_start_block(&self, group: u32) -> u64 {
384 let mut start = self.group_start_block(group) + self.group_metadata_blocks(group) as u64;
385 if group == 0 {
386 start = self.journal_start_block + self.journal_blocks as u64;
387 }
388 start
389 }
390
391 fn group_metadata_blocks(&self, group: u32) -> u32 {
392 self.group_leading_overhead_blocks(group) + 2 + self.inode_table_blocks
393 }
394
395 fn group_used_blocks(&self, group: u32) -> u32 {
396 let mut used = self.group_metadata_blocks(group);
397 if group == 0 {
398 used += 1 + self.journal_blocks; }
400 used.min(self.blocks_in_group(group))
401 }
402
403 fn group_free_blocks(&self, group: u32) -> u32 {
404 self.blocks_in_group(group)
405 .saturating_sub(self.group_used_blocks(group))
406 }
407
408 fn group_free_inodes(&self, group: u32) -> u32 {
409 if group == 0 {
410 EXT4_INODES_PER_GROUP - (EXT4_FIRST_INO - 1)
411 } else {
412 EXT4_INODES_PER_GROUP
413 }
414 }
415
416 #[cfg(test)]
417 fn group_used_dirs(&self, group: u32) -> u32 {
418 if group == 0 { 1 } else { 0 }
419 }
420
421 fn total_free_blocks(&self) -> u64 {
422 (0..self.num_groups)
423 .map(|group| self.group_free_blocks(group) as u64)
424 .sum()
425 }
426
427 fn total_free_inodes(&self) -> u64 {
428 (0..self.num_groups)
429 .map(|group| self.group_free_inodes(group) as u64)
430 .sum()
431 }
432
433 fn total_used_blocks(&self) -> u64 {
434 (0..self.num_groups)
435 .map(|group| self.group_used_blocks(group) as u64)
436 .sum()
437 }
438
439 fn validate_group_metadata(&self) -> Result<(), Ext4Error> {
440 for group in 0..self.num_groups {
441 let blocks_in_group = self.blocks_in_group(group);
442 let metadata_blocks = self.group_metadata_blocks(group);
443 if blocks_in_group < metadata_blocks {
444 return Err(Ext4Error::InvalidSize(format!(
445 "block group {group} has {blocks_in_group} blocks but needs at least {metadata_blocks} metadata blocks; choose a size that leaves either no partial group or a larger final group"
446 )));
447 }
448 }
449
450 Ok(())
451 }
452}
453
454impl BitmapPlan {
455 fn new(layout: &Layout, plans: &[NodePlan]) -> Self {
456 let mut block_extents = Vec::new();
457 block_extents.push((
458 layout.first_data_block,
459 (layout.journal_start_block - layout.first_data_block) as u32,
460 ));
461 block_extents.push((layout.journal_start_block, layout.journal_blocks));
462
463 for plan in plans {
464 if let Some(start) = plan.block_start
465 && plan.block_count > 0
466 {
467 block_extents.push((start, plan.block_count));
468 }
469 }
470
471 let max_used_inode = plans
472 .iter()
473 .map(|plan| plan.inode)
474 .max()
475 .unwrap_or(EXT4_JOURNAL_INO)
476 .max(EXT4_FIRST_INO - 1);
477 let dir_count = plans
478 .iter()
479 .filter(|plan| matches!(plan.kind, NodeKind::Directory { .. }))
480 .count() as u32;
481
482 Self {
483 block_extents,
484 max_used_inode,
485 dir_count,
486 }
487 }
488}
489
490pub fn format_ext4(path: &Path, options: &Ext4FormatOptions) -> Result<(), Ext4Error> {
500 let tree = FileTree::new();
501 format_ext4_with_tree(path, options, tree)
502}
503
504pub fn format_ext4_with_tree(
505 path: &Path,
506 options: &Ext4FormatOptions,
507 tree: FileTree,
508) -> Result<(), Ext4Error> {
509 let mut next_inode = EXT4_FIRST_INO;
510 let mut plans = Vec::new();
511 let root_mode = tree.root.metadata.mode;
512 let root_draft = draft_directory(
513 "/",
514 tree.root,
515 EXT4_ROOT_INO,
516 EXT4_ROOT_INO,
517 &mut next_inode,
518 &mut plans,
519 )?;
520 let root_dir_blocks = blocks_for_len(root_draft.data.len());
521 let layout = Layout::compute_with_root_blocks(options, root_dir_blocks.max(1))?;
522 let mut allocator = DataAllocator::new(&layout);
523
524 for plan in &mut plans {
525 allocate_node_data(&mut allocator, plan)?;
526 }
527
528 let mut all_plans = Vec::with_capacity(plans.len() + 1);
529 all_plans.push(NodePlan {
530 inode: EXT4_ROOT_INO,
531 path: "/".to_string(),
532 permissions: normalize_dir_permissions(root_mode),
533 uid: 0,
534 gid: 0,
535 kind: NodeKind::Directory {
536 children: root_draft.children,
537 data: root_draft.data,
538 },
539 block_start: Some(layout.first_data_block),
540 block_count: root_dir_blocks.max(1),
541 });
542 all_plans.extend(plans);
543 all_plans.sort_by_key(|plan| plan.inode);
544 validate_node_extents(&all_plans)?;
545
546 let bitmap_plan = BitmapPlan::new(&layout, &all_plans);
547 let stats = compute_fs_stats(&layout, &bitmap_plan);
548
549 let raw_file = std::fs::File::create(path)?;
550 raw_file.set_len(options.size_bytes)?;
551 let mut file = BufWriter::new(raw_file);
552
553 write_bitmaps(&mut file, &layout, &bitmap_plan)?;
554 write_tree_data(&mut file, &layout, &all_plans)?;
555 write_inode_table_with_plan(&mut file, &layout, &all_plans)?;
556 write_journal(&mut file, &layout)?;
557
558 let sb_bytes = build_superblock_with_stats(&layout, &stats)?;
559 write_primary_superblock_at(&mut file, &sb_bytes)?;
560
561 let gdt_bytes = build_gdt_with_stats(&layout, &stats)?;
562 write_gdt_at(&mut file, 0, &gdt_bytes)?;
563
564 for g in 1..layout.num_groups {
565 if sparse_super_group(g) {
566 let backup_sb_bytes = build_backup_superblock_with_stats(&layout, &stats, g)?;
567 write_backup_superblock_at(&mut file, layout.group_start_block(g), &backup_sb_bytes)?;
568 write_gdt_at(&mut file, layout.group_start_block(g), &gdt_bytes)?;
569 }
570 }
571
572 file.flush()?;
573 Ok(())
577}
578
579fn draft_directory(
580 path: &str,
581 dir: DirectoryNode,
582 inode: u32,
583 parent_inode: u32,
584 next_inode: &mut u32,
585 plans: &mut Vec<NodePlan>,
586) -> Result<DraftDirectory, Ext4Error> {
587 if !dir.xattrs.is_empty() {
588 return Err(Ext4Error::Layout(format!(
589 "ext4 patch baking does not yet support xattrs on '{path}'"
590 )));
591 }
592
593 let mut children = Vec::new();
594 let mut child_dir_count = 0u16;
595
596 for (name, node) in dir.entries {
597 let name_bytes = name.as_os_str().as_encoded_bytes().to_vec();
598 let child_path = child_path(path, &name_bytes);
599 let child_inode = *next_inode;
600 if child_inode >= EXT4_INODES_PER_GROUP {
601 return Err(Ext4Error::Layout(
602 "too many upper-layer inodes for group 0 inode table".to_string(),
603 ));
604 }
605 *next_inode += 1;
606
607 match node {
608 TreeNode::Directory(child_dir) => {
609 child_dir_count = child_dir_count.saturating_add(1);
610 let dir_mode = child_dir.metadata.mode;
611 let child_draft = draft_directory(
612 &child_path,
613 child_dir,
614 child_inode,
615 inode,
616 next_inode,
617 plans,
618 )?;
619 let block_count = blocks_for_len(child_draft.data.len());
620 plans.push(NodePlan {
621 inode: child_inode,
622 path: child_path.clone(),
623 permissions: normalize_dir_permissions(dir_mode),
624 uid: 0,
625 gid: 0,
626 kind: NodeKind::Directory {
627 children: child_draft.children,
628 data: child_draft.data,
629 },
630 block_start: None,
631 block_count,
632 });
633 children.push(DirEntrySpec {
634 inode: child_inode,
635 file_type: EXT4_FT_DIR,
636 name: name_bytes,
637 });
638 }
639 TreeNode::RegularFile(file) => {
640 if !file.xattrs.is_empty() {
641 return Err(Ext4Error::Layout(format!(
642 "ext4 patch baking does not yet support xattrs on '{child_path}'"
643 )));
644 }
645 plans.push(NodePlan {
646 inode: child_inode,
647 path: child_path.clone(),
648 permissions: normalize_file_permissions(file.metadata.mode),
649 uid: 0,
650 gid: 0,
651 block_count: blocks_for_len(file.data.len()),
652 kind: NodeKind::RegularFile {
653 data: file.data.read_all().map_err(Ext4Error::Io)?,
654 },
655 block_start: None,
656 });
657 children.push(DirEntrySpec {
658 inode: child_inode,
659 file_type: EXT4_FT_REG_FILE,
660 name: name_bytes,
661 });
662 }
663 TreeNode::Symlink(symlink) => {
664 let target_len = symlink.target.len();
665 let inline = target_len <= 59;
666 let block_count = if inline {
667 0
668 } else {
669 blocks_for_len(target_len)
670 };
671 plans.push(NodePlan {
672 inode: child_inode,
673 path: child_path.clone(),
674 permissions: 0o777,
675 uid: 0,
676 gid: 0,
677 kind: NodeKind::Symlink {
678 target: symlink.target,
679 inline,
680 },
681 block_start: None,
682 block_count,
683 });
684 children.push(DirEntrySpec {
685 inode: child_inode,
686 file_type: EXT4_FT_SYMLINK,
687 name: name_bytes,
688 });
689 }
690 TreeNode::CharDevice(device) => {
691 plans.push(NodePlan {
692 inode: child_inode,
693 path: child_path.clone(),
694 permissions: 0,
695 uid: 0,
696 gid: 0,
697 kind: NodeKind::CharDevice {
698 major: device.major,
699 minor: device.minor,
700 },
701 block_start: None,
702 block_count: 0,
703 });
704 children.push(DirEntrySpec {
705 inode: child_inode,
706 file_type: EXT4_FT_CHRDEV,
707 name: name_bytes,
708 });
709 }
710 _ => {
711 return Err(Ext4Error::Layout(format!(
712 "unsupported upper-layer node at '{child_path}'"
713 )));
714 }
715 }
716 }
717
718 let data = build_directory_data(inode, parent_inode, &children, path)?;
719 Ok(DraftDirectory {
720 children: child_dir_count,
721 data,
722 })
723}
724
725fn child_path(parent: &str, name: &[u8]) -> String {
726 let name = String::from_utf8_lossy(name);
727 if parent == "/" {
728 format!("/{name}")
729 } else {
730 format!("{parent}/{name}")
731 }
732}
733
734fn normalize_file_permissions(mode: u16) -> u16 {
735 let perms = mode & 0o7777;
736 if perms == 0 { 0o644 } else { perms }
737}
738
739fn normalize_dir_permissions(mode: u16) -> u16 {
740 let perms = mode & 0o7777;
741 if perms == 0 { 0o755 } else { perms }
742}
743
744fn blocks_for_len(len: usize) -> u32 {
745 if len == 0 {
746 0
747 } else {
748 (len as u64).div_ceil(EXT4_BLOCK_SIZE as u64) as u32
749 }
750}
751
752fn validate_node_extents(plans: &[NodePlan]) -> Result<(), Ext4Error> {
753 for plan in plans {
754 validate_extent_block_count(plan.block_count, &plan.path)?;
755 }
756
757 Ok(())
758}
759
760fn validate_extent_block_count(block_count: u32, label: &str) -> Result<(), Ext4Error> {
761 if block_count > MAX_INITIALIZED_EXTENT_BLOCKS {
762 return Err(Ext4Error::Layout(format!(
763 "'{label}' needs {block_count} blocks but this formatter currently supports one initialized extent of at most {MAX_INITIALIZED_EXTENT_BLOCKS} blocks"
764 )));
765 }
766
767 Ok(())
768}
769
770fn build_directory_data(
771 dir_inode: u32,
772 parent_inode: u32,
773 children: &[DirEntrySpec],
774 path: &str,
775) -> Result<Vec<u8>, Ext4Error> {
776 let mut entries = Vec::with_capacity(children.len() + 2);
777 entries.push(DirEntrySpec {
778 inode: dir_inode,
779 file_type: EXT4_FT_DIR,
780 name: b".".to_vec(),
781 });
782 entries.push(DirEntrySpec {
783 inode: parent_inode,
784 file_type: EXT4_FT_DIR,
785 name: b"..".to_vec(),
786 });
787 entries.extend(children.iter().map(|entry| DirEntrySpec {
788 inode: entry.inode,
789 file_type: entry.file_type,
790 name: entry.name.clone(),
791 }));
792
793 let mut blocks = Vec::new();
794 let mut index = 0usize;
795 while index < entries.len() {
796 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
797 let mut pos = 0usize;
798 let data_limit = EXT4_BLOCK_SIZE as usize - 12;
799 let block_start = index;
800
801 while index < entries.len() {
802 let min_len = dir_entry_len(entries[index].name.len());
803 let needed = if pos == 0 { min_len } else { pos + min_len };
804 if needed > data_limit {
805 if pos == 0 {
806 return Err(Ext4Error::Layout(format!(
807 "directory entry too large for '{path}'"
808 )));
809 }
810 break;
811 }
812 pos += min_len;
813 index += 1;
814 }
815
816 let mut write_pos = 0usize;
817 for (entry_index, entry) in entries[block_start..index].iter().enumerate() {
818 let is_last = entry_index + 1 == index - block_start;
819 let rec_len = if is_last {
820 (data_limit - write_pos) as u16
821 } else {
822 dir_entry_len(entry.name.len()) as u16
823 };
824 put_le32(&mut block, write_pos, entry.inode);
825 put_le16(&mut block, write_pos + 4, rec_len);
826 block[write_pos + 6] = entry.name.len() as u8;
827 block[write_pos + 7] = entry.file_type;
828 block[write_pos + 8..write_pos + 8 + entry.name.len()].copy_from_slice(&entry.name);
829 write_pos += rec_len as usize;
830 }
831
832 let tail = data_limit;
833 put_le32(&mut block, tail, 0);
834 put_le16(&mut block, tail + 4, 12);
835 block[tail + 6] = 0;
836 block[tail + 7] = 0xDE;
837 blocks.extend_from_slice(&block);
838 }
839
840 if blocks.is_empty() {
841 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
842 put_le32(&mut block, 0, dir_inode);
843 put_le16(&mut block, 4, 12);
844 block[6] = 1;
845 block[7] = EXT4_FT_DIR;
846 block[8] = b'.';
847 put_le32(&mut block, 12, parent_inode);
848 put_le16(&mut block, 16, (EXT4_BLOCK_SIZE - 24) as u16);
849 block[18] = 2;
850 block[19] = EXT4_FT_DIR;
851 block[20] = b'.';
852 block[21] = b'.';
853 let tail = EXT4_BLOCK_SIZE as usize - 12;
854 put_le32(&mut block, tail, 0);
855 put_le16(&mut block, tail + 4, 12);
856 block[tail + 7] = 0xDE;
857 blocks = block;
858 }
859
860 Ok(blocks)
861}
862
863fn dir_entry_len(name_len: usize) -> usize {
864 (8 + name_len + 3) & !3
865}
866
867fn allocate_node_data(allocator: &mut DataAllocator, plan: &mut NodePlan) -> Result<(), Ext4Error> {
868 if plan.block_count == 0 {
869 plan.block_start = None;
870 return Ok(());
871 }
872
873 plan.block_start = allocator.allocate(plan.block_count, &plan.path)?;
874 Ok(())
875}
876
877impl DataAllocator {
878 fn new(layout: &Layout) -> Self {
879 let mut regions = Vec::new();
880 for group in 0..layout.num_groups {
881 let group_start = layout.group_start_block(group);
882 let group_end = group_start + layout.blocks_in_group(group) as u64;
883 let start = layout.group_data_start_block(group);
884 if start < group_end {
885 regions.push((start, (group_end - start) as u32));
886 }
887 }
888 Self { regions }
889 }
890
891 fn allocate(&mut self, blocks: u32, path: &str) -> Result<Option<u64>, Ext4Error> {
892 if blocks == 0 {
893 return Ok(None);
894 }
895
896 for region in &mut self.regions {
897 if region.1 >= blocks {
898 let start = region.0;
899 region.0 += blocks as u64;
900 region.1 -= blocks;
901 return Ok(Some(start));
902 }
903 }
904
905 Err(Ext4Error::Layout(format!(
906 "not enough space in upper.ext4 for '{path}'"
907 )))
908 }
909}
910
911fn build_block_bitmap_for_plan(layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
912 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
913 let group_start = layout.group_start_block(group);
914 let group_end = group_start + layout.blocks_in_group(group) as u64;
915
916 for bit in 0..layout.group_metadata_blocks(group) {
917 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
918 }
919
920 for (start, len) in &plan.block_extents {
921 let extent_start = *start;
922 let extent_end = extent_start + *len as u64;
923 let overlap_start = extent_start.max(group_start);
924 let overlap_end = extent_end.min(group_end);
925 if overlap_start < overlap_end {
926 for block in overlap_start..overlap_end {
927 let bit = block - group_start;
928 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
929 }
930 }
931 }
932
933 let blocks_in_group = layout.blocks_in_group(group);
934 for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
935 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
936 }
937
938 bitmap
939}
940
941fn build_inode_bitmap_for_plan(_layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
942 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
943 if group == 0 {
944 for bit in 0..plan.max_used_inode {
945 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
946 }
947 }
948 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
949 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
950 }
951 bitmap
952}
953
954fn compute_fs_stats(layout: &Layout, plan: &BitmapPlan) -> FsStats {
955 let mut group_free_blocks = Vec::with_capacity(layout.num_groups as usize);
956 let mut block_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
957 let mut inode_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
958
959 let mut total_free_blocks = 0u64;
960 let mut total_used_blocks = 0u64;
961 for group in 0..layout.num_groups {
962 let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
963 let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
964 let blocks_in_group = layout.blocks_in_group(group) as usize;
965 let used = count_used_bits(&block_bitmap, blocks_in_group);
966 let free = blocks_in_group.saturating_sub(used) as u32;
967 group_free_blocks.push(free);
968 block_bitmap_checksums.push(bitmap_checksum(
969 layout.csum_seed,
970 &block_bitmap,
971 EXT4_BLOCK_SIZE as usize,
972 ));
973 inode_bitmap_checksums.push(bitmap_checksum(
974 layout.csum_seed,
975 &inode_bitmap,
976 (EXT4_INODES_PER_GROUP / 8) as usize,
977 ));
978 total_free_blocks += free as u64;
979 total_used_blocks += used as u64;
980 }
981
982 let mut group_free_inodes = vec![EXT4_INODES_PER_GROUP; layout.num_groups as usize];
983 group_free_inodes[0] = EXT4_INODES_PER_GROUP - plan.max_used_inode;
984 let total_free_inodes = group_free_inodes.iter().map(|count| *count as u64).sum();
985
986 let mut group_used_dirs = vec![0u32; layout.num_groups as usize];
987 group_used_dirs[0] = plan.dir_count;
988
989 FsStats {
990 group_free_blocks,
991 group_free_inodes,
992 group_used_dirs,
993 block_bitmap_checksums,
994 inode_bitmap_checksums,
995 total_free_blocks,
996 total_free_inodes,
997 total_used_blocks,
998 }
999}
1000
1001fn count_used_bits(bitmap: &[u8], bits: usize) -> usize {
1002 let full_bytes = bits / 8;
1003 let mut used: usize = bitmap[..full_bytes]
1004 .iter()
1005 .map(|b| b.count_ones() as usize)
1006 .sum();
1007
1008 let remaining = bits % 8;
1010 if remaining > 0 {
1011 let mask = (1u8 << remaining) - 1;
1012 used += (bitmap[full_bytes] & mask).count_ones() as usize;
1013 }
1014 used
1015}
1016
1017fn write_tree_data(
1018 file: &mut (impl std::io::Write + std::io::Seek),
1019 layout: &Layout,
1020 plans: &[NodePlan],
1021) -> Result<(), Ext4Error> {
1022 for plan in plans {
1023 match &plan.kind {
1024 NodeKind::Directory { data, .. } => {
1025 let start = plan.block_start.unwrap_or(layout.first_data_block);
1026 let mut bytes = data.clone();
1027 update_dir_block_checksums(layout.csum_seed, plan.inode, &mut bytes);
1028 write_extent_bytes(file, start, &bytes)?;
1029 }
1030 NodeKind::RegularFile { data } => {
1031 if let Some(start) = plan.block_start {
1032 write_extent_bytes(file, start, data)?;
1033 }
1034 }
1035 NodeKind::Symlink { target, inline } => {
1036 if !inline && let Some(start) = plan.block_start {
1037 write_extent_bytes(file, start, target)?;
1038 }
1039 }
1040 NodeKind::CharDevice { .. } => {}
1041 }
1042 }
1043
1044 Ok(())
1045}
1046
1047fn write_extent_bytes(
1048 file: &mut (impl std::io::Write + std::io::Seek),
1049 start_block: u64,
1050 data: &[u8],
1051) -> Result<(), Ext4Error> {
1052 let offset = start_block * EXT4_BLOCK_SIZE as u64;
1053 file.seek(SeekFrom::Start(offset))?;
1054 file.write_all(data)?;
1055
1056 let pad = (EXT4_BLOCK_SIZE as usize - (data.len() % EXT4_BLOCK_SIZE as usize))
1057 % EXT4_BLOCK_SIZE as usize;
1058 if pad > 0 {
1059 static ZEROS: [u8; 4096] = [0u8; 4096];
1060 file.write_all(&ZEROS[..pad])?;
1061 }
1062
1063 Ok(())
1064}
1065
1066fn update_dir_block_checksums(csum_seed: u32, inode: u32, data: &mut [u8]) {
1067 for chunk in data.chunks_exact_mut(EXT4_BLOCK_SIZE as usize) {
1068 let tail = EXT4_BLOCK_SIZE as usize - 12;
1069 let checksum = dir_block_checksum(csum_seed, inode, 0, &chunk[..tail]);
1070 put_le32(chunk, tail + 8, checksum);
1071 }
1072}
1073
1074fn write_inode_table_with_plan(
1075 file: &mut (impl std::io::Write + std::io::Seek),
1076 layout: &Layout,
1077 plans: &[NodePlan],
1078) -> Result<(), Ext4Error> {
1079 let table_offset = layout.inode_table_block * EXT4_BLOCK_SIZE as u64;
1080
1081 let root_inode = build_inode_from_plan(layout, &plans[0])?;
1082 let root_offset = table_offset + (EXT4_ROOT_INO as u64 - 1) * EXT4_INODE_SIZE as u64;
1083 file.seek(SeekFrom::Start(root_offset))?;
1084 file.write_all(&root_inode)?;
1085
1086 let journal_inode = build_journal_inode(layout)?;
1087 let journal_offset = table_offset + (EXT4_JOURNAL_INO as u64 - 1) * EXT4_INODE_SIZE as u64;
1088 file.seek(SeekFrom::Start(journal_offset))?;
1089 file.write_all(&journal_inode)?;
1090
1091 for plan in plans.iter().filter(|plan| plan.inode >= EXT4_FIRST_INO) {
1092 let inode_bytes = build_inode_from_plan(layout, plan)?;
1093 let inode_offset = table_offset + (plan.inode as u64 - 1) * EXT4_INODE_SIZE as u64;
1094 file.seek(SeekFrom::Start(inode_offset))?;
1095 file.write_all(&inode_bytes)?;
1096 }
1097
1098 Ok(())
1099}
1100
1101fn build_inode_from_plan(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
1102 let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1103 let (mode, size, links_count, extents) = match &plan.kind {
1104 NodeKind::Directory { children, data } => (
1105 S_IFDIR | normalize_dir_permissions(plan.permissions),
1106 data.len() as u64,
1107 2 + *children,
1108 true,
1109 ),
1110 NodeKind::RegularFile { data } => (
1111 S_IFREG | normalize_file_permissions(plan.permissions),
1112 data.len() as u64,
1113 1,
1114 true,
1115 ),
1116 NodeKind::Symlink { target, inline } => (S_IFLNK | 0o777, target.len() as u64, 1, !inline),
1117 NodeKind::CharDevice { .. } => (S_IFCHR | plan.permissions, 0, 1, false),
1118 };
1119
1120 put_le16(&mut inode, 0x00, mode);
1121 put_le16(&mut inode, 0x02, plan.uid);
1122 put_le32(&mut inode, 0x04, size as u32);
1123 put_le16(&mut inode, 0x18, plan.gid);
1124 put_le16(&mut inode, 0x1A, links_count);
1125 put_le32(&mut inode, 0x1C, plan.block_count * (EXT4_BLOCK_SIZE / 512));
1126 if extents {
1127 put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1128 }
1129
1130 match &plan.kind {
1131 NodeKind::Directory { .. } | NodeKind::RegularFile { .. } => {
1132 if let Some(start) = plan.block_start {
1133 write_extent_tree(&mut inode, 0x28, start, plan.block_count, &plan.path)?;
1134 } else {
1135 write_empty_extent_tree(&mut inode, 0x28);
1136 }
1137 }
1138 NodeKind::Symlink { target, inline } => {
1139 if *inline {
1140 inode[0x28..0x28 + target.len()].copy_from_slice(target);
1141 } else if let Some(start) = plan.block_start {
1142 write_extent_tree(&mut inode, 0x28, start, plan.block_count, &plan.path)?;
1143 }
1144 }
1145 NodeKind::CharDevice { major, minor } => {
1146 put_le32(&mut inode, 0x28, (*minor & 0xFF) | (major << 8));
1147 }
1148 }
1149
1150 put_le32(&mut inode, 0x64, 0);
1151 put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1152 put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1153
1154 let csum = inode_checksum(layout.csum_seed, plan.inode, 0, &inode);
1155 put_le16(&mut inode, 0x7C, csum as u16);
1156 put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1157
1158 Ok(inode)
1159}
1160
1161fn write_empty_extent_tree(buf: &mut [u8], offset: usize) {
1162 put_le16(buf, offset, EXT4_EH_MAGIC);
1163 put_le16(buf, offset + 2, 0);
1164 put_le16(buf, offset + 4, 4);
1165 put_le16(buf, offset + 6, 0);
1166 put_le32(buf, offset + 8, 0);
1167}
1168
1169fn build_superblock_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1170 build_superblock_with_stats_for_group(layout, stats, 0, true)
1171}
1172
1173fn build_backup_superblock_with_stats(
1174 layout: &Layout,
1175 stats: &FsStats,
1176 group: u32,
1177) -> Result<Vec<u8>, Ext4Error> {
1178 build_superblock_with_stats_for_group(layout, stats, group, false)
1179}
1180
1181fn build_superblock_with_stats_for_group(
1182 layout: &Layout,
1183 stats: &FsStats,
1184 group: u32,
1185 padded_primary: bool,
1186) -> Result<Vec<u8>, Ext4Error> {
1187 let mut block = build_superblock(layout, group, padded_primary)?;
1188 let sb_offset = if padded_primary { 1024 } else { 0 };
1189 let sb = &mut block[sb_offset..sb_offset + 1024];
1190 put_le32(sb, 0x0C, stats.total_free_blocks as u32);
1191 put_le32(sb, 0x10, stats.total_free_inodes as u32);
1192 put_le32(sb, 0x158, (stats.total_free_blocks >> 32) as u32);
1193 put_le32(sb, 0x194, stats.total_used_blocks as u32);
1194 put_le32(sb, 0x3FC, 0);
1195 let checksum = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
1196 put_le32(sb, 0x3FC, checksum);
1197 Ok(block)
1198}
1199
1200fn build_gdt_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1201 let desc_size = EXT4_DESC_SIZE as usize;
1202 let mut gdt = vec![0u8; layout.num_groups as usize * desc_size];
1203
1204 for g in 0..layout.num_groups {
1205 let off = g as usize * desc_size;
1206 let desc = &mut gdt[off..off + desc_size];
1207 let bb = layout.group_block_bitmap_block(g);
1208 let ib = layout.group_inode_bitmap_block(g);
1209 let it = layout.group_inode_table_block(g);
1210 let bb_csum = stats.block_bitmap_checksums[g as usize];
1211 let ib_csum = stats.inode_bitmap_checksums[g as usize];
1212 let free_blocks = stats.group_free_blocks[g as usize];
1213 let free_inodes = stats.group_free_inodes[g as usize];
1214 let used_dirs = stats.group_used_dirs[g as usize];
1215
1216 put_le32(desc, 0x00, bb as u32);
1217 put_le32(desc, 0x04, ib as u32);
1218 put_le32(desc, 0x08, it as u32);
1219 put_le16(desc, 0x0C, free_blocks as u16);
1220 put_le16(desc, 0x0E, free_inodes as u16);
1221 put_le16(desc, 0x10, used_dirs as u16);
1222 put_le16(desc, 0x12, EXT4_BG_INODE_ZEROED);
1223 put_le16(desc, 0x18, bb_csum as u16);
1224 put_le16(desc, 0x1A, ib_csum as u16);
1225 put_le16(desc, 0x1C, free_inodes as u16);
1226 put_le32(desc, 0x20, (bb >> 32) as u32);
1227 put_le32(desc, 0x24, (ib >> 32) as u32);
1228 put_le32(desc, 0x28, (it >> 32) as u32);
1229 put_le16(desc, 0x2C, (free_blocks >> 16) as u16);
1230 put_le16(desc, 0x2E, (free_inodes >> 16) as u16);
1231 put_le16(desc, 0x30, (used_dirs >> 16) as u16);
1232 put_le16(desc, 0x32, (free_inodes >> 16) as u16);
1233 put_le16(desc, 0x38, (bb_csum >> 16) as u16);
1234 put_le16(desc, 0x3A, (ib_csum >> 16) as u16);
1235 put_le16(desc, 0x1E, 0);
1236 let checksum = gdt_checksum(layout.csum_seed, g, desc);
1237 put_le16(desc, 0x1E, checksum);
1238 }
1239
1240 Ok(gdt)
1241}
1242#[cfg(test)]
1243fn build_block_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1244 (0..layout.num_groups)
1245 .map(|group| build_block_bitmap(layout, group))
1246 .collect()
1247}
1248
1249#[cfg(test)]
1250fn build_inode_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1251 (0..layout.num_groups)
1252 .map(|group| build_inode_bitmap(layout, group))
1253 .collect()
1254}
1255
1256#[cfg(test)]
1257fn build_block_bitmap(layout: &Layout, group: u32) -> Vec<u8> {
1258 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1259
1260 let used = layout.group_used_blocks(group);
1263 for bit in 0..used {
1264 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1265 }
1266
1267 let blocks_in_group = layout.blocks_in_group(group);
1269 for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
1270 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1271 }
1272
1273 bitmap
1274}
1275
1276#[cfg(test)]
1277fn build_inode_bitmap(_layout: &Layout, group: u32) -> Vec<u8> {
1278 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1279
1280 if group == 0 {
1281 for bit in 0..(EXT4_FIRST_INO - 1) {
1283 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1284 }
1285 }
1286
1287 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
1290 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1291 }
1292
1293 bitmap
1294}
1295
1296fn write_bitmaps(
1297 file: &mut (impl std::io::Write + std::io::Seek),
1298 layout: &Layout,
1299 plan: &BitmapPlan,
1300) -> Result<(), Ext4Error> {
1301 for group in 0..layout.num_groups {
1302 let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
1303 let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
1304 let block_offset = layout.group_block_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1305 file.seek(SeekFrom::Start(block_offset))?;
1306 file.write_all(&block_bitmap)?;
1307
1308 let inode_offset = layout.group_inode_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1309 file.seek(SeekFrom::Start(inode_offset))?;
1310 file.write_all(&inode_bitmap)?;
1311 }
1312
1313 Ok(())
1314}
1315
1316fn build_journal_inode(layout: &Layout) -> Result<Vec<u8>, Ext4Error> {
1318 let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1319
1320 let mode = S_IFREG | 0o600;
1321 let size = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1322
1323 put_le16(&mut inode, 0x00, mode);
1325 put_le32(&mut inode, 0x04, size as u32);
1327 put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1329 put_le16(&mut inode, 0x1A, 1);
1331 let sectors = (layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64) / 512;
1333 put_le32(&mut inode, 0x1C, sectors as u32);
1334 put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1336
1337 write_extent_tree(
1339 &mut inode,
1340 0x28,
1341 layout.journal_start_block,
1342 layout.journal_blocks,
1343 "journal",
1344 )?;
1345
1346 put_le32(&mut inode, 0x64, 0);
1348
1349 put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1352
1353 let csum = inode_checksum(layout.csum_seed, EXT4_JOURNAL_INO, 0, &inode);
1355 put_le16(&mut inode, 0x7C, csum as u16);
1357 put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1359
1360 Ok(inode)
1361}
1362
1363fn write_extent_tree(
1367 buf: &mut [u8],
1368 offset: usize,
1369 start_block: u64,
1370 block_count: u32,
1371 label: &str,
1372) -> Result<(), Ext4Error> {
1373 validate_extent_block_count(block_count, label)?;
1374 if start_block > u32::MAX as u64 {
1375 return Err(Ext4Error::TooLarge {
1376 requested_blocks: start_block,
1377 max_blocks: u32::MAX as u64,
1378 });
1379 }
1380
1381 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;
1390 put_le32(buf, ext_off, 0); put_le16(buf, ext_off + 4, block_count as u16); put_le16(buf, ext_off + 6, (start_block >> 32) as u16); put_le32(buf, ext_off + 8, start_block as u32); Ok(())
1396}
1397
1398fn write_journal(
1400 file: &mut (impl std::io::Write + std::io::Seek),
1401 layout: &Layout,
1402) -> Result<(), Ext4Error> {
1403 let mut jsb = vec![0u8; EXT4_BLOCK_SIZE as usize];
1404
1405 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);
1420 put_be32(&mut jsb, 36, 0);
1422 put_be32(&mut jsb, 40, 0x13);
1424 put_be32(&mut jsb, 44, 0);
1426
1427 jsb[48..64].copy_from_slice(&layout.uuid);
1429
1430 put_be32(&mut jsb, 64, 1);
1432
1433 put_be32(&mut jsb, 68, 0);
1435
1436 put_be32(&mut jsb, 72, 0);
1438 put_be32(&mut jsb, 76, 0);
1439
1440 jsb[0x50] = 4; let jsb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb[..JBD2_SUPERBLOCK_SIZE]);
1480 put_be32(&mut jsb, 0xFC, jsb_csum);
1481
1482 let offset = layout.journal_start_block * EXT4_BLOCK_SIZE as u64;
1483 file.seek(SeekFrom::Start(offset))?;
1484 file.write_all(&jsb)?;
1485
1486 Ok(())
1487}
1488
1489fn build_superblock(
1491 layout: &Layout,
1492 group: u32,
1493 padded_primary: bool,
1494) -> Result<Vec<u8>, Ext4Error> {
1495 let mut block = if padded_primary {
1496 vec![0u8; EXT4_BLOCK_SIZE as usize]
1497 } else {
1498 vec![0u8; 1024]
1499 };
1500 let sb_offset = if padded_primary { 1024 } else { 0 };
1501 let sb = &mut block[sb_offset..sb_offset + 1024];
1502
1503 let total_blocks = layout.num_blocks;
1504 let total_inodes = layout.num_groups as u64 * EXT4_INODES_PER_GROUP as u64;
1505
1506 let free_blocks = layout.total_free_blocks();
1507 let free_inodes = layout.total_free_inodes();
1508
1509 put_le32(sb, 0x00, total_inodes as u32);
1511 put_le32(sb, 0x04, total_blocks as u32);
1513 put_le32(sb, 0x08, 0);
1515 put_le32(sb, 0x0C, free_blocks as u32);
1517 put_le32(sb, 0x10, free_inodes as u32);
1519 put_le32(sb, 0x14, 0);
1521 put_le32(sb, 0x18, EXT4_LOG_BLOCK_SIZE);
1523 put_le32(sb, 0x1C, EXT4_LOG_BLOCK_SIZE);
1525 put_le32(sb, 0x20, EXT4_BLOCKS_PER_GROUP);
1527 put_le32(sb, 0x24, EXT4_BLOCKS_PER_GROUP);
1529 put_le32(sb, 0x28, EXT4_INODES_PER_GROUP);
1531
1532 put_le16(sb, 0x34, 0);
1537 put_le16(sb, 0x36, 0xFFFF);
1539 put_le16(sb, 0x38, EXT4_SUPER_MAGIC);
1541 put_le16(sb, 0x3A, 1);
1543 put_le16(sb, 0x3C, 1);
1545 put_le16(sb, 0x3E, 0);
1547
1548 put_le32(sb, 0x48, 0);
1553 put_le32(sb, 0x4C, 1);
1555
1556 put_le16(sb, 0x50, 0);
1558 put_le16(sb, 0x52, 0);
1560
1561 put_le32(sb, 0x54, EXT4_FIRST_INO);
1564 put_le16(sb, 0x58, EXT4_INODE_SIZE);
1566 put_le16(sb, 0x5A, group as u16);
1568
1569 put_le32(sb, 0x5C, layout.feature_compat);
1571 put_le32(sb, 0x60, layout.feature_incompat);
1573 put_le32(sb, 0x64, layout.feature_ro_compat);
1575
1576 sb[0x68..0x78].copy_from_slice(&layout.uuid);
1578
1579 put_le32(sb, 0xC8, 0);
1585
1586 sb[0xCC] = 0;
1588 sb[0xCD] = 0;
1589
1590 put_le16(sb, 0xCE, RESERVED_GDT_BLOCKS as u16);
1592
1593 put_le32(sb, 0xE0, EXT4_JOURNAL_INO);
1597 put_le32(sb, 0xE4, 0);
1599 put_le32(sb, 0xE8, 0);
1601
1602 sb[0xEC..0xFC].copy_from_slice(&layout.uuid); sb[0xFC] = 1;
1607 sb[0xFD] = 1;
1609
1610 put_le16(sb, 0xFE, EXT4_DESC_SIZE);
1612
1613 put_le32(sb, 0x100, 0x000C);
1615
1616 put_le32(sb, 0x104, 0);
1618
1619 {
1624 let mut extent_buf = [0u8; 60];
1625 write_extent_tree(
1626 &mut extent_buf,
1627 0,
1628 layout.journal_start_block,
1629 layout.journal_blocks,
1630 "journal",
1631 )?;
1632 sb[0x10C..0x10C + 60].copy_from_slice(&extent_buf);
1634 let jsize = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1636 put_le32(sb, 0x10C + 60, jsize as u32);
1637 put_le32(sb, 0x10C + 64, (jsize >> 32) as u32);
1639 }
1640
1641 put_le32(sb, 0x150, (total_blocks >> 32) as u32);
1644 put_le32(sb, 0x154, 0);
1646 put_le32(sb, 0x158, (free_blocks >> 32) as u32);
1648
1649 put_le16(sb, 0x15C, EXT4_MIN_EXTRA_ISIZE);
1651 put_le16(sb, 0x15E, EXT4_MIN_EXTRA_ISIZE);
1653
1654 put_le32(sb, 0x160, 0);
1656
1657 sb[0x174] = 0;
1659
1660 sb[0x175] = 1;
1662
1663 put_le32(sb, 0x194, layout.total_used_blocks() as u32);
1668
1669 put_le32(sb, 0x270, 0);
1674
1675 put_le16(sb, 0x27C, 0);
1677
1678 let sb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
1680 put_le32(sb, 0x3FC, sb_csum);
1681
1682 Ok(block)
1683}
1684
1685#[cfg(test)]
1688fn build_gdt(
1689 layout: &Layout,
1690 block_bitmaps: &[Vec<u8>],
1691 inode_bitmaps: &[Vec<u8>],
1692) -> Result<Vec<u8>, Ext4Error> {
1693 let desc_size = EXT4_DESC_SIZE as usize;
1694 let mut gdt = vec![0u8; layout.num_groups as usize * desc_size];
1695
1696 for g in 0..layout.num_groups {
1697 let off = g as usize * desc_size;
1698 let desc = &mut gdt[off..off + desc_size];
1699 let bb = layout.group_block_bitmap_block(g);
1700 let ib = layout.group_inode_bitmap_block(g);
1701 let it = layout.group_inode_table_block(g);
1702 let bb_csum = bitmap_checksum(
1703 layout.csum_seed,
1704 &block_bitmaps[g as usize],
1705 EXT4_BLOCK_SIZE as usize,
1706 );
1707 let ib_csum = bitmap_checksum(
1708 layout.csum_seed,
1709 &inode_bitmaps[g as usize],
1710 (EXT4_INODES_PER_GROUP / 8) as usize,
1711 );
1712
1713 put_le32(desc, 0x00, bb as u32);
1714 put_le32(desc, 0x04, ib as u32);
1715 put_le32(desc, 0x08, it as u32);
1716 put_le16(desc, 0x0C, layout.group_free_blocks(g) as u16);
1717 put_le16(desc, 0x0E, layout.group_free_inodes(g) as u16);
1718 put_le16(desc, 0x10, layout.group_used_dirs(g) as u16);
1719 put_le16(desc, 0x12, EXT4_BG_INODE_ZEROED);
1720 put_le32(desc, 0x14, 0);
1721 put_le16(desc, 0x18, bb_csum as u16);
1722 put_le16(desc, 0x1A, ib_csum as u16);
1723 put_le16(desc, 0x1C, layout.group_free_inodes(g) as u16);
1724 put_le32(desc, 0x20, (bb >> 32) as u32);
1725 put_le32(desc, 0x24, (ib >> 32) as u32);
1726 put_le32(desc, 0x28, (it >> 32) as u32);
1727 put_le16(desc, 0x2C, (layout.group_free_blocks(g) >> 16) as u16);
1728 put_le16(desc, 0x2E, (layout.group_free_inodes(g) >> 16) as u16);
1729 put_le16(desc, 0x30, (layout.group_used_dirs(g) >> 16) as u16);
1730 put_le16(desc, 0x32, (layout.group_free_inodes(g) >> 16) as u16);
1731 put_le32(desc, 0x34, 0);
1732 put_le16(desc, 0x38, (bb_csum >> 16) as u16);
1733 put_le16(desc, 0x3A, (ib_csum >> 16) as u16);
1734
1735 put_le16(desc, 0x1E, 0);
1739 let gdt_csum = gdt_checksum(layout.csum_seed, g, desc);
1740 put_le16(desc, 0x1E, gdt_csum);
1741 }
1742
1743 Ok(gdt)
1744}
1745
1746fn write_primary_superblock_at(
1748 file: &mut (impl std::io::Write + std::io::Seek),
1749 sb_block: &[u8],
1750) -> Result<(), Ext4Error> {
1751 file.seek(SeekFrom::Start(0))?;
1752 file.write_all(sb_block)?;
1753 Ok(())
1754}
1755
1756fn write_backup_superblock_at(
1758 file: &mut (impl std::io::Write + std::io::Seek),
1759 group_start_block: u64,
1760 sb_block: &[u8],
1761) -> Result<(), Ext4Error> {
1762 file.seek(SeekFrom::Start(group_start_block * EXT4_BLOCK_SIZE as u64))?;
1763 file.write_all(sb_block)?;
1764 Ok(())
1765}
1766
1767fn write_gdt_at(
1769 file: &mut (impl std::io::Write + std::io::Seek),
1770 group_start_block: u64,
1771 gdt: &[u8],
1772) -> Result<(), Ext4Error> {
1773 let offset = (group_start_block + 1) * EXT4_BLOCK_SIZE as u64;
1774 file.seek(SeekFrom::Start(offset))?;
1775 file.write_all(gdt)?;
1776 Ok(())
1777}
1778
1779fn gdt_checksum(csum_seed: u32, group: u32, desc: &[u8]) -> u16 {
1785 let mut crc = crc32c::crc32c_raw(csum_seed, &group.to_le_bytes());
1786 crc = crc32c::crc32c_raw(crc, desc);
1787 (crc & 0xFFFF) as u16
1788}
1789
1790fn inode_checksum(csum_seed: u32, inum: u32, generation: u32, inode_bytes: &[u8]) -> u32 {
1792 let mut crc = crc32c::crc32c_raw(csum_seed, &inum.to_le_bytes());
1793 crc = crc32c::crc32c_raw(crc, &generation.to_le_bytes());
1794 crc = crc32c::crc32c_raw(crc, &inode_bytes[..0x7C]);
1795 crc = crc32c::crc32c_raw(crc, &[0u8; 2]);
1796 crc = crc32c::crc32c_raw(crc, &inode_bytes[0x7E..0x82]);
1797 crc = crc32c::crc32c_raw(crc, &[0u8; 2]);
1798 crc = crc32c::crc32c_raw(crc, &inode_bytes[0x84..]);
1799 crc
1800}
1801
1802fn bitmap_checksum(csum_seed: u32, bitmap: &[u8], checksum_len: usize) -> u32 {
1810 crc32c::crc32c_raw(csum_seed, &bitmap[..checksum_len])
1811}
1812
1813fn dir_block_checksum(csum_seed: u32, inum: u32, generation: u32, data: &[u8]) -> u32 {
1815 let mut crc = crc32c::crc32c_raw(csum_seed, &inum.to_le_bytes());
1816 crc = crc32c::crc32c_raw(crc, &generation.to_le_bytes());
1817 crc = crc32c::crc32c_raw(crc, data);
1818 crc
1819}
1820
1821fn put_le16(buf: &mut [u8], off: usize, val: u16) {
1826 buf[off..off + 2].copy_from_slice(&val.to_le_bytes());
1827}
1828
1829fn put_le32(buf: &mut [u8], off: usize, val: u32) {
1830 buf[off..off + 4].copy_from_slice(&val.to_le_bytes());
1831}
1832
1833fn put_be32(buf: &mut [u8], off: usize, val: u32) {
1834 buf[off..off + 4].copy_from_slice(&val.to_be_bytes());
1835}
1836
1837pub use super::format::sparse_super_group;
1842
1843#[cfg(test)]
1848mod tests {
1849 use super::*;
1850 use std::io::{Read, Seek, SeekFrom};
1851
1852 fn le_u16(bytes: &[u8], offset: usize) -> u16 {
1853 u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
1854 }
1855
1856 fn le_u32(bytes: &[u8], offset: usize) -> u32 {
1857 u32::from_le_bytes([
1858 bytes[offset],
1859 bytes[offset + 1],
1860 bytes[offset + 2],
1861 bytes[offset + 3],
1862 ])
1863 }
1864
1865 fn read_exact_at(path: &Path, offset: u64, len: usize) -> Vec<u8> {
1866 let mut file = std::fs::File::open(path).unwrap();
1867 file.seek(SeekFrom::Start(offset)).unwrap();
1868 let mut bytes = vec![0u8; len];
1869 file.read_exact(&mut bytes).unwrap();
1870 bytes
1871 }
1872
1873 fn assert_backup_superblock(path: &Path, layout: &Layout, group: u32) {
1874 assert!(sparse_super_group(group));
1875 let backup_offset = layout.group_start_block(group) * EXT4_BLOCK_SIZE as u64;
1876 let bytes = read_exact_at(path, backup_offset, 2048);
1877
1878 assert_eq!(le_u16(&bytes, 0x38), EXT4_SUPER_MAGIC);
1879 assert_eq!(le_u16(&bytes, 0x5A), group as u16);
1880 assert_ne!(le_u16(&bytes, 1024 + 0x38), EXT4_SUPER_MAGIC);
1881
1882 let mut sb = bytes[..1024].to_vec();
1883 let stored = le_u32(&sb, 0x3FC);
1884 put_le32(&mut sb, 0x3FC, 0);
1885 let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
1886
1887 assert_eq!(stored, expected);
1888 }
1889
1890 #[test]
1891 fn test_format_creates_file_of_correct_size() {
1892 let dir = tempfile::tempdir().unwrap();
1893 let path = dir.path().join("test.ext4");
1894
1895 let size: u64 = 256 * 1024 * 1024; let opts = Ext4FormatOptions {
1897 size_bytes: size,
1898 journal_blocks: 4096, };
1900
1901 format_ext4(&path, &opts).unwrap();
1902
1903 let meta = std::fs::metadata(&path).unwrap();
1904 assert_eq!(meta.len(), size);
1905 }
1906
1907 #[test]
1908 fn test_format_too_small() {
1909 let dir = tempfile::tempdir().unwrap();
1910 let path = dir.path().join("tiny.ext4");
1911
1912 let opts = Ext4FormatOptions {
1913 size_bytes: 4096, journal_blocks: 16384,
1915 };
1916
1917 let result = format_ext4(&path, &opts);
1918 assert!(matches!(result, Err(Ext4Error::TooSmall)));
1919 }
1920
1921 #[test]
1922 fn test_layout_rejects_unaligned_size() {
1923 let opts = Ext4FormatOptions {
1924 size_bytes: DEFAULT_SIZE_BYTES + 1,
1925 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
1926 };
1927
1928 let result = Layout::compute(&opts);
1929 assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
1930 }
1931
1932 #[test]
1933 fn test_layout_rejects_tiny_final_group() {
1934 let opts = Ext4FormatOptions {
1935 size_bytes: 128 * 1024 * 1024 + EXT4_BLOCK_SIZE as u64,
1936 journal_blocks: 4096,
1937 };
1938
1939 let result = Layout::compute(&opts);
1940 assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
1941 }
1942
1943 #[test]
1944 fn test_layout_rejects_size_beyond_32_bit_block_addresses() {
1945 let opts = Ext4FormatOptions {
1946 size_bytes: (MAX_BLOCKS + 1) * EXT4_BLOCK_SIZE as u64,
1947 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
1948 };
1949
1950 let result = Layout::compute(&opts);
1951 assert!(matches!(result, Err(Ext4Error::TooLarge { .. })));
1952 }
1953
1954 #[test]
1955 fn test_layout_uses_actual_group_count_beyond_four_gib() {
1956 let opts = Ext4FormatOptions {
1957 size_bytes: 8 * 1024 * 1024 * 1024,
1958 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
1959 };
1960
1961 let layout = Layout::compute(&opts).unwrap();
1962
1963 assert_eq!(layout.num_blocks, opts.size_bytes / EXT4_BLOCK_SIZE as u64);
1964 assert_eq!(layout.num_groups, 64);
1965 assert_eq!(layout.gdt_blocks, 1);
1966 }
1967
1968 #[test]
1969 fn test_format_default_options() {
1970 let dir = tempfile::tempdir().unwrap();
1971 let path = dir.path().join("default.ext4");
1972
1973 let opts = Ext4FormatOptions::default();
1974 format_ext4(&path, &opts).unwrap();
1975
1976 let meta = std::fs::metadata(&path).unwrap();
1977 assert_eq!(meta.len(), DEFAULT_SIZE_BYTES);
1978 }
1979
1980 #[test]
1981 fn test_superblock_magic() {
1982 let dir = tempfile::tempdir().unwrap();
1983 let path = dir.path().join("magic.ext4");
1984
1985 let opts = Ext4FormatOptions {
1986 size_bytes: 256 * 1024 * 1024,
1987 journal_blocks: 4096,
1988 };
1989 format_ext4(&path, &opts).unwrap();
1990
1991 let data = std::fs::read(&path).unwrap();
1993 let magic = u16::from_le_bytes([data[1024 + 0x38], data[1024 + 0x39]]);
1994 assert_eq!(magic, EXT4_SUPER_MAGIC);
1995 }
1996
1997 #[test]
1998 fn test_journal_magic() {
1999 let dir = tempfile::tempdir().unwrap();
2000 let path = dir.path().join("journal.ext4");
2001
2002 let opts = Ext4FormatOptions {
2003 size_bytes: 256 * 1024 * 1024,
2004 journal_blocks: 4096,
2005 };
2006 format_ext4(&path, &opts).unwrap();
2007
2008 let layout = Layout::compute(&opts).unwrap();
2009 let data = std::fs::read(&path).unwrap();
2010
2011 let jsb_offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
2013 let magic = u32::from_be_bytes([
2014 data[jsb_offset],
2015 data[jsb_offset + 1],
2016 data[jsb_offset + 2],
2017 data[jsb_offset + 3],
2018 ]);
2019 assert_eq!(magic, JBD2_MAGIC);
2020 }
2021
2022 #[test]
2023 fn test_root_dir_inode_exists() {
2024 let dir = tempfile::tempdir().unwrap();
2025 let path = dir.path().join("rootdir.ext4");
2026
2027 let opts = Ext4FormatOptions {
2028 size_bytes: 256 * 1024 * 1024,
2029 journal_blocks: 4096,
2030 };
2031 format_ext4(&path, &opts).unwrap();
2032
2033 let layout = Layout::compute(&opts).unwrap();
2034 let data = std::fs::read(&path).unwrap();
2035
2036 let inode_offset = layout.inode_table_block as usize * EXT4_BLOCK_SIZE as usize
2038 + (EXT4_INODE_SIZE as usize);
2039 let mode = u16::from_le_bytes([data[inode_offset], data[inode_offset + 1]]);
2040 assert_eq!(mode, S_IFDIR | 0o755);
2041 }
2042
2043 #[test]
2044 fn test_backup_group_bitmap_starts_after_backup_metadata() {
2045 let opts = Ext4FormatOptions {
2046 size_bytes: 256 * 1024 * 1024,
2047 journal_blocks: 4096,
2048 };
2049 let layout = Layout::compute(&opts).unwrap();
2050 let block_bitmaps = build_block_bitmaps(&layout);
2051 let inode_bitmaps = build_inode_bitmaps(&layout);
2052 let gdt = build_gdt(&layout, &block_bitmaps, &inode_bitmaps).unwrap();
2053
2054 let desc = &gdt[EXT4_DESC_SIZE as usize..(2 * EXT4_DESC_SIZE as usize)];
2055 let block_bitmap = u32::from_le_bytes([desc[0], desc[1], desc[2], desc[3]]) as u64;
2056 let group_start = layout.group_start_block(1);
2057
2058 assert_eq!(block_bitmap, layout.group_block_bitmap_block(1));
2059 assert!(block_bitmap > group_start + layout.gdt_blocks as u64 - 1);
2060 }
2061
2062 #[test]
2063 fn test_backup_superblock_is_written_at_backup_group_start() {
2064 let dir = tempfile::tempdir().unwrap();
2065 let path = dir.path().join("backup-super.ext4");
2066 let opts = Ext4FormatOptions {
2067 size_bytes: 256 * 1024 * 1024,
2068 journal_blocks: 4096,
2069 };
2070
2071 format_ext4(&path, &opts).unwrap();
2072
2073 let layout = Layout::compute(&opts).unwrap();
2074 assert_backup_superblock(&path, &layout, 1);
2075 }
2076
2077 #[test]
2078 fn test_backup_superblock_is_group_specific_in_sixteen_gib_image() {
2079 let dir = tempfile::tempdir().unwrap();
2080 let path = dir.path().join("backup-super-16g.ext4");
2081 let opts = Ext4FormatOptions {
2082 size_bytes: 16 * 1024 * 1024 * 1024,
2083 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2084 };
2085
2086 format_ext4(&path, &opts).unwrap();
2087
2088 let layout = Layout::compute(&opts).unwrap();
2089 assert_eq!(layout.num_groups, 128);
2090 assert_backup_superblock(&path, &layout, 81);
2091 }
2092
2093 #[test]
2094 fn test_format_sparse_image_larger_than_four_gib() {
2095 let dir = tempfile::tempdir().unwrap();
2096 let path = dir.path().join("large.ext4");
2097 let opts = Ext4FormatOptions {
2098 size_bytes: 8 * 1024 * 1024 * 1024,
2099 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2100 };
2101
2102 format_ext4(&path, &opts).unwrap();
2103
2104 let meta = std::fs::metadata(&path).unwrap();
2105 assert_eq!(meta.len(), opts.size_bytes);
2106
2107 let layout = Layout::compute(&opts).unwrap();
2108 let sb = read_exact_at(&path, 1024, 1024);
2109 let blocks = le_u32(&sb, 0x04) as u64 | ((le_u32(&sb, 0x150) as u64) << 32);
2110 let inodes = le_u32(&sb, 0x00);
2111 assert_eq!(blocks, layout.num_blocks);
2112 assert_eq!(inodes, layout.num_groups * EXT4_INODES_PER_GROUP);
2113
2114 let desc_offset = EXT4_BLOCK_SIZE as u64 + 63 * EXT4_DESC_SIZE as u64;
2115 let desc = read_exact_at(&path, desc_offset, EXT4_DESC_SIZE as usize);
2116 let block_bitmap = le_u32(&desc, 0x00) as u64 | ((le_u32(&desc, 0x20) as u64) << 32);
2117 let inode_bitmap = le_u32(&desc, 0x04) as u64 | ((le_u32(&desc, 0x24) as u64) << 32);
2118
2119 assert_eq!(block_bitmap, layout.group_block_bitmap_block(63));
2120 assert_eq!(inode_bitmap, layout.group_inode_bitmap_block(63));
2121 }
2122
2123 #[test]
2124 fn test_inode_bitmap_padding_is_marked_used() {
2125 let layout = Layout::compute(&Ext4FormatOptions {
2126 size_bytes: 256 * 1024 * 1024,
2127 journal_blocks: 4096,
2128 })
2129 .unwrap();
2130
2131 let bitmap = build_inode_bitmap(&layout, 0);
2132 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
2133 assert_ne!(bitmap[(bit / 8) as usize] & (1 << (bit % 8)), 0);
2134 }
2135 }
2136
2137 #[test]
2138 fn test_journal_superblock_checksum_matches_contents() {
2139 let dir = tempfile::tempdir().unwrap();
2140 let path = dir.path().join("journal-csum.ext4");
2141 let opts = Ext4FormatOptions {
2142 size_bytes: 256 * 1024 * 1024,
2143 journal_blocks: 4096,
2144 };
2145
2146 format_ext4(&path, &opts).unwrap();
2147
2148 let layout = Layout::compute(&opts).unwrap();
2149 let data = std::fs::read(&path).unwrap();
2150 let offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
2151 let mut jsb = data[offset..offset + JBD2_SUPERBLOCK_SIZE].to_vec();
2152 let stored = u32::from_be_bytes([jsb[0xFC], jsb[0xFD], jsb[0xFE], jsb[0xFF]]);
2153
2154 jsb[0xFC..0x100].fill(0);
2155 let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb);
2156
2157 assert_eq!(stored, expected);
2158 }
2159
2160 #[test]
2161 fn test_root_dir_checksum_matches_contents() {
2162 let dir = tempfile::tempdir().unwrap();
2163 let path = dir.path().join("rootdir-csum.ext4");
2164 let opts = Ext4FormatOptions {
2165 size_bytes: 256 * 1024 * 1024,
2166 journal_blocks: 4096,
2167 };
2168
2169 format_ext4(&path, &opts).unwrap();
2170
2171 let layout = Layout::compute(&opts).unwrap();
2172 let data = std::fs::read(&path).unwrap();
2173 let sb = &data[1024..2048];
2174 let uuid = &sb[0x68..0x78];
2175 let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, uuid);
2176 let block_offset = layout.first_data_block as usize * EXT4_BLOCK_SIZE as usize;
2177 let tail_offset = block_offset + EXT4_BLOCK_SIZE as usize - 12;
2178 let stored = u32::from_le_bytes([
2179 data[tail_offset + 8],
2180 data[tail_offset + 9],
2181 data[tail_offset + 10],
2182 data[tail_offset + 11],
2183 ]);
2184 let expected = dir_block_checksum(
2185 csum_seed,
2186 EXT4_ROOT_INO,
2187 0,
2188 &data[block_offset..tail_offset],
2189 );
2190
2191 assert_eq!(stored, expected);
2192 }
2193}