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