1use std::collections::HashMap;
2use std::io::{self, BufWriter, SeekFrom, Write};
3#[cfg(windows)]
4use std::os::windows::io::AsRawHandle;
5use std::path::Path;
6#[cfg(windows)]
7use std::ptr;
8
9use super::format::{
10 EXT4_BLOCK_SIZE, EXT4_BLOCKS_PER_GROUP, EXT4_DESC_SIZE, EXT4_EH_MAGIC, EXT4_EXTENTS_FL,
11 EXT4_FEATURE_COMPAT_DIR_INDEX, EXT4_FEATURE_COMPAT_EXT_ATTR, EXT4_FEATURE_COMPAT_HAS_JOURNAL,
12 EXT4_FEATURE_COMPAT_RESIZE_INODE, EXT4_FEATURE_INCOMPAT_64BIT, EXT4_FEATURE_INCOMPAT_EXTENTS,
13 EXT4_FEATURE_INCOMPAT_FILETYPE, EXT4_FEATURE_RO_COMPAT_DIR_NLINK,
14 EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE, EXT4_FEATURE_RO_COMPAT_HUGE_FILE,
15 EXT4_FEATURE_RO_COMPAT_LARGE_FILE, EXT4_FEATURE_RO_COMPAT_METADATA_CSUM,
16 EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER, EXT4_FIRST_INO, EXT4_INODE_SIZE, EXT4_INODES_PER_GROUP,
17 EXT4_JOURNAL_INO, EXT4_LOG_BLOCK_SIZE, EXT4_MIN_EXTRA_ISIZE, EXT4_ROOT_INO,
18 EXT4_SB_ERROR_COUNT_OFFSET, EXT4_SB_OVERHEAD_BLOCKS_OFFSET, EXT4_SUPER_MAGIC, JBD2_MAGIC,
19 JBD2_SUPERBLOCK_V2, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFREG, S_IFSOCK,
20};
21use super::layout::{
22 GroupDescStats, GroupGeometry, MAX_BLOCKS, RESERVED_GDT_BLOCKS, bitmap_checksum,
23 build_block_bitmap_base, build_group_descriptor, build_inode_bitmap_base, count_used_bits,
24 dir_block_checksum, inode_checksum, put_be32, put_le16, put_le32, superblock_checksum,
25 write_backup_superblock_at, write_gdt_at,
26};
27use super::resize_inode::write_resize_inode;
28use crate::crc32c;
29use crate::path_bytes::os_str_bytes;
30use crate::tree::{
31 DirectoryNode, FileData, FileTree, InodeMetadata, RegularFileId, TreeNode, Xattr,
32};
33#[cfg(windows)]
34use windows_sys::Win32::Foundation::HANDLE;
35#[cfg(windows)]
36use windows_sys::Win32::System::IO::DeviceIoControl;
37#[cfg(windows)]
38use windows_sys::Win32::System::Ioctl::FSCTL_SET_SPARSE;
39
40const DEFAULT_SIZE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
46
47const DEFAULT_JOURNAL_BLOCKS: u32 = 16384;
49
50const MAX_INITIALIZED_EXTENT_BLOCKS: u32 = 32768;
52const INLINE_EXTENT_CAPACITY: usize = 4;
53const EXTENT_BLOCK_CAPACITY: usize =
54 (EXT4_BLOCK_SIZE as usize - 12 - std::mem::size_of::<u32>()) / 12;
55const EXT4_XATTR_MAGIC: u32 = 0xEA02_0000;
56const EXT4_XATTR_HEADER_SIZE: usize = 32;
57const EXT4_XATTR_ENTRY_SIZE: usize = 16;
58
59const EXT4_FT_DIR: u8 = 2;
61
62#[allow(dead_code)]
64const EXT4_FT_REG_FILE: u8 = 1;
65
66const EXT4_FT_CHRDEV: u8 = 3;
68
69const EXT4_FT_BLKDEV: u8 = 4;
71
72const EXT4_FT_FIFO: u8 = 5;
74
75const EXT4_FT_SOCK: u8 = 6;
77
78const EXT4_FT_SYMLINK: u8 = 7;
80
81const JBD2_SUPERBLOCK_SIZE: usize = 1024;
83
84pub struct Ext4FormatOptions {
90 pub size_bytes: u64,
93
94 pub journal_blocks: u32,
97}
98
99#[derive(Debug)]
101pub enum Ext4Error {
102 Io(io::Error),
104
105 InvalidSize(String),
107
108 TooSmall,
111
112 TooLarge {
114 requested_blocks: u64,
116
117 max_blocks: u64,
119 },
120
121 Layout(String),
123
124 Unsupported(String),
127
128 ExceedsGdtCapacity {
131 requested_bytes: u64,
133
134 max_size_bytes: u64,
136 },
137}
138
139struct Layout {
141 num_blocks: u64,
142 num_groups: u32,
143 uuid: [u8; 16],
144 gdt_blocks: u32,
145 reserved_gdt_blocks: u32,
147 inode_table_blocks: u32,
149 first_data_block: u64,
151 resize_inode_block: Option<u64>,
155 journal_start_block: u64,
157 journal_blocks: u32,
159 csum_seed: u32,
161 feature_compat: u32,
163 feature_incompat: u32,
165 feature_ro_compat: u32,
167}
168
169struct FsStats {
170 group_free_blocks: Vec<u32>,
171 group_free_inodes: Vec<u32>,
172 group_used_dirs: Vec<u32>,
173 block_bitmap_checksums: Vec<u32>,
174 inode_bitmap_checksums: Vec<u32>,
175 total_free_blocks: u64,
176 total_free_inodes: u64,
177 overhead_blocks: u64,
178}
179
180struct BitmapPlan {
181 block_extents: Vec<(u64, u32)>,
182 max_used_inode: u32,
183 group_used_dirs: Vec<u32>,
184}
185
186enum NodeKind {
187 Directory { data: Vec<u8> },
188 RegularFile { data: FileData },
189 Symlink { target: Vec<u8>, inline: bool },
190 CharDevice { major: u32, minor: u32 },
191 BlockDevice { major: u32, minor: u32 },
192 Fifo,
193 Socket,
194}
195
196struct NodePlan {
197 inode: u32,
198 path: String,
199 metadata: InodeMetadata,
200 links_count: u32,
201 kind: NodeKind,
202 block_count: u32,
203 data_extents: Vec<AllocatedExtent>,
204 extent_tree_block: Option<u64>,
205 xattrs: Vec<Xattr>,
206 inline_xattrs: Option<Vec<u8>>,
207 xattr_block: Option<u64>,
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq)]
211struct AllocatedExtent {
212 logical_block: u32,
213 physical_start: u64,
214 block_count: u32,
215}
216
217#[derive(Clone, Copy)]
218enum TreeEncodingMode {
219 Upper,
220 Rootfs,
221}
222
223struct DraftDirectory {
224 children: u16,
225 data: Vec<u8>,
226 xattrs: Vec<Xattr>,
227}
228
229struct DraftContext<'a> {
230 next_inode: &'a mut u32,
231 plans: &'a mut Vec<NodePlan>,
232 hardlinks: &'a mut HashMap<RegularFileId, (u32, usize)>,
233 encoding_mode: TreeEncodingMode,
234}
235
236struct EncodedXattr<'a> {
237 name_index: u8,
238 name: &'a [u8],
239 value: &'a [u8],
240}
241
242struct DirEntrySpec {
243 inode: u32,
244 file_type: u8,
245 name: Vec<u8>,
246}
247
248struct DataAllocator {
249 regions: Vec<(u64, u32)>,
250}
251
252impl Default for Ext4FormatOptions {
257 fn default() -> Self {
258 Self {
259 size_bytes: DEFAULT_SIZE_BYTES,
260 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
261 }
262 }
263}
264
265impl std::fmt::Display for Ext4Error {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 match self {
268 Ext4Error::Io(e) => write!(f, "ext4 I/O error: {e}"),
269 Ext4Error::InvalidSize(e) => write!(f, "invalid ext4 image size: {e}"),
270 Ext4Error::TooSmall => write!(f, "image size is too small for ext4 formatting"),
271 Ext4Error::TooLarge {
272 requested_blocks,
273 max_blocks,
274 } => write!(
275 f,
276 "image is too large for ext4 formatting: requested {requested_blocks} blocks, maximum is {max_blocks} blocks"
277 ),
278 Ext4Error::Layout(e) => write!(f, "ext4 layout error: {e}"),
279 Ext4Error::Unsupported(e) => write!(f, "unsupported ext4 image: {e}"),
280 Ext4Error::ExceedsGdtCapacity {
281 requested_bytes,
282 max_size_bytes,
283 } => write!(
284 f,
285 "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"
286 ),
287 }
288 }
289}
290
291impl std::error::Error for Ext4Error {
292 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
293 match self {
294 Ext4Error::Io(e) => Some(e),
295 Ext4Error::InvalidSize(_)
296 | Ext4Error::TooSmall
297 | Ext4Error::TooLarge { .. }
298 | Ext4Error::Layout(_)
299 | Ext4Error::Unsupported(_)
300 | Ext4Error::ExceedsGdtCapacity { .. } => None,
301 }
302 }
303}
304
305impl From<io::Error> for Ext4Error {
306 fn from(e: io::Error) -> Self {
307 Ext4Error::Io(e)
308 }
309}
310
311impl Layout {
312 #[cfg(test)]
313 fn compute(opts: &Ext4FormatOptions) -> Result<Self, Ext4Error> {
314 Self::compute_with_root_blocks(opts, 1, RESERVED_GDT_BLOCKS)
315 }
316
317 #[cfg(test)]
318 fn compute_with_root_blocks(
319 opts: &Ext4FormatOptions,
320 root_dir_blocks: u32,
321 reserved_gdt_blocks: u32,
322 ) -> Result<Self, Ext4Error> {
323 Self::compute_with_root_blocks_and_uuid(
324 opts,
325 root_dir_blocks,
326 reserved_gdt_blocks,
327 None,
328 true,
329 )
330 }
331
332 fn compute_with_root_blocks_and_uuid(
333 opts: &Ext4FormatOptions,
334 root_dir_blocks: u32,
335 reserved_gdt_blocks: u32,
336 uuid: Option<[u8; 16]>,
337 with_resize_inode: bool,
338 ) -> Result<Self, Ext4Error> {
339 let block_size = EXT4_BLOCK_SIZE as u64;
340 if !opts.size_bytes.is_multiple_of(block_size) {
341 return Err(Ext4Error::InvalidSize(format!(
342 "image size must be aligned to {block_size} bytes"
343 )));
344 }
345
346 let num_blocks = opts.size_bytes / block_size;
347 if num_blocks > MAX_BLOCKS {
348 return Err(Ext4Error::TooLarge {
349 requested_blocks: num_blocks,
350 max_blocks: MAX_BLOCKS,
351 });
352 }
353
354 if opts.journal_blocks == 0 {
355 return Err(Ext4Error::InvalidSize(
356 "journal must contain at least one block".to_string(),
357 ));
358 }
359 validate_extent_block_count(opts.journal_blocks, "journal")?;
360
361 let num_groups_raw = num_blocks.div_ceil(EXT4_BLOCKS_PER_GROUP as u64);
362 let num_groups = u32::try_from(num_groups_raw).map_err(|_| Ext4Error::TooLarge {
363 requested_blocks: num_blocks,
364 max_blocks: MAX_BLOCKS,
365 })?;
366
367 let inode_table_blocks =
370 (EXT4_INODES_PER_GROUP as u64 * EXT4_INODE_SIZE as u64 / block_size) as u32;
371 let gdt_blocks = (num_groups as u64 * EXT4_DESC_SIZE as u64).div_ceil(block_size) as u32;
372
373 let overhead_blocks = 1u64 + gdt_blocks as u64 + reserved_gdt_blocks as u64;
385 let block_bitmap_block = overhead_blocks;
386 let inode_bitmap_block = block_bitmap_block + 1;
387 let inode_table_block = inode_bitmap_block + 1;
388 let inode_table_end = inode_table_block + inode_table_blocks as u64;
389 let resize_inode_block = with_resize_inode.then_some(inode_table_end);
390 let first_data_block = inode_table_end + u64::from(with_resize_inode);
391 let journal_start_block = first_data_block + root_dir_blocks as u64;
392
393 let min_blocks = journal_start_block + opts.journal_blocks as u64 + 1; if num_blocks < min_blocks {
395 return Err(Ext4Error::TooSmall);
396 }
397
398 let uuid = uuid.unwrap_or_else(Self::generate_uuid);
401
402 let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, &uuid);
403
404 let mut feature_compat = EXT4_FEATURE_COMPAT_HAS_JOURNAL
405 | EXT4_FEATURE_COMPAT_EXT_ATTR
406 | EXT4_FEATURE_COMPAT_DIR_INDEX;
407 if with_resize_inode {
408 feature_compat |= EXT4_FEATURE_COMPAT_RESIZE_INODE;
409 }
410
411 let feature_incompat = EXT4_FEATURE_INCOMPAT_FILETYPE
412 | EXT4_FEATURE_INCOMPAT_EXTENTS
413 | EXT4_FEATURE_INCOMPAT_64BIT;
414
415 let feature_ro_compat = EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER
416 | EXT4_FEATURE_RO_COMPAT_LARGE_FILE
417 | EXT4_FEATURE_RO_COMPAT_HUGE_FILE
418 | EXT4_FEATURE_RO_COMPAT_DIR_NLINK
419 | EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE
420 | EXT4_FEATURE_RO_COMPAT_METADATA_CSUM;
421
422 let layout = Layout {
423 num_blocks,
424 num_groups,
425 uuid,
426 gdt_blocks,
427 reserved_gdt_blocks,
428 inode_table_blocks,
429 first_data_block,
430 resize_inode_block,
431 journal_start_block,
432 journal_blocks: opts.journal_blocks,
433 csum_seed,
434 feature_compat,
435 feature_incompat,
436 feature_ro_compat,
437 };
438 layout.validate_group_metadata()?;
439
440 Ok(layout)
441 }
442
443 fn generate_uuid() -> [u8; 16] {
444 let mut uuid = [0u8; 16];
446 if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
447 use std::io::Read;
448 let _ = f.read_exact(&mut uuid);
449 } else {
450 let now = std::time::SystemTime::now()
452 .duration_since(std::time::UNIX_EPOCH)
453 .unwrap_or_default();
454 let nanos = now.as_nanos();
455 uuid[..8].copy_from_slice(&(nanos as u64).to_le_bytes());
456 uuid[8..16].copy_from_slice(&((nanos >> 64) as u64).to_le_bytes());
457 }
458 uuid[6] = (uuid[6] & 0x0F) | 0x40;
460 uuid[7] = (uuid[7] & 0x3F) | 0x80;
461 uuid
462 }
463
464 fn geometry(&self) -> GroupGeometry {
466 GroupGeometry {
467 num_blocks: self.num_blocks,
468 gdt_blocks: self.gdt_blocks,
469 reserved_gdt_blocks: self.reserved_gdt_blocks,
470 inode_table_blocks: self.inode_table_blocks,
471 }
472 }
473
474 fn group_start_block(&self, group: u32) -> u64 {
475 self.geometry().group_start_block(group)
476 }
477
478 fn blocks_in_group(&self, group: u32) -> u32 {
479 self.geometry().blocks_in_group(group)
480 }
481
482 fn group_block_bitmap_block(&self, group: u32) -> u64 {
483 self.geometry().group_block_bitmap_block(group)
484 }
485
486 fn group_inode_bitmap_block(&self, group: u32) -> u64 {
487 self.geometry().group_inode_bitmap_block(group)
488 }
489
490 fn group_inode_table_block(&self, group: u32) -> u64 {
491 self.geometry().group_inode_table_block(group)
492 }
493
494 fn group_data_start_block(&self, group: u32) -> u64 {
495 let mut start = self.group_start_block(group) + self.group_metadata_blocks(group) as u64;
496 if group == 0 {
497 start = self.journal_start_block + self.journal_blocks as u64;
498 }
499 start
500 }
501
502 fn group_metadata_blocks(&self, group: u32) -> u32 {
503 self.geometry().group_metadata_blocks(group)
504 }
505
506 fn group_used_blocks(&self, group: u32) -> u32 {
507 let mut used = self.group_metadata_blocks(group);
508 if group == 0 {
509 used += 1 + self.journal_blocks + u32::from(self.resize_inode_block.is_some());
512 }
513 used.min(self.blocks_in_group(group))
514 }
515
516 fn group_free_blocks(&self, group: u32) -> u32 {
517 self.blocks_in_group(group)
518 .saturating_sub(self.group_used_blocks(group))
519 }
520
521 fn group_free_inodes(&self, group: u32) -> u32 {
522 if group == 0 {
523 EXT4_INODES_PER_GROUP - (EXT4_FIRST_INO - 1)
524 } else {
525 EXT4_INODES_PER_GROUP
526 }
527 }
528
529 #[cfg(test)]
530 fn group_used_dirs(&self, group: u32) -> u32 {
531 if group == 0 { 1 } else { 0 }
532 }
533
534 fn total_free_blocks(&self) -> u64 {
535 (0..self.num_groups)
536 .map(|group| self.group_free_blocks(group) as u64)
537 .sum()
538 }
539
540 fn total_free_inodes(&self) -> u64 {
541 (0..self.num_groups)
542 .map(|group| self.group_free_inodes(group) as u64)
543 .sum()
544 }
545
546 fn total_overhead_blocks(&self) -> u64 {
547 (0..self.num_groups)
548 .map(|group| u64::from(self.group_metadata_blocks(group)))
549 .sum()
550 }
551
552 fn recorded_overhead_blocks(&self, total_free_blocks: u64) -> (usize, u64) {
553 if self.resize_inode_block.is_none() {
554 (
557 EXT4_SB_ERROR_COUNT_OFFSET,
558 self.num_blocks - total_free_blocks,
559 )
560 } else {
561 (EXT4_SB_OVERHEAD_BLOCKS_OFFSET, self.total_overhead_blocks())
562 }
563 }
564
565 fn validate_group_metadata(&self) -> Result<(), Ext4Error> {
566 for group in 0..self.num_groups {
567 let blocks_in_group = self.blocks_in_group(group);
568 let metadata_blocks = self.group_metadata_blocks(group);
569 if blocks_in_group < metadata_blocks {
570 return Err(Ext4Error::InvalidSize(format!(
571 "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"
572 )));
573 }
574 }
575
576 Ok(())
577 }
578}
579
580impl BitmapPlan {
581 fn new(layout: &Layout, plans: &[NodePlan]) -> Self {
582 let mut block_extents = Vec::new();
583 if let Some(block) = layout.resize_inode_block {
584 block_extents.push((block, 1));
585 }
586 block_extents.push((
587 layout.first_data_block,
588 (layout.journal_start_block - layout.first_data_block) as u32,
589 ));
590 block_extents.push((layout.journal_start_block, layout.journal_blocks));
591
592 for plan in plans {
593 for extent in &plan.data_extents {
594 block_extents.push((extent.physical_start, extent.block_count));
595 }
596 if let Some(block) = plan.extent_tree_block {
597 block_extents.push((block, 1));
598 }
599 if let Some(block) = plan.xattr_block {
600 block_extents.push((block, 1));
601 }
602 }
603
604 let max_used_inode = plans
605 .iter()
606 .map(|plan| plan.inode)
607 .max()
608 .unwrap_or(EXT4_JOURNAL_INO)
609 .max(EXT4_FIRST_INO - 1);
610 let mut group_used_dirs = vec![0u32; layout.num_groups as usize];
611 for plan in plans
612 .iter()
613 .filter(|plan| matches!(plan.kind, NodeKind::Directory { .. }))
614 {
615 let group = (plan.inode - 1) / EXT4_INODES_PER_GROUP;
616 group_used_dirs[group as usize] += 1;
617 }
618
619 Self {
620 block_extents,
621 max_used_inode,
622 group_used_dirs,
623 }
624 }
625}
626
627pub fn format_ext4(path: &Path, options: &Ext4FormatOptions) -> Result<(), Ext4Error> {
637 let tree = FileTree::new();
638 format_ext4_with_tree(path, options, tree)
639}
640
641pub fn format_ext4_with_tree(
642 path: &Path,
643 options: &Ext4FormatOptions,
644 tree: FileTree,
645) -> Result<(), Ext4Error> {
646 format_ext4_with_tree_and_reserved(path, options, tree, RESERVED_GDT_BLOCKS)
647}
648
649#[cfg(test)]
652pub(super) fn format_ext4_for_test_with_reserved_gdt(
653 path: &Path,
654 options: &Ext4FormatOptions,
655 reserved_gdt_blocks: u32,
656) -> Result<(), Ext4Error> {
657 format_ext4_with_tree_and_reserved(path, options, FileTree::new(), reserved_gdt_blocks)
658}
659
660fn format_ext4_with_tree_and_reserved(
661 path: &Path,
662 options: &Ext4FormatOptions,
663 tree: FileTree,
664 reserved_gdt_blocks: u32,
665) -> Result<(), Ext4Error> {
666 format_ext4_with_tree_and_reserved_uuid(
667 path,
668 options,
669 tree,
670 reserved_gdt_blocks,
671 None,
672 TreeEncodingMode::Upper,
673 true,
674 )
675}
676
677pub(super) fn format_ext4_rootfs_with_tree_and_uuid(
678 path: &Path,
679 options: &Ext4FormatOptions,
680 tree: FileTree,
681 uuid: [u8; 16],
682) -> Result<(), Ext4Error> {
683 format_ext4_with_tree_and_reserved_uuid(
684 path,
685 options,
686 tree,
687 RESERVED_GDT_BLOCKS,
688 Some(uuid),
689 TreeEncodingMode::Rootfs,
690 true,
691 )
692}
693
694#[cfg(test)]
699pub(super) fn format_ext4_legacy_for_test(
700 path: &Path,
701 options: &Ext4FormatOptions,
702) -> Result<(), Ext4Error> {
703 format_ext4_with_tree_and_reserved_uuid(
704 path,
705 options,
706 FileTree::new(),
707 RESERVED_GDT_BLOCKS,
708 None,
709 TreeEncodingMode::Upper,
710 false,
711 )
712}
713
714fn format_ext4_with_tree_and_reserved_uuid(
715 path: &Path,
716 options: &Ext4FormatOptions,
717 tree: FileTree,
718 reserved_gdt_blocks: u32,
719 uuid: Option<[u8; 16]>,
720 encoding_mode: TreeEncodingMode,
721 with_resize_inode: bool,
722) -> Result<(), Ext4Error> {
723 let mut next_inode = EXT4_FIRST_INO;
724 let mut plans = Vec::new();
725 let root_metadata = planned_metadata(&tree.root.metadata, true, encoding_mode);
726 let mut hardlinks = HashMap::new();
727 let root_draft = draft_directory(
728 "/",
729 tree.root,
730 EXT4_ROOT_INO,
731 EXT4_ROOT_INO,
732 &mut DraftContext {
733 next_inode: &mut next_inode,
734 plans: &mut plans,
735 hardlinks: &mut hardlinks,
736 encoding_mode,
737 },
738 )?;
739 let root_dir_blocks = blocks_for_len(root_draft.data.len());
740 let layout = Layout::compute_with_root_blocks_and_uuid(
741 options,
742 root_dir_blocks.max(1),
743 reserved_gdt_blocks,
744 uuid,
745 with_resize_inode,
746 )?;
747 let max_inode = next_inode.saturating_sub(1);
748 let available_inodes = layout.num_groups.saturating_mul(EXT4_INODES_PER_GROUP);
749 if max_inode > available_inodes {
750 return Err(Ext4Error::Layout(format!(
751 "rootfs needs inode {max_inode}, but the filesystem geometry provides only {available_inodes} inodes"
752 )));
753 }
754 let mut allocator = DataAllocator::new(&layout);
755 let root_data_extents = split_physical_range(layout.first_data_block, root_dir_blocks.max(1));
756 let root_extent_tree_block = if root_data_extents.len() > INLINE_EXTENT_CAPACITY {
757 Some(allocator.allocate_block("/")?)
758 } else {
759 None
760 };
761 let root_inline_xattrs = build_inline_xattrs(&root_draft.xattrs, "/")?;
762 let root_xattr_block = if root_draft.xattrs.is_empty() || root_inline_xattrs.is_some() {
763 None
764 } else {
765 Some(allocator.allocate_block("/")?)
766 };
767
768 for plan in &mut plans {
769 allocate_node_data(&mut allocator, plan)?;
770 }
771
772 let mut all_plans = Vec::with_capacity(plans.len() + 1);
773 all_plans.push(NodePlan {
774 inode: EXT4_ROOT_INO,
775 path: "/".to_string(),
776 metadata: root_metadata,
777 links_count: 2 + u32::from(root_draft.children),
778 kind: NodeKind::Directory {
779 data: root_draft.data,
780 },
781 block_count: root_dir_blocks.max(1),
782 data_extents: root_data_extents,
783 extent_tree_block: root_extent_tree_block,
784 xattrs: root_draft.xattrs,
785 inline_xattrs: root_inline_xattrs,
786 xattr_block: root_xattr_block,
787 });
788 all_plans.extend(plans);
789 all_plans.sort_by_key(|plan| plan.inode);
790 validate_node_extents(&all_plans)?;
791
792 let bitmap_plan = BitmapPlan::new(&layout, &all_plans);
793 let stats = compute_fs_stats(&layout, &bitmap_plan);
794
795 let raw_file = std::fs::File::create(path)?;
796 mark_sparse(&raw_file)?;
797 raw_file.set_len(options.size_bytes)?;
798 let mut file = BufWriter::new(raw_file);
799
800 write_bitmaps(&mut file, &layout, &bitmap_plan)?;
801 write_tree_data(&mut file, &layout, &all_plans)?;
802 write_inode_table_with_plan(&mut file, &layout, &all_plans)?;
803 if let Some(resize_inode_block) = layout.resize_inode_block {
804 write_resize_inode(
805 &mut file,
806 &layout.geometry(),
807 layout.group_inode_table_block(0),
808 resize_inode_block,
809 layout.csum_seed,
810 layout.num_groups,
811 )?;
812 }
813 write_journal(&mut file, &layout)?;
814
815 let sb_bytes = build_superblock_with_stats(&layout, &stats)?;
816 write_primary_superblock_at(&mut file, &sb_bytes)?;
817
818 let gdt_bytes = build_gdt_with_stats(&layout, &stats)?;
819 write_gdt_at(&mut file, 0, &gdt_bytes)?;
820
821 for g in 1..layout.num_groups {
822 if sparse_super_group(g) {
823 let backup_sb_bytes = build_backup_superblock_with_stats(&layout, &stats, g)?;
824 write_backup_superblock_at(&mut file, layout.group_start_block(g), &backup_sb_bytes)?;
825 write_gdt_at(&mut file, layout.group_start_block(g), &gdt_bytes)?;
826 }
827 }
828
829 file.flush()?;
830 Ok(())
834}
835
836#[cfg(not(windows))]
837pub(super) fn mark_sparse(_file: &std::fs::File) -> Result<(), Ext4Error> {
838 Ok(())
839}
840
841#[cfg(windows)]
842pub(super) fn mark_sparse(file: &std::fs::File) -> Result<(), Ext4Error> {
843 let mut bytes_returned = 0;
844 let ok = unsafe {
845 DeviceIoControl(
846 file.as_raw_handle() as HANDLE,
847 FSCTL_SET_SPARSE,
848 ptr::null(),
849 0,
850 ptr::null_mut(),
851 0,
852 &mut bytes_returned,
853 ptr::null_mut(),
854 )
855 };
856 if ok == 0 {
857 return Err(Ext4Error::Io(io::Error::last_os_error()));
858 }
859
860 Ok(())
861}
862
863fn draft_directory(
864 path: &str,
865 dir: DirectoryNode,
866 inode: u32,
867 parent_inode: u32,
868 context: &mut DraftContext<'_>,
869) -> Result<DraftDirectory, Ext4Error> {
870 let DirectoryNode {
871 entries, xattrs, ..
872 } = dir;
873
874 let mut children = Vec::new();
875 let mut child_dir_count = 0u16;
876
877 for (name, node) in entries {
878 let name_bytes = os_str_bytes(name.as_os_str()).to_vec();
879 let child_path = child_path(path, &name_bytes);
880
881 if let TreeNode::RegularFile(file) = &node
884 && let Some((inode, plan_index)) = context.hardlinks.get(&file.id).copied()
885 {
886 let plan = &mut context.plans[plan_index];
887 if !hardlink_matches_plan(plan, file, context.encoding_mode) {
888 return Err(Ext4Error::Layout(format!(
889 "hardlink alias '{child_path}' conflicts with the metadata or content of inode {inode}"
890 )));
891 }
892 plan.links_count = plan.links_count.checked_add(1).ok_or_else(|| {
893 Ext4Error::Layout(format!("too many hardlinks for '{child_path}'"))
894 })?;
895 children.push(DirEntrySpec {
896 inode,
897 file_type: EXT4_FT_REG_FILE,
898 name: name_bytes,
899 });
900 continue;
901 }
902
903 let child_inode = *context.next_inode;
904 *context.next_inode = child_inode.checked_add(1).ok_or_else(|| {
905 Ext4Error::Layout("rootfs inode numbering overflowed u32".to_string())
906 })?;
907
908 match node {
909 TreeNode::Directory(child_dir) => {
910 child_dir_count = child_dir_count.saturating_add(1);
911 let metadata = planned_metadata(&child_dir.metadata, true, context.encoding_mode);
912 let child_draft =
913 draft_directory(&child_path, child_dir, child_inode, inode, context)?;
914 let block_count = blocks_for_len(child_draft.data.len());
915 context.plans.push(NodePlan {
916 inode: child_inode,
917 path: child_path.clone(),
918 metadata,
919 links_count: 2 + u32::from(child_draft.children),
920 kind: NodeKind::Directory {
921 data: child_draft.data,
922 },
923 block_count,
924 data_extents: Vec::new(),
925 extent_tree_block: None,
926 xattrs: child_draft.xattrs,
927 inline_xattrs: None,
928 xattr_block: None,
929 });
930 children.push(DirEntrySpec {
931 inode: child_inode,
932 file_type: EXT4_FT_DIR,
933 name: name_bytes,
934 });
935 }
936 TreeNode::RegularFile(file) => {
937 let plan_index = context.plans.len();
938 let file_id = file.id;
939 context.plans.push(NodePlan {
940 inode: child_inode,
941 path: child_path.clone(),
942 metadata: planned_metadata(&file.metadata, false, context.encoding_mode),
943 links_count: 1,
944 block_count: blocks_for_len(file.data.len()),
945 kind: NodeKind::RegularFile { data: file.data },
946 data_extents: Vec::new(),
947 extent_tree_block: None,
948 xattrs: file.xattrs,
949 inline_xattrs: None,
950 xattr_block: None,
951 });
952 context.hardlinks.insert(file_id, (child_inode, plan_index));
953 children.push(DirEntrySpec {
954 inode: child_inode,
955 file_type: EXT4_FT_REG_FILE,
956 name: name_bytes,
957 });
958 }
959 TreeNode::Symlink(symlink) => {
960 let target_len = symlink.target.len();
961 let inline = target_len <= 59;
962 let block_count = if inline {
963 0
964 } else {
965 blocks_for_len(target_len)
966 };
967 context.plans.push(NodePlan {
968 inode: child_inode,
969 path: child_path.clone(),
970 metadata: planned_metadata(&symlink.metadata, false, context.encoding_mode),
971 links_count: 1,
972 kind: NodeKind::Symlink {
973 target: symlink.target,
974 inline,
975 },
976 block_count,
977 data_extents: Vec::new(),
978 extent_tree_block: None,
979 xattrs: Vec::new(),
980 inline_xattrs: None,
981 xattr_block: None,
982 });
983 children.push(DirEntrySpec {
984 inode: child_inode,
985 file_type: EXT4_FT_SYMLINK,
986 name: name_bytes,
987 });
988 }
989 TreeNode::CharDevice(device) => {
990 context.plans.push(NodePlan {
991 inode: child_inode,
992 path: child_path.clone(),
993 metadata: planned_metadata(&device.metadata, false, context.encoding_mode),
994 links_count: 1,
995 kind: NodeKind::CharDevice {
996 major: device.major,
997 minor: device.minor,
998 },
999 block_count: 0,
1000 data_extents: Vec::new(),
1001 extent_tree_block: None,
1002 xattrs: Vec::new(),
1003 inline_xattrs: None,
1004 xattr_block: None,
1005 });
1006 children.push(DirEntrySpec {
1007 inode: child_inode,
1008 file_type: EXT4_FT_CHRDEV,
1009 name: name_bytes,
1010 });
1011 }
1012 TreeNode::BlockDevice(device) => {
1013 context.plans.push(NodePlan {
1014 inode: child_inode,
1015 path: child_path.clone(),
1016 metadata: planned_metadata(&device.metadata, false, context.encoding_mode),
1017 links_count: 1,
1018 kind: NodeKind::BlockDevice {
1019 major: device.major,
1020 minor: device.minor,
1021 },
1022 block_count: 0,
1023 data_extents: Vec::new(),
1024 extent_tree_block: None,
1025 xattrs: Vec::new(),
1026 inline_xattrs: None,
1027 xattr_block: None,
1028 });
1029 children.push(DirEntrySpec {
1030 inode: child_inode,
1031 file_type: EXT4_FT_BLKDEV,
1032 name: name_bytes,
1033 });
1034 }
1035 TreeNode::Fifo(metadata) => {
1036 context.plans.push(NodePlan {
1037 inode: child_inode,
1038 path: child_path.clone(),
1039 metadata: planned_metadata(&metadata, false, context.encoding_mode),
1040 links_count: 1,
1041 kind: NodeKind::Fifo,
1042 block_count: 0,
1043 data_extents: Vec::new(),
1044 extent_tree_block: None,
1045 xattrs: Vec::new(),
1046 inline_xattrs: None,
1047 xattr_block: None,
1048 });
1049 children.push(DirEntrySpec {
1050 inode: child_inode,
1051 file_type: EXT4_FT_FIFO,
1052 name: name_bytes,
1053 });
1054 }
1055 TreeNode::Socket(metadata) => {
1056 context.plans.push(NodePlan {
1057 inode: child_inode,
1058 path: child_path.clone(),
1059 metadata: planned_metadata(&metadata, false, context.encoding_mode),
1060 links_count: 1,
1061 kind: NodeKind::Socket,
1062 block_count: 0,
1063 data_extents: Vec::new(),
1064 extent_tree_block: None,
1065 xattrs: Vec::new(),
1066 inline_xattrs: None,
1067 xattr_block: None,
1068 });
1069 children.push(DirEntrySpec {
1070 inode: child_inode,
1071 file_type: EXT4_FT_SOCK,
1072 name: name_bytes,
1073 });
1074 }
1075 }
1076 }
1077
1078 let data = build_directory_data(inode, parent_inode, &children, path)?;
1079 Ok(DraftDirectory {
1080 children: child_dir_count,
1081 data,
1082 xattrs,
1083 })
1084}
1085
1086fn planned_metadata(
1087 metadata: &InodeMetadata,
1088 directory: bool,
1089 encoding_mode: TreeEncodingMode,
1090) -> InodeMetadata {
1091 match encoding_mode {
1092 TreeEncodingMode::Upper => InodeMetadata {
1093 uid: 0,
1094 gid: 0,
1095 mode: if directory {
1096 normalize_dir_permissions(metadata.mode)
1097 } else {
1098 normalize_file_permissions(metadata.mode)
1099 },
1100 mtime: 0,
1101 mtime_nsec: 0,
1102 },
1103 TreeEncodingMode::Rootfs => InodeMetadata {
1104 uid: metadata.uid,
1105 gid: metadata.gid,
1106 mode: metadata.mode & 0o7777,
1107 mtime: metadata.mtime,
1108 mtime_nsec: metadata.mtime_nsec,
1109 },
1110 }
1111}
1112
1113fn child_path(parent: &str, name: &[u8]) -> String {
1114 let name = String::from_utf8_lossy(name);
1115 if parent == "/" {
1116 format!("/{name}")
1117 } else {
1118 format!("{parent}/{name}")
1119 }
1120}
1121
1122fn normalize_file_permissions(mode: u16) -> u16 {
1123 let perms = mode & 0o7777;
1124 if perms == 0 { 0o644 } else { perms }
1125}
1126
1127fn hardlink_matches_plan(
1128 plan: &NodePlan,
1129 file: &crate::tree::RegularFileNode,
1130 encoding_mode: TreeEncodingMode,
1131) -> bool {
1132 let metadata = &plan.metadata;
1133 let candidate = planned_metadata(&file.metadata, false, encoding_mode);
1134 let metadata_matches = metadata.uid == candidate.uid
1135 && metadata.gid == candidate.gid
1136 && metadata.mode == candidate.mode
1137 && metadata.mtime == candidate.mtime
1138 && metadata.mtime_nsec == candidate.mtime_nsec;
1139 let xattrs_match = plan.xattrs.len() == file.xattrs.len()
1140 && plan.xattrs.iter().all(|left| {
1141 file.xattrs
1142 .iter()
1143 .any(|right| left.name == right.name && left.value == right.value)
1144 });
1145 let data_matches = match (&plan.kind, &file.data) {
1146 (NodeKind::RegularFile { data: left }, right) => same_file_data(left, right),
1147 _ => false,
1148 };
1149 metadata_matches && xattrs_match && data_matches
1150}
1151
1152fn same_file_data(left: &FileData, right: &FileData) -> bool {
1153 match (left, right) {
1154 (FileData::Memory(left), FileData::Memory(right)) => left == right,
1155 (FileData::SharedMemory(left), FileData::SharedMemory(right)) => {
1156 std::sync::Arc::ptr_eq(left, right) || left.as_ref() == right.as_ref()
1157 }
1158 (FileData::Memory(left), FileData::SharedMemory(right))
1159 | (FileData::SharedMemory(right), FileData::Memory(left)) => {
1160 left.as_slice() == right.as_ref()
1161 }
1162 (
1163 FileData::Spool {
1164 spool: left_spool,
1165 offset: left_offset,
1166 len: left_len,
1167 },
1168 FileData::Spool {
1169 spool: right_spool,
1170 offset: right_offset,
1171 len: right_len,
1172 },
1173 ) => {
1174 std::sync::Arc::ptr_eq(left_spool, right_spool)
1175 && left_offset == right_offset
1176 && left_len == right_len
1177 }
1178 _ => false,
1179 }
1180}
1181
1182fn normalize_dir_permissions(mode: u16) -> u16 {
1183 let perms = mode & 0o7777;
1184 if perms == 0 { 0o755 } else { perms }
1185}
1186
1187fn blocks_for_len(len: usize) -> u32 {
1188 if len == 0 {
1189 0
1190 } else {
1191 (len as u64).div_ceil(EXT4_BLOCK_SIZE as u64) as u32
1192 }
1193}
1194
1195fn validate_node_extents(plans: &[NodePlan]) -> Result<(), Ext4Error> {
1196 for plan in plans {
1197 if plan.data_extents.len() > EXTENT_BLOCK_CAPACITY {
1198 return Err(Ext4Error::Layout(format!(
1199 "'{}' needs {} extents, but a depth-one ext4 extent tree holds at most {EXTENT_BLOCK_CAPACITY}",
1200 plan.path,
1201 plan.data_extents.len()
1202 )));
1203 }
1204 for extent in &plan.data_extents {
1205 validate_extent_block_count(extent.block_count, &plan.path)?;
1206 }
1207 }
1208
1209 Ok(())
1210}
1211
1212fn validate_extent_block_count(block_count: u32, label: &str) -> Result<(), Ext4Error> {
1213 if block_count > MAX_INITIALIZED_EXTENT_BLOCKS {
1214 return Err(Ext4Error::Layout(format!(
1215 "'{label}' needs {block_count} blocks but this formatter currently supports one initialized extent of at most {MAX_INITIALIZED_EXTENT_BLOCKS} blocks"
1216 )));
1217 }
1218
1219 Ok(())
1220}
1221
1222fn split_physical_range(start: u64, blocks: u32) -> Vec<AllocatedExtent> {
1223 let mut extents = Vec::new();
1224 let mut remaining = blocks;
1225 let mut logical_block = 0u32;
1226 let mut physical_start = start;
1227 while remaining > 0 {
1228 let block_count = remaining.min(MAX_INITIALIZED_EXTENT_BLOCKS);
1229 extents.push(AllocatedExtent {
1230 logical_block,
1231 physical_start,
1232 block_count,
1233 });
1234 logical_block += block_count;
1235 physical_start += u64::from(block_count);
1236 remaining -= block_count;
1237 }
1238 extents
1239}
1240
1241fn build_directory_data(
1242 dir_inode: u32,
1243 parent_inode: u32,
1244 children: &[DirEntrySpec],
1245 path: &str,
1246) -> Result<Vec<u8>, Ext4Error> {
1247 let mut entries = Vec::with_capacity(children.len() + 2);
1248 entries.push(DirEntrySpec {
1249 inode: dir_inode,
1250 file_type: EXT4_FT_DIR,
1251 name: b".".to_vec(),
1252 });
1253 entries.push(DirEntrySpec {
1254 inode: parent_inode,
1255 file_type: EXT4_FT_DIR,
1256 name: b"..".to_vec(),
1257 });
1258 entries.extend(children.iter().map(|entry| DirEntrySpec {
1259 inode: entry.inode,
1260 file_type: entry.file_type,
1261 name: entry.name.clone(),
1262 }));
1263
1264 let mut blocks = Vec::new();
1265 let mut index = 0usize;
1266 while index < entries.len() {
1267 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1268 let mut pos = 0usize;
1269 let data_limit = EXT4_BLOCK_SIZE as usize - 12;
1270 let block_start = index;
1271
1272 while index < entries.len() {
1273 let min_len = dir_entry_len(entries[index].name.len());
1274 let needed = if pos == 0 { min_len } else { pos + min_len };
1275 if needed > data_limit {
1276 if pos == 0 {
1277 return Err(Ext4Error::Layout(format!(
1278 "directory entry too large for '{path}'"
1279 )));
1280 }
1281 break;
1282 }
1283 pos += min_len;
1284 index += 1;
1285 }
1286
1287 let mut write_pos = 0usize;
1288 for (entry_index, entry) in entries[block_start..index].iter().enumerate() {
1289 let is_last = entry_index + 1 == index - block_start;
1290 let rec_len = if is_last {
1291 (data_limit - write_pos) as u16
1292 } else {
1293 dir_entry_len(entry.name.len()) as u16
1294 };
1295 put_le32(&mut block, write_pos, entry.inode);
1296 put_le16(&mut block, write_pos + 4, rec_len);
1297 block[write_pos + 6] = entry.name.len() as u8;
1298 block[write_pos + 7] = entry.file_type;
1299 block[write_pos + 8..write_pos + 8 + entry.name.len()].copy_from_slice(&entry.name);
1300 write_pos += rec_len as usize;
1301 }
1302
1303 let tail = data_limit;
1304 put_le32(&mut block, tail, 0);
1305 put_le16(&mut block, tail + 4, 12);
1306 block[tail + 6] = 0;
1307 block[tail + 7] = 0xDE;
1308 blocks.extend_from_slice(&block);
1309 }
1310
1311 if blocks.is_empty() {
1312 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1313 put_le32(&mut block, 0, dir_inode);
1314 put_le16(&mut block, 4, 12);
1315 block[6] = 1;
1316 block[7] = EXT4_FT_DIR;
1317 block[8] = b'.';
1318 put_le32(&mut block, 12, parent_inode);
1319 put_le16(&mut block, 16, (EXT4_BLOCK_SIZE - 24) as u16);
1320 block[18] = 2;
1321 block[19] = EXT4_FT_DIR;
1322 block[20] = b'.';
1323 block[21] = b'.';
1324 let tail = EXT4_BLOCK_SIZE as usize - 12;
1325 put_le32(&mut block, tail, 0);
1326 put_le16(&mut block, tail + 4, 12);
1327 block[tail + 7] = 0xDE;
1328 blocks = block;
1329 }
1330
1331 Ok(blocks)
1332}
1333
1334fn dir_entry_len(name_len: usize) -> usize {
1335 (8 + name_len + 3) & !3
1336}
1337
1338fn allocate_node_data(allocator: &mut DataAllocator, plan: &mut NodePlan) -> Result<(), Ext4Error> {
1339 if plan.block_count > 0 {
1340 plan.data_extents = allocator.allocate_extents(plan.block_count, &plan.path)?;
1341 if plan.data_extents.len() > INLINE_EXTENT_CAPACITY {
1342 plan.extent_tree_block = Some(allocator.allocate_block(&plan.path)?);
1343 }
1344 }
1345
1346 if !plan.xattrs.is_empty() {
1347 plan.inline_xattrs = build_inline_xattrs(&plan.xattrs, &plan.path)?;
1348 if plan.inline_xattrs.is_none() {
1349 plan.xattr_block = Some(allocator.allocate_block(&plan.path)?);
1350 }
1351 }
1352 Ok(())
1353}
1354
1355impl DataAllocator {
1356 fn new(layout: &Layout) -> Self {
1357 let mut regions = Vec::new();
1358 for group in 0..layout.num_groups {
1359 let group_start = layout.group_start_block(group);
1360 let group_end = group_start + layout.blocks_in_group(group) as u64;
1361 let start = layout.group_data_start_block(group);
1362 if start < group_end {
1363 regions.push((start, (group_end - start) as u32));
1364 }
1365 }
1366 Self { regions }
1367 }
1368
1369 fn allocate_extents(
1370 &mut self,
1371 blocks: u32,
1372 path: &str,
1373 ) -> Result<Vec<AllocatedExtent>, Ext4Error> {
1374 let mut extents = Vec::new();
1375 let mut remaining = blocks;
1376 let mut logical_block = 0u32;
1377
1378 for region in &mut self.regions {
1379 while remaining > 0 && region.1 > 0 {
1380 let block_count = remaining.min(region.1).min(MAX_INITIALIZED_EXTENT_BLOCKS);
1381 extents.push(AllocatedExtent {
1382 logical_block,
1383 physical_start: region.0,
1384 block_count,
1385 });
1386 region.0 += u64::from(block_count);
1387 region.1 -= block_count;
1388 remaining -= block_count;
1389 logical_block += block_count;
1390 }
1391 if remaining == 0 {
1392 return Ok(extents);
1393 }
1394 }
1395
1396 Err(Ext4Error::Layout(format!(
1397 "not enough space in ext4 image for '{path}'"
1398 )))
1399 }
1400
1401 fn allocate_block(&mut self, path: &str) -> Result<u64, Ext4Error> {
1402 self.allocate_extents(1, path)?
1403 .first()
1404 .map(|extent| extent.physical_start)
1405 .ok_or_else(|| {
1406 Ext4Error::Layout(format!("could not allocate extent tree for '{path}'"))
1407 })
1408 }
1409}
1410
1411fn build_block_bitmap_for_plan(layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
1412 let mut bitmap = build_block_bitmap_base(&layout.geometry(), group);
1413 let group_start = layout.group_start_block(group);
1414 let group_end = group_start + layout.blocks_in_group(group) as u64;
1415
1416 for (start, len) in &plan.block_extents {
1417 let extent_start = *start;
1418 let extent_end = extent_start + *len as u64;
1419 let overlap_start = extent_start.max(group_start);
1420 let overlap_end = extent_end.min(group_end);
1421 if overlap_start < overlap_end {
1422 for block in overlap_start..overlap_end {
1423 let bit = block - group_start;
1424 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1425 }
1426 }
1427 }
1428
1429 bitmap
1430}
1431
1432fn build_inode_bitmap_for_plan(_layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
1433 let first_inode = group * EXT4_INODES_PER_GROUP + 1;
1434 let last_inode = first_inode + EXT4_INODES_PER_GROUP - 1;
1435 let used_inodes = if plan.max_used_inode < first_inode {
1436 0
1437 } else {
1438 plan.max_used_inode.min(last_inode) - first_inode + 1
1439 };
1440 build_inode_bitmap_base(used_inodes)
1441}
1442
1443fn compute_fs_stats(layout: &Layout, plan: &BitmapPlan) -> FsStats {
1444 let mut group_free_blocks = Vec::with_capacity(layout.num_groups as usize);
1445 let mut block_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
1446 let mut inode_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
1447
1448 let mut total_free_blocks = 0u64;
1449 for group in 0..layout.num_groups {
1450 let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
1451 let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
1452 let blocks_in_group = layout.blocks_in_group(group) as usize;
1453 let used = count_used_bits(&block_bitmap, blocks_in_group);
1454 let free = blocks_in_group.saturating_sub(used) as u32;
1455 group_free_blocks.push(free);
1456 block_bitmap_checksums.push(bitmap_checksum(
1457 layout.csum_seed,
1458 &block_bitmap,
1459 EXT4_BLOCK_SIZE as usize,
1460 ));
1461 inode_bitmap_checksums.push(bitmap_checksum(
1462 layout.csum_seed,
1463 &inode_bitmap,
1464 (EXT4_INODES_PER_GROUP / 8) as usize,
1465 ));
1466 total_free_blocks += free as u64;
1467 }
1468
1469 let group_free_inodes = (0..layout.num_groups)
1470 .map(|group| {
1471 let first_inode = group * EXT4_INODES_PER_GROUP + 1;
1472 let last_inode = first_inode + EXT4_INODES_PER_GROUP - 1;
1473 let used_inodes = if plan.max_used_inode < first_inode {
1474 0
1475 } else {
1476 plan.max_used_inode.min(last_inode) - first_inode + 1
1477 };
1478 EXT4_INODES_PER_GROUP - used_inodes
1479 })
1480 .collect::<Vec<_>>();
1481 let total_free_inodes = group_free_inodes.iter().map(|count| *count as u64).sum();
1482
1483 let group_used_dirs = plan.group_used_dirs.clone();
1484
1485 FsStats {
1486 group_free_blocks,
1487 group_free_inodes,
1488 group_used_dirs,
1489 block_bitmap_checksums,
1490 inode_bitmap_checksums,
1491 total_free_blocks,
1492 total_free_inodes,
1493 overhead_blocks: layout.total_overhead_blocks(),
1494 }
1495}
1496
1497fn write_tree_data(
1498 file: &mut (impl std::io::Write + std::io::Seek),
1499 layout: &Layout,
1500 plans: &[NodePlan],
1501) -> Result<(), Ext4Error> {
1502 for plan in plans {
1503 match &plan.kind {
1504 NodeKind::Directory { data, .. } => {
1505 let mut bytes = data.clone();
1506 update_dir_block_checksums(layout.csum_seed, plan.inode, &mut bytes);
1507 write_bytes_to_extents(file, &plan.data_extents, &bytes)?;
1508 }
1509 NodeKind::RegularFile { data } => {
1510 write_file_data(file, &plan.data_extents, data)?;
1511 }
1512 NodeKind::Symlink { target, inline } => {
1513 if !inline {
1514 write_bytes_to_extents(file, &plan.data_extents, target)?;
1515 }
1516 }
1517 NodeKind::CharDevice { .. }
1518 | NodeKind::BlockDevice { .. }
1519 | NodeKind::Fifo
1520 | NodeKind::Socket => {}
1521 }
1522
1523 if let Some(block) = plan.extent_tree_block {
1524 let bytes = build_extent_leaf_block(layout, plan)?;
1525 write_extent_bytes(file, block, &bytes)?;
1526 }
1527 if let Some(block) = plan.xattr_block {
1528 let bytes = build_xattr_block(layout, block, &plan.xattrs, &plan.path)?;
1529 write_extent_bytes(file, block, &bytes)?;
1530 }
1531 }
1532
1533 Ok(())
1534}
1535
1536fn write_file_data(
1537 file: &mut (impl std::io::Write + std::io::Seek),
1538 extents: &[AllocatedExtent],
1539 data: &FileData,
1540) -> Result<(), Ext4Error> {
1541 let mut data_offset = 0usize;
1542 for extent in extents {
1543 let remaining = data.len().saturating_sub(data_offset);
1544 let extent_bytes = extent.block_count as usize * EXT4_BLOCK_SIZE as usize;
1545 let write_len = remaining.min(extent_bytes);
1546 file.seek(SeekFrom::Start(
1547 extent.physical_start * EXT4_BLOCK_SIZE as u64,
1548 ))?;
1549 data.write_range(data_offset, write_len, file)?;
1550 write_zero_padding(file, extent_bytes - write_len)?;
1551 data_offset += write_len;
1552 }
1553
1554 debug_assert_eq!(data_offset, data.len());
1555
1556 Ok(())
1557}
1558
1559fn write_bytes_to_extents(
1560 file: &mut (impl std::io::Write + std::io::Seek),
1561 extents: &[AllocatedExtent],
1562 data: &[u8],
1563) -> Result<(), Ext4Error> {
1564 let mut data_offset = 0usize;
1565 for extent in extents {
1566 let remaining = data.len().saturating_sub(data_offset);
1567 let extent_bytes = extent.block_count as usize * EXT4_BLOCK_SIZE as usize;
1568 let write_len = remaining.min(extent_bytes);
1569 file.seek(SeekFrom::Start(
1570 extent.physical_start * EXT4_BLOCK_SIZE as u64,
1571 ))?;
1572 file.write_all(&data[data_offset..data_offset + write_len])?;
1573 write_zero_padding(file, extent_bytes - write_len)?;
1574 data_offset += write_len;
1575 }
1576
1577 debug_assert_eq!(data_offset, data.len());
1578 Ok(())
1579}
1580
1581fn write_zero_padding(file: &mut impl std::io::Write, mut len: usize) -> Result<(), Ext4Error> {
1582 static ZEROS: [u8; EXT4_BLOCK_SIZE as usize] = [0u8; EXT4_BLOCK_SIZE as usize];
1583 while len > 0 {
1584 let chunk = len.min(ZEROS.len());
1585 file.write_all(&ZEROS[..chunk])?;
1586 len -= chunk;
1587 }
1588 Ok(())
1589}
1590
1591fn write_extent_bytes(
1592 file: &mut (impl std::io::Write + std::io::Seek),
1593 start_block: u64,
1594 data: &[u8],
1595) -> Result<(), Ext4Error> {
1596 let offset = start_block * EXT4_BLOCK_SIZE as u64;
1597 file.seek(SeekFrom::Start(offset))?;
1598 file.write_all(data)?;
1599
1600 let pad = (EXT4_BLOCK_SIZE as usize - (data.len() % EXT4_BLOCK_SIZE as usize))
1601 % EXT4_BLOCK_SIZE as usize;
1602 if pad > 0 {
1603 static ZEROS: [u8; 4096] = [0u8; 4096];
1604 file.write_all(&ZEROS[..pad])?;
1605 }
1606
1607 Ok(())
1608}
1609
1610fn update_dir_block_checksums(csum_seed: u32, inode: u32, data: &mut [u8]) {
1611 for chunk in data.as_chunks_mut::<{ EXT4_BLOCK_SIZE as usize }>().0 {
1612 let tail = EXT4_BLOCK_SIZE as usize - 12;
1613 let checksum = dir_block_checksum(csum_seed, inode, 0, &chunk[..tail]);
1614 put_le32(chunk, tail + 8, checksum);
1615 }
1616}
1617
1618fn write_inode_table_with_plan(
1619 file: &mut (impl std::io::Write + std::io::Seek),
1620 layout: &Layout,
1621 plans: &[NodePlan],
1622) -> Result<(), Ext4Error> {
1623 let root_inode = build_inode_from_plan(layout, &plans[0])?;
1624 let root_offset = inode_offset(layout, EXT4_ROOT_INO);
1625 file.seek(SeekFrom::Start(root_offset))?;
1626 file.write_all(&root_inode)?;
1627
1628 let journal_inode = build_journal_inode(layout)?;
1629 let journal_offset = inode_offset(layout, EXT4_JOURNAL_INO);
1630 file.seek(SeekFrom::Start(journal_offset))?;
1631 file.write_all(&journal_inode)?;
1632
1633 for plan in plans.iter().filter(|plan| plan.inode >= EXT4_FIRST_INO) {
1634 let inode_bytes = build_inode_from_plan(layout, plan)?;
1635 file.seek(SeekFrom::Start(inode_offset(layout, plan.inode)))?;
1636 file.write_all(&inode_bytes)?;
1637 }
1638
1639 Ok(())
1640}
1641
1642fn inode_offset(layout: &Layout, inode: u32) -> u64 {
1643 let zero_based = inode - 1;
1644 let group = zero_based / EXT4_INODES_PER_GROUP;
1645 let index = zero_based % EXT4_INODES_PER_GROUP;
1646 layout.group_inode_table_block(group) * EXT4_BLOCK_SIZE as u64
1647 + u64::from(index) * EXT4_INODE_SIZE as u64
1648}
1649
1650fn build_inode_from_plan(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
1651 let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1652 let (mode, size, links_count, extents) = match &plan.kind {
1653 NodeKind::Directory { data, .. } => (
1654 S_IFDIR | plan.metadata.mode,
1655 data.len() as u64,
1656 plan.links_count,
1657 true,
1658 ),
1659 NodeKind::RegularFile { data } => (
1660 S_IFREG | plan.metadata.mode,
1661 data.len() as u64,
1662 plan.links_count,
1663 true,
1664 ),
1665 NodeKind::Symlink { target, inline } => (
1666 S_IFLNK | plan.metadata.mode,
1667 target.len() as u64,
1668 plan.links_count,
1669 !inline,
1670 ),
1671 NodeKind::CharDevice { .. } => (S_IFCHR | plan.metadata.mode, 0, plan.links_count, false),
1672 NodeKind::BlockDevice { .. } => (S_IFBLK | plan.metadata.mode, 0, plan.links_count, false),
1673 NodeKind::Fifo => (S_IFIFO | plan.metadata.mode, 0, plan.links_count, false),
1674 NodeKind::Socket => (S_IFSOCK | plan.metadata.mode, 0, plan.links_count, false),
1675 };
1676
1677 put_le16(&mut inode, 0x00, mode);
1678 put_le16(&mut inode, 0x02, plan.metadata.uid as u16);
1679 put_le32(&mut inode, 0x04, size as u32);
1680 write_inode_time(&mut inode, 0x08, 0x8c, &plan.metadata)?;
1681 write_inode_time(&mut inode, 0x0c, 0x84, &plan.metadata)?;
1682 write_inode_time(&mut inode, 0x10, 0x88, &plan.metadata)?;
1683 put_le16(&mut inode, 0x18, plan.metadata.gid as u16);
1684 put_le16(
1685 &mut inode,
1686 0x1A,
1687 u16::try_from(links_count).map_err(|_| {
1688 Ext4Error::Layout(format!(
1689 "link count exceeds ext4 inode field for '{}'",
1690 plan.path
1691 ))
1692 })?,
1693 );
1694 let allocated_blocks = plan.block_count
1695 + u32::from(plan.extent_tree_block.is_some())
1696 + u32::from(plan.xattr_block.is_some());
1697 put_le32(&mut inode, 0x1C, allocated_blocks * (EXT4_BLOCK_SIZE / 512));
1698 if extents {
1699 put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1700 }
1701
1702 match &plan.kind {
1703 NodeKind::Directory { .. } | NodeKind::RegularFile { .. } => {
1704 write_extent_plan(&mut inode, 0x28, plan)?;
1705 }
1706 NodeKind::Symlink { target, inline } => {
1707 if *inline {
1708 inode[0x28..0x28 + target.len()].copy_from_slice(target);
1709 } else {
1710 write_extent_plan(&mut inode, 0x28, plan)?;
1711 }
1712 }
1713 NodeKind::CharDevice { major, minor } | NodeKind::BlockDevice { major, minor } => {
1714 write_device_number(&mut inode, *major, *minor, &plan.path)?;
1715 }
1716 NodeKind::Fifo | NodeKind::Socket => {}
1717 }
1718
1719 put_le32(&mut inode, 0x64, 0);
1720 if let Some(block) = plan.xattr_block {
1721 put_le32(&mut inode, 0x68, block as u32);
1722 put_le16(&mut inode, 0x76, (block >> 32) as u16);
1723 }
1724 put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1725 put_le16(&mut inode, 0x78, (plan.metadata.uid >> 16) as u16);
1726 put_le16(&mut inode, 0x7A, (plan.metadata.gid >> 16) as u16);
1727 put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1728 write_inode_time(&mut inode, 0x90, 0x94, &plan.metadata)?;
1729 if let Some(xattrs) = &plan.inline_xattrs {
1730 inode[0xA0..0xA0 + xattrs.len()].copy_from_slice(xattrs);
1731 }
1732
1733 let csum = inode_checksum(layout.csum_seed, plan.inode, 0, &inode);
1734 put_le16(&mut inode, 0x7C, csum as u16);
1735 put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1736
1737 Ok(inode)
1738}
1739
1740fn write_inode_time(
1741 inode: &mut [u8],
1742 seconds_offset: usize,
1743 extra_offset: usize,
1744 metadata: &InodeMetadata,
1745) -> Result<(), Ext4Error> {
1746 if metadata.mtime_nsec >= 1_000_000_000 {
1747 return Err(Ext4Error::Layout(format!(
1748 "inode timestamp nanoseconds are out of range: {}",
1749 metadata.mtime_nsec
1750 )));
1751 }
1752 if metadata.mtime > 0x3_ffff_ffff {
1753 return Err(Ext4Error::Layout(format!(
1754 "inode timestamp seconds exceed ext4 epoch range: {}",
1755 metadata.mtime
1756 )));
1757 }
1758
1759 put_le32(inode, seconds_offset, metadata.mtime as u32);
1760 let epoch = ((metadata.mtime >> 32) & 0x3) as u32;
1761 put_le32(inode, extra_offset, (metadata.mtime_nsec << 2) | epoch);
1762 Ok(())
1763}
1764
1765fn write_device_number(
1766 inode: &mut [u8],
1767 major: u32,
1768 minor: u32,
1769 path: &str,
1770) -> Result<(), Ext4Error> {
1771 if major > 0x0fff || minor > 0x0f_ffff {
1772 return Err(Ext4Error::Layout(format!(
1773 "device number is out of Linux ext4 range for '{path}': {major}:{minor}"
1774 )));
1775 }
1776
1777 if major < 256 && minor < 256 {
1778 put_le32(inode, 0x28, (major << 8) | minor);
1779 } else {
1780 let encoded = (minor & 0xff) | (major << 8) | ((minor & !0xff) << 12);
1781 put_le32(inode, 0x2c, encoded);
1782 }
1783 Ok(())
1784}
1785
1786fn write_empty_extent_tree(buf: &mut [u8], offset: usize) {
1787 put_le16(buf, offset, EXT4_EH_MAGIC);
1788 put_le16(buf, offset + 2, 0);
1789 put_le16(buf, offset + 4, 4);
1790 put_le16(buf, offset + 6, 0);
1791 put_le32(buf, offset + 8, 0);
1792}
1793
1794fn build_superblock_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1795 build_superblock_with_stats_for_group(layout, stats, 0, true)
1796}
1797
1798fn build_backup_superblock_with_stats(
1799 layout: &Layout,
1800 stats: &FsStats,
1801 group: u32,
1802) -> Result<Vec<u8>, Ext4Error> {
1803 build_superblock_with_stats_for_group(layout, stats, group, false)
1804}
1805
1806fn build_superblock_with_stats_for_group(
1807 layout: &Layout,
1808 stats: &FsStats,
1809 group: u32,
1810 padded_primary: bool,
1811) -> Result<Vec<u8>, Ext4Error> {
1812 let mut block = build_superblock(layout, group, padded_primary)?;
1813 let sb_offset = if padded_primary { 1024 } else { 0 };
1814 let sb = &mut block[sb_offset..sb_offset + 1024];
1815 put_le32(sb, 0x0C, stats.total_free_blocks as u32);
1816 put_le32(sb, 0x10, stats.total_free_inodes as u32);
1817 put_le32(sb, 0x158, (stats.total_free_blocks >> 32) as u32);
1818 let (overhead_offset, overhead_blocks) = if layout.resize_inode_block.is_none() {
1819 layout.recorded_overhead_blocks(stats.total_free_blocks)
1820 } else {
1821 (EXT4_SB_OVERHEAD_BLOCKS_OFFSET, stats.overhead_blocks)
1822 };
1823 put_le32(sb, overhead_offset, overhead_blocks as u32);
1824 let checksum = superblock_checksum(sb);
1825 put_le32(sb, 0x3FC, checksum);
1826 Ok(block)
1827}
1828
1829fn build_gdt_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1830 let desc_size = EXT4_DESC_SIZE as usize;
1831 let geometry = layout.geometry();
1832 let mut gdt = Vec::with_capacity(layout.num_groups as usize * desc_size);
1833
1834 for g in 0..layout.num_groups {
1835 let desc_stats = GroupDescStats {
1836 free_blocks: stats.group_free_blocks[g as usize],
1837 free_inodes: stats.group_free_inodes[g as usize],
1838 used_dirs: stats.group_used_dirs[g as usize],
1839 block_bitmap_csum: stats.block_bitmap_checksums[g as usize],
1840 inode_bitmap_csum: stats.inode_bitmap_checksums[g as usize],
1841 };
1842 gdt.extend_from_slice(&build_group_descriptor(
1843 &geometry,
1844 g,
1845 &desc_stats,
1846 layout.csum_seed,
1847 ));
1848 }
1849
1850 Ok(gdt)
1851}
1852#[cfg(test)]
1853fn build_block_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1854 (0..layout.num_groups)
1855 .map(|group| build_block_bitmap(layout, group))
1856 .collect()
1857}
1858
1859#[cfg(test)]
1860fn build_inode_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1861 (0..layout.num_groups)
1862 .map(|group| build_inode_bitmap(layout, group))
1863 .collect()
1864}
1865
1866#[cfg(test)]
1867fn build_block_bitmap(layout: &Layout, group: u32) -> Vec<u8> {
1868 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1869
1870 let used = layout.group_used_blocks(group);
1873 for bit in 0..used {
1874 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1875 }
1876
1877 let blocks_in_group = layout.blocks_in_group(group);
1879 for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
1880 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1881 }
1882
1883 bitmap
1884}
1885
1886#[cfg(test)]
1887fn build_inode_bitmap(_layout: &Layout, group: u32) -> Vec<u8> {
1888 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1889
1890 if group == 0 {
1891 for bit in 0..(EXT4_FIRST_INO - 1) {
1893 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1894 }
1895 }
1896
1897 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
1900 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1901 }
1902
1903 bitmap
1904}
1905
1906fn write_bitmaps(
1907 file: &mut (impl std::io::Write + std::io::Seek),
1908 layout: &Layout,
1909 plan: &BitmapPlan,
1910) -> Result<(), Ext4Error> {
1911 for group in 0..layout.num_groups {
1912 let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
1913 let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
1914 let block_offset = layout.group_block_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1915 file.seek(SeekFrom::Start(block_offset))?;
1916 file.write_all(&block_bitmap)?;
1917
1918 let inode_offset = layout.group_inode_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1919 file.seek(SeekFrom::Start(inode_offset))?;
1920 file.write_all(&inode_bitmap)?;
1921 }
1922
1923 Ok(())
1924}
1925
1926fn build_journal_inode(layout: &Layout) -> Result<Vec<u8>, Ext4Error> {
1928 let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1929
1930 let mode = S_IFREG | 0o600;
1931 let size = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1932
1933 put_le16(&mut inode, 0x00, mode);
1935 put_le32(&mut inode, 0x04, size as u32);
1937 put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1939 put_le16(&mut inode, 0x1A, 1);
1941 let sectors = (layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64) / 512;
1943 put_le32(&mut inode, 0x1C, sectors as u32);
1944 put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1946
1947 write_extent_tree(
1949 &mut inode,
1950 0x28,
1951 layout.journal_start_block,
1952 layout.journal_blocks,
1953 "journal",
1954 )?;
1955
1956 put_le32(&mut inode, 0x64, 0);
1958
1959 put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1962
1963 let csum = inode_checksum(layout.csum_seed, EXT4_JOURNAL_INO, 0, &inode);
1965 put_le16(&mut inode, 0x7C, csum as u16);
1967 put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1969
1970 Ok(inode)
1971}
1972
1973fn write_extent_tree(
1977 buf: &mut [u8],
1978 offset: usize,
1979 start_block: u64,
1980 block_count: u32,
1981 label: &str,
1982) -> Result<(), Ext4Error> {
1983 validate_extent_block_count(block_count, label)?;
1984 if start_block > u32::MAX as u64 {
1985 return Err(Ext4Error::TooLarge {
1986 requested_blocks: start_block,
1987 max_blocks: u32::MAX as u64,
1988 });
1989 }
1990
1991 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;
2000 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(())
2006}
2007
2008fn write_extent_plan(buf: &mut [u8], offset: usize, plan: &NodePlan) -> Result<(), Ext4Error> {
2009 if plan.data_extents.is_empty() {
2010 write_empty_extent_tree(buf, offset);
2011 return Ok(());
2012 }
2013
2014 if plan.data_extents.len() <= INLINE_EXTENT_CAPACITY {
2015 write_extent_header(
2016 buf,
2017 offset,
2018 plan.data_extents.len() as u16,
2019 INLINE_EXTENT_CAPACITY as u16,
2020 0,
2021 );
2022 for (index, extent) in plan.data_extents.iter().enumerate() {
2023 write_extent_entry(buf, offset + 12 + index * 12, extent, &plan.path)?;
2024 }
2025 return Ok(());
2026 }
2027
2028 let leaf = plan.extent_tree_block.ok_or_else(|| {
2029 Ext4Error::Layout(format!("missing extent-tree block for '{}'", plan.path))
2030 })?;
2031 write_extent_header(buf, offset, 1, INLINE_EXTENT_CAPACITY as u16, 1);
2032 let index_offset = offset + 12;
2033 put_le32(buf, index_offset, plan.data_extents[0].logical_block);
2034 put_le32(buf, index_offset + 4, leaf as u32);
2035 put_le16(buf, index_offset + 8, (leaf >> 32) as u16);
2036 put_le16(buf, index_offset + 10, 0);
2037 Ok(())
2038}
2039
2040fn build_extent_leaf_block(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
2041 if plan.data_extents.len() > EXTENT_BLOCK_CAPACITY {
2042 return Err(Ext4Error::Layout(format!(
2043 "'{}' exceeds the depth-one extent-tree capacity",
2044 plan.path
2045 )));
2046 }
2047
2048 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
2049 write_extent_header(
2050 &mut block,
2051 0,
2052 plan.data_extents.len() as u16,
2053 EXTENT_BLOCK_CAPACITY as u16,
2054 0,
2055 );
2056 for (index, extent) in plan.data_extents.iter().enumerate() {
2057 write_extent_entry(&mut block, 12 + index * 12, extent, &plan.path)?;
2058 }
2059
2060 let tail = EXT4_BLOCK_SIZE as usize - std::mem::size_of::<u32>();
2061 let checksum = dir_block_checksum(layout.csum_seed, plan.inode, 0, &block[..tail]);
2062 put_le32(&mut block, tail, checksum);
2063 Ok(block)
2064}
2065
2066fn build_xattr_block(
2067 layout: &Layout,
2068 block_number: u64,
2069 xattrs: &[Xattr],
2070 path: &str,
2071) -> Result<Vec<u8>, Ext4Error> {
2072 let encoded = encode_xattrs(xattrs, path)?;
2073
2074 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
2075 put_le32(&mut block, 0, EXT4_XATTR_MAGIC);
2076 put_le32(&mut block, 4, 1);
2077 put_le32(&mut block, 8, 1);
2078
2079 let mut entry_offset = EXT4_XATTR_HEADER_SIZE;
2080 let mut value_offset = EXT4_BLOCK_SIZE as usize;
2081 let mut block_hash = 0u32;
2082 for xattr in encoded {
2083 let entry_len = align_four(EXT4_XATTR_ENTRY_SIZE + xattr.name.len());
2084 let padded_value_len = align_four(xattr.value.len());
2085 value_offset = value_offset.checked_sub(padded_value_len).ok_or_else(|| {
2086 Ext4Error::Layout(format!(
2087 "extended attributes on '{path}' exceed an ext4 block"
2088 ))
2089 })?;
2090 if entry_offset + entry_len + 4 > value_offset {
2091 return Err(Ext4Error::Layout(format!(
2092 "extended attributes on '{path}' do not fit in one ext4 xattr block"
2093 )));
2094 }
2095
2096 block[value_offset..value_offset + xattr.value.len()].copy_from_slice(xattr.value);
2097 let entry_hash = xattr_entry_hash(
2098 xattr.name,
2099 &block[value_offset..value_offset + padded_value_len],
2100 );
2101 block[entry_offset] = xattr.name.len() as u8;
2102 block[entry_offset + 1] = xattr.name_index;
2103 put_le16(&mut block, entry_offset + 2, value_offset as u16);
2104 put_le32(&mut block, entry_offset + 4, 0);
2105 put_le32(&mut block, entry_offset + 8, xattr.value.len() as u32);
2106 put_le32(&mut block, entry_offset + 12, entry_hash);
2107 block[entry_offset + 16..entry_offset + 16 + xattr.name.len()].copy_from_slice(xattr.name);
2108 entry_offset += entry_len;
2109 block_hash = block_hash.rotate_left(16) ^ entry_hash;
2110 }
2111
2112 put_le32(&mut block, 12, block_hash);
2113 let checksum = xattr_block_checksum(layout.csum_seed, block_number, &block);
2114 put_le32(&mut block, 16, checksum);
2115 Ok(block)
2116}
2117
2118fn build_inline_xattrs(xattrs: &[Xattr], path: &str) -> Result<Option<Vec<u8>>, Ext4Error> {
2119 if xattrs.is_empty() {
2120 return Ok(None);
2121 }
2122
2123 let encoded = encode_xattrs(xattrs, path)?;
2124 let mut bytes = vec![0u8; EXT4_INODE_SIZE as usize - 0xA0];
2125 put_le32(&mut bytes, 0, EXT4_XATTR_MAGIC);
2126 let first_entry = 4usize;
2127 let mut entry_offset = first_entry;
2128 let mut value_offset = bytes.len();
2129 for xattr in encoded {
2130 let entry_len = align_four(EXT4_XATTR_ENTRY_SIZE + xattr.name.len());
2131 let padded_value_len = align_four(xattr.value.len());
2132 let Some(next_value_offset) = value_offset.checked_sub(padded_value_len) else {
2133 return Ok(None);
2134 };
2135 if entry_offset + entry_len + 4 > next_value_offset {
2136 return Ok(None);
2137 }
2138 value_offset = next_value_offset;
2139 bytes[value_offset..value_offset + xattr.value.len()].copy_from_slice(xattr.value);
2140 bytes[entry_offset] = xattr.name.len() as u8;
2141 bytes[entry_offset + 1] = xattr.name_index;
2142 put_le16(
2143 &mut bytes,
2144 entry_offset + 2,
2145 (value_offset - first_entry) as u16,
2146 );
2147 put_le32(&mut bytes, entry_offset + 4, 0);
2148 put_le32(&mut bytes, entry_offset + 8, xattr.value.len() as u32);
2149 put_le32(&mut bytes, entry_offset + 12, 0);
2150 bytes[entry_offset + 16..entry_offset + 16 + xattr.name.len()].copy_from_slice(xattr.name);
2151 entry_offset += entry_len;
2152 }
2153
2154 Ok(Some(bytes))
2155}
2156
2157fn encode_xattrs<'a>(xattrs: &'a [Xattr], path: &str) -> Result<Vec<EncodedXattr<'a>>, Ext4Error> {
2158 let mut encoded = xattrs
2159 .iter()
2160 .map(|xattr| encode_xattr_name(xattr, path))
2161 .collect::<Result<Vec<_>, _>>()?;
2162 encoded.sort_by(|left, right| {
2163 (left.name_index, left.name.len(), left.name).cmp(&(
2164 right.name_index,
2165 right.name.len(),
2166 right.name,
2167 ))
2168 });
2169 if encoded
2170 .windows(2)
2171 .any(|pair| pair[0].name_index == pair[1].name_index && pair[0].name == pair[1].name)
2172 {
2173 return Err(Ext4Error::Layout(format!(
2174 "duplicate extended attribute on '{path}'"
2175 )));
2176 }
2177 Ok(encoded)
2178}
2179
2180fn encode_xattr_name<'a>(xattr: &'a Xattr, path: &str) -> Result<EncodedXattr<'a>, Ext4Error> {
2181 if xattr.name.is_empty() || xattr.name.len() > 255 || xattr.name.contains(&0) {
2182 return Err(Ext4Error::Layout(format!(
2183 "invalid extended-attribute name on '{path}'"
2184 )));
2185 }
2186
2187 let (name_index, name) = if xattr.name.as_slice() == b"system.posix_acl_access" {
2188 (2, &[][..])
2189 } else if xattr.name.as_slice() == b"system.posix_acl_default" {
2190 (3, &[][..])
2191 } else if let Some(name) = xattr.name.strip_prefix(b"user.") {
2192 (1, name)
2193 } else if let Some(name) = xattr.name.strip_prefix(b"trusted.") {
2194 (4, name)
2195 } else if let Some(name) = xattr.name.strip_prefix(b"security.") {
2196 (6, name)
2197 } else {
2198 return Err(Ext4Error::Layout(format!(
2199 "unsupported extended-attribute namespace on '{path}'"
2200 )));
2201 };
2202 if name.is_empty() && name_index != 2 && name_index != 3 {
2203 return Err(Ext4Error::Layout(format!(
2204 "extended-attribute namespace has an empty name on '{path}'"
2205 )));
2206 }
2207
2208 Ok(EncodedXattr {
2209 name_index,
2210 name,
2211 value: &xattr.value,
2212 })
2213}
2214
2215fn xattr_entry_hash(name: &[u8], padded_value: &[u8]) -> u32 {
2216 let mut hash = 0u32;
2217 for byte in name {
2218 hash = hash.rotate_left(5) ^ u32::from(*byte);
2219 }
2220 for word in padded_value.as_chunks::<4>().0 {
2221 hash = hash.rotate_left(16) ^ u32::from_le_bytes(*word);
2222 }
2223 hash
2224}
2225
2226fn xattr_block_checksum(csum_seed: u32, block_number: u64, block: &[u8]) -> u32 {
2227 let mut checksum = crc32c::crc32c_raw(csum_seed, &block_number.to_le_bytes());
2228 checksum = crc32c::crc32c_raw(checksum, &block[..16]);
2229 checksum = crc32c::crc32c_raw(checksum, &[0u8; 4]);
2230 crc32c::crc32c_raw(checksum, &block[20..])
2231}
2232
2233fn align_four(value: usize) -> usize {
2234 (value + 3) & !3
2235}
2236
2237fn write_extent_header(buf: &mut [u8], offset: usize, entries: u16, max: u16, depth: u16) {
2238 put_le16(buf, offset, EXT4_EH_MAGIC);
2239 put_le16(buf, offset + 2, entries);
2240 put_le16(buf, offset + 4, max);
2241 put_le16(buf, offset + 6, depth);
2242 put_le32(buf, offset + 8, 0);
2243}
2244
2245fn write_extent_entry(
2246 buf: &mut [u8],
2247 offset: usize,
2248 extent: &AllocatedExtent,
2249 label: &str,
2250) -> Result<(), Ext4Error> {
2251 validate_extent_block_count(extent.block_count, label)?;
2252 put_le32(buf, offset, extent.logical_block);
2253 put_le16(buf, offset + 4, extent.block_count as u16);
2254 put_le16(buf, offset + 6, (extent.physical_start >> 32) as u16);
2255 put_le32(buf, offset + 8, extent.physical_start as u32);
2256 Ok(())
2257}
2258
2259fn write_journal(
2261 file: &mut (impl std::io::Write + std::io::Seek),
2262 layout: &Layout,
2263) -> Result<(), Ext4Error> {
2264 let mut jsb = vec![0u8; EXT4_BLOCK_SIZE as usize];
2265
2266 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);
2281 put_be32(&mut jsb, 36, 0);
2283 put_be32(&mut jsb, 40, 0x13);
2285 put_be32(&mut jsb, 44, 0);
2287
2288 jsb[48..64].copy_from_slice(&layout.uuid);
2290
2291 put_be32(&mut jsb, 64, 1);
2293
2294 put_be32(&mut jsb, 68, 0);
2296
2297 put_be32(&mut jsb, 72, 0);
2299 put_be32(&mut jsb, 76, 0);
2300
2301 jsb[0x50] = 4; let jsb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb[..JBD2_SUPERBLOCK_SIZE]);
2341 put_be32(&mut jsb, 0xFC, jsb_csum);
2342
2343 let offset = layout.journal_start_block * EXT4_BLOCK_SIZE as u64;
2344 file.seek(SeekFrom::Start(offset))?;
2345 file.write_all(&jsb)?;
2346
2347 Ok(())
2348}
2349
2350fn build_superblock(
2352 layout: &Layout,
2353 group: u32,
2354 padded_primary: bool,
2355) -> Result<Vec<u8>, Ext4Error> {
2356 let mut block = if padded_primary {
2357 vec![0u8; EXT4_BLOCK_SIZE as usize]
2358 } else {
2359 vec![0u8; 1024]
2360 };
2361 let sb_offset = if padded_primary { 1024 } else { 0 };
2362 let sb = &mut block[sb_offset..sb_offset + 1024];
2363
2364 let total_blocks = layout.num_blocks;
2365 let total_inodes = layout.num_groups as u64 * EXT4_INODES_PER_GROUP as u64;
2366
2367 let free_blocks = layout.total_free_blocks();
2368 let free_inodes = layout.total_free_inodes();
2369
2370 put_le32(sb, 0x00, total_inodes as u32);
2372 put_le32(sb, 0x04, total_blocks as u32);
2374 put_le32(sb, 0x08, 0);
2376 put_le32(sb, 0x0C, free_blocks as u32);
2378 put_le32(sb, 0x10, free_inodes as u32);
2380 put_le32(sb, 0x14, 0);
2382 put_le32(sb, 0x18, EXT4_LOG_BLOCK_SIZE);
2384 put_le32(sb, 0x1C, EXT4_LOG_BLOCK_SIZE);
2386 put_le32(sb, 0x20, EXT4_BLOCKS_PER_GROUP);
2388 put_le32(sb, 0x24, EXT4_BLOCKS_PER_GROUP);
2390 put_le32(sb, 0x28, EXT4_INODES_PER_GROUP);
2392
2393 put_le16(sb, 0x34, 0);
2398 put_le16(sb, 0x36, 0xFFFF);
2400 put_le16(sb, 0x38, EXT4_SUPER_MAGIC);
2402 put_le16(sb, 0x3A, 1);
2404 put_le16(sb, 0x3C, 1);
2406 put_le16(sb, 0x3E, 0);
2408
2409 put_le32(sb, 0x48, 0);
2414 put_le32(sb, 0x4C, 1);
2416
2417 put_le16(sb, 0x50, 0);
2419 put_le16(sb, 0x52, 0);
2421
2422 put_le32(sb, 0x54, EXT4_FIRST_INO);
2425 put_le16(sb, 0x58, EXT4_INODE_SIZE);
2427 put_le16(sb, 0x5A, group as u16);
2429
2430 put_le32(sb, 0x5C, layout.feature_compat);
2432 put_le32(sb, 0x60, layout.feature_incompat);
2434 put_le32(sb, 0x64, layout.feature_ro_compat);
2436
2437 sb[0x68..0x78].copy_from_slice(&layout.uuid);
2439
2440 put_le32(sb, 0xC8, 0);
2446
2447 sb[0xCC] = 0;
2449 sb[0xCD] = 0;
2450
2451 put_le16(sb, 0xCE, layout.reserved_gdt_blocks as u16);
2453
2454 put_le32(sb, 0xE0, EXT4_JOURNAL_INO);
2458 put_le32(sb, 0xE4, 0);
2460 put_le32(sb, 0xE8, 0);
2462
2463 sb[0xEC..0xFC].copy_from_slice(&layout.uuid); sb[0xFC] = 1;
2468 sb[0xFD] = 1;
2470
2471 put_le16(sb, 0xFE, EXT4_DESC_SIZE);
2473
2474 put_le32(sb, 0x100, 0x000C);
2476
2477 put_le32(sb, 0x104, 0);
2479
2480 {
2485 let mut extent_buf = [0u8; 60];
2486 write_extent_tree(
2487 &mut extent_buf,
2488 0,
2489 layout.journal_start_block,
2490 layout.journal_blocks,
2491 "journal",
2492 )?;
2493 sb[0x10C..0x10C + 60].copy_from_slice(&extent_buf);
2495 let jsize = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
2497 put_le32(sb, 0x10C + 60, jsize as u32);
2498 put_le32(sb, 0x10C + 64, (jsize >> 32) as u32);
2500 }
2501
2502 put_le32(sb, 0x150, (total_blocks >> 32) as u32);
2505 put_le32(sb, 0x154, 0);
2507 put_le32(sb, 0x158, (free_blocks >> 32) as u32);
2509
2510 put_le16(sb, 0x15C, EXT4_MIN_EXTRA_ISIZE);
2512 put_le16(sb, 0x15E, EXT4_MIN_EXTRA_ISIZE);
2514
2515 put_le32(sb, 0x160, 0);
2517
2518 sb[0x174] = 0;
2520
2521 sb[0x175] = 1;
2523
2524 let (overhead_offset, overhead_blocks) =
2530 layout.recorded_overhead_blocks(layout.total_free_blocks());
2531 put_le32(sb, overhead_offset, overhead_blocks as u32);
2532
2533 put_le32(sb, 0x270, 0);
2538
2539 put_le16(sb, 0x27C, 0);
2541
2542 let sb_csum = superblock_checksum(sb);
2544 put_le32(sb, 0x3FC, sb_csum);
2545
2546 Ok(block)
2547}
2548
2549#[cfg(test)]
2552fn build_gdt(
2553 layout: &Layout,
2554 block_bitmaps: &[Vec<u8>],
2555 inode_bitmaps: &[Vec<u8>],
2556) -> Result<Vec<u8>, Ext4Error> {
2557 let geometry = layout.geometry();
2558 let mut gdt = Vec::with_capacity(layout.num_groups as usize * EXT4_DESC_SIZE as usize);
2559
2560 for g in 0..layout.num_groups {
2561 let desc_stats = GroupDescStats {
2562 free_blocks: layout.group_free_blocks(g),
2563 free_inodes: layout.group_free_inodes(g),
2564 used_dirs: layout.group_used_dirs(g),
2565 block_bitmap_csum: bitmap_checksum(
2566 layout.csum_seed,
2567 &block_bitmaps[g as usize],
2568 EXT4_BLOCK_SIZE as usize,
2569 ),
2570 inode_bitmap_csum: bitmap_checksum(
2571 layout.csum_seed,
2572 &inode_bitmaps[g as usize],
2573 (EXT4_INODES_PER_GROUP / 8) as usize,
2574 ),
2575 };
2576 gdt.extend_from_slice(&build_group_descriptor(
2577 &geometry,
2578 g,
2579 &desc_stats,
2580 layout.csum_seed,
2581 ));
2582 }
2583
2584 Ok(gdt)
2585}
2586
2587fn write_primary_superblock_at(
2589 file: &mut (impl std::io::Write + std::io::Seek),
2590 sb_block: &[u8],
2591) -> Result<(), Ext4Error> {
2592 file.seek(SeekFrom::Start(0))?;
2593 file.write_all(sb_block)?;
2594 Ok(())
2595}
2596
2597pub use super::format::sparse_super_group;
2602
2603#[cfg(test)]
2608mod tests {
2609 use super::*;
2610 use std::io::{Read, Seek, SeekFrom};
2611 #[cfg(windows)]
2612 use std::os::windows::fs::MetadataExt;
2613
2614 #[cfg(windows)]
2615 use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SPARSE_FILE;
2616
2617 fn le_u16(bytes: &[u8], offset: usize) -> u16 {
2618 u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
2619 }
2620
2621 fn le_u32(bytes: &[u8], offset: usize) -> u32 {
2622 u32::from_le_bytes([
2623 bytes[offset],
2624 bytes[offset + 1],
2625 bytes[offset + 2],
2626 bytes[offset + 3],
2627 ])
2628 }
2629
2630 fn read_exact_at(path: &Path, offset: u64, len: usize) -> Vec<u8> {
2631 let mut file = std::fs::File::open(path).unwrap();
2632 file.seek(SeekFrom::Start(offset)).unwrap();
2633 let mut bytes = vec![0u8; len];
2634 file.read_exact(&mut bytes).unwrap();
2635 bytes
2636 }
2637
2638 fn assert_backup_superblock(path: &Path, layout: &Layout, group: u32) {
2639 assert!(sparse_super_group(group));
2640 let backup_offset = layout.group_start_block(group) * EXT4_BLOCK_SIZE as u64;
2641 let bytes = read_exact_at(path, backup_offset, 2048);
2642
2643 assert_eq!(le_u16(&bytes, 0x38), EXT4_SUPER_MAGIC);
2644 assert_eq!(le_u16(&bytes, 0x5A), group as u16);
2645 assert_ne!(le_u16(&bytes, 1024 + 0x38), EXT4_SUPER_MAGIC);
2646
2647 let mut sb = bytes[..1024].to_vec();
2648 let stored = le_u32(&sb, 0x3FC);
2649 put_le32(&mut sb, 0x3FC, 0);
2650 let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
2651
2652 assert_eq!(stored, expected);
2653 }
2654
2655 #[test]
2656 fn test_format_creates_file_of_correct_size() {
2657 let dir = tempfile::tempdir().unwrap();
2658 let path = dir.path().join("test.ext4");
2659
2660 let size: u64 = 256 * 1024 * 1024; let opts = Ext4FormatOptions {
2662 size_bytes: size,
2663 journal_blocks: 4096, };
2665
2666 format_ext4(&path, &opts).unwrap();
2667
2668 let meta = std::fs::metadata(&path).unwrap();
2669 assert_eq!(meta.len(), size);
2670 }
2671
2672 #[test]
2673 fn test_inode_allocation_crosses_block_group_boundary() {
2674 let opts = Ext4FormatOptions {
2675 size_bytes: 256 * 1024 * 1024,
2676 journal_blocks: 4096,
2677 };
2678 let layout = Layout::compute(&opts).unwrap();
2679 let plan = BitmapPlan {
2680 block_extents: Vec::new(),
2681 max_used_inode: EXT4_INODES_PER_GROUP + 1,
2682 group_used_dirs: vec![1, 0],
2683 };
2684
2685 let first_bitmap = build_inode_bitmap_for_plan(&layout, &plan, 0);
2686 let second_bitmap = build_inode_bitmap_for_plan(&layout, &plan, 1);
2687 assert!(
2688 first_bitmap[..(EXT4_INODES_PER_GROUP / 8) as usize]
2689 .iter()
2690 .all(|byte| *byte == 0xff)
2691 );
2692 assert_eq!(second_bitmap[0] & 0x01, 0x01);
2693 assert_eq!(second_bitmap[0] & 0xfe, 0);
2694
2695 assert_eq!(
2696 inode_offset(&layout, EXT4_INODES_PER_GROUP + 1),
2697 layout.group_inode_table_block(1) * EXT4_BLOCK_SIZE as u64
2698 );
2699 }
2700
2701 #[test]
2702 fn test_allocator_builds_depth_one_extent_tree_across_groups() {
2703 let opts = Ext4FormatOptions {
2704 size_bytes: 1024 * 1024 * 1024,
2705 journal_blocks: 4096,
2706 };
2707 let layout = Layout::compute(&opts).unwrap();
2708 let mut allocator = DataAllocator::new(&layout);
2709 let block_count = 150_000;
2710 let data_extents = allocator
2711 .allocate_extents(block_count, "/large-layer")
2712 .unwrap();
2713 assert!(data_extents.len() > INLINE_EXTENT_CAPACITY);
2714 assert_eq!(
2715 data_extents
2716 .iter()
2717 .map(|extent| extent.block_count)
2718 .sum::<u32>(),
2719 block_count
2720 );
2721 for pair in data_extents.windows(2) {
2722 assert_eq!(
2723 pair[1].logical_block,
2724 pair[0].logical_block + pair[0].block_count
2725 );
2726 }
2727
2728 let extent_tree_block = allocator.allocate_block("/large-layer").unwrap();
2729 let plan = NodePlan {
2730 inode: EXT4_FIRST_INO,
2731 path: "/large-layer".to_string(),
2732 metadata: InodeMetadata::default(),
2733 links_count: 1,
2734 kind: NodeKind::RegularFile {
2735 data: FileData::Memory(Vec::new()),
2736 },
2737 block_count,
2738 data_extents,
2739 extent_tree_block: Some(extent_tree_block),
2740 xattrs: Vec::new(),
2741 inline_xattrs: None,
2742 xattr_block: None,
2743 };
2744 let mut inode_block = [0u8; 60];
2745 write_extent_plan(&mut inode_block, 0, &plan).unwrap();
2746 assert_eq!(le_u16(&inode_block, 0), EXT4_EH_MAGIC);
2747 assert_eq!(le_u16(&inode_block, 2), 1);
2748 assert_eq!(le_u16(&inode_block, 6), 1);
2749
2750 let leaf = build_extent_leaf_block(&layout, &plan).unwrap();
2751 assert_eq!(le_u16(&leaf, 0), EXT4_EH_MAGIC);
2752 assert_eq!(le_u16(&leaf, 2) as usize, plan.data_extents.len());
2753 assert_eq!(le_u16(&leaf, 6), 0);
2754 let tail = EXT4_BLOCK_SIZE as usize - 4;
2755 assert_eq!(
2756 le_u32(&leaf, tail),
2757 dir_block_checksum(layout.csum_seed, plan.inode, 0, &leaf[..tail])
2758 );
2759 }
2760
2761 #[test]
2762 fn test_xattr_block_is_sorted_and_checksummed() {
2763 let opts = Ext4FormatOptions {
2764 size_bytes: 256 * 1024 * 1024,
2765 journal_blocks: 4096,
2766 };
2767 let layout = Layout::compute(&opts).unwrap();
2768 let block_number = layout.group_data_start_block(1);
2769 let xattrs = vec![
2770 Xattr {
2771 name: b"security.capability".to_vec(),
2772 value: (0u8..20).collect(),
2773 },
2774 Xattr {
2775 name: b"user.owner".to_vec(),
2776 value: b"microsandbox".to_vec(),
2777 },
2778 ];
2779
2780 let block = build_xattr_block(&layout, block_number, &xattrs, "/bin/tool").unwrap();
2781 assert_eq!(le_u32(&block, 0), EXT4_XATTR_MAGIC);
2782 assert_eq!(le_u32(&block, 4), 1);
2783 assert_eq!(le_u32(&block, 8), 1);
2784 assert_eq!(block[EXT4_XATTR_HEADER_SIZE + 1], 1);
2785 let second_entry = EXT4_XATTR_HEADER_SIZE + align_four(EXT4_XATTR_ENTRY_SIZE + 5);
2786 assert_eq!(block[second_entry + 1], 6);
2787
2788 let stored_checksum = le_u32(&block, 16);
2789 let mut checksum_input = block.clone();
2790 put_le32(&mut checksum_input, 16, 0);
2791 assert_eq!(
2792 stored_checksum,
2793 xattr_block_checksum(layout.csum_seed, block_number, &checksum_input)
2794 );
2795 }
2796
2797 #[test]
2798 fn test_small_xattr_is_encoded_in_inode_body() {
2799 let xattrs = vec![Xattr {
2800 name: b"security.capability".to_vec(),
2801 value: (0u8..20).collect(),
2802 }];
2803
2804 let bytes = build_inline_xattrs(&xattrs, "/bin/tool").unwrap().unwrap();
2805 assert_eq!(le_u32(&bytes, 0), EXT4_XATTR_MAGIC);
2806 assert_eq!(bytes[5], 6);
2807 assert_eq!(&bytes[20..30], b"capability");
2808 assert_eq!(le_u32(&bytes, 16), 0);
2809 }
2810
2811 #[test]
2812 fn test_format_too_small() {
2813 let dir = tempfile::tempdir().unwrap();
2814 let path = dir.path().join("tiny.ext4");
2815
2816 let opts = Ext4FormatOptions {
2817 size_bytes: 4096, journal_blocks: 16384,
2819 };
2820
2821 let result = format_ext4(&path, &opts);
2822 assert!(matches!(result, Err(Ext4Error::TooSmall)));
2823 }
2824
2825 #[test]
2826 fn test_layout_rejects_unaligned_size() {
2827 let opts = Ext4FormatOptions {
2828 size_bytes: DEFAULT_SIZE_BYTES + 1,
2829 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2830 };
2831
2832 let result = Layout::compute(&opts);
2833 assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
2834 }
2835
2836 #[test]
2837 fn test_layout_rejects_tiny_final_group() {
2838 let opts = Ext4FormatOptions {
2839 size_bytes: 128 * 1024 * 1024 + EXT4_BLOCK_SIZE as u64,
2840 journal_blocks: 4096,
2841 };
2842
2843 let result = Layout::compute(&opts);
2844 assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
2845 }
2846
2847 #[test]
2848 fn test_layout_rejects_size_beyond_32_bit_block_addresses() {
2849 let opts = Ext4FormatOptions {
2850 size_bytes: (MAX_BLOCKS + 1) * EXT4_BLOCK_SIZE as u64,
2851 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2852 };
2853
2854 let result = Layout::compute(&opts);
2855 assert!(matches!(result, Err(Ext4Error::TooLarge { .. })));
2856 }
2857
2858 #[test]
2859 fn test_layout_uses_actual_group_count_beyond_four_gib() {
2860 let opts = Ext4FormatOptions {
2861 size_bytes: 8 * 1024 * 1024 * 1024,
2862 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2863 };
2864
2865 let layout = Layout::compute(&opts).unwrap();
2866
2867 assert_eq!(layout.num_blocks, opts.size_bytes / EXT4_BLOCK_SIZE as u64);
2868 assert_eq!(layout.num_groups, 64);
2869 assert_eq!(layout.gdt_blocks, 1);
2870 }
2871
2872 #[test]
2873 fn test_format_default_options() {
2874 let dir = tempfile::tempdir().unwrap();
2875 let path = dir.path().join("default.ext4");
2876
2877 let opts = Ext4FormatOptions::default();
2878 format_ext4(&path, &opts).unwrap();
2879
2880 let meta = std::fs::metadata(&path).unwrap();
2881 assert_eq!(meta.len(), DEFAULT_SIZE_BYTES);
2882 }
2883
2884 #[test]
2885 fn test_superblock_magic() {
2886 let dir = tempfile::tempdir().unwrap();
2887 let path = dir.path().join("magic.ext4");
2888
2889 let opts = Ext4FormatOptions {
2890 size_bytes: 256 * 1024 * 1024,
2891 journal_blocks: 4096,
2892 };
2893 format_ext4(&path, &opts).unwrap();
2894
2895 let data = std::fs::read(&path).unwrap();
2897 let magic = u16::from_le_bytes([data[1024 + 0x38], data[1024 + 0x39]]);
2898 assert_eq!(magic, EXT4_SUPER_MAGIC);
2899 }
2900
2901 #[test]
2902 fn test_journal_magic() {
2903 let dir = tempfile::tempdir().unwrap();
2904 let path = dir.path().join("journal.ext4");
2905
2906 let opts = Ext4FormatOptions {
2907 size_bytes: 256 * 1024 * 1024,
2908 journal_blocks: 4096,
2909 };
2910 format_ext4(&path, &opts).unwrap();
2911
2912 let layout = Layout::compute(&opts).unwrap();
2913 let data = std::fs::read(&path).unwrap();
2914
2915 let jsb_offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
2917 let magic = u32::from_be_bytes([
2918 data[jsb_offset],
2919 data[jsb_offset + 1],
2920 data[jsb_offset + 2],
2921 data[jsb_offset + 3],
2922 ]);
2923 assert_eq!(magic, JBD2_MAGIC);
2924 }
2925
2926 #[test]
2927 fn test_root_dir_inode_exists() {
2928 let dir = tempfile::tempdir().unwrap();
2929 let path = dir.path().join("rootdir.ext4");
2930
2931 let opts = Ext4FormatOptions {
2932 size_bytes: 256 * 1024 * 1024,
2933 journal_blocks: 4096,
2934 };
2935 format_ext4(&path, &opts).unwrap();
2936
2937 let layout = Layout::compute(&opts).unwrap();
2938 let data = std::fs::read(&path).unwrap();
2939
2940 let inode_offset = layout.group_inode_table_block(0) as usize * EXT4_BLOCK_SIZE as usize
2942 + (EXT4_INODE_SIZE as usize);
2943 let mode = u16::from_le_bytes([data[inode_offset], data[inode_offset + 1]]);
2944 assert_eq!(mode, S_IFDIR | 0o755);
2945 }
2946
2947 #[test]
2948 fn test_backup_group_bitmap_starts_after_backup_metadata() {
2949 let opts = Ext4FormatOptions {
2950 size_bytes: 256 * 1024 * 1024,
2951 journal_blocks: 4096,
2952 };
2953 let layout = Layout::compute(&opts).unwrap();
2954 let block_bitmaps = build_block_bitmaps(&layout);
2955 let inode_bitmaps = build_inode_bitmaps(&layout);
2956 let gdt = build_gdt(&layout, &block_bitmaps, &inode_bitmaps).unwrap();
2957
2958 let desc = &gdt[EXT4_DESC_SIZE as usize..(2 * EXT4_DESC_SIZE as usize)];
2959 let block_bitmap = u32::from_le_bytes([desc[0], desc[1], desc[2], desc[3]]) as u64;
2960 let group_start = layout.group_start_block(1);
2961
2962 assert_eq!(block_bitmap, layout.group_block_bitmap_block(1));
2963 assert!(block_bitmap > group_start + layout.gdt_blocks as u64 - 1);
2964 }
2965
2966 #[test]
2967 fn test_backup_superblock_is_written_at_backup_group_start() {
2968 let dir = tempfile::tempdir().unwrap();
2969 let path = dir.path().join("backup-super.ext4");
2970 let opts = Ext4FormatOptions {
2971 size_bytes: 256 * 1024 * 1024,
2972 journal_blocks: 4096,
2973 };
2974
2975 format_ext4(&path, &opts).unwrap();
2976
2977 let layout = Layout::compute(&opts).unwrap();
2978 assert_backup_superblock(&path, &layout, 1);
2979 }
2980
2981 #[test]
2982 fn test_backup_superblock_is_group_specific_in_sixteen_gib_image() {
2983 let dir = tempfile::tempdir().unwrap();
2984 let path = dir.path().join("backup-super-16g.ext4");
2985 let opts = Ext4FormatOptions {
2986 size_bytes: 16 * 1024 * 1024 * 1024,
2987 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2988 };
2989
2990 format_ext4(&path, &opts).unwrap();
2991
2992 let layout = Layout::compute(&opts).unwrap();
2993 assert_eq!(layout.num_groups, 128);
2994 assert_backup_superblock(&path, &layout, 81);
2995 }
2996
2997 #[test]
2998 fn test_format_sparse_image_larger_than_four_gib() {
2999 let dir = tempfile::tempdir().unwrap();
3000 let path = dir.path().join("large.ext4");
3001 let opts = Ext4FormatOptions {
3002 size_bytes: 8 * 1024 * 1024 * 1024,
3003 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
3004 };
3005
3006 format_ext4(&path, &opts).unwrap();
3007
3008 let meta = std::fs::metadata(&path).unwrap();
3009 assert_eq!(meta.len(), opts.size_bytes);
3010
3011 let layout = Layout::compute(&opts).unwrap();
3012 let sb = read_exact_at(&path, 1024, 1024);
3013 let blocks = le_u32(&sb, 0x04) as u64 | ((le_u32(&sb, 0x150) as u64) << 32);
3014 let inodes = le_u32(&sb, 0x00);
3015 assert_eq!(blocks, layout.num_blocks);
3016 assert_eq!(inodes, layout.num_groups * EXT4_INODES_PER_GROUP);
3017
3018 let desc_offset = EXT4_BLOCK_SIZE as u64 + 63 * EXT4_DESC_SIZE as u64;
3019 let desc = read_exact_at(&path, desc_offset, EXT4_DESC_SIZE as usize);
3020 let block_bitmap = le_u32(&desc, 0x00) as u64 | ((le_u32(&desc, 0x20) as u64) << 32);
3021 let inode_bitmap = le_u32(&desc, 0x04) as u64 | ((le_u32(&desc, 0x24) as u64) << 32);
3022
3023 assert_eq!(block_bitmap, layout.group_block_bitmap_block(63));
3024 assert_eq!(inode_bitmap, layout.group_inode_bitmap_block(63));
3025 }
3026
3027 #[cfg(windows)]
3028 #[test]
3029 fn test_format_marks_image_sparse_on_windows() {
3030 let dir = tempfile::tempdir().unwrap();
3031 let path = dir.path().join("sparse.ext4");
3032 let opts = Ext4FormatOptions {
3033 size_bytes: 256 * 1024 * 1024,
3034 journal_blocks: 4096,
3035 };
3036
3037 format_ext4(&path, &opts).unwrap();
3038
3039 let meta = std::fs::metadata(&path).unwrap();
3040 assert_eq!(meta.len(), opts.size_bytes);
3041 assert_ne!(meta.file_attributes() & FILE_ATTRIBUTE_SPARSE_FILE, 0);
3042 }
3043
3044 #[test]
3045 fn test_inode_bitmap_padding_is_marked_used() {
3046 let layout = Layout::compute(&Ext4FormatOptions {
3047 size_bytes: 256 * 1024 * 1024,
3048 journal_blocks: 4096,
3049 })
3050 .unwrap();
3051
3052 let bitmap = build_inode_bitmap(&layout, 0);
3053 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
3054 assert_ne!(bitmap[(bit / 8) as usize] & (1 << (bit % 8)), 0);
3055 }
3056 }
3057
3058 #[test]
3059 fn test_journal_superblock_checksum_matches_contents() {
3060 let dir = tempfile::tempdir().unwrap();
3061 let path = dir.path().join("journal-csum.ext4");
3062 let opts = Ext4FormatOptions {
3063 size_bytes: 256 * 1024 * 1024,
3064 journal_blocks: 4096,
3065 };
3066
3067 format_ext4(&path, &opts).unwrap();
3068
3069 let layout = Layout::compute(&opts).unwrap();
3070 let data = std::fs::read(&path).unwrap();
3071 let offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
3072 let mut jsb = data[offset..offset + JBD2_SUPERBLOCK_SIZE].to_vec();
3073 let stored = u32::from_be_bytes([jsb[0xFC], jsb[0xFD], jsb[0xFE], jsb[0xFF]]);
3074
3075 jsb[0xFC..0x100].fill(0);
3076 let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb);
3077
3078 assert_eq!(stored, expected);
3079 }
3080
3081 #[test]
3082 fn test_root_dir_checksum_matches_contents() {
3083 let dir = tempfile::tempdir().unwrap();
3084 let path = dir.path().join("rootdir-csum.ext4");
3085 let opts = Ext4FormatOptions {
3086 size_bytes: 256 * 1024 * 1024,
3087 journal_blocks: 4096,
3088 };
3089
3090 format_ext4(&path, &opts).unwrap();
3091
3092 let layout = Layout::compute(&opts).unwrap();
3093 let data = std::fs::read(&path).unwrap();
3094 let sb = &data[1024..2048];
3095 let uuid = &sb[0x68..0x78];
3096 let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, uuid);
3097 let block_offset = layout.first_data_block as usize * EXT4_BLOCK_SIZE as usize;
3098 let tail_offset = block_offset + EXT4_BLOCK_SIZE as usize - 12;
3099 let stored = u32::from_le_bytes([
3100 data[tail_offset + 8],
3101 data[tail_offset + 9],
3102 data[tail_offset + 10],
3103 data[tail_offset + 11],
3104 ]);
3105 let expected = dir_block_checksum(
3106 csum_seed,
3107 EXT4_ROOT_INO,
3108 0,
3109 &data[block_offset..tail_offset],
3110 );
3111
3112 assert_eq!(stored, expected);
3113 }
3114}