Files
wehub-resource-sync 680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

198 lines
9.5 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationOS
import Foundation
// JBD2 on-disk format reference:
// https://www.kernel.org/doc/html/latest/filesystems/ext4/journal.html
extension EXT4.Formatter {
/// Entry point called from close() when journaling is enabled.
func initializeJournal(
config: EXT4.JournalConfig,
filesystemUUID: (
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
)
) throws -> UInt32 {
let journalBlocks = try calculateJournalSize(requestedSize: config.size, usableBlocks: blockCount)
// Align to block boundary before recording start.
if self.pos % self.blockSize != 0 {
try self.seek(block: self.currentBlock + 1)
}
let journalStartBlock = self.currentBlock
try writeJournalSuperblock(journalBlocks: journalBlocks, filesystemUUID: filesystemUUID)
try zeroJournalBlocks(count: journalBlocks - 1)
try setupJournalInode(startBlock: journalStartBlock, blockCount: journalBlocks)
return journalBlocks
}
// MARK: - Private helpers
private func calculateJournalSize(requestedSize: UInt64?, usableBlocks: UInt32) throws -> UInt32 {
if let size = requestedSize {
let blocks = size / UInt64(self.blockSize)
// JBD2_MIN_JOURNAL_BLOCKS: the kernel refuses to mount with fewer.
// blocks == 0 would also cause a UInt32 underflow in the caller.
guard blocks >= EXT4.MinJournalBlocks else {
throw EXT4.Formatter.Error.journalTooSmall(size)
}
// Safe: any journal large enough to overflow UInt32 (>16 TiB at 4 KiB block size)
// would fail at the I/O layer before this conversion is reached.
return UInt32(blocks)
}
// Default sizing: scale with the usable content area, with a floor determined by
// JBD2_MIN_JOURNAL_BLOCKS and a ceiling that follows e2fsprogs convention: 128 MiB for
// filesystems up to 128 GiB, and 1 GiB for larger filesystems. The larger ceiling was
// introduced in e2fsprogs 1.43.2:
// https://e2fsprogs.sourceforge.net/e2fsprogs-release.html#1.43.2
let usableBytes = UInt64(usableBlocks) * UInt64(self.blockSize)
let scaledBytes = usableBytes / 64 // 1/64th of the usable area, matching e2fsprogs defaults
let minBytes: UInt64 = UInt64(EXT4.MinJournalBlocks) * UInt64(self.blockSize)
let maxBytes: UInt64 = usableBytes > 128.gib() ? 1.gib() : 128.mib()
let clampedBytes = min(max(scaledBytes, minBytes), maxBytes)
// Safe: clampedBytes ≤ 1 GiB and blockSize ≥ 1, so the quotient fits in UInt32.
return UInt32(clampedBytes / UInt64(self.blockSize))
}
private func writeJournalSuperblock(
journalBlocks: UInt32,
filesystemUUID: (
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
)
) throws {
// Safe: blockSize is UInt32; widening to Int (64-bit on all supported platforms) never truncates.
var buf = [UInt8](repeating: 0, count: Int(self.blockSize))
// JBD2 is a big-endian format regardless of host byte order (§3.6.1).
func writeU32BigEndian(_ value: UInt32, at offset: Int) {
buf[offset] = UInt8((value >> 24) & 0xFF)
buf[offset + 1] = UInt8((value >> 16) & 0xFF)
buf[offset + 2] = UInt8((value >> 8) & 0xFF)
buf[offset + 3] = UInt8(value & 0xFF)
}
// JBD2 block header (§3.6.3): https://www.kernel.org/doc/html/latest/filesystems/ext4/journal.html#block-header
writeU32BigEndian(EXT4.JournalMagic, at: 0x00) // h_magic
writeU32BigEndian(4, at: 0x04) // h_blocktype = superblock v2
writeU32BigEndian(1, at: 0x08) // h_sequence
// JBD2 superblock body (§3.6.4): https://www.kernel.org/doc/html/latest/filesystems/ext4/journal.html#super-block
writeU32BigEndian(self.blockSize, at: 0x0C) // s_blocksize
writeU32BigEndian(journalBlocks, at: 0x10) // s_maxlen
writeU32BigEndian(1, at: 0x14) // s_first (first usable block)
writeU32BigEndian(1, at: 0x18) // s_sequence
// 0x1C s_start: left zero — kernel treats zero as "journal empty, begin at s_first"
// 0x20 s_errno: left zero — no prior abort error
// 0x24 s_feature_compat: left zero — no optional features (e.g. data-block checksums)
// 0x28 s_feature_incompat: left zero — non-zero unrecognised flags would cause mount refusal
// 0x2C s_feature_ro_compat: left zero — no flags defined by the spec
// s_uuid at 0x30 (16 bytes)
let uuidBytes = [
filesystemUUID.0, filesystemUUID.1, filesystemUUID.2, filesystemUUID.3,
filesystemUUID.4, filesystemUUID.5, filesystemUUID.6, filesystemUUID.7,
filesystemUUID.8, filesystemUUID.9, filesystemUUID.10, filesystemUUID.11,
filesystemUUID.12, filesystemUUID.13, filesystemUUID.14, filesystemUUID.15,
]
buf[0x30..<0x40] = uuidBytes[...]
writeU32BigEndian(1, at: 0x40) // s_nr_users
let maxTrans = min(journalBlocks / 4, 32768)
writeU32BigEndian(maxTrans, at: 0x48) // s_max_transaction
writeU32BigEndian(maxTrans, at: 0x4C) // s_max_trans_data
// s_users[0] at 0x100 (first entry of 768-byte users array)
buf[0x100..<0x110] = uuidBytes[...]
try self.handle.write(contentsOf: buf)
}
private func zeroJournalBlocks(count: UInt32) throws {
guard count > 0 else { return }
let chunkSize = 1.mib()
// Safe: both operands are UInt32, so their product peaks at ~17 TiB, which fits
// in Int64 (the width of Int on all 64-bit Apple platforms).
let totalBytes = Int(count) * Int(self.blockSize)
let zeroBuf = [UInt8](repeating: 0, count: min(Int(chunkSize), totalBytes))
var remaining = totalBytes
while remaining > 0 {
let toWrite = min(zeroBuf.count, remaining)
try self.handle.write(contentsOf: zeroBuf[0..<toWrite])
remaining -= toWrite
}
}
private func setupJournalInode(startBlock: UInt32, blockCount: UInt32) throws {
var journalInode = EXT4.Inode()
journalInode.mode = EXT4.Inode.Mode(.S_IFREG, 0o600)
journalInode.uid = 0
journalInode.gid = 0
let size = UInt64(blockCount) * UInt64(self.blockSize)
journalInode.sizeLow = size.lo
journalInode.sizeHigh = size.hi
let now = Date().fs()
journalInode.atime = now.lo
journalInode.atimeExtra = now.hi
journalInode.ctime = now.lo
journalInode.ctimeExtra = now.hi
journalInode.mtime = now.lo
journalInode.mtimeExtra = now.hi
journalInode.crtime = now.lo
journalInode.crtimeExtra = now.hi
journalInode.linksCount = 1
journalInode.extraIsize = UInt16(EXT4.ExtraIsize)
journalInode.flags = EXT4.InodeFlag.extents.rawValue | EXT4.InodeFlag.hugeFile.rawValue
// Journal is one contiguous allocation → numExtents = 1 → extent tree fits inline
// in the inode, so writeExtents needs no extra disk I/O for extent index blocks.
// Safe: blockCount is at most UInt32.max and startBlock ≥ 0, so the addition could
// theoretically overflow — but zeroJournalBlocks would have already failed with an
// I/O error if the journal extended past the end of the filesystem image.
journalInode = try self.writeExtents(journalInode, (startBlock, startBlock + blockCount))
self.inodes[Int(EXT4.JournalInode) - 1].pointee = journalInode
}
func journalInodeBlockBackup() -> (
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
UInt32
) {
let ji = self.inodes[Int(EXT4.JournalInode) - 1].pointee
// s_jnl_blocks layout (§4.1.2): first 15 words = i_block[] extent-tree data,
// 16th word (index 15) = i_size_high, 17th word (index 16) = i_size.
var words = [UInt32](repeating: 0, count: 17)
withUnsafeBytes(of: ji.block) { bytes in
for i in 0..<15 {
words[i] = bytes.load(fromByteOffset: i * 4, as: UInt32.self)
}
}
words[15] = ji.sizeHigh // i_size_high (16th element per spec)
words[16] = ji.sizeLow // i_size (17th element per spec)
return (
words[0], words[1], words[2], words[3],
words[4], words[5], words[6], words[7],
words[8], words[9], words[10], words[11],
words[12], words[13], words[14], words[15],
words[16]
)
}
}