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 {
1097 uid: metadata.uid,
1098 gid: metadata.gid,
1099 mode: if directory {
1100 metadata.mode & 0o7777
1101 } else {
1102 normalize_file_permissions(metadata.mode)
1103 },
1104 mtime: metadata.mtime,
1105 mtime_nsec: metadata.mtime_nsec,
1106 },
1107 TreeEncodingMode::Rootfs => InodeMetadata {
1108 uid: metadata.uid,
1109 gid: metadata.gid,
1110 mode: metadata.mode & 0o7777,
1111 mtime: metadata.mtime,
1112 mtime_nsec: metadata.mtime_nsec,
1113 },
1114 }
1115}
1116
1117fn child_path(parent: &str, name: &[u8]) -> String {
1118 let name = String::from_utf8_lossy(name);
1119 if parent == "/" {
1120 format!("/{name}")
1121 } else {
1122 format!("{parent}/{name}")
1123 }
1124}
1125
1126fn normalize_file_permissions(mode: u16) -> u16 {
1127 let perms = mode & 0o7777;
1128 if perms == 0 { 0o644 } else { perms }
1129}
1130
1131fn hardlink_matches_plan(
1132 plan: &NodePlan,
1133 file: &crate::tree::RegularFileNode,
1134 encoding_mode: TreeEncodingMode,
1135) -> bool {
1136 let metadata = &plan.metadata;
1137 let candidate = planned_metadata(&file.metadata, false, encoding_mode);
1138 let metadata_matches = metadata.uid == candidate.uid
1139 && metadata.gid == candidate.gid
1140 && metadata.mode == candidate.mode
1141 && metadata.mtime == candidate.mtime
1142 && metadata.mtime_nsec == candidate.mtime_nsec;
1143 let xattrs_match = plan.xattrs.len() == file.xattrs.len()
1144 && plan.xattrs.iter().all(|left| {
1145 file.xattrs
1146 .iter()
1147 .any(|right| left.name == right.name && left.value == right.value)
1148 });
1149 let data_matches = match (&plan.kind, &file.data) {
1150 (NodeKind::RegularFile { data: left }, right) => same_file_data(left, right),
1151 _ => false,
1152 };
1153 metadata_matches && xattrs_match && data_matches
1154}
1155
1156fn same_file_data(left: &FileData, right: &FileData) -> bool {
1157 match (left, right) {
1158 (FileData::Memory(left), FileData::Memory(right)) => left == right,
1159 (FileData::SharedMemory(left), FileData::SharedMemory(right)) => {
1160 std::sync::Arc::ptr_eq(left, right) || left.as_ref() == right.as_ref()
1161 }
1162 (FileData::Memory(left), FileData::SharedMemory(right))
1163 | (FileData::SharedMemory(right), FileData::Memory(left)) => {
1164 left.as_slice() == right.as_ref()
1165 }
1166 (
1167 FileData::Spool {
1168 spool: left_spool,
1169 offset: left_offset,
1170 len: left_len,
1171 },
1172 FileData::Spool {
1173 spool: right_spool,
1174 offset: right_offset,
1175 len: right_len,
1176 },
1177 ) => {
1178 std::sync::Arc::ptr_eq(left_spool, right_spool)
1179 && left_offset == right_offset
1180 && left_len == right_len
1181 }
1182 _ => false,
1183 }
1184}
1185
1186fn blocks_for_len(len: usize) -> u32 {
1187 if len == 0 {
1188 0
1189 } else {
1190 (len as u64).div_ceil(EXT4_BLOCK_SIZE as u64) as u32
1191 }
1192}
1193
1194fn validate_node_extents(plans: &[NodePlan]) -> Result<(), Ext4Error> {
1195 for plan in plans {
1196 if plan.data_extents.len() > EXTENT_BLOCK_CAPACITY {
1197 return Err(Ext4Error::Layout(format!(
1198 "'{}' needs {} extents, but a depth-one ext4 extent tree holds at most {EXTENT_BLOCK_CAPACITY}",
1199 plan.path,
1200 plan.data_extents.len()
1201 )));
1202 }
1203 for extent in &plan.data_extents {
1204 validate_extent_block_count(extent.block_count, &plan.path)?;
1205 }
1206 }
1207
1208 Ok(())
1209}
1210
1211fn validate_extent_block_count(block_count: u32, label: &str) -> Result<(), Ext4Error> {
1212 if block_count > MAX_INITIALIZED_EXTENT_BLOCKS {
1213 return Err(Ext4Error::Layout(format!(
1214 "'{label}' needs {block_count} blocks but this formatter currently supports one initialized extent of at most {MAX_INITIALIZED_EXTENT_BLOCKS} blocks"
1215 )));
1216 }
1217
1218 Ok(())
1219}
1220
1221fn split_physical_range(start: u64, blocks: u32) -> Vec<AllocatedExtent> {
1222 let mut extents = Vec::new();
1223 let mut remaining = blocks;
1224 let mut logical_block = 0u32;
1225 let mut physical_start = start;
1226 while remaining > 0 {
1227 let block_count = remaining.min(MAX_INITIALIZED_EXTENT_BLOCKS);
1228 extents.push(AllocatedExtent {
1229 logical_block,
1230 physical_start,
1231 block_count,
1232 });
1233 logical_block += block_count;
1234 physical_start += u64::from(block_count);
1235 remaining -= block_count;
1236 }
1237 extents
1238}
1239
1240fn build_directory_data(
1241 dir_inode: u32,
1242 parent_inode: u32,
1243 children: &[DirEntrySpec],
1244 path: &str,
1245) -> Result<Vec<u8>, Ext4Error> {
1246 let mut entries = Vec::with_capacity(children.len() + 2);
1247 entries.push(DirEntrySpec {
1248 inode: dir_inode,
1249 file_type: EXT4_FT_DIR,
1250 name: b".".to_vec(),
1251 });
1252 entries.push(DirEntrySpec {
1253 inode: parent_inode,
1254 file_type: EXT4_FT_DIR,
1255 name: b"..".to_vec(),
1256 });
1257 entries.extend(children.iter().map(|entry| DirEntrySpec {
1258 inode: entry.inode,
1259 file_type: entry.file_type,
1260 name: entry.name.clone(),
1261 }));
1262
1263 let mut blocks = Vec::new();
1264 let mut index = 0usize;
1265 while index < entries.len() {
1266 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1267 let mut pos = 0usize;
1268 let data_limit = EXT4_BLOCK_SIZE as usize - 12;
1269 let block_start = index;
1270
1271 while index < entries.len() {
1272 let min_len = dir_entry_len(entries[index].name.len());
1273 let needed = if pos == 0 { min_len } else { pos + min_len };
1274 if needed > data_limit {
1275 if pos == 0 {
1276 return Err(Ext4Error::Layout(format!(
1277 "directory entry too large for '{path}'"
1278 )));
1279 }
1280 break;
1281 }
1282 pos += min_len;
1283 index += 1;
1284 }
1285
1286 let mut write_pos = 0usize;
1287 for (entry_index, entry) in entries[block_start..index].iter().enumerate() {
1288 let is_last = entry_index + 1 == index - block_start;
1289 let rec_len = if is_last {
1290 (data_limit - write_pos) as u16
1291 } else {
1292 dir_entry_len(entry.name.len()) as u16
1293 };
1294 put_le32(&mut block, write_pos, entry.inode);
1295 put_le16(&mut block, write_pos + 4, rec_len);
1296 block[write_pos + 6] = entry.name.len() as u8;
1297 block[write_pos + 7] = entry.file_type;
1298 block[write_pos + 8..write_pos + 8 + entry.name.len()].copy_from_slice(&entry.name);
1299 write_pos += rec_len as usize;
1300 }
1301
1302 let tail = data_limit;
1303 put_le32(&mut block, tail, 0);
1304 put_le16(&mut block, tail + 4, 12);
1305 block[tail + 6] = 0;
1306 block[tail + 7] = 0xDE;
1307 blocks.extend_from_slice(&block);
1308 }
1309
1310 if blocks.is_empty() {
1311 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1312 put_le32(&mut block, 0, dir_inode);
1313 put_le16(&mut block, 4, 12);
1314 block[6] = 1;
1315 block[7] = EXT4_FT_DIR;
1316 block[8] = b'.';
1317 put_le32(&mut block, 12, parent_inode);
1318 put_le16(&mut block, 16, (EXT4_BLOCK_SIZE - 24) as u16);
1319 block[18] = 2;
1320 block[19] = EXT4_FT_DIR;
1321 block[20] = b'.';
1322 block[21] = b'.';
1323 let tail = EXT4_BLOCK_SIZE as usize - 12;
1324 put_le32(&mut block, tail, 0);
1325 put_le16(&mut block, tail + 4, 12);
1326 block[tail + 7] = 0xDE;
1327 blocks = block;
1328 }
1329
1330 Ok(blocks)
1331}
1332
1333fn dir_entry_len(name_len: usize) -> usize {
1334 (8 + name_len + 3) & !3
1335}
1336
1337fn allocate_node_data(allocator: &mut DataAllocator, plan: &mut NodePlan) -> Result<(), Ext4Error> {
1338 if plan.block_count > 0 {
1339 plan.data_extents = allocator.allocate_extents(plan.block_count, &plan.path)?;
1340 if plan.data_extents.len() > INLINE_EXTENT_CAPACITY {
1341 plan.extent_tree_block = Some(allocator.allocate_block(&plan.path)?);
1342 }
1343 }
1344
1345 if !plan.xattrs.is_empty() {
1346 plan.inline_xattrs = build_inline_xattrs(&plan.xattrs, &plan.path)?;
1347 if plan.inline_xattrs.is_none() {
1348 plan.xattr_block = Some(allocator.allocate_block(&plan.path)?);
1349 }
1350 }
1351 Ok(())
1352}
1353
1354impl DataAllocator {
1355 fn new(layout: &Layout) -> Self {
1356 let mut regions = Vec::new();
1357 for group in 0..layout.num_groups {
1358 let group_start = layout.group_start_block(group);
1359 let group_end = group_start + layout.blocks_in_group(group) as u64;
1360 let start = layout.group_data_start_block(group);
1361 if start < group_end {
1362 regions.push((start, (group_end - start) as u32));
1363 }
1364 }
1365 Self { regions }
1366 }
1367
1368 fn allocate_extents(
1369 &mut self,
1370 blocks: u32,
1371 path: &str,
1372 ) -> Result<Vec<AllocatedExtent>, Ext4Error> {
1373 let mut extents = Vec::new();
1374 let mut remaining = blocks;
1375 let mut logical_block = 0u32;
1376
1377 for region in &mut self.regions {
1378 while remaining > 0 && region.1 > 0 {
1379 let block_count = remaining.min(region.1).min(MAX_INITIALIZED_EXTENT_BLOCKS);
1380 extents.push(AllocatedExtent {
1381 logical_block,
1382 physical_start: region.0,
1383 block_count,
1384 });
1385 region.0 += u64::from(block_count);
1386 region.1 -= block_count;
1387 remaining -= block_count;
1388 logical_block += block_count;
1389 }
1390 if remaining == 0 {
1391 return Ok(extents);
1392 }
1393 }
1394
1395 Err(Ext4Error::Layout(format!(
1396 "not enough space in ext4 image for '{path}'"
1397 )))
1398 }
1399
1400 fn allocate_block(&mut self, path: &str) -> Result<u64, Ext4Error> {
1401 self.allocate_extents(1, path)?
1402 .first()
1403 .map(|extent| extent.physical_start)
1404 .ok_or_else(|| {
1405 Ext4Error::Layout(format!("could not allocate extent tree for '{path}'"))
1406 })
1407 }
1408}
1409
1410fn build_block_bitmap_for_plan(layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
1411 let mut bitmap = build_block_bitmap_base(&layout.geometry(), group);
1412 let group_start = layout.group_start_block(group);
1413 let group_end = group_start + layout.blocks_in_group(group) as u64;
1414
1415 for (start, len) in &plan.block_extents {
1416 let extent_start = *start;
1417 let extent_end = extent_start + *len as u64;
1418 let overlap_start = extent_start.max(group_start);
1419 let overlap_end = extent_end.min(group_end);
1420 if overlap_start < overlap_end {
1421 for block in overlap_start..overlap_end {
1422 let bit = block - group_start;
1423 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1424 }
1425 }
1426 }
1427
1428 bitmap
1429}
1430
1431fn build_inode_bitmap_for_plan(_layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
1432 let first_inode = group * EXT4_INODES_PER_GROUP + 1;
1433 let last_inode = first_inode + EXT4_INODES_PER_GROUP - 1;
1434 let used_inodes = if plan.max_used_inode < first_inode {
1435 0
1436 } else {
1437 plan.max_used_inode.min(last_inode) - first_inode + 1
1438 };
1439 build_inode_bitmap_base(used_inodes)
1440}
1441
1442fn compute_fs_stats(layout: &Layout, plan: &BitmapPlan) -> FsStats {
1443 let mut group_free_blocks = Vec::with_capacity(layout.num_groups as usize);
1444 let mut block_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
1445 let mut inode_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
1446
1447 let mut total_free_blocks = 0u64;
1448 for group in 0..layout.num_groups {
1449 let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
1450 let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
1451 let blocks_in_group = layout.blocks_in_group(group) as usize;
1452 let used = count_used_bits(&block_bitmap, blocks_in_group);
1453 let free = blocks_in_group.saturating_sub(used) as u32;
1454 group_free_blocks.push(free);
1455 block_bitmap_checksums.push(bitmap_checksum(
1456 layout.csum_seed,
1457 &block_bitmap,
1458 EXT4_BLOCK_SIZE as usize,
1459 ));
1460 inode_bitmap_checksums.push(bitmap_checksum(
1461 layout.csum_seed,
1462 &inode_bitmap,
1463 (EXT4_INODES_PER_GROUP / 8) as usize,
1464 ));
1465 total_free_blocks += free as u64;
1466 }
1467
1468 let group_free_inodes = (0..layout.num_groups)
1469 .map(|group| {
1470 let first_inode = group * EXT4_INODES_PER_GROUP + 1;
1471 let last_inode = first_inode + EXT4_INODES_PER_GROUP - 1;
1472 let used_inodes = if plan.max_used_inode < first_inode {
1473 0
1474 } else {
1475 plan.max_used_inode.min(last_inode) - first_inode + 1
1476 };
1477 EXT4_INODES_PER_GROUP - used_inodes
1478 })
1479 .collect::<Vec<_>>();
1480 let total_free_inodes = group_free_inodes.iter().map(|count| *count as u64).sum();
1481
1482 let group_used_dirs = plan.group_used_dirs.clone();
1483
1484 FsStats {
1485 group_free_blocks,
1486 group_free_inodes,
1487 group_used_dirs,
1488 block_bitmap_checksums,
1489 inode_bitmap_checksums,
1490 total_free_blocks,
1491 total_free_inodes,
1492 overhead_blocks: layout.total_overhead_blocks(),
1493 }
1494}
1495
1496fn write_tree_data(
1497 file: &mut (impl std::io::Write + std::io::Seek),
1498 layout: &Layout,
1499 plans: &[NodePlan],
1500) -> Result<(), Ext4Error> {
1501 for plan in plans {
1502 match &plan.kind {
1503 NodeKind::Directory { data, .. } => {
1504 let mut bytes = data.clone();
1505 update_dir_block_checksums(layout.csum_seed, plan.inode, &mut bytes);
1506 write_bytes_to_extents(file, &plan.data_extents, &bytes)?;
1507 }
1508 NodeKind::RegularFile { data } => {
1509 write_file_data(file, &plan.data_extents, data)?;
1510 }
1511 NodeKind::Symlink { target, inline } => {
1512 if !inline {
1513 write_bytes_to_extents(file, &plan.data_extents, target)?;
1514 }
1515 }
1516 NodeKind::CharDevice { .. }
1517 | NodeKind::BlockDevice { .. }
1518 | NodeKind::Fifo
1519 | NodeKind::Socket => {}
1520 }
1521
1522 if let Some(block) = plan.extent_tree_block {
1523 let bytes = build_extent_leaf_block(layout, plan)?;
1524 write_extent_bytes(file, block, &bytes)?;
1525 }
1526 if let Some(block) = plan.xattr_block {
1527 let bytes = build_xattr_block(layout, block, &plan.xattrs, &plan.path)?;
1528 write_extent_bytes(file, block, &bytes)?;
1529 }
1530 }
1531
1532 Ok(())
1533}
1534
1535fn write_file_data(
1536 file: &mut (impl std::io::Write + std::io::Seek),
1537 extents: &[AllocatedExtent],
1538 data: &FileData,
1539) -> Result<(), Ext4Error> {
1540 let mut data_offset = 0usize;
1541 for extent in extents {
1542 let remaining = data.len().saturating_sub(data_offset);
1543 let extent_bytes = extent.block_count as usize * EXT4_BLOCK_SIZE as usize;
1544 let write_len = remaining.min(extent_bytes);
1545 file.seek(SeekFrom::Start(
1546 extent.physical_start * EXT4_BLOCK_SIZE as u64,
1547 ))?;
1548 data.write_range(data_offset, write_len, file)?;
1549 write_zero_padding(file, extent_bytes - write_len)?;
1550 data_offset += write_len;
1551 }
1552
1553 debug_assert_eq!(data_offset, data.len());
1554
1555 Ok(())
1556}
1557
1558fn write_bytes_to_extents(
1559 file: &mut (impl std::io::Write + std::io::Seek),
1560 extents: &[AllocatedExtent],
1561 data: &[u8],
1562) -> Result<(), Ext4Error> {
1563 let mut data_offset = 0usize;
1564 for extent in extents {
1565 let remaining = data.len().saturating_sub(data_offset);
1566 let extent_bytes = extent.block_count as usize * EXT4_BLOCK_SIZE as usize;
1567 let write_len = remaining.min(extent_bytes);
1568 file.seek(SeekFrom::Start(
1569 extent.physical_start * EXT4_BLOCK_SIZE as u64,
1570 ))?;
1571 file.write_all(&data[data_offset..data_offset + write_len])?;
1572 write_zero_padding(file, extent_bytes - write_len)?;
1573 data_offset += write_len;
1574 }
1575
1576 debug_assert_eq!(data_offset, data.len());
1577 Ok(())
1578}
1579
1580fn write_zero_padding(file: &mut impl std::io::Write, mut len: usize) -> Result<(), Ext4Error> {
1581 static ZEROS: [u8; EXT4_BLOCK_SIZE as usize] = [0u8; EXT4_BLOCK_SIZE as usize];
1582 while len > 0 {
1583 let chunk = len.min(ZEROS.len());
1584 file.write_all(&ZEROS[..chunk])?;
1585 len -= chunk;
1586 }
1587 Ok(())
1588}
1589
1590fn write_extent_bytes(
1591 file: &mut (impl std::io::Write + std::io::Seek),
1592 start_block: u64,
1593 data: &[u8],
1594) -> Result<(), Ext4Error> {
1595 let offset = start_block * EXT4_BLOCK_SIZE as u64;
1596 file.seek(SeekFrom::Start(offset))?;
1597 file.write_all(data)?;
1598
1599 let pad = (EXT4_BLOCK_SIZE as usize - (data.len() % EXT4_BLOCK_SIZE as usize))
1600 % EXT4_BLOCK_SIZE as usize;
1601 if pad > 0 {
1602 static ZEROS: [u8; 4096] = [0u8; 4096];
1603 file.write_all(&ZEROS[..pad])?;
1604 }
1605
1606 Ok(())
1607}
1608
1609fn update_dir_block_checksums(csum_seed: u32, inode: u32, data: &mut [u8]) {
1610 for chunk in data.as_chunks_mut::<{ EXT4_BLOCK_SIZE as usize }>().0 {
1611 let tail = EXT4_BLOCK_SIZE as usize - 12;
1612 let checksum = dir_block_checksum(csum_seed, inode, 0, &chunk[..tail]);
1613 put_le32(chunk, tail + 8, checksum);
1614 }
1615}
1616
1617fn write_inode_table_with_plan(
1618 file: &mut (impl std::io::Write + std::io::Seek),
1619 layout: &Layout,
1620 plans: &[NodePlan],
1621) -> Result<(), Ext4Error> {
1622 let root_inode = build_inode_from_plan(layout, &plans[0])?;
1623 let root_offset = inode_offset(layout, EXT4_ROOT_INO);
1624 file.seek(SeekFrom::Start(root_offset))?;
1625 file.write_all(&root_inode)?;
1626
1627 let journal_inode = build_journal_inode(layout)?;
1628 let journal_offset = inode_offset(layout, EXT4_JOURNAL_INO);
1629 file.seek(SeekFrom::Start(journal_offset))?;
1630 file.write_all(&journal_inode)?;
1631
1632 for plan in plans.iter().filter(|plan| plan.inode >= EXT4_FIRST_INO) {
1633 let inode_bytes = build_inode_from_plan(layout, plan)?;
1634 file.seek(SeekFrom::Start(inode_offset(layout, plan.inode)))?;
1635 file.write_all(&inode_bytes)?;
1636 }
1637
1638 Ok(())
1639}
1640
1641fn inode_offset(layout: &Layout, inode: u32) -> u64 {
1642 let zero_based = inode - 1;
1643 let group = zero_based / EXT4_INODES_PER_GROUP;
1644 let index = zero_based % EXT4_INODES_PER_GROUP;
1645 layout.group_inode_table_block(group) * EXT4_BLOCK_SIZE as u64
1646 + u64::from(index) * EXT4_INODE_SIZE as u64
1647}
1648
1649fn build_inode_from_plan(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
1650 let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1651 let (mode, size, links_count, extents) = match &plan.kind {
1652 NodeKind::Directory { data, .. } => (
1653 S_IFDIR | plan.metadata.mode,
1654 data.len() as u64,
1655 plan.links_count,
1656 true,
1657 ),
1658 NodeKind::RegularFile { data } => (
1659 S_IFREG | plan.metadata.mode,
1660 data.len() as u64,
1661 plan.links_count,
1662 true,
1663 ),
1664 NodeKind::Symlink { target, inline } => (
1665 S_IFLNK | plan.metadata.mode,
1666 target.len() as u64,
1667 plan.links_count,
1668 !inline,
1669 ),
1670 NodeKind::CharDevice { .. } => (S_IFCHR | plan.metadata.mode, 0, plan.links_count, false),
1671 NodeKind::BlockDevice { .. } => (S_IFBLK | plan.metadata.mode, 0, plan.links_count, false),
1672 NodeKind::Fifo => (S_IFIFO | plan.metadata.mode, 0, plan.links_count, false),
1673 NodeKind::Socket => (S_IFSOCK | plan.metadata.mode, 0, plan.links_count, false),
1674 };
1675
1676 put_le16(&mut inode, 0x00, mode);
1677 put_le16(&mut inode, 0x02, plan.metadata.uid as u16);
1678 put_le32(&mut inode, 0x04, size as u32);
1679 write_inode_time(&mut inode, 0x08, 0x8c, &plan.metadata)?;
1680 write_inode_time(&mut inode, 0x0c, 0x84, &plan.metadata)?;
1681 write_inode_time(&mut inode, 0x10, 0x88, &plan.metadata)?;
1682 put_le16(&mut inode, 0x18, plan.metadata.gid as u16);
1683 put_le16(
1684 &mut inode,
1685 0x1A,
1686 u16::try_from(links_count).map_err(|_| {
1687 Ext4Error::Layout(format!(
1688 "link count exceeds ext4 inode field for '{}'",
1689 plan.path
1690 ))
1691 })?,
1692 );
1693 let allocated_blocks = plan.block_count
1694 + u32::from(plan.extent_tree_block.is_some())
1695 + u32::from(plan.xattr_block.is_some());
1696 put_le32(&mut inode, 0x1C, allocated_blocks * (EXT4_BLOCK_SIZE / 512));
1697 if extents {
1698 put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1699 }
1700
1701 match &plan.kind {
1702 NodeKind::Directory { .. } | NodeKind::RegularFile { .. } => {
1703 write_extent_plan(&mut inode, 0x28, plan)?;
1704 }
1705 NodeKind::Symlink { target, inline } => {
1706 if *inline {
1707 inode[0x28..0x28 + target.len()].copy_from_slice(target);
1708 } else {
1709 write_extent_plan(&mut inode, 0x28, plan)?;
1710 }
1711 }
1712 NodeKind::CharDevice { major, minor } | NodeKind::BlockDevice { major, minor } => {
1713 write_device_number(&mut inode, *major, *minor, &plan.path)?;
1714 }
1715 NodeKind::Fifo | NodeKind::Socket => {}
1716 }
1717
1718 put_le32(&mut inode, 0x64, 0);
1719 if let Some(block) = plan.xattr_block {
1720 put_le32(&mut inode, 0x68, block as u32);
1721 put_le16(&mut inode, 0x76, (block >> 32) as u16);
1722 }
1723 put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1724 put_le16(&mut inode, 0x78, (plan.metadata.uid >> 16) as u16);
1725 put_le16(&mut inode, 0x7A, (plan.metadata.gid >> 16) as u16);
1726 put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1727 write_inode_time(&mut inode, 0x90, 0x94, &plan.metadata)?;
1728 if let Some(xattrs) = &plan.inline_xattrs {
1729 inode[0xA0..0xA0 + xattrs.len()].copy_from_slice(xattrs);
1730 }
1731
1732 let csum = inode_checksum(layout.csum_seed, plan.inode, 0, &inode);
1733 put_le16(&mut inode, 0x7C, csum as u16);
1734 put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1735
1736 Ok(inode)
1737}
1738
1739fn write_inode_time(
1740 inode: &mut [u8],
1741 seconds_offset: usize,
1742 extra_offset: usize,
1743 metadata: &InodeMetadata,
1744) -> Result<(), Ext4Error> {
1745 if metadata.mtime_nsec >= 1_000_000_000 {
1746 return Err(Ext4Error::Layout(format!(
1747 "inode timestamp nanoseconds are out of range: {}",
1748 metadata.mtime_nsec
1749 )));
1750 }
1751 if metadata.mtime > 0x3_ffff_ffff {
1752 return Err(Ext4Error::Layout(format!(
1753 "inode timestamp seconds exceed ext4 epoch range: {}",
1754 metadata.mtime
1755 )));
1756 }
1757
1758 put_le32(inode, seconds_offset, metadata.mtime as u32);
1759 let epoch = ((metadata.mtime >> 32) & 0x3) as u32;
1760 put_le32(inode, extra_offset, (metadata.mtime_nsec << 2) | epoch);
1761 Ok(())
1762}
1763
1764fn write_device_number(
1765 inode: &mut [u8],
1766 major: u32,
1767 minor: u32,
1768 path: &str,
1769) -> Result<(), Ext4Error> {
1770 if major > 0x0fff || minor > 0x0f_ffff {
1771 return Err(Ext4Error::Layout(format!(
1772 "device number is out of Linux ext4 range for '{path}': {major}:{minor}"
1773 )));
1774 }
1775
1776 if major < 256 && minor < 256 {
1777 put_le32(inode, 0x28, (major << 8) | minor);
1778 } else {
1779 let encoded = (minor & 0xff) | (major << 8) | ((minor & !0xff) << 12);
1780 put_le32(inode, 0x2c, encoded);
1781 }
1782 Ok(())
1783}
1784
1785fn write_empty_extent_tree(buf: &mut [u8], offset: usize) {
1786 put_le16(buf, offset, EXT4_EH_MAGIC);
1787 put_le16(buf, offset + 2, 0);
1788 put_le16(buf, offset + 4, 4);
1789 put_le16(buf, offset + 6, 0);
1790 put_le32(buf, offset + 8, 0);
1791}
1792
1793fn build_superblock_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1794 build_superblock_with_stats_for_group(layout, stats, 0, true)
1795}
1796
1797fn build_backup_superblock_with_stats(
1798 layout: &Layout,
1799 stats: &FsStats,
1800 group: u32,
1801) -> Result<Vec<u8>, Ext4Error> {
1802 build_superblock_with_stats_for_group(layout, stats, group, false)
1803}
1804
1805fn build_superblock_with_stats_for_group(
1806 layout: &Layout,
1807 stats: &FsStats,
1808 group: u32,
1809 padded_primary: bool,
1810) -> Result<Vec<u8>, Ext4Error> {
1811 let mut block = build_superblock(layout, group, padded_primary)?;
1812 let sb_offset = if padded_primary { 1024 } else { 0 };
1813 let sb = &mut block[sb_offset..sb_offset + 1024];
1814 put_le32(sb, 0x0C, stats.total_free_blocks as u32);
1815 put_le32(sb, 0x10, stats.total_free_inodes as u32);
1816 put_le32(sb, 0x158, (stats.total_free_blocks >> 32) as u32);
1817 let (overhead_offset, overhead_blocks) = if layout.resize_inode_block.is_none() {
1818 layout.recorded_overhead_blocks(stats.total_free_blocks)
1819 } else {
1820 (EXT4_SB_OVERHEAD_BLOCKS_OFFSET, stats.overhead_blocks)
1821 };
1822 put_le32(sb, overhead_offset, overhead_blocks as u32);
1823 let checksum = superblock_checksum(sb);
1824 put_le32(sb, 0x3FC, checksum);
1825 Ok(block)
1826}
1827
1828fn build_gdt_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1829 let desc_size = EXT4_DESC_SIZE as usize;
1830 let geometry = layout.geometry();
1831 let mut gdt = Vec::with_capacity(layout.num_groups as usize * desc_size);
1832
1833 for g in 0..layout.num_groups {
1834 let desc_stats = GroupDescStats {
1835 free_blocks: stats.group_free_blocks[g as usize],
1836 free_inodes: stats.group_free_inodes[g as usize],
1837 used_dirs: stats.group_used_dirs[g as usize],
1838 block_bitmap_csum: stats.block_bitmap_checksums[g as usize],
1839 inode_bitmap_csum: stats.inode_bitmap_checksums[g as usize],
1840 };
1841 gdt.extend_from_slice(&build_group_descriptor(
1842 &geometry,
1843 g,
1844 &desc_stats,
1845 layout.csum_seed,
1846 ));
1847 }
1848
1849 Ok(gdt)
1850}
1851#[cfg(test)]
1852fn build_block_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1853 (0..layout.num_groups)
1854 .map(|group| build_block_bitmap(layout, group))
1855 .collect()
1856}
1857
1858#[cfg(test)]
1859fn build_inode_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1860 (0..layout.num_groups)
1861 .map(|group| build_inode_bitmap(layout, group))
1862 .collect()
1863}
1864
1865#[cfg(test)]
1866fn build_block_bitmap(layout: &Layout, group: u32) -> Vec<u8> {
1867 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1868
1869 let used = layout.group_used_blocks(group);
1872 for bit in 0..used {
1873 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1874 }
1875
1876 let blocks_in_group = layout.blocks_in_group(group);
1878 for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
1879 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1880 }
1881
1882 bitmap
1883}
1884
1885#[cfg(test)]
1886fn build_inode_bitmap(_layout: &Layout, group: u32) -> Vec<u8> {
1887 let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1888
1889 if group == 0 {
1890 for bit in 0..(EXT4_FIRST_INO - 1) {
1892 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1893 }
1894 }
1895
1896 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
1899 bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1900 }
1901
1902 bitmap
1903}
1904
1905fn write_bitmaps(
1906 file: &mut (impl std::io::Write + std::io::Seek),
1907 layout: &Layout,
1908 plan: &BitmapPlan,
1909) -> Result<(), Ext4Error> {
1910 for group in 0..layout.num_groups {
1911 let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
1912 let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
1913 let block_offset = layout.group_block_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1914 file.seek(SeekFrom::Start(block_offset))?;
1915 file.write_all(&block_bitmap)?;
1916
1917 let inode_offset = layout.group_inode_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1918 file.seek(SeekFrom::Start(inode_offset))?;
1919 file.write_all(&inode_bitmap)?;
1920 }
1921
1922 Ok(())
1923}
1924
1925fn build_journal_inode(layout: &Layout) -> Result<Vec<u8>, Ext4Error> {
1927 let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1928
1929 let mode = S_IFREG | 0o600;
1930 let size = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1931
1932 put_le16(&mut inode, 0x00, mode);
1934 put_le32(&mut inode, 0x04, size as u32);
1936 put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1938 put_le16(&mut inode, 0x1A, 1);
1940 let sectors = (layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64) / 512;
1942 put_le32(&mut inode, 0x1C, sectors as u32);
1943 put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1945
1946 write_extent_tree(
1948 &mut inode,
1949 0x28,
1950 layout.journal_start_block,
1951 layout.journal_blocks,
1952 "journal",
1953 )?;
1954
1955 put_le32(&mut inode, 0x64, 0);
1957
1958 put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1961
1962 let csum = inode_checksum(layout.csum_seed, EXT4_JOURNAL_INO, 0, &inode);
1964 put_le16(&mut inode, 0x7C, csum as u16);
1966 put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1968
1969 Ok(inode)
1970}
1971
1972fn write_extent_tree(
1976 buf: &mut [u8],
1977 offset: usize,
1978 start_block: u64,
1979 block_count: u32,
1980 label: &str,
1981) -> Result<(), Ext4Error> {
1982 validate_extent_block_count(block_count, label)?;
1983 if start_block > u32::MAX as u64 {
1984 return Err(Ext4Error::TooLarge {
1985 requested_blocks: start_block,
1986 max_blocks: u32::MAX as u64,
1987 });
1988 }
1989
1990 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;
1999 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(())
2005}
2006
2007fn write_extent_plan(buf: &mut [u8], offset: usize, plan: &NodePlan) -> Result<(), Ext4Error> {
2008 if plan.data_extents.is_empty() {
2009 write_empty_extent_tree(buf, offset);
2010 return Ok(());
2011 }
2012
2013 if plan.data_extents.len() <= INLINE_EXTENT_CAPACITY {
2014 write_extent_header(
2015 buf,
2016 offset,
2017 plan.data_extents.len() as u16,
2018 INLINE_EXTENT_CAPACITY as u16,
2019 0,
2020 );
2021 for (index, extent) in plan.data_extents.iter().enumerate() {
2022 write_extent_entry(buf, offset + 12 + index * 12, extent, &plan.path)?;
2023 }
2024 return Ok(());
2025 }
2026
2027 let leaf = plan.extent_tree_block.ok_or_else(|| {
2028 Ext4Error::Layout(format!("missing extent-tree block for '{}'", plan.path))
2029 })?;
2030 write_extent_header(buf, offset, 1, INLINE_EXTENT_CAPACITY as u16, 1);
2031 let index_offset = offset + 12;
2032 put_le32(buf, index_offset, plan.data_extents[0].logical_block);
2033 put_le32(buf, index_offset + 4, leaf as u32);
2034 put_le16(buf, index_offset + 8, (leaf >> 32) as u16);
2035 put_le16(buf, index_offset + 10, 0);
2036 Ok(())
2037}
2038
2039fn build_extent_leaf_block(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
2040 if plan.data_extents.len() > EXTENT_BLOCK_CAPACITY {
2041 return Err(Ext4Error::Layout(format!(
2042 "'{}' exceeds the depth-one extent-tree capacity",
2043 plan.path
2044 )));
2045 }
2046
2047 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
2048 write_extent_header(
2049 &mut block,
2050 0,
2051 plan.data_extents.len() as u16,
2052 EXTENT_BLOCK_CAPACITY as u16,
2053 0,
2054 );
2055 for (index, extent) in plan.data_extents.iter().enumerate() {
2056 write_extent_entry(&mut block, 12 + index * 12, extent, &plan.path)?;
2057 }
2058
2059 let tail = EXT4_BLOCK_SIZE as usize - std::mem::size_of::<u32>();
2060 let checksum = dir_block_checksum(layout.csum_seed, plan.inode, 0, &block[..tail]);
2061 put_le32(&mut block, tail, checksum);
2062 Ok(block)
2063}
2064
2065fn build_xattr_block(
2066 layout: &Layout,
2067 block_number: u64,
2068 xattrs: &[Xattr],
2069 path: &str,
2070) -> Result<Vec<u8>, Ext4Error> {
2071 let encoded = encode_xattrs(xattrs, path)?;
2072
2073 let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
2074 put_le32(&mut block, 0, EXT4_XATTR_MAGIC);
2075 put_le32(&mut block, 4, 1);
2076 put_le32(&mut block, 8, 1);
2077
2078 let mut entry_offset = EXT4_XATTR_HEADER_SIZE;
2079 let mut value_offset = EXT4_BLOCK_SIZE as usize;
2080 let mut block_hash = 0u32;
2081 for xattr in encoded {
2082 let entry_len = align_four(EXT4_XATTR_ENTRY_SIZE + xattr.name.len());
2083 let padded_value_len = align_four(xattr.value.len());
2084 value_offset = value_offset.checked_sub(padded_value_len).ok_or_else(|| {
2085 Ext4Error::Layout(format!(
2086 "extended attributes on '{path}' exceed an ext4 block"
2087 ))
2088 })?;
2089 if entry_offset + entry_len + 4 > value_offset {
2090 return Err(Ext4Error::Layout(format!(
2091 "extended attributes on '{path}' do not fit in one ext4 xattr block"
2092 )));
2093 }
2094
2095 block[value_offset..value_offset + xattr.value.len()].copy_from_slice(xattr.value);
2096 let entry_hash = xattr_entry_hash(
2097 xattr.name,
2098 &block[value_offset..value_offset + padded_value_len],
2099 );
2100 block[entry_offset] = xattr.name.len() as u8;
2101 block[entry_offset + 1] = xattr.name_index;
2102 put_le16(&mut block, entry_offset + 2, value_offset as u16);
2103 put_le32(&mut block, entry_offset + 4, 0);
2104 put_le32(&mut block, entry_offset + 8, xattr.value.len() as u32);
2105 put_le32(&mut block, entry_offset + 12, entry_hash);
2106 block[entry_offset + 16..entry_offset + 16 + xattr.name.len()].copy_from_slice(xattr.name);
2107 entry_offset += entry_len;
2108 block_hash = block_hash.rotate_left(16) ^ entry_hash;
2109 }
2110
2111 put_le32(&mut block, 12, block_hash);
2112 let checksum = xattr_block_checksum(layout.csum_seed, block_number, &block);
2113 put_le32(&mut block, 16, checksum);
2114 Ok(block)
2115}
2116
2117fn build_inline_xattrs(xattrs: &[Xattr], path: &str) -> Result<Option<Vec<u8>>, Ext4Error> {
2118 if xattrs.is_empty() {
2119 return Ok(None);
2120 }
2121
2122 let encoded = encode_xattrs(xattrs, path)?;
2123 let mut bytes = vec![0u8; EXT4_INODE_SIZE as usize - 0xA0];
2124 put_le32(&mut bytes, 0, EXT4_XATTR_MAGIC);
2125 let first_entry = 4usize;
2126 let mut entry_offset = first_entry;
2127 let mut value_offset = bytes.len();
2128 for xattr in encoded {
2129 let entry_len = align_four(EXT4_XATTR_ENTRY_SIZE + xattr.name.len());
2130 let padded_value_len = align_four(xattr.value.len());
2131 let Some(next_value_offset) = value_offset.checked_sub(padded_value_len) else {
2132 return Ok(None);
2133 };
2134 if entry_offset + entry_len + 4 > next_value_offset {
2135 return Ok(None);
2136 }
2137 value_offset = next_value_offset;
2138 bytes[value_offset..value_offset + xattr.value.len()].copy_from_slice(xattr.value);
2139 bytes[entry_offset] = xattr.name.len() as u8;
2140 bytes[entry_offset + 1] = xattr.name_index;
2141 put_le16(
2142 &mut bytes,
2143 entry_offset + 2,
2144 (value_offset - first_entry) as u16,
2145 );
2146 put_le32(&mut bytes, entry_offset + 4, 0);
2147 put_le32(&mut bytes, entry_offset + 8, xattr.value.len() as u32);
2148 put_le32(&mut bytes, entry_offset + 12, 0);
2149 bytes[entry_offset + 16..entry_offset + 16 + xattr.name.len()].copy_from_slice(xattr.name);
2150 entry_offset += entry_len;
2151 }
2152
2153 Ok(Some(bytes))
2154}
2155
2156fn encode_xattrs<'a>(xattrs: &'a [Xattr], path: &str) -> Result<Vec<EncodedXattr<'a>>, Ext4Error> {
2157 let mut encoded = xattrs
2158 .iter()
2159 .map(|xattr| encode_xattr_name(xattr, path))
2160 .collect::<Result<Vec<_>, _>>()?;
2161 encoded.sort_by(|left, right| {
2162 (left.name_index, left.name.len(), left.name).cmp(&(
2163 right.name_index,
2164 right.name.len(),
2165 right.name,
2166 ))
2167 });
2168 if encoded
2169 .windows(2)
2170 .any(|pair| pair[0].name_index == pair[1].name_index && pair[0].name == pair[1].name)
2171 {
2172 return Err(Ext4Error::Layout(format!(
2173 "duplicate extended attribute on '{path}'"
2174 )));
2175 }
2176 Ok(encoded)
2177}
2178
2179fn encode_xattr_name<'a>(xattr: &'a Xattr, path: &str) -> Result<EncodedXattr<'a>, Ext4Error> {
2180 if xattr.name.is_empty() || xattr.name.len() > 255 || xattr.name.contains(&0) {
2181 return Err(Ext4Error::Layout(format!(
2182 "invalid extended-attribute name on '{path}'"
2183 )));
2184 }
2185
2186 let (name_index, name) = if xattr.name.as_slice() == b"system.posix_acl_access" {
2187 (2, &[][..])
2188 } else if xattr.name.as_slice() == b"system.posix_acl_default" {
2189 (3, &[][..])
2190 } else if let Some(name) = xattr.name.strip_prefix(b"user.") {
2191 (1, name)
2192 } else if let Some(name) = xattr.name.strip_prefix(b"trusted.") {
2193 (4, name)
2194 } else if let Some(name) = xattr.name.strip_prefix(b"security.") {
2195 (6, name)
2196 } else {
2197 return Err(Ext4Error::Layout(format!(
2198 "unsupported extended-attribute namespace on '{path}'"
2199 )));
2200 };
2201 if name.is_empty() && name_index != 2 && name_index != 3 {
2202 return Err(Ext4Error::Layout(format!(
2203 "extended-attribute namespace has an empty name on '{path}'"
2204 )));
2205 }
2206
2207 Ok(EncodedXattr {
2208 name_index,
2209 name,
2210 value: &xattr.value,
2211 })
2212}
2213
2214fn xattr_entry_hash(name: &[u8], padded_value: &[u8]) -> u32 {
2215 let mut hash = 0u32;
2216 for byte in name {
2217 hash = hash.rotate_left(5) ^ u32::from(*byte);
2218 }
2219 for word in padded_value.as_chunks::<4>().0 {
2220 hash = hash.rotate_left(16) ^ u32::from_le_bytes(*word);
2221 }
2222 hash
2223}
2224
2225fn xattr_block_checksum(csum_seed: u32, block_number: u64, block: &[u8]) -> u32 {
2226 let mut checksum = crc32c::crc32c_raw(csum_seed, &block_number.to_le_bytes());
2227 checksum = crc32c::crc32c_raw(checksum, &block[..16]);
2228 checksum = crc32c::crc32c_raw(checksum, &[0u8; 4]);
2229 crc32c::crc32c_raw(checksum, &block[20..])
2230}
2231
2232fn align_four(value: usize) -> usize {
2233 (value + 3) & !3
2234}
2235
2236fn write_extent_header(buf: &mut [u8], offset: usize, entries: u16, max: u16, depth: u16) {
2237 put_le16(buf, offset, EXT4_EH_MAGIC);
2238 put_le16(buf, offset + 2, entries);
2239 put_le16(buf, offset + 4, max);
2240 put_le16(buf, offset + 6, depth);
2241 put_le32(buf, offset + 8, 0);
2242}
2243
2244fn write_extent_entry(
2245 buf: &mut [u8],
2246 offset: usize,
2247 extent: &AllocatedExtent,
2248 label: &str,
2249) -> Result<(), Ext4Error> {
2250 validate_extent_block_count(extent.block_count, label)?;
2251 put_le32(buf, offset, extent.logical_block);
2252 put_le16(buf, offset + 4, extent.block_count as u16);
2253 put_le16(buf, offset + 6, (extent.physical_start >> 32) as u16);
2254 put_le32(buf, offset + 8, extent.physical_start as u32);
2255 Ok(())
2256}
2257
2258fn write_journal(
2260 file: &mut (impl std::io::Write + std::io::Seek),
2261 layout: &Layout,
2262) -> Result<(), Ext4Error> {
2263 let mut jsb = vec![0u8; EXT4_BLOCK_SIZE as usize];
2264
2265 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);
2280 put_be32(&mut jsb, 36, 0);
2282 put_be32(&mut jsb, 40, 0x13);
2284 put_be32(&mut jsb, 44, 0);
2286
2287 jsb[48..64].copy_from_slice(&layout.uuid);
2289
2290 put_be32(&mut jsb, 64, 1);
2292
2293 put_be32(&mut jsb, 68, 0);
2295
2296 put_be32(&mut jsb, 72, 0);
2298 put_be32(&mut jsb, 76, 0);
2299
2300 jsb[0x50] = 4; let jsb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb[..JBD2_SUPERBLOCK_SIZE]);
2340 put_be32(&mut jsb, 0xFC, jsb_csum);
2341
2342 let offset = layout.journal_start_block * EXT4_BLOCK_SIZE as u64;
2343 file.seek(SeekFrom::Start(offset))?;
2344 file.write_all(&jsb)?;
2345
2346 Ok(())
2347}
2348
2349fn build_superblock(
2351 layout: &Layout,
2352 group: u32,
2353 padded_primary: bool,
2354) -> Result<Vec<u8>, Ext4Error> {
2355 let mut block = if padded_primary {
2356 vec![0u8; EXT4_BLOCK_SIZE as usize]
2357 } else {
2358 vec![0u8; 1024]
2359 };
2360 let sb_offset = if padded_primary { 1024 } else { 0 };
2361 let sb = &mut block[sb_offset..sb_offset + 1024];
2362
2363 let total_blocks = layout.num_blocks;
2364 let total_inodes = layout.num_groups as u64 * EXT4_INODES_PER_GROUP as u64;
2365
2366 let free_blocks = layout.total_free_blocks();
2367 let free_inodes = layout.total_free_inodes();
2368
2369 put_le32(sb, 0x00, total_inodes as u32);
2371 put_le32(sb, 0x04, total_blocks as u32);
2373 put_le32(sb, 0x08, 0);
2375 put_le32(sb, 0x0C, free_blocks as u32);
2377 put_le32(sb, 0x10, free_inodes as u32);
2379 put_le32(sb, 0x14, 0);
2381 put_le32(sb, 0x18, EXT4_LOG_BLOCK_SIZE);
2383 put_le32(sb, 0x1C, EXT4_LOG_BLOCK_SIZE);
2385 put_le32(sb, 0x20, EXT4_BLOCKS_PER_GROUP);
2387 put_le32(sb, 0x24, EXT4_BLOCKS_PER_GROUP);
2389 put_le32(sb, 0x28, EXT4_INODES_PER_GROUP);
2391
2392 put_le16(sb, 0x34, 0);
2397 put_le16(sb, 0x36, 0xFFFF);
2399 put_le16(sb, 0x38, EXT4_SUPER_MAGIC);
2401 put_le16(sb, 0x3A, 1);
2403 put_le16(sb, 0x3C, 1);
2405 put_le16(sb, 0x3E, 0);
2407
2408 put_le32(sb, 0x48, 0);
2413 put_le32(sb, 0x4C, 1);
2415
2416 put_le16(sb, 0x50, 0);
2418 put_le16(sb, 0x52, 0);
2420
2421 put_le32(sb, 0x54, EXT4_FIRST_INO);
2424 put_le16(sb, 0x58, EXT4_INODE_SIZE);
2426 put_le16(sb, 0x5A, group as u16);
2428
2429 put_le32(sb, 0x5C, layout.feature_compat);
2431 put_le32(sb, 0x60, layout.feature_incompat);
2433 put_le32(sb, 0x64, layout.feature_ro_compat);
2435
2436 sb[0x68..0x78].copy_from_slice(&layout.uuid);
2438
2439 put_le32(sb, 0xC8, 0);
2445
2446 sb[0xCC] = 0;
2448 sb[0xCD] = 0;
2449
2450 put_le16(sb, 0xCE, layout.reserved_gdt_blocks as u16);
2452
2453 put_le32(sb, 0xE0, EXT4_JOURNAL_INO);
2457 put_le32(sb, 0xE4, 0);
2459 put_le32(sb, 0xE8, 0);
2461
2462 sb[0xEC..0xFC].copy_from_slice(&layout.uuid); sb[0xFC] = 1;
2467 sb[0xFD] = 1;
2469
2470 put_le16(sb, 0xFE, EXT4_DESC_SIZE);
2472
2473 put_le32(sb, 0x100, 0x000C);
2475
2476 put_le32(sb, 0x104, 0);
2478
2479 {
2484 let mut extent_buf = [0u8; 60];
2485 write_extent_tree(
2486 &mut extent_buf,
2487 0,
2488 layout.journal_start_block,
2489 layout.journal_blocks,
2490 "journal",
2491 )?;
2492 sb[0x10C..0x10C + 60].copy_from_slice(&extent_buf);
2494 let jsize = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
2496 put_le32(sb, 0x10C + 60, jsize as u32);
2497 put_le32(sb, 0x10C + 64, (jsize >> 32) as u32);
2499 }
2500
2501 put_le32(sb, 0x150, (total_blocks >> 32) as u32);
2504 put_le32(sb, 0x154, 0);
2506 put_le32(sb, 0x158, (free_blocks >> 32) as u32);
2508
2509 put_le16(sb, 0x15C, EXT4_MIN_EXTRA_ISIZE);
2511 put_le16(sb, 0x15E, EXT4_MIN_EXTRA_ISIZE);
2513
2514 put_le32(sb, 0x160, 0);
2516
2517 sb[0x174] = 0;
2519
2520 sb[0x175] = 1;
2522
2523 let (overhead_offset, overhead_blocks) =
2529 layout.recorded_overhead_blocks(layout.total_free_blocks());
2530 put_le32(sb, overhead_offset, overhead_blocks as u32);
2531
2532 put_le32(sb, 0x270, 0);
2537
2538 put_le16(sb, 0x27C, 0);
2540
2541 let sb_csum = superblock_checksum(sb);
2543 put_le32(sb, 0x3FC, sb_csum);
2544
2545 Ok(block)
2546}
2547
2548#[cfg(test)]
2551fn build_gdt(
2552 layout: &Layout,
2553 block_bitmaps: &[Vec<u8>],
2554 inode_bitmaps: &[Vec<u8>],
2555) -> Result<Vec<u8>, Ext4Error> {
2556 let geometry = layout.geometry();
2557 let mut gdt = Vec::with_capacity(layout.num_groups as usize * EXT4_DESC_SIZE as usize);
2558
2559 for g in 0..layout.num_groups {
2560 let desc_stats = GroupDescStats {
2561 free_blocks: layout.group_free_blocks(g),
2562 free_inodes: layout.group_free_inodes(g),
2563 used_dirs: layout.group_used_dirs(g),
2564 block_bitmap_csum: bitmap_checksum(
2565 layout.csum_seed,
2566 &block_bitmaps[g as usize],
2567 EXT4_BLOCK_SIZE as usize,
2568 ),
2569 inode_bitmap_csum: bitmap_checksum(
2570 layout.csum_seed,
2571 &inode_bitmaps[g as usize],
2572 (EXT4_INODES_PER_GROUP / 8) as usize,
2573 ),
2574 };
2575 gdt.extend_from_slice(&build_group_descriptor(
2576 &geometry,
2577 g,
2578 &desc_stats,
2579 layout.csum_seed,
2580 ));
2581 }
2582
2583 Ok(gdt)
2584}
2585
2586fn write_primary_superblock_at(
2588 file: &mut (impl std::io::Write + std::io::Seek),
2589 sb_block: &[u8],
2590) -> Result<(), Ext4Error> {
2591 file.seek(SeekFrom::Start(0))?;
2592 file.write_all(sb_block)?;
2593 Ok(())
2594}
2595
2596pub use super::format::sparse_super_group;
2601
2602#[cfg(test)]
2607mod tests {
2608 use super::*;
2609 use std::io::{Read, Seek, SeekFrom};
2610 #[cfg(windows)]
2611 use std::os::windows::fs::MetadataExt;
2612
2613 #[cfg(windows)]
2614 use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SPARSE_FILE;
2615
2616 fn le_u16(bytes: &[u8], offset: usize) -> u16 {
2617 u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
2618 }
2619
2620 fn le_u32(bytes: &[u8], offset: usize) -> u32 {
2621 u32::from_le_bytes([
2622 bytes[offset],
2623 bytes[offset + 1],
2624 bytes[offset + 2],
2625 bytes[offset + 3],
2626 ])
2627 }
2628
2629 fn read_exact_at(path: &Path, offset: u64, len: usize) -> Vec<u8> {
2630 let mut file = std::fs::File::open(path).unwrap();
2631 file.seek(SeekFrom::Start(offset)).unwrap();
2632 let mut bytes = vec![0u8; len];
2633 file.read_exact(&mut bytes).unwrap();
2634 bytes
2635 }
2636
2637 fn assert_backup_superblock(path: &Path, layout: &Layout, group: u32) {
2638 assert!(sparse_super_group(group));
2639 let backup_offset = layout.group_start_block(group) * EXT4_BLOCK_SIZE as u64;
2640 let bytes = read_exact_at(path, backup_offset, 2048);
2641
2642 assert_eq!(le_u16(&bytes, 0x38), EXT4_SUPER_MAGIC);
2643 assert_eq!(le_u16(&bytes, 0x5A), group as u16);
2644 assert_ne!(le_u16(&bytes, 1024 + 0x38), EXT4_SUPER_MAGIC);
2645
2646 let mut sb = bytes[..1024].to_vec();
2647 let stored = le_u32(&sb, 0x3FC);
2648 put_le32(&mut sb, 0x3FC, 0);
2649 let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
2650
2651 assert_eq!(stored, expected);
2652 }
2653
2654 #[test]
2655 fn test_upper_metadata_keeps_tree_ownership_and_directory_mode() {
2656 let copied_up = InodeMetadata {
2657 uid: 0x12345,
2658 gid: 1000,
2659 mode: 0o2755,
2660 mtime: 1_800_000_000,
2661 mtime_nsec: 123,
2662 };
2663 let planned = planned_metadata(&copied_up, true, TreeEncodingMode::Upper);
2664 assert_eq!(
2665 (
2666 planned.uid,
2667 planned.gid,
2668 planned.mode,
2669 planned.mtime,
2670 planned.mtime_nsec
2671 ),
2672 (0x12345, 1000, 0o2755, 1_800_000_000, 123)
2673 );
2674
2675 let empty = InodeMetadata {
2676 uid: 0,
2677 gid: 0,
2678 mode: 0,
2679 mtime: 0,
2680 mtime_nsec: 0,
2681 };
2682 assert_eq!(
2685 planned_metadata(&empty, true, TreeEncodingMode::Upper).mode,
2686 0
2687 );
2688 assert_eq!(
2689 planned_metadata(&empty, false, TreeEncodingMode::Upper).mode,
2690 0o644
2691 );
2692 }
2693
2694 #[test]
2695 fn test_format_creates_file_of_correct_size() {
2696 let dir = tempfile::tempdir().unwrap();
2697 let path = dir.path().join("test.ext4");
2698
2699 let size: u64 = 256 * 1024 * 1024; let opts = Ext4FormatOptions {
2701 size_bytes: size,
2702 journal_blocks: 4096, };
2704
2705 format_ext4(&path, &opts).unwrap();
2706
2707 let meta = std::fs::metadata(&path).unwrap();
2708 assert_eq!(meta.len(), size);
2709 }
2710
2711 #[test]
2712 fn test_inode_allocation_crosses_block_group_boundary() {
2713 let opts = Ext4FormatOptions {
2714 size_bytes: 256 * 1024 * 1024,
2715 journal_blocks: 4096,
2716 };
2717 let layout = Layout::compute(&opts).unwrap();
2718 let plan = BitmapPlan {
2719 block_extents: Vec::new(),
2720 max_used_inode: EXT4_INODES_PER_GROUP + 1,
2721 group_used_dirs: vec![1, 0],
2722 };
2723
2724 let first_bitmap = build_inode_bitmap_for_plan(&layout, &plan, 0);
2725 let second_bitmap = build_inode_bitmap_for_plan(&layout, &plan, 1);
2726 assert!(
2727 first_bitmap[..(EXT4_INODES_PER_GROUP / 8) as usize]
2728 .iter()
2729 .all(|byte| *byte == 0xff)
2730 );
2731 assert_eq!(second_bitmap[0] & 0x01, 0x01);
2732 assert_eq!(second_bitmap[0] & 0xfe, 0);
2733
2734 assert_eq!(
2735 inode_offset(&layout, EXT4_INODES_PER_GROUP + 1),
2736 layout.group_inode_table_block(1) * EXT4_BLOCK_SIZE as u64
2737 );
2738 }
2739
2740 #[test]
2741 fn test_allocator_builds_depth_one_extent_tree_across_groups() {
2742 let opts = Ext4FormatOptions {
2743 size_bytes: 1024 * 1024 * 1024,
2744 journal_blocks: 4096,
2745 };
2746 let layout = Layout::compute(&opts).unwrap();
2747 let mut allocator = DataAllocator::new(&layout);
2748 let block_count = 150_000;
2749 let data_extents = allocator
2750 .allocate_extents(block_count, "/large-layer")
2751 .unwrap();
2752 assert!(data_extents.len() > INLINE_EXTENT_CAPACITY);
2753 assert_eq!(
2754 data_extents
2755 .iter()
2756 .map(|extent| extent.block_count)
2757 .sum::<u32>(),
2758 block_count
2759 );
2760 for pair in data_extents.windows(2) {
2761 assert_eq!(
2762 pair[1].logical_block,
2763 pair[0].logical_block + pair[0].block_count
2764 );
2765 }
2766
2767 let extent_tree_block = allocator.allocate_block("/large-layer").unwrap();
2768 let plan = NodePlan {
2769 inode: EXT4_FIRST_INO,
2770 path: "/large-layer".to_string(),
2771 metadata: InodeMetadata::default(),
2772 links_count: 1,
2773 kind: NodeKind::RegularFile {
2774 data: FileData::Memory(Vec::new()),
2775 },
2776 block_count,
2777 data_extents,
2778 extent_tree_block: Some(extent_tree_block),
2779 xattrs: Vec::new(),
2780 inline_xattrs: None,
2781 xattr_block: None,
2782 };
2783 let mut inode_block = [0u8; 60];
2784 write_extent_plan(&mut inode_block, 0, &plan).unwrap();
2785 assert_eq!(le_u16(&inode_block, 0), EXT4_EH_MAGIC);
2786 assert_eq!(le_u16(&inode_block, 2), 1);
2787 assert_eq!(le_u16(&inode_block, 6), 1);
2788
2789 let leaf = build_extent_leaf_block(&layout, &plan).unwrap();
2790 assert_eq!(le_u16(&leaf, 0), EXT4_EH_MAGIC);
2791 assert_eq!(le_u16(&leaf, 2) as usize, plan.data_extents.len());
2792 assert_eq!(le_u16(&leaf, 6), 0);
2793 let tail = EXT4_BLOCK_SIZE as usize - 4;
2794 assert_eq!(
2795 le_u32(&leaf, tail),
2796 dir_block_checksum(layout.csum_seed, plan.inode, 0, &leaf[..tail])
2797 );
2798 }
2799
2800 #[test]
2801 fn test_xattr_block_is_sorted_and_checksummed() {
2802 let opts = Ext4FormatOptions {
2803 size_bytes: 256 * 1024 * 1024,
2804 journal_blocks: 4096,
2805 };
2806 let layout = Layout::compute(&opts).unwrap();
2807 let block_number = layout.group_data_start_block(1);
2808 let xattrs = vec![
2809 Xattr {
2810 name: b"security.capability".to_vec(),
2811 value: (0u8..20).collect(),
2812 },
2813 Xattr {
2814 name: b"user.owner".to_vec(),
2815 value: b"microsandbox".to_vec(),
2816 },
2817 ];
2818
2819 let block = build_xattr_block(&layout, block_number, &xattrs, "/bin/tool").unwrap();
2820 assert_eq!(le_u32(&block, 0), EXT4_XATTR_MAGIC);
2821 assert_eq!(le_u32(&block, 4), 1);
2822 assert_eq!(le_u32(&block, 8), 1);
2823 assert_eq!(block[EXT4_XATTR_HEADER_SIZE + 1], 1);
2824 let second_entry = EXT4_XATTR_HEADER_SIZE + align_four(EXT4_XATTR_ENTRY_SIZE + 5);
2825 assert_eq!(block[second_entry + 1], 6);
2826
2827 let stored_checksum = le_u32(&block, 16);
2828 let mut checksum_input = block.clone();
2829 put_le32(&mut checksum_input, 16, 0);
2830 assert_eq!(
2831 stored_checksum,
2832 xattr_block_checksum(layout.csum_seed, block_number, &checksum_input)
2833 );
2834 }
2835
2836 #[test]
2837 fn test_small_xattr_is_encoded_in_inode_body() {
2838 let xattrs = vec![Xattr {
2839 name: b"security.capability".to_vec(),
2840 value: (0u8..20).collect(),
2841 }];
2842
2843 let bytes = build_inline_xattrs(&xattrs, "/bin/tool").unwrap().unwrap();
2844 assert_eq!(le_u32(&bytes, 0), EXT4_XATTR_MAGIC);
2845 assert_eq!(bytes[5], 6);
2846 assert_eq!(&bytes[20..30], b"capability");
2847 assert_eq!(le_u32(&bytes, 16), 0);
2848 }
2849
2850 #[test]
2851 fn test_format_too_small() {
2852 let dir = tempfile::tempdir().unwrap();
2853 let path = dir.path().join("tiny.ext4");
2854
2855 let opts = Ext4FormatOptions {
2856 size_bytes: 4096, journal_blocks: 16384,
2858 };
2859
2860 let result = format_ext4(&path, &opts);
2861 assert!(matches!(result, Err(Ext4Error::TooSmall)));
2862 }
2863
2864 #[test]
2865 fn test_layout_rejects_unaligned_size() {
2866 let opts = Ext4FormatOptions {
2867 size_bytes: DEFAULT_SIZE_BYTES + 1,
2868 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2869 };
2870
2871 let result = Layout::compute(&opts);
2872 assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
2873 }
2874
2875 #[test]
2876 fn test_layout_rejects_tiny_final_group() {
2877 let opts = Ext4FormatOptions {
2878 size_bytes: 128 * 1024 * 1024 + EXT4_BLOCK_SIZE as u64,
2879 journal_blocks: 4096,
2880 };
2881
2882 let result = Layout::compute(&opts);
2883 assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
2884 }
2885
2886 #[test]
2887 fn test_layout_rejects_size_beyond_32_bit_block_addresses() {
2888 let opts = Ext4FormatOptions {
2889 size_bytes: (MAX_BLOCKS + 1) * EXT4_BLOCK_SIZE as u64,
2890 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2891 };
2892
2893 let result = Layout::compute(&opts);
2894 assert!(matches!(result, Err(Ext4Error::TooLarge { .. })));
2895 }
2896
2897 #[test]
2898 fn test_layout_uses_actual_group_count_beyond_four_gib() {
2899 let opts = Ext4FormatOptions {
2900 size_bytes: 8 * 1024 * 1024 * 1024,
2901 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2902 };
2903
2904 let layout = Layout::compute(&opts).unwrap();
2905
2906 assert_eq!(layout.num_blocks, opts.size_bytes / EXT4_BLOCK_SIZE as u64);
2907 assert_eq!(layout.num_groups, 64);
2908 assert_eq!(layout.gdt_blocks, 1);
2909 }
2910
2911 #[test]
2912 fn test_format_default_options() {
2913 let dir = tempfile::tempdir().unwrap();
2914 let path = dir.path().join("default.ext4");
2915
2916 let opts = Ext4FormatOptions::default();
2917 format_ext4(&path, &opts).unwrap();
2918
2919 let meta = std::fs::metadata(&path).unwrap();
2920 assert_eq!(meta.len(), DEFAULT_SIZE_BYTES);
2921 }
2922
2923 #[test]
2924 fn test_superblock_magic() {
2925 let dir = tempfile::tempdir().unwrap();
2926 let path = dir.path().join("magic.ext4");
2927
2928 let opts = Ext4FormatOptions {
2929 size_bytes: 256 * 1024 * 1024,
2930 journal_blocks: 4096,
2931 };
2932 format_ext4(&path, &opts).unwrap();
2933
2934 let data = std::fs::read(&path).unwrap();
2936 let magic = u16::from_le_bytes([data[1024 + 0x38], data[1024 + 0x39]]);
2937 assert_eq!(magic, EXT4_SUPER_MAGIC);
2938 }
2939
2940 #[test]
2941 fn test_journal_magic() {
2942 let dir = tempfile::tempdir().unwrap();
2943 let path = dir.path().join("journal.ext4");
2944
2945 let opts = Ext4FormatOptions {
2946 size_bytes: 256 * 1024 * 1024,
2947 journal_blocks: 4096,
2948 };
2949 format_ext4(&path, &opts).unwrap();
2950
2951 let layout = Layout::compute(&opts).unwrap();
2952 let data = std::fs::read(&path).unwrap();
2953
2954 let jsb_offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
2956 let magic = u32::from_be_bytes([
2957 data[jsb_offset],
2958 data[jsb_offset + 1],
2959 data[jsb_offset + 2],
2960 data[jsb_offset + 3],
2961 ]);
2962 assert_eq!(magic, JBD2_MAGIC);
2963 }
2964
2965 #[test]
2966 fn test_root_dir_inode_exists() {
2967 let dir = tempfile::tempdir().unwrap();
2968 let path = dir.path().join("rootdir.ext4");
2969
2970 let opts = Ext4FormatOptions {
2971 size_bytes: 256 * 1024 * 1024,
2972 journal_blocks: 4096,
2973 };
2974 format_ext4(&path, &opts).unwrap();
2975
2976 let layout = Layout::compute(&opts).unwrap();
2977 let data = std::fs::read(&path).unwrap();
2978
2979 let inode_offset = layout.group_inode_table_block(0) as usize * EXT4_BLOCK_SIZE as usize
2981 + (EXT4_INODE_SIZE as usize);
2982 let mode = u16::from_le_bytes([data[inode_offset], data[inode_offset + 1]]);
2983 assert_eq!(mode, S_IFDIR | 0o755);
2984 }
2985
2986 #[test]
2987 fn test_backup_group_bitmap_starts_after_backup_metadata() {
2988 let opts = Ext4FormatOptions {
2989 size_bytes: 256 * 1024 * 1024,
2990 journal_blocks: 4096,
2991 };
2992 let layout = Layout::compute(&opts).unwrap();
2993 let block_bitmaps = build_block_bitmaps(&layout);
2994 let inode_bitmaps = build_inode_bitmaps(&layout);
2995 let gdt = build_gdt(&layout, &block_bitmaps, &inode_bitmaps).unwrap();
2996
2997 let desc = &gdt[EXT4_DESC_SIZE as usize..(2 * EXT4_DESC_SIZE as usize)];
2998 let block_bitmap = u32::from_le_bytes([desc[0], desc[1], desc[2], desc[3]]) as u64;
2999 let group_start = layout.group_start_block(1);
3000
3001 assert_eq!(block_bitmap, layout.group_block_bitmap_block(1));
3002 assert!(block_bitmap > group_start + layout.gdt_blocks as u64 - 1);
3003 }
3004
3005 #[test]
3006 fn test_backup_superblock_is_written_at_backup_group_start() {
3007 let dir = tempfile::tempdir().unwrap();
3008 let path = dir.path().join("backup-super.ext4");
3009 let opts = Ext4FormatOptions {
3010 size_bytes: 256 * 1024 * 1024,
3011 journal_blocks: 4096,
3012 };
3013
3014 format_ext4(&path, &opts).unwrap();
3015
3016 let layout = Layout::compute(&opts).unwrap();
3017 assert_backup_superblock(&path, &layout, 1);
3018 }
3019
3020 #[test]
3021 fn test_backup_superblock_is_group_specific_in_sixteen_gib_image() {
3022 let dir = tempfile::tempdir().unwrap();
3023 let path = dir.path().join("backup-super-16g.ext4");
3024 let opts = Ext4FormatOptions {
3025 size_bytes: 16 * 1024 * 1024 * 1024,
3026 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
3027 };
3028
3029 format_ext4(&path, &opts).unwrap();
3030
3031 let layout = Layout::compute(&opts).unwrap();
3032 assert_eq!(layout.num_groups, 128);
3033 assert_backup_superblock(&path, &layout, 81);
3034 }
3035
3036 #[test]
3037 fn test_format_sparse_image_larger_than_four_gib() {
3038 let dir = tempfile::tempdir().unwrap();
3039 let path = dir.path().join("large.ext4");
3040 let opts = Ext4FormatOptions {
3041 size_bytes: 8 * 1024 * 1024 * 1024,
3042 journal_blocks: DEFAULT_JOURNAL_BLOCKS,
3043 };
3044
3045 format_ext4(&path, &opts).unwrap();
3046
3047 let meta = std::fs::metadata(&path).unwrap();
3048 assert_eq!(meta.len(), opts.size_bytes);
3049
3050 let layout = Layout::compute(&opts).unwrap();
3051 let sb = read_exact_at(&path, 1024, 1024);
3052 let blocks = le_u32(&sb, 0x04) as u64 | ((le_u32(&sb, 0x150) as u64) << 32);
3053 let inodes = le_u32(&sb, 0x00);
3054 assert_eq!(blocks, layout.num_blocks);
3055 assert_eq!(inodes, layout.num_groups * EXT4_INODES_PER_GROUP);
3056
3057 let desc_offset = EXT4_BLOCK_SIZE as u64 + 63 * EXT4_DESC_SIZE as u64;
3058 let desc = read_exact_at(&path, desc_offset, EXT4_DESC_SIZE as usize);
3059 let block_bitmap = le_u32(&desc, 0x00) as u64 | ((le_u32(&desc, 0x20) as u64) << 32);
3060 let inode_bitmap = le_u32(&desc, 0x04) as u64 | ((le_u32(&desc, 0x24) as u64) << 32);
3061
3062 assert_eq!(block_bitmap, layout.group_block_bitmap_block(63));
3063 assert_eq!(inode_bitmap, layout.group_inode_bitmap_block(63));
3064 }
3065
3066 #[cfg(windows)]
3067 #[test]
3068 fn test_format_marks_image_sparse_on_windows() {
3069 let dir = tempfile::tempdir().unwrap();
3070 let path = dir.path().join("sparse.ext4");
3071 let opts = Ext4FormatOptions {
3072 size_bytes: 256 * 1024 * 1024,
3073 journal_blocks: 4096,
3074 };
3075
3076 format_ext4(&path, &opts).unwrap();
3077
3078 let meta = std::fs::metadata(&path).unwrap();
3079 assert_eq!(meta.len(), opts.size_bytes);
3080 assert_ne!(meta.file_attributes() & FILE_ATTRIBUTE_SPARSE_FILE, 0);
3081 }
3082
3083 #[test]
3084 fn test_inode_bitmap_padding_is_marked_used() {
3085 let layout = Layout::compute(&Ext4FormatOptions {
3086 size_bytes: 256 * 1024 * 1024,
3087 journal_blocks: 4096,
3088 })
3089 .unwrap();
3090
3091 let bitmap = build_inode_bitmap(&layout, 0);
3092 for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
3093 assert_ne!(bitmap[(bit / 8) as usize] & (1 << (bit % 8)), 0);
3094 }
3095 }
3096
3097 #[test]
3098 fn test_journal_superblock_checksum_matches_contents() {
3099 let dir = tempfile::tempdir().unwrap();
3100 let path = dir.path().join("journal-csum.ext4");
3101 let opts = Ext4FormatOptions {
3102 size_bytes: 256 * 1024 * 1024,
3103 journal_blocks: 4096,
3104 };
3105
3106 format_ext4(&path, &opts).unwrap();
3107
3108 let layout = Layout::compute(&opts).unwrap();
3109 let data = std::fs::read(&path).unwrap();
3110 let offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
3111 let mut jsb = data[offset..offset + JBD2_SUPERBLOCK_SIZE].to_vec();
3112 let stored = u32::from_be_bytes([jsb[0xFC], jsb[0xFD], jsb[0xFE], jsb[0xFF]]);
3113
3114 jsb[0xFC..0x100].fill(0);
3115 let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb);
3116
3117 assert_eq!(stored, expected);
3118 }
3119
3120 #[test]
3121 fn test_root_dir_checksum_matches_contents() {
3122 let dir = tempfile::tempdir().unwrap();
3123 let path = dir.path().join("rootdir-csum.ext4");
3124 let opts = Ext4FormatOptions {
3125 size_bytes: 256 * 1024 * 1024,
3126 journal_blocks: 4096,
3127 };
3128
3129 format_ext4(&path, &opts).unwrap();
3130
3131 let layout = Layout::compute(&opts).unwrap();
3132 let data = std::fs::read(&path).unwrap();
3133 let sb = &data[1024..2048];
3134 let uuid = &sb[0x68..0x78];
3135 let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, uuid);
3136 let block_offset = layout.first_data_block as usize * EXT4_BLOCK_SIZE as usize;
3137 let tail_offset = block_offset + EXT4_BLOCK_SIZE as usize - 12;
3138 let stored = u32::from_le_bytes([
3139 data[tail_offset + 8],
3140 data[tail_offset + 9],
3141 data[tail_offset + 10],
3142 data[tail_offset + 11],
3143 ]);
3144 let expected = dir_block_checksum(
3145 csum_seed,
3146 EXT4_ROOT_INO,
3147 0,
3148 &data[block_offset..tail_offset],
3149 );
3150
3151 assert_eq!(stored, expected);
3152 }
3153}