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