Skip to content
Open
94 changes: 83 additions & 11 deletions Sources/ContainerizationEXT4/EXT4+Formatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ extension EXT4 {
blockSize / groupDescriptorSize
}

private var blockCount: UInt32 {
// internally accessed by journal setup
var blockCount: UInt32 {
((size - 1) / blockSize) + 1
}

Expand All @@ -64,15 +65,17 @@ extension EXT4 {
/// - devicePath: The path to the block device where the ext4 filesystem will be created.
/// - blockSize: The filesystem block size in bytes. Must be a power of two in the set
/// {1024, 2048, 4096}. Defaults to 4096.
/// - blockSize: The filesystem block size.
/// - minDiskSize: The minimum disk size required for the formatted filesystem.
/// - journal: The JBD2 journal size and mode, or nil for an unjournalled filesystem.
///
/// - Note: This ext4 formatter is designed for creating block devices out of container images and does not support all the
/// features and options available in the full ext4 filesystem implementation. It focuses
/// on the core functionality required for formatting a block device with ext4.
///
/// - Important: Ensure that the destination block device is accessible and has sufficient permissions
/// for formatting. The formatting process will erase all existing data on the device.
public init(_ devicePath: FilePath, blockSize: UInt32 = 4096, minDiskSize: UInt64 = 256.kib()) throws {
public init(_ devicePath: FilePath, blockSize: UInt32 = 4096, minDiskSize: UInt64 = 256.kib(), journal: JournalConfig? = nil) throws {
/// The constructor performs the following steps:
///
/// 1. Creates the first 10 inodes:
Expand Down Expand Up @@ -128,6 +131,7 @@ extension EXT4 {
}
// step #2
self.tree = FileTree(EXT4.RootInode, "/")
self.journalConfig = journal
// skip past the superblock and block descriptor table
try self.seek(block: self.groupDescriptorBlocks + 1)
// lost+found directory is required for e2fsck to pass
Expand Down Expand Up @@ -610,6 +614,19 @@ extension EXT4 {
}
breadthWiseChildTree.append(contentsOf: child.pointee.children.map { (child, $0) })
}

// Generate UUID once; shared by filesystem superblock and JBD2 superblock.
let filesystemUUID = UUID().uuid

// Journal init MUST precede optimizeBlockGroupLayout() and commitInodeTable().
// Reason 1: optimizeBlockGroupLayout reads self.currentBlock — journal blocks
// must already be written to be counted in the layout calculation.
// Reason 2: commitInodeTable writes inode 8 to disk — setupJournalInode must
// have updated self.inodes[7] in memory first.
if let config = journalConfig {
try initializeJournal(config: config, filesystemUUID: filesystemUUID)
}

let blockGroupSize = optimizeBlockGroupLayout(blocks: self.currentBlock, inodes: UInt32(self.inodes.count))
let inodeTableOffset = try self.commitInodeTable(
blockGroups: blockGroupSize.blockGroups,
Expand Down Expand Up @@ -867,39 +884,85 @@ extension EXT4 {
superblock.firstInode = EXT4.FirstInode
superblock.lpfInode = EXT4.LostAndFoundInode
superblock.inodeSize = UInt16(EXT4.InodeSize)
superblock.featureCompat = CompatFeature.sparseSuper2 | CompatFeature.extAttr
superblock.featureIncompat =
IncompatFeature.filetype | IncompatFeature.extents | IncompatFeature.flexBg
superblock.featureRoCompat =
RoCompatFeature.largeFile | RoCompatFeature.hugeFile | RoCompatFeature.extraIsize
superblock.minExtraIsize = EXT4.ExtraIsize
superblock.wantExtraIsize = EXT4.ExtraIsize
superblock.logGroupsPerFlex = 31
superblock.uuid = UUID().uuid
superblock.uuid = filesystemUUID
var compatFeatures: UInt32 = CompatFeature.sparseSuper2 | CompatFeature.extAttr
if let config = journalConfig {
compatFeatures |= CompatFeature.hasJournal.rawValue
superblock.journalInum = EXT4.JournalInode
superblock.journalUUID = filesystemUUID
superblock.journalBlocks = journalInodeBlockBackup()
superblock.journalBackupType = 1 // s_jnl_backup_type: 1 = s_jnl_blocks[] holds a valid inode backup
if let mode = config.defaultMode {
switch mode {
case .writeback: superblock.defaultMountOpts = DefaultMountOpts.journalWriteback
case .ordered: superblock.defaultMountOpts = DefaultMountOpts.journalOrdered
case .journal: superblock.defaultMountOpts = DefaultMountOpts.journalData
}
}
}
superblock.featureCompat = compatFeatures

// Fields intentionally left at zero:
// s_r_blocks_count_lo: no blocks reserved for root
// s_mtime / s_wtime: never mounted/written; kernel updates on first access
// s_mnt_count / s_max_mnt_count: no forced-fsck-after-N-mounts policy
// s_lastcheck / s_checkinterval: no time-based fsck scheduling
// s_def_resuid / s_def_resgid: reserved blocks owned by uid/gid 0 (root)
// s_block_group_nr: this superblock resides in group 0
// s_volume_name: no volume label
// s_last_mounted: no recorded prior mount path
// s_algorithm_usage_bitmap: obsolete compression field, not used
// s_prealloc_blocks / s_prealloc_dir_blocks: block preallocation not enabled
// s_reserved_gdt_blocks: online resize not supported
// s_journal_dev: journal is internal (inode 8), not on an external device
// s_last_orphan: fresh filesystem, no pending orphan cleanup
// s_hash_seed / s_def_hash_version: kernel initialises htree hash seed at first mount
// s_first_meta_bg: meta block group feature not enabled
// s_mkfs_time: creation timestamp not recorded
// s_raid_stride / s_mmp_interval / s_mmp_block / s_raid_stripe_width: no RAID or MMP
// s_checksum_type / s_checksum_seed: metadata checksums not enabled (no csum feature bit)
// s_snapshot_*: snapshot feature not enabled
// s_error_count / s_first_error_* / s_last_error_*: fresh filesystem, no recorded errors
// s_usr_quota_inum / s_grp_quota_inum / s_prj_quota_inum: quotas not enabled
// s_overhead_clusters: kernel computes dynamically; zero is always safe
// s_backup_bgs: sparse_super2 active but no secondary backup groups requested
// s_encrypt_algos / s_encrypt_pw_salt: encryption not enabled
// s_checksum: superblock checksum not enabled (no metadata_csum feature bit)

try withUnsafeLittleEndianBytes(of: superblock) { bytes in
try self.handle.write(contentsOf: bytes)
}
try self.handle.write(contentsOf: Array<UInt8>.init(repeating: 0, count: 2048))
}

// MARK: Private Methods and Properties
private var handle: FileHandle
private var inodes: [Ptr<Inode>]
// MARK: Private and internal methods and properties
private var tree: FileTree
private var deletedBlocks: [(start: UInt32, end: UInt32)] = []

private var pos: UInt64 {
// internally accessed by journal setup
var handle: FileHandle
var inodes: [Ptr<Inode>]
let journalConfig: JournalConfig?

var pos: UInt64 {
guard let offset = try? self.handle.offset() else {
return 0
}
return offset
}

private var currentBlock: UInt32 {
var currentBlock: UInt32 {
self.pos / self.blockSize
}

private func seek(block: UInt32) throws {
func seek(block: UInt32) throws {
try self.handle.seek(toOffset: UInt64(block) * blockSize)
}

Expand Down Expand Up @@ -1028,7 +1091,7 @@ extension EXT4 {
}
}

private func writeExtents(_ inode: Inode, _ blocks: (start: UInt32, end: UInt32)) throws -> Inode {
func writeExtents(_ inode: Inode, _ blocks: (start: UInt32, end: UInt32)) throws -> Inode {
var inode = inode
// rest of code assumes that extents MUST go into a new block
if self.pos % self.blockSize != 0 {
Expand Down Expand Up @@ -1233,6 +1296,9 @@ extension EXT4 {
case cannotCreateSparseFile(_ path: FilePath)
case cannotResizeFS(_ size: UInt64)
case invalidBlockSize(_ size: UInt32)
case journalTooSmall(_ size: UInt64)
case journalTooLarge(_ size: UInt64)
case filesystemTooSmallForJournal
public var description: String {
switch self {
case .notDirectory(let path):
Expand Down Expand Up @@ -1267,6 +1333,12 @@ extension EXT4 {
return "cannot resize fs to \(size) bytes"
case .invalidBlockSize(let size):
return "invalid block size \(size): must be 1024, 2048, or 4096"
case .journalTooSmall(let size):
return "requested journal size \(size) bytes is too small; minimum is \(EXT4.MinJournalBlocks) blocks (JBD2_MIN_JOURNAL_BLOCKS)"
case .journalTooLarge(let size):
return "requested journal size \(size) bytes exceeds half the filesystem size; a journal this large is unlikely to be useful"
case .filesystemTooSmallForJournal:
return "filesystem is too small to accommodate a minimum-sized journal; increase minDiskSize to at least \(2 * EXT4.MinJournalBlocks) blocks"
}
}
}
Expand Down
Loading
Loading