chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# ``ContainerizationEXT4``
|
||||
|
||||
`ContainerizationEXT4` provides functionality to read the superblock of an existing ext4 block device and format a new block device with
|
||||
the ext4 file system.
|
||||
@@ -0,0 +1,97 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension EXT4.InodeFlag {
|
||||
public static func | (lhs: Self, rhs: Self) -> Self {
|
||||
Self(rawValue: lhs.rawValue | rhs.rawValue)
|
||||
}
|
||||
|
||||
public static func | (lhs: Self, rhs: Self) -> UInt32 {
|
||||
lhs.rawValue | rhs.rawValue
|
||||
}
|
||||
|
||||
public static func | (lhs: Self, rhs: UInt32) -> UInt32 {
|
||||
lhs.rawValue | rhs
|
||||
}
|
||||
}
|
||||
|
||||
extension EXT4.CompatFeature {
|
||||
public static func | (lhs: Self, rhs: Self) -> Self {
|
||||
EXT4.CompatFeature(rawValue: lhs.rawValue | rhs.rawValue)
|
||||
}
|
||||
|
||||
public static func | (lhs: Self, rhs: Self) -> UInt32 {
|
||||
lhs.rawValue | rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension EXT4.IncompatFeature {
|
||||
public static func | (lhs: Self, rhs: Self) -> Self {
|
||||
EXT4.IncompatFeature(rawValue: lhs.rawValue | rhs.rawValue)
|
||||
}
|
||||
|
||||
public static func | (lhs: Self, rhs: Self) -> UInt32 {
|
||||
lhs.rawValue | rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension EXT4.RoCompatFeature {
|
||||
public static func | (lhs: Self, rhs: Self) -> Self {
|
||||
EXT4.RoCompatFeature(rawValue: lhs.rawValue | rhs.rawValue)
|
||||
}
|
||||
|
||||
public static func | (lhs: Self, rhs: Self) -> UInt32 {
|
||||
lhs.rawValue | rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension EXT4.FileModeFlag {
|
||||
public static func | (lhs: Self, rhs: Self) -> Self {
|
||||
Self(rawValue: lhs.rawValue | rhs.rawValue)
|
||||
}
|
||||
|
||||
public static func | (lhs: Self, rhs: Self) -> UInt16 {
|
||||
lhs.rawValue | rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension EXT4.XAttrEntry {
|
||||
init(using bytes: [UInt8]) throws {
|
||||
guard bytes.count == 16 else {
|
||||
throw EXT4.Error.invalidXattrEntry
|
||||
}
|
||||
nameLength = bytes[0]
|
||||
nameIndex = bytes[1]
|
||||
let rawValue = Array(bytes[2...3])
|
||||
valueOffset = rawValue.withUnsafeBytes { $0.loadLittleEndian(as: UInt16.self) }
|
||||
|
||||
let rawValueInum = Array(bytes[4...7])
|
||||
valueInum = rawValueInum.withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
|
||||
|
||||
let rawSize = Array(bytes[8...11])
|
||||
valueSize = rawSize.withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
|
||||
|
||||
let rawHash = Array(bytes[12...])
|
||||
hash = rawHash.withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
|
||||
}
|
||||
}
|
||||
|
||||
extension EXT4 {
|
||||
static func tupleToArray<T>(_ tuple: T) -> [UInt8] {
|
||||
let reflection = Mirror(reflecting: tuple)
|
||||
return reflection.children.compactMap { $0.value as? UInt8 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 Foundation
|
||||
import SystemPackage
|
||||
|
||||
extension EXT4 {
|
||||
class FileTree {
|
||||
class FileTreeNode {
|
||||
let inode: InodeNumber
|
||||
let name: String
|
||||
var children: [Ptr<FileTreeNode>] = []
|
||||
var blocks: (start: UInt32, end: UInt32)?
|
||||
var additionalBlocks: [(start: UInt32, end: UInt32)]?
|
||||
var link: InodeNumber?
|
||||
private weak var parent: Ptr<FileTreeNode>?
|
||||
|
||||
init(
|
||||
inode: InodeNumber,
|
||||
name: String,
|
||||
parent: Ptr<FileTreeNode>?,
|
||||
children: [Ptr<FileTreeNode>] = [],
|
||||
blocks: (start: UInt32, end: UInt32)? = nil,
|
||||
additionalBlocks: [(start: UInt32, end: UInt32)]? = nil,
|
||||
link: InodeNumber? = nil
|
||||
) {
|
||||
self.inode = inode
|
||||
self.name = name
|
||||
self.children = children
|
||||
self.blocks = blocks
|
||||
self.additionalBlocks = additionalBlocks
|
||||
self.link = link
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.children.removeAll()
|
||||
self.children = []
|
||||
self.blocks = nil
|
||||
self.additionalBlocks = nil
|
||||
self.link = nil
|
||||
}
|
||||
|
||||
var path: FilePath? {
|
||||
var components: [String] = [self.name]
|
||||
var _ptr = self.parent
|
||||
while let ptr = _ptr {
|
||||
components.append(ptr.pointee.name)
|
||||
_ptr = ptr.pointee.parent
|
||||
}
|
||||
let path = components.reversed().joined(separator: "/")
|
||||
return FilePath(path).lexicallyNormalized()
|
||||
}
|
||||
}
|
||||
|
||||
var root: Ptr<FileTreeNode>
|
||||
|
||||
init(_ root: InodeNumber, _ name: String) {
|
||||
self.root = Ptr(FileTreeNode(inode: root, name: name, parent: nil))
|
||||
}
|
||||
|
||||
func lookup(path: FilePath) -> Ptr<FileTreeNode>? {
|
||||
var components: [String] = path.items
|
||||
var node = self.root
|
||||
if components.first == "/" {
|
||||
components = Array(components.dropFirst())
|
||||
}
|
||||
if components.count == 0 {
|
||||
return node
|
||||
}
|
||||
for component in components {
|
||||
var found = false
|
||||
for childPtr in node.pointee.children {
|
||||
let child = childPtr.pointee
|
||||
if child.name == component {
|
||||
node = childPtr
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
guard found else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,197 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension EXT4 {
|
||||
class Ptr<T> {
|
||||
let underlying: UnsafeMutablePointer<T>
|
||||
|
||||
var pointee: T {
|
||||
get { underlying.pointee }
|
||||
set { underlying.pointee = newValue }
|
||||
}
|
||||
|
||||
init(_ value: T) {
|
||||
self.underlying = .allocate(capacity: 1)
|
||||
self.underlying.initialize(to: value)
|
||||
}
|
||||
|
||||
deinit {
|
||||
underlying.deinitialize(count: 1)
|
||||
underlying.deallocate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 Foundation
|
||||
import SystemPackage
|
||||
|
||||
extension EXT4 {
|
||||
/// The `EXT4Reader` opens a block device, parses the superblock, and loads group descriptors & inodes.
|
||||
public class EXT4Reader {
|
||||
public var superBlock: EXT4.SuperBlock {
|
||||
self._superBlock
|
||||
}
|
||||
|
||||
let handle: FileHandle
|
||||
let _superBlock: EXT4.SuperBlock
|
||||
|
||||
private var groupDescriptors: [UInt32: EXT4.GroupDescriptor] = [:]
|
||||
private var inodes: [InodeNumber: EXT4.Inode] = [:]
|
||||
|
||||
var hardlinks: [FilePath: InodeNumber] = [:]
|
||||
var tree: EXT4.FileTree = EXT4.FileTree(EXT4.RootInode, ".")
|
||||
var blockSize: UInt64 { UInt64(_superBlock.blockSize) }
|
||||
|
||||
private var groupDescriptorSize: UInt16 {
|
||||
if _superBlock.featureIncompat & EXT4.IncompatFeature.bit64.rawValue != 0 {
|
||||
return _superBlock.descSize
|
||||
}
|
||||
return UInt16(MemoryLayout<EXT4.GroupDescriptor>.size)
|
||||
}
|
||||
|
||||
public init(blockDevice: FilePath) throws {
|
||||
guard FileManager.default.fileExists(atPath: blockDevice.description) else {
|
||||
throw EXT4.Error.notFound(blockDevice.description)
|
||||
}
|
||||
|
||||
guard let fileHandle = FileHandle(forReadingAtPath: blockDevice) else {
|
||||
throw Error.notFound(blockDevice.description)
|
||||
}
|
||||
self.handle = fileHandle
|
||||
try handle.seek(toOffset: EXT4.SuperBlockOffset)
|
||||
|
||||
let superBlockSize = MemoryLayout<EXT4.SuperBlock>.size
|
||||
guard let data = try? self.handle.read(upToCount: superBlockSize) else {
|
||||
throw EXT4.Error.couldNotReadSuperBlock(blockDevice.description, EXT4.SuperBlockOffset, superBlockSize)
|
||||
}
|
||||
let sb = data.withUnsafeBytes { ptr in
|
||||
ptr.loadLittleEndian(as: EXT4.SuperBlock.self)
|
||||
}
|
||||
guard sb.magic == EXT4.SuperBlockMagic else {
|
||||
throw EXT4.Error.invalidSuperBlock
|
||||
}
|
||||
self._superBlock = sb
|
||||
var items: [(item: Ptr<EXT4.FileTree.FileTreeNode>, inode: InodeNumber)] = [
|
||||
(self.tree.root, EXT4.RootInode)
|
||||
]
|
||||
while items.count > 0 {
|
||||
guard let item = items.popLast() else {
|
||||
break
|
||||
}
|
||||
let (itemPtr, inodeNum) = item
|
||||
let childItems = try self.children(of: inodeNum)
|
||||
let root = itemPtr.pointee
|
||||
for (itemName, itemInodeNum) in childItems {
|
||||
if itemName == "." || itemName == ".." {
|
||||
continue
|
||||
}
|
||||
|
||||
if self.inodes[itemInodeNum] != nil {
|
||||
// we have seen this inode before, we will hard link this file to it
|
||||
guard let parentPath = itemPtr.pointee.path else {
|
||||
continue
|
||||
}
|
||||
let path = parentPath.join(itemName)
|
||||
self.hardlinks[path] = itemInodeNum
|
||||
continue
|
||||
}
|
||||
|
||||
let blocks = try self.getExtents(inode: itemInodeNum)
|
||||
let itemTreeNode = FileTree.FileTreeNode(
|
||||
inode: itemInodeNum,
|
||||
name: itemName,
|
||||
parent: itemPtr,
|
||||
children: []
|
||||
)
|
||||
if let blocks {
|
||||
if blocks.count > 1 {
|
||||
itemTreeNode.additionalBlocks = Array(blocks.dropFirst())
|
||||
}
|
||||
itemTreeNode.blocks = blocks.first
|
||||
}
|
||||
let itemTreeNodePtr = Ptr(itemTreeNode)
|
||||
root.children.append(itemTreeNodePtr)
|
||||
itemPtr.pointee = root
|
||||
let itemInode = try self.getInode(number: itemInodeNum)
|
||||
if itemInode.mode.isDir() {
|
||||
items.append((itemTreeNodePtr, itemInodeNum))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
try? self.handle.close()
|
||||
}
|
||||
|
||||
private func readGroupDescriptor(_ number: UInt32) throws -> GroupDescriptor {
|
||||
let bs = self.blockSize
|
||||
let offset = bs + UInt64(number) * UInt64(self.groupDescriptorSize)
|
||||
try self.handle.seek(toOffset: offset)
|
||||
guard let data = try? self.handle.read(upToCount: MemoryLayout<EXT4.GroupDescriptor>.size) else {
|
||||
throw EXT4.Error.couldNotReadGroup(number)
|
||||
}
|
||||
let gd = data.withUnsafeBytes { ptr in
|
||||
ptr.loadLittleEndian(as: EXT4.GroupDescriptor.self)
|
||||
}
|
||||
return gd
|
||||
}
|
||||
|
||||
private func readInode(_ number: UInt32) throws -> Inode {
|
||||
let inodeGroupNumber = ((number - 1) / self._superBlock.inodesPerGroup)
|
||||
let numberInGroup = UInt64((number - 1) % self._superBlock.inodesPerGroup)
|
||||
|
||||
let gd = try getGroupDescriptor(inodeGroupNumber)
|
||||
let inodeTableStart = UInt64(gd.inodeTableLow) * self.blockSize
|
||||
|
||||
let inodeOffset: UInt64 = inodeTableStart + numberInGroup * UInt64(_superBlock.inodeSize)
|
||||
try self.handle.seek(toOffset: inodeOffset)
|
||||
guard let inodeData = try self.handle.read(upToCount: MemoryLayout<EXT4.Inode>.size) else {
|
||||
throw EXT4.Error.couldNotReadInode(number)
|
||||
}
|
||||
let inode = inodeData.withUnsafeBytes { ptr in
|
||||
ptr.loadLittleEndian(as: EXT4.Inode.self)
|
||||
}
|
||||
return inode
|
||||
}
|
||||
|
||||
private func getDirTree(_ number: InodeNumber) throws -> [(String, InodeNumber)] {
|
||||
var children: [(String, InodeNumber)] = []
|
||||
let extents = try getExtents(inode: number) ?? []
|
||||
for (start, end) in extents {
|
||||
try self.seek(block: start)
|
||||
for i in 0..<(end - start) {
|
||||
guard let dirEntryBlock = try self.handle.read(upToCount: Int(self.blockSize)) else {
|
||||
throw EXT4.Error.couldNotReadBlock(start + i)
|
||||
}
|
||||
let childEntries = try getDirEntries(dirTree: dirEntryBlock)
|
||||
children.append(contentsOf: childEntries)
|
||||
}
|
||||
}
|
||||
return children.sorted { a, b in
|
||||
a.0 < b.0
|
||||
}
|
||||
}
|
||||
|
||||
private func getDirEntries(dirTree: Data) throws -> [(String, InodeNumber)] {
|
||||
var children: [(String, InodeNumber)] = []
|
||||
var offset = 0
|
||||
let entrySize = MemoryLayout<DirectoryEntry>.size
|
||||
while offset < dirTree.count {
|
||||
let dirEntry = dirTree.subdata(in: offset..<offset + entrySize).withUnsafeBytes {
|
||||
$0.loadLittleEndian(as: DirectoryEntry.self)
|
||||
}
|
||||
guard dirEntry.recordLength >= entrySize else {
|
||||
break
|
||||
}
|
||||
if dirEntry.inode == 0 {
|
||||
offset += Int(dirEntry.recordLength)
|
||||
continue
|
||||
}
|
||||
let nameData = dirTree.subdata(in: offset + 8..<offset + 8 + Int(dirEntry.nameLength))
|
||||
let name = String(data: nameData, encoding: .utf8) ?? ""
|
||||
children.append((name, dirEntry.inode))
|
||||
offset += Int(dirEntry.recordLength)
|
||||
}
|
||||
return children.sorted { a, b in
|
||||
a.0 < b.0
|
||||
}
|
||||
}
|
||||
|
||||
func getExtents(inode: InodeNumber) throws -> [(start: UInt32, end: UInt32)]? {
|
||||
let inode = try self.getInode(number: inode)
|
||||
let inodeBlock = Data(tupleToArray(inode.block))
|
||||
var offset = 0
|
||||
var extents: [(start: UInt32, end: UInt32)] = []
|
||||
|
||||
let extentHeaderSize = MemoryLayout<ExtentHeader>.size
|
||||
let extentIndexSize = MemoryLayout<ExtentIndex>.size
|
||||
let extentLeafSize = MemoryLayout<ExtentLeaf>.size
|
||||
// read extent header
|
||||
let header = inodeBlock.subdata(in: offset..<offset + extentHeaderSize).withUnsafeBytes {
|
||||
$0.loadLittleEndian(as: ExtentHeader.self)
|
||||
}
|
||||
guard header.magic == EXT4.ExtentHeaderMagic else {
|
||||
return []
|
||||
}
|
||||
offset += extentHeaderSize // Jump to entries
|
||||
switch header.depth {
|
||||
case 0:
|
||||
// When depth is 0 the extent header is followed by extent leaves
|
||||
for _ in 0..<header.entries {
|
||||
let leaf = inodeBlock.subdata(in: offset..<offset + extentLeafSize).withUnsafeBytes {
|
||||
$0.loadLittleEndian(as: ExtentLeaf.self)
|
||||
}
|
||||
extents.append((leaf.startLow, leaf.startLow + UInt32(leaf.length)))
|
||||
offset += extentLeafSize
|
||||
}
|
||||
case 1:
|
||||
// When depth is 1 the extent header is followed by extent indices which point to leaves
|
||||
for _ in 0..<header.entries {
|
||||
let indexNode = inodeBlock.subdata(in: offset..<offset + extentIndexSize).withUnsafeBytes {
|
||||
$0.loadLittleEndian(as: ExtentIndex.self)
|
||||
}
|
||||
try self.seek(block: indexNode.leafLow)
|
||||
guard let block = try self.handle.read(upToCount: Int(self.blockSize)) else {
|
||||
throw EXT4.Error.couldNotReadBlock(indexNode.leafLow)
|
||||
}
|
||||
var blockOffset = 0
|
||||
let leafHeader = block.subdata(in: blockOffset..<extentHeaderSize).withUnsafeBytes {
|
||||
$0.loadLittleEndian(as: ExtentHeader.self)
|
||||
}
|
||||
guard leafHeader.magic == EXT4.ExtentHeaderMagic else {
|
||||
throw Error.invalidExtents
|
||||
}
|
||||
blockOffset += extentHeaderSize
|
||||
for _ in 0..<leafHeader.entries {
|
||||
let leaf = block.subdata(in: blockOffset..<blockOffset + extentLeafSize).withUnsafeBytes {
|
||||
$0.loadLittleEndian(as: ExtentLeaf.self)
|
||||
}
|
||||
extents.append((leaf.startLow, leaf.startLow + UInt32(leaf.length)))
|
||||
blockOffset += extentLeafSize
|
||||
}
|
||||
offset += extentIndexSize
|
||||
}
|
||||
default:
|
||||
throw Error.deepExtentsUnimplemented
|
||||
}
|
||||
return extents
|
||||
}
|
||||
|
||||
// MARK: Internal functions
|
||||
func getInode(number: UInt32) throws -> Inode {
|
||||
if let inode = self.inodes[number] {
|
||||
return inode
|
||||
}
|
||||
|
||||
let inode = try readInode(number)
|
||||
self.inodes[number] = inode
|
||||
return inode
|
||||
}
|
||||
|
||||
func getGroupDescriptor(_ number: UInt32) throws -> GroupDescriptor {
|
||||
if let gd = self.groupDescriptors[number] {
|
||||
return gd
|
||||
}
|
||||
let gd = try readGroupDescriptor(number)
|
||||
self.groupDescriptors[number] = gd
|
||||
return gd
|
||||
}
|
||||
|
||||
func children(of number: EXT4.InodeNumber) throws -> [(String, InodeNumber)] {
|
||||
try getDirTree(number)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
|
||||
import Foundation
|
||||
|
||||
extension EXT4 {
|
||||
public struct SuperBlock {
|
||||
public var inodesCount: UInt32 = 0
|
||||
public var blocksCountLow: UInt32 = 0
|
||||
public var reservedBlocksCountLow: UInt32 = 0
|
||||
public var freeBlocksCountLow: UInt32 = 0
|
||||
public var freeInodesCount: UInt32 = 0
|
||||
public var firstDataBlock: UInt32 = 0
|
||||
public var logBlockSize: UInt32 = 0
|
||||
public var logClusterSize: UInt32 = 0
|
||||
public var blockSize: UInt32 { 1024 << logBlockSize }
|
||||
public var blocksPerGroup: UInt32 = 0
|
||||
public var clustersPerGroup: UInt32 = 0
|
||||
public var inodesPerGroup: UInt32 = 0
|
||||
public var mtime: UInt32 = 0
|
||||
public var wtime: UInt32 = 0
|
||||
public var mountCount: UInt16 = 0
|
||||
public var maxMountCount: UInt16 = 0
|
||||
public var magic: UInt16 = 0
|
||||
public var state: UInt16 = 0
|
||||
public var errors: UInt16 = 0
|
||||
public var minorRevisionLevel: UInt16 = 0
|
||||
public var lastCheck: UInt32 = 0
|
||||
public var checkInterval: UInt32 = 0
|
||||
public var creatorOS: UInt32 = 0
|
||||
public var revisionLevel: UInt32 = 0
|
||||
public var defaultReservedUid: UInt16 = 0
|
||||
public var defaultReservedGid: UInt16 = 0
|
||||
public var firstInode: UInt32 = 0
|
||||
public var inodeSize: UInt16 = 0
|
||||
public var blockGroupNr: UInt16 = 0
|
||||
public var featureCompat: UInt32 = 0
|
||||
public var featureIncompat: UInt32 = 0
|
||||
public var featureRoCompat: UInt32 = 0
|
||||
public var uuid:
|
||||
(
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var volumeName:
|
||||
(
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var lastMounted:
|
||||
(
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var algorithmUsageBitmap: UInt32 = 0
|
||||
public var preallocBlocks: UInt8 = 0
|
||||
public var preallocDirBlocks: UInt8 = 0
|
||||
public var reservedGdtBlocks: UInt16 = 0
|
||||
public var journalUUID:
|
||||
(
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var journalInum: UInt32 = 0
|
||||
public var journalDev: UInt32 = 0
|
||||
public var lastOrphan: UInt32 = 0
|
||||
public var hashSeed: (UInt32, UInt32, UInt32, UInt32) = (0, 0, 0, 0)
|
||||
public var defHashVersion: UInt8 = 0
|
||||
public var journalBackupType: UInt8 = 0
|
||||
public var descSize: UInt16 = UInt16(MemoryLayout<GroupDescriptor>.size)
|
||||
public var defaultMountOpts: UInt32 = 0
|
||||
public var firstMetaBg: UInt32 = 0
|
||||
public var mkfsTime: UInt32 = 0
|
||||
public var journalBlocks:
|
||||
(
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0
|
||||
)
|
||||
public var blocksCountHigh: UInt32 = 0
|
||||
public var reservedBlocksCountHigh: UInt32 = 0
|
||||
public var freeBlocksCountHigh: UInt32 = 0
|
||||
public var minExtraIsize: UInt16 = 0
|
||||
public var wantExtraIsize: UInt16 = 0
|
||||
public var flags: UInt32 = 0
|
||||
public var raidStride: UInt16 = 0
|
||||
public var mmpInterval: UInt16 = 0
|
||||
public var mmpBlock: UInt64 = 0
|
||||
public var raidStripeWidth: UInt32 = 0
|
||||
public var logGroupsPerFlex: UInt8 = 0
|
||||
public var checksumType: UInt8 = 0
|
||||
public var reservedPad: UInt16 = 0
|
||||
public var kbytesWritten: UInt64 = 0
|
||||
public var snapshotInum: UInt32 = 0
|
||||
public var snapshotID: UInt32 = 0
|
||||
public var snapshotRBlocksCount: UInt64 = 0
|
||||
public var snapshotList: UInt32 = 0
|
||||
public var errorCount: UInt32 = 0
|
||||
public var firstErrorTime: UInt32 = 0
|
||||
public var firstErrorInode: UInt32 = 0
|
||||
public var firstErrorBlock: UInt64 = 0
|
||||
public var firstErrorFunc:
|
||||
(
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var firstErrorLine: UInt32 = 0
|
||||
public var lastErrorTime: UInt32 = 0
|
||||
public var lastErrorInode: UInt32 = 0
|
||||
public var lastErrorLine: UInt32 = 0
|
||||
public var lastErrorBlock: UInt64 = 0
|
||||
public var lastErrorFunc:
|
||||
(
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var mountOpts:
|
||||
(
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var userQuotaInum: UInt32 = 0
|
||||
public var groupQuotaInum: UInt32 = 0
|
||||
public var overheadBlocks: UInt32 = 0
|
||||
public var backupBgs: (UInt32, UInt32) = (0, 0)
|
||||
public var encryptAlgos: (UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0)
|
||||
public var encryptPwSalt:
|
||||
(
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var lpfInode: UInt32 = 0
|
||||
public var projectQuotaInum: UInt32 = 0
|
||||
public var checksumSeed: UInt32 = 0
|
||||
public var wtimeHigh: UInt8 = 0
|
||||
public var mtimeHigh: UInt8 = 0
|
||||
public var mkfsTimeHigh: UInt8 = 0
|
||||
public var lastcheckHigh: UInt8 = 0
|
||||
public var firstErrorTimeHigh: UInt8 = 0
|
||||
public var lastErrorTimeHigh: UInt8 = 0
|
||||
public var pad: (UInt8, UInt8) = (0, 0)
|
||||
public var reserved:
|
||||
(
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32,
|
||||
UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var checksum: UInt32 = 0
|
||||
}
|
||||
|
||||
static let JournalMagic: UInt32 = 0xC03B_3998
|
||||
static let JournalInode: InodeNumber = 8
|
||||
static let MinJournalBlocks: UInt32 = 1024 // JBD2_MIN_JOURNAL_BLOCKS
|
||||
|
||||
struct DefaultMountOpts {
|
||||
static let journalData: UInt32 = 0x0020 // data=journal
|
||||
static let journalOrdered: UInt32 = 0x0040 // data=ordered
|
||||
static let journalWriteback: UInt32 = 0x0060 // data=writeback
|
||||
}
|
||||
|
||||
struct CompatFeature {
|
||||
let rawValue: UInt32
|
||||
|
||||
static let dirPrealloc = CompatFeature(rawValue: 0x1)
|
||||
static let imagicInodes = CompatFeature(rawValue: 0x2)
|
||||
static let hasJournal = CompatFeature(rawValue: 0x4)
|
||||
static let extAttr = CompatFeature(rawValue: 0x8)
|
||||
static let resizeInode = CompatFeature(rawValue: 0x10)
|
||||
static let dirIndex = CompatFeature(rawValue: 0x20)
|
||||
static let lazyBg = CompatFeature(rawValue: 0x40)
|
||||
static let excludeInode = CompatFeature(rawValue: 0x80)
|
||||
static let excludeBitmap = CompatFeature(rawValue: 0x100)
|
||||
static let sparseSuper2 = CompatFeature(rawValue: 0x200)
|
||||
}
|
||||
|
||||
struct IncompatFeature {
|
||||
let rawValue: UInt32
|
||||
|
||||
static let compression = IncompatFeature(rawValue: 0x1)
|
||||
static let filetype = IncompatFeature(rawValue: 0x2)
|
||||
static let recover = IncompatFeature(rawValue: 0x4)
|
||||
static let journalDev = IncompatFeature(rawValue: 0x8)
|
||||
static let metaBg = IncompatFeature(rawValue: 0x10)
|
||||
static let extents = IncompatFeature(rawValue: 0x40)
|
||||
static let bit64 = IncompatFeature(rawValue: 0x80)
|
||||
static let mmp = IncompatFeature(rawValue: 0x100)
|
||||
static let flexBg = IncompatFeature(rawValue: 0x200)
|
||||
static let eaInode = IncompatFeature(rawValue: 0x400)
|
||||
static let dirdata = IncompatFeature(rawValue: 0x1000)
|
||||
static let csumSeed = IncompatFeature(rawValue: 0x2000)
|
||||
static let largedir = IncompatFeature(rawValue: 0x4000)
|
||||
static let inlineData = IncompatFeature(rawValue: 0x8000)
|
||||
static let encrypt = IncompatFeature(rawValue: 0x10000)
|
||||
}
|
||||
|
||||
struct RoCompatFeature {
|
||||
let rawValue: UInt32
|
||||
|
||||
static let sparseSuper = RoCompatFeature(rawValue: 0x1)
|
||||
static let largeFile = RoCompatFeature(rawValue: 0x2)
|
||||
static let btreeDir = RoCompatFeature(rawValue: 0x4)
|
||||
static let hugeFile = RoCompatFeature(rawValue: 0x8)
|
||||
static let gdtCsum = RoCompatFeature(rawValue: 0x10)
|
||||
static let dirNlink = RoCompatFeature(rawValue: 0x20)
|
||||
static let extraIsize = RoCompatFeature(rawValue: 0x40)
|
||||
static let hasSnapshot = RoCompatFeature(rawValue: 0x80)
|
||||
static let quota = RoCompatFeature(rawValue: 0x100)
|
||||
static let bigalloc = RoCompatFeature(rawValue: 0x200)
|
||||
static let metadataCsum = RoCompatFeature(rawValue: 0x400)
|
||||
static let replica = RoCompatFeature(rawValue: 0x800)
|
||||
static let readonly = RoCompatFeature(rawValue: 0x1000)
|
||||
static let project = RoCompatFeature(rawValue: 0x2000)
|
||||
}
|
||||
|
||||
struct BlockGroupFlag {
|
||||
let rawValue: UInt16
|
||||
|
||||
static let inodeUninit = BlockGroupFlag(rawValue: 0x1)
|
||||
static let blockUninit = BlockGroupFlag(rawValue: 0x2)
|
||||
static let inodeZeroed = BlockGroupFlag(rawValue: 0x4)
|
||||
}
|
||||
|
||||
struct GroupDescriptor {
|
||||
let blockBitmapLow: UInt32
|
||||
let inodeBitmapLow: UInt32
|
||||
let inodeTableLow: UInt32
|
||||
let freeBlocksCountLow: UInt16
|
||||
let freeInodesCountLow: UInt16
|
||||
let usedDirsCountLow: UInt16
|
||||
let flags: UInt16
|
||||
let excludeBitmapLow: UInt32
|
||||
let blockBitmapCsumLow: UInt16
|
||||
let inodeBitmapCsumLow: UInt16
|
||||
let itableUnusedLow: UInt16
|
||||
let checksum: UInt16
|
||||
}
|
||||
|
||||
struct GroupDescriptor64 {
|
||||
let groupDescriptor: GroupDescriptor
|
||||
let blockBitmapHigh: UInt32
|
||||
let inodeBitmapHigh: UInt32
|
||||
let inodeTableHigh: UInt32
|
||||
let freeBlocksCountHigh: UInt16
|
||||
let freeInodesCountHigh: UInt16
|
||||
let usedDirsCountHigh: UInt16
|
||||
let itableUnusedHigh: UInt16
|
||||
let excludeBitmapHigh: UInt32
|
||||
let blockBitmapCsumHigh: UInt16
|
||||
let inodeBitmapCsumHigh: UInt16
|
||||
let reserved: UInt32
|
||||
}
|
||||
|
||||
public struct FileModeFlag: Sendable {
|
||||
let rawValue: UInt16
|
||||
|
||||
public static let S_IXOTH = FileModeFlag(rawValue: 0x1)
|
||||
public static let S_IWOTH = FileModeFlag(rawValue: 0x2)
|
||||
public static let S_IROTH = FileModeFlag(rawValue: 0x4)
|
||||
public static let S_IXGRP = FileModeFlag(rawValue: 0x8)
|
||||
public static let S_IWGRP = FileModeFlag(rawValue: 0x10)
|
||||
public static let S_IRGRP = FileModeFlag(rawValue: 0x20)
|
||||
public static let S_IXUSR = FileModeFlag(rawValue: 0x40)
|
||||
public static let S_IWUSR = FileModeFlag(rawValue: 0x80)
|
||||
public static let S_IRUSR = FileModeFlag(rawValue: 0x100)
|
||||
public static let S_ISVTX = FileModeFlag(rawValue: 0x200)
|
||||
public static let S_ISGID = FileModeFlag(rawValue: 0x400)
|
||||
public static let S_ISUID = FileModeFlag(rawValue: 0x800)
|
||||
public static let S_IFIFO = FileModeFlag(rawValue: 0x1000)
|
||||
public static let S_IFCHR = FileModeFlag(rawValue: 0x2000)
|
||||
public static let S_IFDIR = FileModeFlag(rawValue: 0x4000)
|
||||
public static let S_IFBLK = FileModeFlag(rawValue: 0x6000)
|
||||
public static let S_IFREG = FileModeFlag(rawValue: 0x8000)
|
||||
public static let S_IFLNK = FileModeFlag(rawValue: 0xA000)
|
||||
public static let S_IFSOCK = FileModeFlag(rawValue: 0xC000)
|
||||
|
||||
public static let TypeMask = FileModeFlag(rawValue: 0xF000)
|
||||
}
|
||||
|
||||
public typealias InodeNumber = UInt32
|
||||
|
||||
public struct Inode {
|
||||
public var mode: UInt16 = 0
|
||||
public var uid: UInt16 = 0
|
||||
public var sizeLow: UInt32 = 0
|
||||
public var atime: UInt32 = 0
|
||||
public var ctime: UInt32 = 0
|
||||
public var mtime: UInt32 = 0
|
||||
public var dtime: UInt32 = 0
|
||||
public var gid: UInt16 = 0
|
||||
public var linksCount: UInt16 = 0
|
||||
public var blocksLow: UInt32 = 0
|
||||
public var flags: UInt32 = 0
|
||||
public var version: UInt32 = 0
|
||||
public var block:
|
||||
(
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
)
|
||||
public var generation: UInt32 = 0
|
||||
public var xattrBlockLow: UInt32 = 0
|
||||
public var sizeHigh: UInt32 = 0
|
||||
public var obsoleteFragmentAddr: UInt32 = 0
|
||||
public var blocksHigh: UInt16 = 0
|
||||
public var xattrBlockHigh: UInt16 = 0
|
||||
public var uidHigh: UInt16 = 0
|
||||
public var gidHigh: UInt16 = 0
|
||||
public var checksumLow: UInt16 = 0
|
||||
public var reserved: UInt16 = 0
|
||||
public var extraIsize: UInt16 = 0
|
||||
public var checksumHigh: UInt16 = 0
|
||||
public var ctimeExtra: UInt32 = 0
|
||||
public var mtimeExtra: UInt32 = 0
|
||||
public var atimeExtra: UInt32 = 0
|
||||
public var crtime: UInt32 = 0
|
||||
public var crtimeExtra: UInt32 = 0
|
||||
public var versionHigh: UInt32 = 0
|
||||
public var projid: UInt32 = 0 // Size until this point is 160 bytes
|
||||
public var inlineXattrs:
|
||||
( // 96 bytes for extended attributes
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||||
) = (
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0
|
||||
)
|
||||
|
||||
public static func Mode(_ mode: FileModeFlag, _ perm: UInt16) -> UInt16 {
|
||||
mode.rawValue | perm
|
||||
}
|
||||
}
|
||||
|
||||
struct InodeFlag {
|
||||
let rawValue: UInt32
|
||||
|
||||
static let secRm = InodeFlag(rawValue: 0x1)
|
||||
static let unRm = InodeFlag(rawValue: 0x2)
|
||||
static let compressed = InodeFlag(rawValue: 0x4)
|
||||
static let sync = InodeFlag(rawValue: 0x8)
|
||||
static let immutable = InodeFlag(rawValue: 0x10)
|
||||
static let append = InodeFlag(rawValue: 0x20)
|
||||
static let noDump = InodeFlag(rawValue: 0x40)
|
||||
static let noAtime = InodeFlag(rawValue: 0x80)
|
||||
static let dirtyCompressed = InodeFlag(rawValue: 0x100)
|
||||
static let compressedClusters = InodeFlag(rawValue: 0x200)
|
||||
static let noCompress = InodeFlag(rawValue: 0x400)
|
||||
static let encrypted = InodeFlag(rawValue: 0x800)
|
||||
static let hashedIndex = InodeFlag(rawValue: 0x1000)
|
||||
static let magic = InodeFlag(rawValue: 0x2000)
|
||||
static let journalData = InodeFlag(rawValue: 0x4000)
|
||||
static let noTail = InodeFlag(rawValue: 0x8000)
|
||||
static let dirSync = InodeFlag(rawValue: 0x10000)
|
||||
static let topDir = InodeFlag(rawValue: 0x20000)
|
||||
static let hugeFile = InodeFlag(rawValue: 0x40000)
|
||||
static let extents = InodeFlag(rawValue: 0x80000)
|
||||
static let eaInode = InodeFlag(rawValue: 0x200000)
|
||||
static let eofBlocks = InodeFlag(rawValue: 0x400000)
|
||||
static let snapfile = InodeFlag(rawValue: 0x0100_0000)
|
||||
static let snapfileDeleted = InodeFlag(rawValue: 0x0400_0000)
|
||||
static let snapfileShrunk = InodeFlag(rawValue: 0x0800_0000)
|
||||
static let inlineData = InodeFlag(rawValue: 0x1000_0000)
|
||||
static let projectIDInherit = InodeFlag(rawValue: 0x2000_0000)
|
||||
static let reserved = InodeFlag(rawValue: 0x8000_0000)
|
||||
}
|
||||
|
||||
struct ExtentHeader {
|
||||
let magic: UInt16
|
||||
let entries: UInt16
|
||||
let max: UInt16
|
||||
let depth: UInt16
|
||||
let generation: UInt32
|
||||
}
|
||||
|
||||
struct ExtentIndex {
|
||||
let block: UInt32
|
||||
let leafLow: UInt32
|
||||
let leafHigh: UInt16
|
||||
let unused: UInt16
|
||||
}
|
||||
|
||||
struct ExtentLeaf {
|
||||
let block: UInt32
|
||||
let length: UInt16
|
||||
let startHigh: UInt16
|
||||
let startLow: UInt32
|
||||
}
|
||||
|
||||
struct ExtentTail {
|
||||
let checksum: UInt32
|
||||
}
|
||||
|
||||
struct ExtentIndexNode {
|
||||
var header: ExtentHeader
|
||||
var indices: [ExtentIndex]
|
||||
}
|
||||
|
||||
struct ExtentLeafNode {
|
||||
var header: ExtentHeader
|
||||
var leaves: [ExtentLeaf]
|
||||
}
|
||||
|
||||
struct DirectoryEntry {
|
||||
let inode: InodeNumber
|
||||
let recordLength: UInt16
|
||||
let nameLength: UInt8
|
||||
let fileType: UInt8
|
||||
// let name: [UInt8]
|
||||
}
|
||||
|
||||
enum FileType: UInt8 {
|
||||
case unknown = 0x0
|
||||
case regular = 0x1
|
||||
case directory = 0x2
|
||||
case character = 0x3
|
||||
case block = 0x4
|
||||
case fifo = 0x5
|
||||
case socket = 0x6
|
||||
case symbolicLink = 0x7
|
||||
}
|
||||
|
||||
struct DirectoryEntryTail {
|
||||
let reservedZero1: UInt32
|
||||
let recordLength: UInt16
|
||||
let reservedZero2: UInt8
|
||||
let fileType: UInt8
|
||||
let checksum: UInt32
|
||||
}
|
||||
|
||||
struct DirectoryTreeRoot {
|
||||
let dot: DirectoryEntry
|
||||
let dotName: [UInt8]
|
||||
let dotDot: DirectoryEntry
|
||||
let dotDotName: [UInt8]
|
||||
let reservedZero: UInt32
|
||||
let hashVersion: UInt8
|
||||
let infoLength: UInt8
|
||||
let indirectLevels: UInt8
|
||||
let unusedFlags: UInt8
|
||||
let limit: UInt16
|
||||
let count: UInt16
|
||||
let block: UInt32
|
||||
// let entries: [DirectoryTreeEntry]
|
||||
}
|
||||
|
||||
struct DirectoryTreeNode {
|
||||
let fakeInode: UInt32
|
||||
let fakeRecordLength: UInt16
|
||||
let nameLength: UInt8
|
||||
let fileType: UInt8
|
||||
let limit: UInt16
|
||||
let count: UInt16
|
||||
let block: UInt32
|
||||
// let entries: [DirectoryTreeEntry]
|
||||
}
|
||||
|
||||
struct DirectoryTreeEntry {
|
||||
let hash: UInt32
|
||||
let block: UInt32
|
||||
}
|
||||
|
||||
struct DirectoryTreeTail {
|
||||
let reserved: UInt32
|
||||
let checksum: UInt32
|
||||
}
|
||||
|
||||
struct XAttrEntry {
|
||||
let nameLength: UInt8
|
||||
let nameIndex: UInt8
|
||||
let valueOffset: UInt16
|
||||
let valueInum: UInt32
|
||||
let valueSize: UInt32
|
||||
let hash: UInt32
|
||||
}
|
||||
|
||||
struct XAttrHeader {
|
||||
let magic: UInt32
|
||||
let referenceCount: UInt32
|
||||
let blocks: UInt32
|
||||
let hash: UInt32
|
||||
let checksum: UInt32
|
||||
let reserved: [UInt32]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension EXT4.Inode {
|
||||
public static func Root() -> EXT4.Inode {
|
||||
var inode = Self() // inode
|
||||
inode.mode = Self.Mode(.S_IFDIR, 0o755)
|
||||
inode.linksCount = 2
|
||||
inode.uid = 0
|
||||
inode.gid = 0
|
||||
// time
|
||||
let now = Date().fs()
|
||||
let now_lo: UInt32 = now.lo
|
||||
let now_hi: UInt32 = now.hi
|
||||
inode.atime = now_lo
|
||||
inode.atimeExtra = now_hi
|
||||
inode.ctime = now_lo
|
||||
inode.ctimeExtra = now_hi
|
||||
inode.mtime = now_lo
|
||||
inode.mtimeExtra = now_hi
|
||||
inode.crtime = now_lo
|
||||
inode.crtimeExtra = now_hi
|
||||
inode.flags = EXT4.InodeFlag.hugeFile.rawValue
|
||||
inode.extraIsize = UInt16(EXT4.ExtraIsize)
|
||||
return inode
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 Foundation
|
||||
|
||||
/*
|
||||
* Note: Both the entries and values for the attributes need to occupy a size that is a multiple of 4,
|
||||
* meaning, in cases where the attribute name or value is not a multiple of 4, it is padded with 0
|
||||
* until it reaches that size.
|
||||
*/
|
||||
|
||||
extension EXT4 {
|
||||
public struct ExtendedAttribute {
|
||||
public static let prefixMap: [Int: String] = [
|
||||
1: "user.",
|
||||
2: "system.posix_acl_access",
|
||||
3: "system.posix_acl_default",
|
||||
4: "trusted.",
|
||||
6: "security.",
|
||||
7: "system.",
|
||||
8: "system.richacl",
|
||||
]
|
||||
|
||||
let name: String
|
||||
let index: UInt8
|
||||
let value: [UInt8]
|
||||
|
||||
var sizeValue: UInt32 {
|
||||
UInt32((value.count + 3) & ~3)
|
||||
}
|
||||
|
||||
var sizeEntry: UInt32 {
|
||||
UInt32((name.count + 3) & ~3 + 16) // 16 bytes are needed to store other metadata for the xattr entry
|
||||
}
|
||||
|
||||
var size: UInt32 {
|
||||
sizeEntry + sizeValue
|
||||
}
|
||||
|
||||
var fullName: String {
|
||||
Self.decompressName(id: Int(index), suffix: name)
|
||||
}
|
||||
|
||||
var hash: UInt32 {
|
||||
var hash: UInt32 = 0
|
||||
for char in name {
|
||||
hash = (hash << 5) ^ (hash >> 27) ^ UInt32(char.asciiValue!)
|
||||
}
|
||||
var i = 0
|
||||
while i + 3 < value.count {
|
||||
let s = value[i..<i + 4]
|
||||
let v = s.withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
|
||||
hash = (hash << 16) ^ (hash >> 16) ^ v
|
||||
i += 4
|
||||
}
|
||||
if value.count % 4 != 0 {
|
||||
let last = value.count & ~3
|
||||
var buff: [UInt8] = [0, 0, 0, 0]
|
||||
for (i, byte) in value[last...].enumerated() {
|
||||
buff[i] = byte
|
||||
}
|
||||
let v = buff.withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
|
||||
hash = (hash << 16) ^ (hash >> 16) ^ v
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
init(name: String, value: [UInt8]) {
|
||||
let compressed = Self.compressName(name)
|
||||
self.name = compressed.str
|
||||
self.index = compressed.id
|
||||
self.value = value
|
||||
}
|
||||
|
||||
init(idx: UInt8, compressedName name: String, value: [UInt8]) {
|
||||
self.name = name
|
||||
self.index = idx
|
||||
self.value = value
|
||||
}
|
||||
|
||||
// MARK: Class methods
|
||||
public static func compressName(_ name: String) -> (id: UInt8, str: String) {
|
||||
for (id, prefix) in prefixMap.sorted(by: { $1.1.count < $0.1.count }) where name.hasPrefix(prefix) {
|
||||
return (UInt8(id), String(name.dropFirst(prefix.count)))
|
||||
}
|
||||
return (0, name)
|
||||
}
|
||||
|
||||
public static func decompressName(id: Int, suffix: String) -> String {
|
||||
guard let prefix = prefixMap[id] else {
|
||||
return suffix
|
||||
}
|
||||
return "\(prefix)\(suffix)"
|
||||
}
|
||||
}
|
||||
|
||||
public struct FileXattrsState {
|
||||
private let inodeCapacity: UInt32
|
||||
private let blockCapacity: UInt32
|
||||
private let inode: UInt32 // the inode number for which we are tracking these xattrs
|
||||
|
||||
var inlineAttributes: [ExtendedAttribute] = []
|
||||
var blockAttributes: [ExtendedAttribute] = []
|
||||
private var usedSizeInline: UInt32 = 0
|
||||
private var usedSizeBlock: UInt32 = 0
|
||||
|
||||
private var inodeFreeBytes: UInt32 {
|
||||
self.inodeCapacity - EXT4.XattrInodeHeaderSize - usedSizeInline - 4 // need to have 4 null bytes b/w xattr entries and values
|
||||
}
|
||||
|
||||
private var blockFreeBytes: UInt32 {
|
||||
self.blockCapacity - EXT4.XattrBlockHeaderSize - usedSizeBlock - 4
|
||||
}
|
||||
|
||||
init(inode: UInt32, inodeXattrCapacity: UInt32, blockCapacity: UInt32) {
|
||||
self.inode = inode
|
||||
self.inodeCapacity = inodeXattrCapacity
|
||||
self.blockCapacity = blockCapacity
|
||||
}
|
||||
|
||||
public mutating func add(_ attribute: ExtendedAttribute) throws {
|
||||
let size = attribute.size
|
||||
if size <= inodeFreeBytes {
|
||||
usedSizeInline += size
|
||||
inlineAttributes.append(attribute)
|
||||
return
|
||||
}
|
||||
if size <= blockFreeBytes {
|
||||
usedSizeBlock += size
|
||||
blockAttributes.append(attribute)
|
||||
return
|
||||
}
|
||||
throw Error.insufficientSpace(Int(self.inode))
|
||||
}
|
||||
|
||||
public func writeInlineAttributes(buffer: inout [UInt8]) throws {
|
||||
var idx = 0
|
||||
withUnsafeLittleEndianBytes(
|
||||
of: EXT4.XAttrHeaderMagic,
|
||||
body: { bytes in
|
||||
for byte in bytes {
|
||||
buffer[idx] = byte
|
||||
idx += 1
|
||||
}
|
||||
})
|
||||
try Self.write(buffer: &buffer, attrs: self.inlineAttributes, start: UInt16(idx), delta: 0, inline: true)
|
||||
}
|
||||
|
||||
public func writeBlockAttributes(buffer: inout [UInt8]) throws {
|
||||
var idx = 0
|
||||
for val in [EXT4.XAttrHeaderMagic, 1, 1] {
|
||||
withUnsafeLittleEndianBytes(
|
||||
of: UInt32(val),
|
||||
body: { bytes in
|
||||
for byte in bytes {
|
||||
buffer[idx] = byte
|
||||
idx += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
while idx != 32 {
|
||||
buffer[idx] = 0
|
||||
idx += 1
|
||||
}
|
||||
var attributes = self.blockAttributes
|
||||
attributes.sort(by: {
|
||||
if $0.index != $1.index {
|
||||
return $0.index < $1.index
|
||||
}
|
||||
if $0.name.count != $1.name.count {
|
||||
return $0.name.count < $1.name.count
|
||||
}
|
||||
return $0.name < $1.name
|
||||
})
|
||||
try Self.write(buffer: &buffer, attrs: attributes, start: UInt16(idx), delta: UInt16(idx), inline: false)
|
||||
}
|
||||
|
||||
/// Writes the specified list of extended attribute entries and their values to the provided
|
||||
/// This method does not fill in any headers (Inode inline / block level) that may be required to parse these attributes
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - buffer: An array of [UInt8] where the data will be written into
|
||||
/// - attrs: The list of ExtendedAttributes to write
|
||||
/// - start: the index from where data should be put into the buffer - useful when if you dont want this method to be overwriting existing data
|
||||
/// - delta: index from where the begin the offset calculations
|
||||
/// - inline: if the byte buffer being written into is an inline data block for an inode: Determines the hash calculation
|
||||
private static func write(
|
||||
buffer: inout [UInt8], attrs: [ExtendedAttribute], start: UInt16, delta: UInt16, inline: Bool
|
||||
) throws {
|
||||
var offset: UInt16 = UInt16(buffer.count) + delta - start
|
||||
var front = Int(start)
|
||||
var end = buffer.count
|
||||
|
||||
for attribute in attrs {
|
||||
guard end - front >= 4 else {
|
||||
throw Error.malformedXattrBuffer
|
||||
}
|
||||
|
||||
var out: [UInt8] = []
|
||||
let v = attribute.sizeValue
|
||||
offset -= UInt16(v)
|
||||
out.append(UInt8(attribute.name.count))
|
||||
out.append(attribute.index)
|
||||
withUnsafeLittleEndianBytes(
|
||||
of: UInt16(offset),
|
||||
body: { bytes in
|
||||
out.append(contentsOf: bytes)
|
||||
})
|
||||
out.append(contentsOf: [0, 0, 0, 0]) // these next four bytes indicate that the attr values are in the same block
|
||||
withUnsafeLittleEndianBytes(
|
||||
of: UInt32(attribute.value.count),
|
||||
body: { bytes in
|
||||
out.append(contentsOf: bytes)
|
||||
})
|
||||
if !inline {
|
||||
withUnsafeLittleEndianBytes(
|
||||
of: UInt32(attribute.hash),
|
||||
body: { bytes in
|
||||
out.append(contentsOf: bytes)
|
||||
})
|
||||
} else {
|
||||
out.append(contentsOf: [0, 0, 0, 0])
|
||||
}
|
||||
guard let name = attribute.name.data(using: .ascii) else {
|
||||
throw Error.convertAsciiString(attribute.name)
|
||||
}
|
||||
out.append(contentsOf: [UInt8](name))
|
||||
while out.count < Int(attribute.sizeEntry) { // ensure that xattr entry size is a multiple of 4
|
||||
out.append(0)
|
||||
}
|
||||
for (i, byte) in out.enumerated() {
|
||||
buffer[front + i] = byte
|
||||
}
|
||||
front += out.count
|
||||
|
||||
end -= Int(attribute.sizeValue)
|
||||
for (i, byte) in attribute.value.enumerated() {
|
||||
buffer[end + i] = byte
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func read(buffer: [UInt8], start: Int, offset: Int) throws -> [ExtendedAttribute] {
|
||||
var i = start
|
||||
var attribs: [ExtendedAttribute] = []
|
||||
// 16 is the size of 1 XAttrEntry
|
||||
while i + 16 <= buffer.count {
|
||||
let attributeStart = i
|
||||
let rawXattrEntry = Array(buffer[i..<i + 16])
|
||||
let xattrEntry = try EXT4.XAttrEntry(using: rawXattrEntry)
|
||||
i += 16
|
||||
var endIndex = i + Int(xattrEntry.nameLength)
|
||||
guard endIndex <= buffer.count else {
|
||||
continue
|
||||
}
|
||||
let rawName = buffer[i..<endIndex]
|
||||
guard let name = String(bytes: rawName, encoding: .ascii) else {
|
||||
throw Error.nonAsciiXattrName
|
||||
}
|
||||
let valueStart = Int(xattrEntry.valueOffset) + offset
|
||||
let valueEnd = Int(xattrEntry.valueOffset) + Int(xattrEntry.valueSize) + offset
|
||||
guard valueEnd <= buffer.count else {
|
||||
break
|
||||
}
|
||||
let value = [UInt8](buffer[valueStart..<valueEnd])
|
||||
let xattr = ExtendedAttribute(idx: xattrEntry.nameIndex, compressedName: name, value: value)
|
||||
attribs.append(xattr)
|
||||
i = attributeStart + xattr.sizeEntry
|
||||
// The next 4 bytes being null indicate that there are no more attributes to read
|
||||
endIndex = i + 3
|
||||
guard endIndex < buffer.count else {
|
||||
continue
|
||||
}
|
||||
if Array(buffer[i...i + 3]) == [0, 0, 0, 0] {
|
||||
break
|
||||
}
|
||||
}
|
||||
return attribs
|
||||
}
|
||||
|
||||
public enum Error: CustomStringConvertible, Swift.Error {
|
||||
case insufficientSpace(_ inode: Int)
|
||||
case malformedXattrBuffer
|
||||
case convertAsciiString(_ s: String)
|
||||
case nonAsciiXattrName
|
||||
case missingXAttrHeader
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .insufficientSpace(let inode):
|
||||
return "cannot fit xattr for inode \(inode)"
|
||||
case .malformedXattrBuffer:
|
||||
return "malformed extended attribute buffer"
|
||||
case .convertAsciiString(let s):
|
||||
return "cannot convert string \(s) to a list of ASCII characters"
|
||||
case .nonAsciiXattrName:
|
||||
return "extended attribute name contains non-ASCII bytes"
|
||||
case .missingXAttrHeader:
|
||||
return "missing header for extended attribute entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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
|
||||
|
||||
/**
|
||||
```
|
||||
# EXT4 Filesystem Layout
|
||||
|
||||
The EXT4 filesystem divides the disk into an upfront metadata section followed by several logical groups known as block groups. The
|
||||
metadata section looks like this:
|
||||
|
||||
+--------------------------+
|
||||
| Boot Sector (1024) |
|
||||
+--------------------------+
|
||||
| Superblock (1024) |
|
||||
+--------------------------+
|
||||
| Empty (2048) |
|
||||
+--------------------------+
|
||||
| |
|
||||
| [Block Group Descriptors]|
|
||||
| |
|
||||
| - Free/used block bitmap |
|
||||
| - Free/used inode bitmap |
|
||||
| - Inode table pointer |
|
||||
| - Other metadata |
|
||||
| |
|
||||
+--------------------------+
|
||||
|
||||
## Block Groups
|
||||
|
||||
Each block group optionally stores a copy of the superblock and group descriptor table for disaster recovery.
|
||||
The rest of the block group comprises of data blocks. The size of each block group is dynamically decided
|
||||
while formatting, based on total amount of space available on the disk.
|
||||
|
||||
+--------------------------+
|
||||
| Block Group 0 |
|
||||
| +------------------+ |
|
||||
| | Super Block | |
|
||||
| +------------------+ |
|
||||
| | Group Desc. | |
|
||||
| +------------------+ |
|
||||
| | Data Blocks | |
|
||||
| | | |
|
||||
| +------------------+ |
|
||||
+--------------------------+
|
||||
| Block Group 1 |
|
||||
| +------------------+ |
|
||||
| | Super Block | |
|
||||
| +------------------+ |
|
||||
| | Group Desc. | |
|
||||
| +------------------+ |
|
||||
| | Data Blocks | |
|
||||
| | | |
|
||||
| +------------------+ |
|
||||
+--------------------------+
|
||||
| ... |
|
||||
+--------------------------+
|
||||
| Block Group N |
|
||||
| +------------------+ |
|
||||
| | Super Block | |
|
||||
| +------------------+ |
|
||||
| | Group Desc. | |
|
||||
| +------------------+ |
|
||||
| | Data Blocks | |
|
||||
| | | |
|
||||
| +------------------+ |
|
||||
+--------------------------+
|
||||
|
||||
The descriptor for each block group contain the following information:
|
||||
|
||||
- Block Bitmap
|
||||
- Inode Bitmap
|
||||
- Pointer to Inode Table
|
||||
- other metadata such as used block count, num. dirs etc.
|
||||
|
||||
### Block Bitmap
|
||||
|
||||
A sequence of bits, where each bit represents a block in the block group.
|
||||
|
||||
1: In use block
|
||||
0: Free block
|
||||
|
||||
+---------------+---------------+
|
||||
| Block |
|
||||
| Bitmap |
|
||||
+---------------+---------------+
|
||||
| 1 0 1 0 1 1 0 0 |
|
||||
+---------------+---------------+
|
||||
| | | | | | | |
|
||||
| | | | | | | |
|
||||
v v v v v v v v
|
||||
+---+---+---+---+---+---+---+---+
|
||||
| B | | B | | B | B | | |
|
||||
+---+---+---+---+---+---+---+---+
|
||||
|
||||
Whenever a file is created, free data blocks are identified by using this table.
|
||||
When it is deleted, the corresponding data blocks are marked as free.
|
||||
|
||||
### Inode Bitmap
|
||||
|
||||
A sequence of bits, where each bit represents an inode in the block group. Since
|
||||
inodes per group is a fixed number, this bitmap is made to be of sufficient length
|
||||
to accommodate that many inodes
|
||||
|
||||
1: In use inode
|
||||
0: Free inode
|
||||
|
||||
+---------------+---------------+
|
||||
| Inode |
|
||||
| Bitmap |
|
||||
+---------------+---------------+
|
||||
| 1 0 1 0 1 1 0 0 |
|
||||
+---------------+---------------+
|
||||
| | | | | | | |
|
||||
| | | | | | | |
|
||||
v v v v v v v v
|
||||
+---+---+---+---+---+---+---+---+
|
||||
| I | | I | | I | I | | |
|
||||
+---+---+---+---+---+---+---+---+
|
||||
|
||||
## Inode table
|
||||
|
||||
Inode table provides a mapping from Inode -> Data blocks. In this implementation, inode size is set to 256 bytes.
|
||||
Inode table uses extents to efficiently describe the mapping.
|
||||
|
||||
+-----------------------+
|
||||
| Inode Table |
|
||||
+-----------------------+
|
||||
| Inode | Metadata |
|
||||
+-------+---------------+
|
||||
| 1 | permissions |
|
||||
| | size |
|
||||
| | user ID |
|
||||
| | group ID |
|
||||
| | timestamps |
|
||||
| | block |
|
||||
| | blocks count |
|
||||
+-------+---------------+
|
||||
| 2 | ... |
|
||||
+-------+---------------+
|
||||
| ... | ... |
|
||||
+-------+---------------+
|
||||
|
||||
The length of `block` field in the inode table is 60 bytes. This field contains an extent tree
|
||||
that holds information about ranges of blocks used by the file. For smaller files, the entire extent
|
||||
tree can be stored within this field.
|
||||
|
||||
+-----------------------+
|
||||
| Inode |
|
||||
+-----------------------+
|
||||
| Metadata |
|
||||
+-----------------------+
|
||||
| Extent Tree |
|
||||
| +-------------------+ |
|
||||
| | Extent Leaf Node | |
|
||||
| +-------------------+ |
|
||||
| | - Start Block | |
|
||||
| | - Block Count | |
|
||||
| | - ... | |
|
||||
| +-------------------+ |
|
||||
+-----------------------+
|
||||
|
||||
For larger files which span across multiple non-contiguous blocks, extent tree's root points to extent
|
||||
blocks, which in-turn point to the blocks used by the file
|
||||
|
||||
+-----------------------+
|
||||
| Extent Tree |
|
||||
| +-------------------+ |
|
||||
| | Extent Root | |
|
||||
| +-------------------+ |
|
||||
| | - Pointers to | |
|
||||
| | Extent Blocks | |
|
||||
| +-------------------+ |
|
||||
+-----------------------+
|
||||
|
|
||||
v
|
||||
+-----------------------+
|
||||
| Extent Block |
|
||||
+-----------------------+
|
||||
| +-------------------+ |
|
||||
| | Extent Leaf Node | |
|
||||
| +-------------------+ |
|
||||
| | - Start Block | |
|
||||
| | - Block Count | |
|
||||
| | - ... | |
|
||||
| +-------------------+ |
|
||||
| +-------------------+ |
|
||||
| | Extent Leaf Node | |
|
||||
| +-------------------+ |
|
||||
| | - Start Block | |
|
||||
| | - Block Count | |
|
||||
| | - ... | |
|
||||
| +-------------------+ |
|
||||
+-----------------------+
|
||||
|
||||
## Directory entries
|
||||
|
||||
The data blocks for directory inodes point to a list of directory entries. Each entry
|
||||
consists of only a name and inode number. The name and inode number correspond to the
|
||||
name and inode number of the children of the directory
|
||||
|
||||
+-------------------------+
|
||||
| Directory Entry |
|
||||
+-------------------------+
|
||||
| inode | rec_len | name |
|
||||
+-------------------------+
|
||||
| 2 | 1 | "." |
|
||||
+-------------------------+
|
||||
| Directory Entry |
|
||||
+-------------------------+
|
||||
| inode | rec_len | name |
|
||||
+-------------------------+
|
||||
| 1 | 2 | ".." |
|
||||
+-------------------------+
|
||||
| Directory Entry |
|
||||
+-------------------------+
|
||||
| inode | rec_len | name |
|
||||
+-------------------------+
|
||||
| 11 | 10 | lost& |
|
||||
| | | found |
|
||||
+-------------------------+
|
||||
|
||||
More details can be found here https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout
|
||||
|
||||
```
|
||||
*/
|
||||
|
||||
/// A type for interacting with ext4 file systems.
|
||||
///
|
||||
/// The `Ext4` class provides functionality to read the superblock of an existing ext4 block device
|
||||
/// and format a new block device with the ext4 file system.
|
||||
///
|
||||
/// Usage:
|
||||
/// - To read the superblock of an existing ext4 block device, create an instance of `Ext4` with the
|
||||
/// path to the block device
|
||||
/// - To format a new block device with ext4, create an instance of `Ext4.Formatter` with the path to the block
|
||||
/// device and call the `close()` method.
|
||||
///
|
||||
/// Example 1: Read an existing block device
|
||||
/// ```swift
|
||||
/// let blockDevice = URL(filePath: "/dev/sdb")
|
||||
/// // succeeds if a valid ext4 fs is found at path
|
||||
/// let ext4 = try Ext4(blockDevice: blockDevice)
|
||||
/// print("Block size: \(ext4.blockSize)")
|
||||
/// print("Total size: \(ext4.size)")
|
||||
///
|
||||
/// // Reading the superblock
|
||||
/// let superblock = ext4.superblock
|
||||
/// print("Superblock information:")
|
||||
/// print("Total blocks: \(superblock.blocksCountLow)")
|
||||
/// ```
|
||||
///
|
||||
/// Example 2: Format a new block device (Refer [`EXT4.Formatter`](x-source-tag://EXT4.Formatter) for more info)
|
||||
/// ```swift
|
||||
/// let devicePath = URL(filePath: "/dev/sdc")
|
||||
/// let formatter = try EXT4.Formatter(devicePath, blockSize: 4096)
|
||||
/// try formatter.close()
|
||||
/// ```
|
||||
public enum EXT4 {
|
||||
public static let SuperBlockMagic: UInt16 = 0xef53
|
||||
|
||||
static let ExtentHeaderMagic: UInt16 = 0xf30a
|
||||
static let XAttrHeaderMagic: UInt32 = 0xea02_0000
|
||||
|
||||
static let DefectiveBlockInode: InodeNumber = 1
|
||||
static let RootInode: InodeNumber = 2
|
||||
static let FirstInode: InodeNumber = 11
|
||||
static let LostAndFoundInode: InodeNumber = 11
|
||||
|
||||
static let InodeActualSize: UInt32 = 160 // 160 bytes used by metadata
|
||||
static let InodeExtraSize: UInt32 = 96 // 96 bytes for inline xattrs
|
||||
static let InodeSize: UInt32 = UInt32(MemoryLayout<Inode>.size) // 256 bytes. This is the max size of an inode
|
||||
static let XattrInodeHeaderSize: UInt32 = 4
|
||||
static let XattrBlockHeaderSize: UInt32 = 32
|
||||
static let ExtraIsize: UInt16 = UInt16(InodeActualSize) - 128
|
||||
|
||||
static let MaxLinks: UInt32 = 65000
|
||||
static let MaxBlocksPerExtent: UInt32 = 0x8000
|
||||
static let MaxFileSize: UInt64 = 128.gib()
|
||||
static let SuperBlockOffset: UInt64 = 1024
|
||||
|
||||
public struct JournalConfig: Sendable {
|
||||
public var size: UInt64?
|
||||
public var defaultMode: JournalMode?
|
||||
|
||||
public enum JournalMode: Sendable {
|
||||
case writeback
|
||||
case ordered
|
||||
case journal
|
||||
}
|
||||
|
||||
public init(size: UInt64? = nil, defaultMode: JournalMode? = nil) {
|
||||
self.size = size
|
||||
self.defaultMode = defaultMode
|
||||
}
|
||||
|
||||
public static let `default` = JournalConfig()
|
||||
}
|
||||
}
|
||||
|
||||
extension EXT4 {
|
||||
// `EXT4` errors.
|
||||
public enum Error: Swift.Error, CustomStringConvertible, Sendable, Equatable {
|
||||
case notFound(_ path: String)
|
||||
case couldNotReadSuperBlock(_ path: String, _ offset: UInt64, _ size: Int)
|
||||
case invalidSuperBlock
|
||||
case deepExtentsUnimplemented
|
||||
case invalidExtents
|
||||
case invalidXattrEntry
|
||||
case couldNotReadBlock(_ block: UInt32)
|
||||
case invalidPathEncoding(_ path: String)
|
||||
case couldNotReadInode(_ inode: UInt32)
|
||||
case couldNotReadGroup(_ group: UInt32)
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .notFound(let path):
|
||||
return "file at path \(path) not found"
|
||||
case .couldNotReadSuperBlock(let path, let offset, let size):
|
||||
return "could not read \(size) bytes of superblock from \(path) at offset \(offset)"
|
||||
case .invalidSuperBlock:
|
||||
return "not a valid EXT4 superblock"
|
||||
case .deepExtentsUnimplemented:
|
||||
return "deep extents are not supported"
|
||||
case .invalidExtents:
|
||||
return "extents invalid or corrupted"
|
||||
case .invalidXattrEntry:
|
||||
return "invalid extended attribute entry"
|
||||
case .couldNotReadBlock(let block):
|
||||
return "could not read block \(block)"
|
||||
case .invalidPathEncoding(let path):
|
||||
return "path encoding for '\(path)' is invalid, must be ascii or utf8"
|
||||
case .couldNotReadInode(let inode):
|
||||
return "could not read inode \(inode)"
|
||||
case .couldNotReadGroup(let group):
|
||||
return "could not read group descriptor \(group)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ContainerizationArchive
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
|
||||
extension EXT4.EXT4Reader {
|
||||
public func export(archive: FilePath) throws {
|
||||
let config = ArchiveWriterConfiguration(
|
||||
format: .paxRestricted, filter: .none, options: [Options.xattrformat(.schily)])
|
||||
let writer = try ArchiveWriter(configuration: config)
|
||||
try writer.open(file: archive.url)
|
||||
var items = self.tree.root.pointee.children
|
||||
let hardlinkedInodes = Set(self.hardlinks.values)
|
||||
var hardlinkTargets: [EXT4.InodeNumber: FilePath] = [:]
|
||||
|
||||
while items.count > 0 {
|
||||
let itemPtr = items.removeFirst()
|
||||
let item = itemPtr.pointee
|
||||
let inode = try self.getInode(number: item.inode)
|
||||
let entry = WriteEntry()
|
||||
let mode = inode.mode
|
||||
let size: UInt64 = (UInt64(inode.sizeHigh) << 32) | UInt64(inode.sizeLow)
|
||||
entry.permissions = mode_t(mode)
|
||||
guard let path = item.path else {
|
||||
continue
|
||||
}
|
||||
if hardlinkedInodes.contains(item.inode) {
|
||||
hardlinkTargets[item.inode] = path
|
||||
}
|
||||
guard self.hardlinks[path] == nil else {
|
||||
continue
|
||||
}
|
||||
var attributes: [EXT4.ExtendedAttribute] = []
|
||||
let buffer: [UInt8] = EXT4.tupleToArray(inode.inlineXattrs)
|
||||
if !buffer.allZeros {
|
||||
try attributes.append(contentsOf: Self.readInlineExtendedAttributes(from: buffer))
|
||||
}
|
||||
if inode.xattrBlockLow != 0 {
|
||||
let block = inode.xattrBlockLow
|
||||
try self.seek(block: block)
|
||||
guard let buffer = try self.handle.read(upToCount: Int(self.blockSize)) else {
|
||||
throw EXT4.Error.couldNotReadBlock(block)
|
||||
}
|
||||
try attributes.append(contentsOf: Self.readBlockExtendedAttributes(from: [UInt8](buffer)))
|
||||
}
|
||||
|
||||
var xattrs: [String: Data] = [:]
|
||||
for attribute in attributes {
|
||||
guard attribute.fullName != "system.data" else {
|
||||
continue
|
||||
}
|
||||
xattrs[attribute.fullName] = Data(attribute.value)
|
||||
}
|
||||
|
||||
let pathStr = path.description
|
||||
entry.path = pathStr
|
||||
entry.size = Int64(size)
|
||||
entry.group = gid_t(inode.gidHigh) << 16 | gid_t(inode.gid)
|
||||
entry.owner = uid_t(inode.uidHigh) << 16 | uid_t(inode.uid)
|
||||
entry.creationDate = Date(fsTimestamp: UInt64(inode.crtimeExtra) << 32 | UInt64(inode.crtime))
|
||||
entry.modificationDate = Date(fsTimestamp: UInt64(inode.mtimeExtra) << 32 | UInt64(inode.mtime))
|
||||
entry.contentAccessDate = Date(fsTimestamp: UInt64(inode.atimeExtra) << 32 | UInt64(inode.atime))
|
||||
entry.xattrs = xattrs
|
||||
|
||||
if mode.isDir() {
|
||||
entry.fileType = .directory
|
||||
for child in item.children {
|
||||
items.append(child)
|
||||
}
|
||||
if pathStr == "" {
|
||||
continue
|
||||
}
|
||||
try writer.writeEntry(entry: entry, data: nil)
|
||||
} else if mode.isReg() {
|
||||
entry.fileType = .regular
|
||||
var data = Data()
|
||||
var remaining: UInt64 = size
|
||||
if let block = item.blocks {
|
||||
for dataBlock in block.start..<block.end {
|
||||
try self.seek(block: dataBlock)
|
||||
var count: UInt64
|
||||
if remaining > self.blockSize {
|
||||
count = self.blockSize
|
||||
} else {
|
||||
count = remaining
|
||||
}
|
||||
guard let dataBytes = try self.handle.read(upToCount: Int(count)) else {
|
||||
throw EXT4.Error.couldNotReadBlock(dataBlock)
|
||||
}
|
||||
data.append(dataBytes)
|
||||
remaining -= UInt64(dataBytes.count)
|
||||
}
|
||||
}
|
||||
if let additionalBlocks = item.additionalBlocks {
|
||||
for block in additionalBlocks {
|
||||
for dataBlock in block.start..<block.end {
|
||||
try self.seek(block: dataBlock)
|
||||
var count: UInt64
|
||||
if remaining > self.blockSize {
|
||||
count = self.blockSize
|
||||
} else {
|
||||
count = remaining
|
||||
}
|
||||
guard let dataBytes = try self.handle.read(upToCount: Int(count)) else {
|
||||
throw EXT4.Error.couldNotReadBlock(dataBlock)
|
||||
}
|
||||
data.append(dataBytes)
|
||||
remaining -= UInt64(dataBytes.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
try writer.writeEntry(entry: entry, data: data)
|
||||
} else if mode.isLink() {
|
||||
entry.fileType = .symbolicLink
|
||||
if size < 60 {
|
||||
let linkBytes = EXT4.tupleToArray(inode.block)
|
||||
entry.symlinkTarget = String(bytes: linkBytes.prefix(Int(size)), encoding: .utf8) ?? ""
|
||||
} else {
|
||||
if let block = item.blocks {
|
||||
try self.seek(block: block.start)
|
||||
guard let linkBytes = try self.handle.read(upToCount: Int(size)) else {
|
||||
throw EXT4.Error.couldNotReadBlock(block.start)
|
||||
}
|
||||
entry.symlinkTarget = String(bytes: linkBytes, encoding: .utf8) ?? ""
|
||||
}
|
||||
}
|
||||
try writer.writeEntry(entry: entry, data: nil)
|
||||
} else { // do not process sockets, fifo, character and block devices
|
||||
continue
|
||||
}
|
||||
}
|
||||
for (path, number) in self.hardlinks {
|
||||
guard let targetPath = hardlinkTargets[number] else {
|
||||
continue
|
||||
}
|
||||
let inode = try self.getInode(number: number)
|
||||
let entry = WriteEntry()
|
||||
entry.path = path.description
|
||||
entry.hardlink = targetPath.description
|
||||
entry.permissions = mode_t(inode.mode)
|
||||
entry.group = gid_t(inode.gidHigh) << 16 | gid_t(inode.gid)
|
||||
entry.owner = uid_t(inode.uidHigh) << 16 | uid_t(inode.uid)
|
||||
entry.creationDate = Date(fsTimestamp: UInt64(inode.crtimeExtra) << 32 | UInt64(inode.crtime))
|
||||
entry.modificationDate = Date(fsTimestamp: UInt64(inode.mtimeExtra) << 32 | UInt64(inode.mtime))
|
||||
entry.contentAccessDate = Date(fsTimestamp: UInt64(inode.atimeExtra) << 32 | UInt64(inode.atime))
|
||||
try writer.writeEntry(entry: entry, data: nil)
|
||||
}
|
||||
try writer.finishEncoding()
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "readInlineExtendedAttributes(from:)")
|
||||
public static func readInlineExtenedAttributes(from buffer: [UInt8]) throws -> [EXT4.ExtendedAttribute] {
|
||||
try readInlineExtendedAttributes(from: buffer)
|
||||
}
|
||||
|
||||
public static func readInlineExtendedAttributes(from buffer: [UInt8]) throws -> [EXT4.ExtendedAttribute] {
|
||||
let header = buffer[0..<4].withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
|
||||
if header != EXT4.XAttrHeaderMagic {
|
||||
throw EXT4.FileXattrsState.Error.missingXAttrHeader
|
||||
}
|
||||
return try EXT4.FileXattrsState.read(buffer: buffer, start: 4, offset: 4)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "readBlockExtendedAttributes(from:)")
|
||||
public static func readBlockExtenedAttributes(from buffer: [UInt8]) throws -> [EXT4.ExtendedAttribute] {
|
||||
try readBlockExtendedAttributes(from: buffer)
|
||||
}
|
||||
|
||||
public static func readBlockExtendedAttributes(from buffer: [UInt8]) throws -> [EXT4.ExtendedAttribute] {
|
||||
let header = buffer[0..<4].withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
|
||||
if header != EXT4.XAttrHeaderMagic {
|
||||
throw EXT4.FileXattrsState.Error.missingXAttrHeader
|
||||
}
|
||||
|
||||
return try EXT4.FileXattrsState.read(buffer: [UInt8](buffer), start: 32, offset: 0)
|
||||
}
|
||||
|
||||
func seek(block: UInt32) throws {
|
||||
try self.handle.seek(toOffset: UInt64(block) * blockSize)
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
init(fsTimestamp: UInt64) {
|
||||
if fsTimestamp == 0 {
|
||||
self = Date.distantPast
|
||||
return
|
||||
}
|
||||
|
||||
// 32 bits - base: seconds since January 1, 1970, signed (negative for pre-1970 dates)
|
||||
// 2 bits - epoch: overflow counter (0-3), how many times the 32-bit seconds field has wrapped
|
||||
// 30 bits - nanoseconds (0-999,999,999)
|
||||
let base = Int32(truncatingIfNeeded: fsTimestamp)
|
||||
let epoch = Int64(fsTimestamp & 0x3_0000_0000)
|
||||
let seconds = Int64(base) + epoch
|
||||
let nanoseconds = Double(fsTimestamp >> 34) / 1_000_000_000
|
||||
|
||||
self = Date(timeIntervalSince1970: Double(seconds) + nanoseconds)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 Foundation
|
||||
import SystemPackage
|
||||
|
||||
extension EXT4 {
|
||||
public enum PathIOError: Swift.Error, CustomStringConvertible {
|
||||
case notFound(String)
|
||||
case notAFile(String)
|
||||
case isDirectory(String)
|
||||
case notADirectory(String)
|
||||
case symlinkLoop(String)
|
||||
case invalidPath(String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .notFound(let p): return "no such file or directory: \(p)"
|
||||
case .notAFile(let p): return "not a regular file: \(p)"
|
||||
case .isDirectory(let p): return "is a directory: \(p)"
|
||||
case .notADirectory(let p): return "not a directory: \(p)"
|
||||
case .symlinkLoop(let p): return "symlink loop while resolving: \(p)"
|
||||
case .invalidPath(let p): return "invalid path: \(p)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
extension EXT4.EXT4Reader {
|
||||
/// Return true if a path exists (file or directory) in this ext4 device.
|
||||
public func exists(_ path: FilePath, followSymlinks: Bool = true) -> Bool {
|
||||
(try? resolvePath(path, followSymlinks: followSymlinks).inode) != nil
|
||||
}
|
||||
|
||||
/// Get the total number of blocks in the filesystem
|
||||
private var totalBlocks: UInt64 {
|
||||
let lo = UInt64(_superBlock.blocksCountLow)
|
||||
let hi = UInt64(_superBlock.blocksCountHigh)
|
||||
return lo | (hi << 32)
|
||||
}
|
||||
|
||||
/// Validate that a physical block address is within device bounds
|
||||
private func validateBlockAddress(_ block: UInt32) throws {
|
||||
guard UInt64(block) < totalBlocks else {
|
||||
throw EXT4.PathIOError.invalidPath("block address \(block) exceeds device bounds (\(totalBlocks) blocks)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata (inode + inode number) for a path.
|
||||
public func stat(_ path: FilePath, followSymlinks: Bool = true) throws -> (inodeNumber: EXT4.InodeNumber, inode: EXT4.Inode) {
|
||||
let resolved = try resolvePath(path, followSymlinks: followSymlinks)
|
||||
return (resolved.inodeNum, try getInode(number: resolved.inodeNum))
|
||||
}
|
||||
|
||||
/// List a directory's entries (names only). Does not include "." or "..".
|
||||
public func listDirectory(_ path: FilePath) throws -> [String] {
|
||||
let (inoNum, ino) = try stat(path)
|
||||
guard ino.mode.isDir() else {
|
||||
throw EXT4.PathIOError.notADirectory(path.description)
|
||||
}
|
||||
let children = try children(of: inoNum)
|
||||
return
|
||||
children
|
||||
.map { $0.0 }
|
||||
.filter { $0 != "." && $0 != ".." }
|
||||
.sorted()
|
||||
}
|
||||
|
||||
/// Read bytes from a regular file at `path` into `buffer`, starting at `offset`.
|
||||
/// Returns the number of bytes written to `buffer` (may be less than `buffer.count` at EOF).
|
||||
/// - Note: Semantics mirror `read(2)`: partial reads are possible.
|
||||
@discardableResult
|
||||
public func readFile(
|
||||
at path: FilePath,
|
||||
into buffer: UnsafeMutableRawBufferPointer,
|
||||
offset: UInt64 = 0,
|
||||
followSymlinks: Bool = true
|
||||
) throws -> Int {
|
||||
let context = try prepareRead(path: path, offset: offset, followSymlinks: followSymlinks)
|
||||
if buffer.count == 0 || context.maxReadable == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
let want = min(UInt64(buffer.count), context.maxReadable)
|
||||
return try performRead(
|
||||
inodeNumber: context.inodeNumber,
|
||||
start: context.start,
|
||||
wantedBytes: want,
|
||||
into: buffer
|
||||
)
|
||||
}
|
||||
|
||||
/// Read bytes from a regular file at `path` starting at `offset`.
|
||||
/// If `count` is nil, reads to EOF. Returns exactly the requested bytes (or less at EOF).
|
||||
public func readFile(
|
||||
at path: FilePath,
|
||||
offset: UInt64 = 0,
|
||||
count: Int? = nil,
|
||||
followSymlinks: Bool = true
|
||||
) throws -> Data {
|
||||
let context = try prepareRead(path: path, offset: offset, followSymlinks: followSymlinks)
|
||||
let want = count.map { min(UInt64($0), context.maxReadable) } ?? context.maxReadable
|
||||
if want == 0 {
|
||||
return Data()
|
||||
}
|
||||
|
||||
var out = Data(count: Int(want))
|
||||
let wrote = try out.withUnsafeMutableBytes {
|
||||
try performRead(
|
||||
inodeNumber: context.inodeNumber,
|
||||
start: context.start,
|
||||
wantedBytes: want,
|
||||
into: $0
|
||||
)
|
||||
}
|
||||
if wrote < Int(want) {
|
||||
out.removeSubrange(wrote..<Int(want))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private func prepareRead(
|
||||
path: FilePath,
|
||||
offset: UInt64,
|
||||
followSymlinks: Bool
|
||||
) throws -> (inodeNumber: EXT4.InodeNumber, start: UInt64, maxReadable: UInt64) {
|
||||
let (inoNum, inode) = try stat(path, followSymlinks: followSymlinks)
|
||||
|
||||
if inode.mode.isDir() {
|
||||
throw EXT4.PathIOError.isDirectory(path.description)
|
||||
}
|
||||
|
||||
if !inode.mode.isReg() {
|
||||
throw EXT4.PathIOError.notAFile(path.description)
|
||||
}
|
||||
|
||||
let fileSize: UInt64 = inodeFileSize(inode)
|
||||
let start = min(offset, fileSize)
|
||||
let maxReadable = fileSize - start
|
||||
return (inodeNumber: inoNum, start: start, maxReadable: maxReadable)
|
||||
}
|
||||
|
||||
private func performRead(
|
||||
inodeNumber: EXT4.InodeNumber,
|
||||
start: UInt64,
|
||||
wantedBytes: UInt64,
|
||||
into buffer: UnsafeMutableRawBufferPointer
|
||||
) throws -> Int {
|
||||
if wantedBytes == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
guard let extents = try self.getExtents(inode: inodeNumber), !extents.isEmpty else {
|
||||
return 0
|
||||
}
|
||||
|
||||
for (physStartBlk, physEndBlk) in extents {
|
||||
try validateBlockAddress(physStartBlk)
|
||||
if physEndBlk > physStartBlk {
|
||||
try validateBlockAddress(physEndBlk - 1)
|
||||
}
|
||||
}
|
||||
|
||||
guard let base = buffer.baseAddress else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let desiredBytes = Int(min(wantedBytes, UInt64(buffer.count)))
|
||||
if desiredBytes == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
let blockSizeBytes = self.blockSize
|
||||
let reqStart = start
|
||||
let reqEnd = start + UInt64(desiredBytes)
|
||||
var logicalOffset: UInt64 = 0
|
||||
var bytesWritten = 0
|
||||
|
||||
for (physStartBlk, physEndBlk) in extents {
|
||||
let extentBytes = UInt64(physEndBlk - physStartBlk) * blockSizeBytes
|
||||
let logicalEnd = logicalOffset + extentBytes
|
||||
|
||||
if logicalEnd <= reqStart {
|
||||
logicalOffset = logicalEnd
|
||||
continue
|
||||
}
|
||||
if logicalOffset >= reqEnd {
|
||||
break
|
||||
}
|
||||
|
||||
let overlapStart = max(logicalOffset, reqStart)
|
||||
let overlapEnd = min(logicalEnd, reqEnd)
|
||||
var remaining = overlapEnd - overlapStart
|
||||
if remaining == 0 {
|
||||
logicalOffset = logicalEnd
|
||||
continue
|
||||
}
|
||||
|
||||
let offsetIntoExtent = overlapStart - logicalOffset
|
||||
let absoluteByteOffset = (UInt64(physStartBlk) * blockSizeBytes) + offsetIntoExtent
|
||||
|
||||
do {
|
||||
try self.handle.seek(toOffset: absoluteByteOffset)
|
||||
} catch {
|
||||
if bytesWritten > 0 {
|
||||
return bytesWritten
|
||||
}
|
||||
throw EXT4.PathIOError.invalidPath("failed to seek to offset \(absoluteByteOffset): \(error)")
|
||||
}
|
||||
|
||||
while remaining > 0 && bytesWritten < desiredBytes {
|
||||
let chunk = min(desiredBytes - bytesWritten, Int(min(remaining, UInt64(1 << 20))))
|
||||
let dest = UnsafeMutableRawBufferPointer(
|
||||
start: base.advanced(by: bytesWritten),
|
||||
count: chunk
|
||||
)
|
||||
|
||||
do {
|
||||
guard let data = try self.handle.read(upToCount: chunk) else {
|
||||
return bytesWritten
|
||||
}
|
||||
|
||||
if data.count == 0 {
|
||||
return bytesWritten
|
||||
}
|
||||
|
||||
// Copy the data to the destination buffer
|
||||
data.withUnsafeBytes { sourceBytes in
|
||||
dest.copyMemory(from: UnsafeRawBufferPointer(sourceBytes))
|
||||
}
|
||||
|
||||
bytesWritten += data.count
|
||||
remaining -= UInt64(data.count)
|
||||
|
||||
if data.count < chunk && remaining > 0 {
|
||||
return bytesWritten
|
||||
}
|
||||
} catch {
|
||||
if bytesWritten > 0 {
|
||||
return bytesWritten
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
logicalOffset = logicalEnd
|
||||
if bytesWritten >= desiredBytes {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return bytesWritten
|
||||
}
|
||||
|
||||
// MARK: - Internals inside EXT4Reader
|
||||
public struct ResolvedPath {
|
||||
let inodeNum: EXT4.InodeNumber
|
||||
let inode: EXT4.Inode
|
||||
}
|
||||
|
||||
/// Resolve a path to an inode (optionally following symlinks).
|
||||
/// Paths may be absolute ("/...") or relative (from "/").
|
||||
public func resolvePath(_ path: FilePath, followSymlinks: Bool, maxSymlinks: Int = 40) throws -> ResolvedPath {
|
||||
var components: [String] = normalize(path: path)
|
||||
var current: EXT4.InodeNumber = EXT4.RootInode
|
||||
var parentStack: [EXT4.InodeNumber] = [] // Track parent chain for proper ".." handling
|
||||
|
||||
var symlinkHops = 0
|
||||
|
||||
// Process components one at a time to handle symlinks in the middle of paths
|
||||
var componentIndex = 0
|
||||
|
||||
while componentIndex < components.count {
|
||||
let name = components[componentIndex]
|
||||
|
||||
if name == "." {
|
||||
componentIndex += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if name == ".." {
|
||||
// Handle parent directory traversal
|
||||
if current == EXT4.RootInode {
|
||||
// At root, ".." points to itself
|
||||
componentIndex += 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Use parent stack if available
|
||||
if !parentStack.isEmpty {
|
||||
current = parentStack.removeLast()
|
||||
} else {
|
||||
// Fallback: look up ".." entry in filesystem
|
||||
let entries = try children(of: current)
|
||||
if let parent = entries.first(where: { $0.0 == ".." })?.1 {
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
componentIndex += 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Regular component: verify current is a directory and look up child
|
||||
let currentInode = try getInode(number: current)
|
||||
guard currentInode.mode.isDir() else {
|
||||
throw EXT4.PathIOError.notADirectory(name)
|
||||
}
|
||||
|
||||
let entries = try children(of: current)
|
||||
guard let child = entries.first(where: { $0.0 == name }) else {
|
||||
throw EXT4.PathIOError.notFound(name)
|
||||
}
|
||||
|
||||
// Check if child is a symlink
|
||||
let childInode = try getInode(number: child.1)
|
||||
if childInode.mode.isLink() && followSymlinks {
|
||||
// Enforce max symlink depth
|
||||
symlinkHops += 1
|
||||
if symlinkHops > maxSymlinks {
|
||||
throw EXT4.PathIOError.symlinkLoop(FilePath(components.joined(separator: "/")).description)
|
||||
}
|
||||
|
||||
// Read symlink target
|
||||
let linkBytes = try readFileFromInode(inodeNum: child.1)
|
||||
guard let linkTarget = String(data: linkBytes, encoding: .utf8), !linkTarget.isEmpty else {
|
||||
throw EXT4.PathIOError.invalidPath("empty symlink target")
|
||||
}
|
||||
|
||||
// Parse symlink target into components
|
||||
let targetComponents = normalize(path: FilePath(linkTarget))
|
||||
|
||||
// Replace current component with symlink target components and continue
|
||||
if linkTarget.hasPrefix("/") {
|
||||
// Absolute symlink: reset to root
|
||||
current = EXT4.RootInode
|
||||
parentStack = []
|
||||
// Replace the symlink component with target components + remaining path
|
||||
components = targetComponents + Array(components[(componentIndex + 1)...])
|
||||
componentIndex = 0 // Start from beginning with new path
|
||||
} else {
|
||||
// Relative symlink: continue from current directory
|
||||
// Replace the symlink component with target components + remaining path
|
||||
components = Array(components[0..<componentIndex]) + targetComponents + Array(components[(componentIndex + 1)...])
|
||||
// Don't change componentIndex - continue from same position with expanded path
|
||||
}
|
||||
} else {
|
||||
// Not a symlink or not following symlinks - descend into directory
|
||||
parentStack.append(current)
|
||||
current = child.1
|
||||
componentIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
// All components processed - return final inode
|
||||
let finalInode = try getInode(number: current)
|
||||
return ResolvedPath(inodeNum: current, inode: finalInode)
|
||||
}
|
||||
|
||||
/// Normalize a path into components, handling absolute and relative paths.
|
||||
private func normalize(path: FilePath) -> [String] {
|
||||
let s = path.description
|
||||
let trimmed = s.hasPrefix("/") ? String(s.dropFirst()) : s
|
||||
if trimmed.isEmpty { return [] }
|
||||
return trimmed.split(separator: "/").map(String.init)
|
||||
}
|
||||
|
||||
/// Read entire file content of a regular file given an inode (used for symlink targets).
|
||||
private func readFileFromInode(inodeNum: EXT4.InodeNumber) throws -> Data {
|
||||
let ino = try getInode(number: inodeNum)
|
||||
guard ino.mode.isReg() || ino.mode.isLink() else {
|
||||
return Data()
|
||||
}
|
||||
let size = inodeFileSize(ino)
|
||||
if size == 0 { return Data() }
|
||||
|
||||
// Handle fast symlinks (target stored directly in inode block field)
|
||||
if ino.mode.isLink() && size < 60 {
|
||||
// Extract target from inode block field
|
||||
let blockData = withUnsafeBytes(of: ino.block) { Data($0) }
|
||||
return blockData.prefix(Int(size))
|
||||
}
|
||||
|
||||
return try readFileBytesFromExtents(inodeNum: inodeNum, offset: 0, count: size)
|
||||
}
|
||||
|
||||
/// Low-level read using extents, with explicit offset & length (in bytes).
|
||||
private func readFileBytesFromExtents(inodeNum: EXT4.InodeNumber, offset: UInt64, count: UInt64) throws -> Data {
|
||||
guard let extents = try self.getExtents(inode: inodeNum), !extents.isEmpty else {
|
||||
return Data()
|
||||
}
|
||||
|
||||
// Validate all extent blocks are within device bounds
|
||||
for (startBlk, endBlk) in extents {
|
||||
try validateBlockAddress(startBlk)
|
||||
if endBlk > startBlk {
|
||||
try validateBlockAddress(endBlk - 1)
|
||||
}
|
||||
}
|
||||
|
||||
var out = Data(capacity: Int(count))
|
||||
var logicalOffset: UInt64 = 0
|
||||
var bytesReadSuccessfully: Int = 0
|
||||
let reqStart = offset
|
||||
let reqEnd = offset + count
|
||||
let bs = self.blockSize
|
||||
|
||||
for (startBlk, endBlk) in extents {
|
||||
let extentBytes = UInt64(endBlk - startBlk) * bs
|
||||
let logicalEnd = logicalOffset + extentBytes
|
||||
if logicalEnd <= reqStart {
|
||||
logicalOffset = logicalEnd
|
||||
continue
|
||||
}
|
||||
if logicalOffset >= reqEnd { break }
|
||||
|
||||
let ovlStart = max(logicalOffset, reqStart)
|
||||
let ovlEnd = min(logicalEnd, reqEnd)
|
||||
let ovlLen = ovlEnd - ovlStart
|
||||
if ovlLen == 0 {
|
||||
logicalOffset = logicalEnd
|
||||
continue
|
||||
}
|
||||
|
||||
let offsetIntoExtent = ovlStart - logicalOffset
|
||||
let absByteOffset = UInt64(startBlk) * bs + offsetIntoExtent
|
||||
|
||||
do {
|
||||
try self.handle.seek(toOffset: absByteOffset)
|
||||
} catch {
|
||||
if bytesReadSuccessfully > 0 {
|
||||
// Return partial data that was successfully read
|
||||
return out
|
||||
}
|
||||
throw EXT4.PathIOError.invalidPath("failed to seek to offset \(absByteOffset): \(error)")
|
||||
}
|
||||
|
||||
var left = ovlLen
|
||||
while left > 0 {
|
||||
let chunk = Int(min(left, 1 << 20))
|
||||
|
||||
do {
|
||||
guard let data = try self.handle.read(upToCount: chunk) else {
|
||||
let blk = UInt32(absByteOffset / bs)
|
||||
throw EXT4.Error.couldNotReadBlock(blk)
|
||||
}
|
||||
|
||||
out.append(data)
|
||||
bytesReadSuccessfully += data.count
|
||||
left -= UInt64(data.count)
|
||||
|
||||
if data.count < chunk && left > 0 {
|
||||
// Partial read - return what we have
|
||||
return out
|
||||
}
|
||||
} catch {
|
||||
if bytesReadSuccessfully > 0 {
|
||||
// Return partial data on error
|
||||
return out
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
logicalOffset = logicalEnd
|
||||
if out.count >= Int(count) { break }
|
||||
}
|
||||
if out.count > Int(count) { out.removeSubrange(Int(count)..<out.count) }
|
||||
return out
|
||||
}
|
||||
|
||||
/// Compute 64-bit file size from the inode fields (i_size).
|
||||
/// ext4 stores low 32 bits in i_size_lo and the high 32 bits in i_size_high when 64-bit sizes are enabled.
|
||||
private func inodeFileSize(_ inode: EXT4.Inode) -> UInt64 {
|
||||
// The Containerization EXT4 Inode struct exposes mode and block fields; size fields
|
||||
// are commonly named sizeLo/sizeHigh in this codebase.
|
||||
// EXT4 supports 64-bit file sizes - always use both low and high parts.
|
||||
let lo = UInt64(inode.sizeLow)
|
||||
let hi = UInt64(inode.sizeHigh)
|
||||
return lo | (hi << 32)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 Foundation
|
||||
import SystemPackage
|
||||
|
||||
extension FilePath {
|
||||
public static let Separator: String = "/"
|
||||
|
||||
public var bytes: [UInt8] {
|
||||
self.withCString { cstr in
|
||||
var ptr = cstr
|
||||
var rawBytes: [UInt8] = []
|
||||
while UInt(bitPattern: ptr) != 0 {
|
||||
if ptr.pointee == 0x00 { break }
|
||||
rawBytes.append(UInt8(bitPattern: ptr.pointee))
|
||||
ptr = ptr.successor()
|
||||
}
|
||||
return rawBytes
|
||||
}
|
||||
}
|
||||
|
||||
public var base: String {
|
||||
self.lastComponent?.string ?? "/"
|
||||
}
|
||||
|
||||
public var dir: FilePath {
|
||||
self.removingLastComponent()
|
||||
}
|
||||
|
||||
public var url: URL {
|
||||
URL(fileURLWithPath: self.string)
|
||||
}
|
||||
|
||||
public var items: [String] {
|
||||
self.components.map { $0.string }
|
||||
}
|
||||
|
||||
public var isRoot: Bool { // platform agnostic
|
||||
self.removingRoot().isEmpty
|
||||
}
|
||||
|
||||
public init(_ url: URL) {
|
||||
self.init(url.path(percentEncoded: false))
|
||||
}
|
||||
|
||||
public func join(_ path: FilePath) -> FilePath {
|
||||
self.pushing(path)
|
||||
}
|
||||
|
||||
public func join(_ path: String) -> FilePath {
|
||||
self.join(FilePath(path))
|
||||
}
|
||||
|
||||
public func split() -> (dir: FilePath, base: String) {
|
||||
(self.dir, self.base)
|
||||
}
|
||||
|
||||
public func clean() -> FilePath {
|
||||
self.lexicallyNormalized()
|
||||
}
|
||||
|
||||
public static func rel(_ basepath: String, _ targpath: String) -> FilePath {
|
||||
let base = FilePath(basepath)
|
||||
let targ = FilePath(targpath)
|
||||
|
||||
if base == targ {
|
||||
return "."
|
||||
}
|
||||
|
||||
let baseComponents = base.items
|
||||
let targComponents = targ.items
|
||||
|
||||
var commonPrefix = 0
|
||||
while commonPrefix < min(baseComponents.count, targComponents.count)
|
||||
&& baseComponents[commonPrefix] == targComponents[commonPrefix]
|
||||
{
|
||||
commonPrefix += 1
|
||||
}
|
||||
|
||||
let upCount = baseComponents.count - commonPrefix
|
||||
let relComponents = Array(repeating: "..", count: upCount) + targComponents[commonPrefix...]
|
||||
|
||||
return FilePath(relComponents.joined(separator: Self.Separator))
|
||||
}
|
||||
}
|
||||
|
||||
extension FileHandle {
|
||||
public convenience init?(forWritingTo path: FilePath) {
|
||||
self.init(forWritingAtPath: path.description)
|
||||
}
|
||||
|
||||
public convenience init?(forReadingAtPath path: FilePath) {
|
||||
self.init(forReadingAtPath: path.description)
|
||||
}
|
||||
|
||||
public convenience init?(forReadingFrom path: FilePath) {
|
||||
self.init(forReadingAtPath: path.description)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 Foundation
|
||||
|
||||
public struct FileTimestamps {
|
||||
public var access: Date
|
||||
public var modification: Date
|
||||
public var creation: Date
|
||||
public var now: Date
|
||||
|
||||
public var accessLo: UInt32 {
|
||||
access.fs().lo
|
||||
}
|
||||
|
||||
public var accessHi: UInt32 {
|
||||
access.fs().hi
|
||||
}
|
||||
|
||||
public var modificationLo: UInt32 {
|
||||
modification.fs().lo
|
||||
}
|
||||
|
||||
public var modificationHi: UInt32 {
|
||||
modification.fs().hi
|
||||
}
|
||||
|
||||
public var creationLo: UInt32 {
|
||||
creation.fs().lo
|
||||
}
|
||||
|
||||
public var creationHi: UInt32 {
|
||||
creation.fs().hi
|
||||
}
|
||||
|
||||
public var nowLo: UInt32 {
|
||||
now.fs().lo
|
||||
}
|
||||
|
||||
public var nowHi: UInt32 {
|
||||
now.fs().hi
|
||||
}
|
||||
|
||||
public init(access: Date?, modification: Date?, creation: Date?) {
|
||||
now = Date()
|
||||
self.access = access ?? now
|
||||
self.modification = modification ?? now
|
||||
self.creation = creation ?? now
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.init(access: nil, modification: nil, creation: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ContainerizationArchive
|
||||
import ContainerizationExtras
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
|
||||
private typealias Hardlinks = [FilePath: FilePath]
|
||||
|
||||
extension EXT4.Formatter {
|
||||
/// Unpack the provided archive on to the ext4 filesystem.
|
||||
public func unpack(reader: ArchiveReader, progress: ProgressHandler? = nil) async throws {
|
||||
try await self.unpackEntries(reader: reader, progress: progress)
|
||||
}
|
||||
|
||||
/// Unpack an archive at the source URL on to the ext4 filesystem.
|
||||
public func unpack(
|
||||
source: URL,
|
||||
format: ContainerizationArchive.Format = .paxRestricted,
|
||||
compression: ContainerizationArchive.Filter = .gzip,
|
||||
progress: ProgressHandler? = nil
|
||||
) async throws {
|
||||
// For zstd, decompress once and reuse for both passes to avoid double decompression.
|
||||
let fileToRead: URL
|
||||
let readerFilter: ContainerizationArchive.Filter
|
||||
var decompressedFile: URL?
|
||||
if progress != nil && compression == .zstd {
|
||||
let decompressed = try ArchiveReader.decompressZstd(source)
|
||||
fileToRead = decompressed
|
||||
readerFilter = .none
|
||||
decompressedFile = decompressed
|
||||
} else {
|
||||
fileToRead = source
|
||||
readerFilter = compression
|
||||
}
|
||||
defer {
|
||||
if let decompressedFile {
|
||||
ArchiveReader.cleanUpDecompressedZstd(decompressedFile)
|
||||
}
|
||||
}
|
||||
|
||||
if let progress {
|
||||
// First pass: scan headers to get totals (fast, metadata only)
|
||||
let totals = try Self.scanArchiveHeaders(format: format, filter: readerFilter, file: fileToRead)
|
||||
var totalEvents: [ProgressEvent] = []
|
||||
if totals.size > 0 {
|
||||
totalEvents.append(.addTotalSize(totals.size))
|
||||
}
|
||||
if totals.items > 0 {
|
||||
totalEvents.append(.addTotalItems(totals.items))
|
||||
}
|
||||
if !totalEvents.isEmpty {
|
||||
await progress(totalEvents)
|
||||
}
|
||||
}
|
||||
|
||||
// Unpack pass
|
||||
let reader = try ArchiveReader(
|
||||
format: format,
|
||||
filter: readerFilter,
|
||||
file: fileToRead
|
||||
)
|
||||
try await self.unpackEntries(reader: reader, progress: progress)
|
||||
}
|
||||
|
||||
/// Scan archive headers to count the total number of bytes in regular files
|
||||
/// and the total number of entries.
|
||||
public static func scanArchiveHeaders(
|
||||
format: ContainerizationArchive.Format,
|
||||
filter: ContainerizationArchive.Filter,
|
||||
file: URL
|
||||
) throws -> (size: Int64, items: Int) {
|
||||
let reader = try ArchiveReader(format: format, filter: filter, file: file)
|
||||
var totalSize: Int64 = 0
|
||||
var totalItems: Int = 0
|
||||
for (entry, _) in reader.makeStreamingIterator() {
|
||||
try Task.checkCancellation()
|
||||
guard entry.path != nil else { continue }
|
||||
totalItems += 1
|
||||
if entry.fileType == .regular, entry.hardlink == nil, let size = entry.size {
|
||||
totalSize += Int64(size)
|
||||
}
|
||||
}
|
||||
return (size: totalSize, items: totalItems)
|
||||
}
|
||||
|
||||
/// Core unpack logic. When `progress` is nil the handler calls are skipped.
|
||||
private func unpackEntries(reader: ArchiveReader, progress: ProgressHandler?) async throws {
|
||||
var hardlinks: Hardlinks = [:]
|
||||
// Allocate a single 128KiB reusable buffer for all files to minimize allocations
|
||||
// and reduce the number of read calls to libarchive.
|
||||
let bufferSize = 128 * 1024
|
||||
let reusableBuffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: bufferSize)
|
||||
defer { reusableBuffer.deallocate() }
|
||||
|
||||
for (entry, streamReader) in reader.makeStreamingIterator() {
|
||||
try Task.checkCancellation()
|
||||
guard var pathEntry = entry.path else {
|
||||
continue
|
||||
}
|
||||
|
||||
pathEntry = preProcessPath(s: pathEntry)
|
||||
let path = FilePath(pathEntry)
|
||||
|
||||
if path.base.hasPrefix(".wh.") {
|
||||
if path.base == ".wh..wh..opq" { // whiteout directory
|
||||
try self.unlink(path: path.dir, directoryWhiteout: true)
|
||||
if let progress {
|
||||
await progress([.addItems(1)])
|
||||
}
|
||||
continue
|
||||
}
|
||||
let startIndex = path.base.index(path.base.startIndex, offsetBy: ".wh.".count)
|
||||
let filePath = String(path.base[startIndex...])
|
||||
let dir: FilePath = path.dir
|
||||
try self.unlink(path: dir.join(filePath))
|
||||
if let progress {
|
||||
await progress([.addItems(1)])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if let hardlink = entry.hardlink {
|
||||
let hl = preProcessPath(s: hardlink)
|
||||
hardlinks[path] = FilePath(hl)
|
||||
if let progress {
|
||||
await progress([.addItems(1)])
|
||||
}
|
||||
continue
|
||||
}
|
||||
let ts = FileTimestamps(
|
||||
access: entry.contentAccessDate, modification: entry.modificationDate, creation: entry.creationDate)
|
||||
switch entry.fileType {
|
||||
case .directory:
|
||||
try self.create(
|
||||
path: path, mode: EXT4.Inode.Mode(.S_IFDIR, UInt16(entry.permissions)), ts: ts, uid: entry.owner,
|
||||
gid: entry.group,
|
||||
xattrs: entry.xattrs)
|
||||
case .regular:
|
||||
try self.create(
|
||||
path: path, mode: EXT4.Inode.Mode(.S_IFREG, UInt16(entry.permissions)), ts: ts, buf: streamReader,
|
||||
uid: entry.owner,
|
||||
gid: entry.group, xattrs: entry.xattrs, fileBuffer: reusableBuffer)
|
||||
|
||||
if let progress, let size = entry.size {
|
||||
await progress([.addSize(Int64(size))])
|
||||
}
|
||||
case .symbolicLink:
|
||||
var symlinkTarget: FilePath?
|
||||
if let target = entry.symlinkTarget {
|
||||
symlinkTarget = FilePath(target)
|
||||
}
|
||||
try self.create(
|
||||
path: path, link: symlinkTarget, mode: EXT4.Inode.Mode(.S_IFLNK, UInt16(entry.permissions)), ts: ts,
|
||||
uid: entry.owner,
|
||||
gid: entry.group, xattrs: entry.xattrs)
|
||||
default:
|
||||
if let progress {
|
||||
await progress([.addItems(1)])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if let progress {
|
||||
await progress([.addItems(1)])
|
||||
}
|
||||
}
|
||||
guard hardlinks.acyclic else {
|
||||
throw UnpackError.circularLinks
|
||||
}
|
||||
for (path, _) in hardlinks {
|
||||
if let resolvedTarget = try hardlinks.resolve(path) {
|
||||
try self.link(link: path, target: resolvedTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func preProcessPath(s: String) -> String {
|
||||
var p = s
|
||||
if p.hasPrefix("./") {
|
||||
p = String(p.dropFirst())
|
||||
}
|
||||
if !p.hasPrefix("/") {
|
||||
p = "/" + p
|
||||
}
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
/// Common errors for unpacking an archive onto an ext4 filesystem.
|
||||
public enum UnpackError: Swift.Error, CustomStringConvertible, Sendable, Equatable {
|
||||
/// The name is invalid.
|
||||
case invalidName(_ name: String)
|
||||
/// A circular link is found.
|
||||
case circularLinks
|
||||
|
||||
/// The description of the error.
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidName(let name):
|
||||
return "'\(name)' is an invalid name"
|
||||
case .circularLinks:
|
||||
return "circular links found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Hardlinks {
|
||||
fileprivate var acyclic: Bool {
|
||||
for (_, target) in self {
|
||||
var visited: Set<FilePath> = [target]
|
||||
var next = target
|
||||
while let item = self[next] {
|
||||
if visited.contains(item) {
|
||||
return false
|
||||
}
|
||||
next = item
|
||||
visited.insert(next)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fileprivate func resolve(_ key: FilePath) throws -> FilePath? {
|
||||
let target = self[key]
|
||||
guard let target else {
|
||||
return nil
|
||||
}
|
||||
var next = target
|
||||
var visited: Set<FilePath> = [next]
|
||||
while let item = self[next] {
|
||||
if visited.contains(item) {
|
||||
throw UnpackError.circularLinks
|
||||
}
|
||||
next = item
|
||||
visited.insert(next)
|
||||
}
|
||||
return next
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension UInt64 {
|
||||
public var lo: UInt32 {
|
||||
UInt32(self & 0xffff_ffff)
|
||||
}
|
||||
|
||||
public var hi: UInt32 {
|
||||
UInt32(self >> 32)
|
||||
}
|
||||
|
||||
public static func - (lhs: Self, rhs: UInt32) -> UInt64 {
|
||||
lhs - UInt64(rhs)
|
||||
}
|
||||
|
||||
public static func % (lhs: Self, rhs: UInt32) -> UInt64 {
|
||||
lhs % UInt64(rhs)
|
||||
}
|
||||
|
||||
public static func / (lhs: Self, rhs: UInt32) -> UInt32 {
|
||||
UInt32(lhs / UInt64(rhs))
|
||||
}
|
||||
|
||||
public static func * (lhs: Self, rhs: UInt32) -> UInt64 {
|
||||
lhs * UInt64(rhs)
|
||||
}
|
||||
|
||||
public static func * (lhs: Self, rhs: Int) -> UInt64 {
|
||||
lhs * UInt64(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt32 {
|
||||
public var lo: UInt16 {
|
||||
UInt16(self & 0xffff)
|
||||
}
|
||||
|
||||
public var hi: UInt16 {
|
||||
UInt16(self >> 16)
|
||||
}
|
||||
|
||||
public static func + (lhs: Self, rhs: Int.IntegerLiteralType) -> UInt32 {
|
||||
lhs + UInt32(rhs)
|
||||
}
|
||||
|
||||
public static func - (lhs: Self, rhs: Int.IntegerLiteralType) -> UInt32 {
|
||||
lhs - UInt32(rhs)
|
||||
}
|
||||
|
||||
public static func / (lhs: Self, rhs: Int.IntegerLiteralType) -> UInt32 {
|
||||
lhs / UInt32(rhs)
|
||||
}
|
||||
|
||||
public static func - (lhs: Self, rhs: UInt16) -> UInt32 {
|
||||
lhs - UInt32(rhs)
|
||||
}
|
||||
|
||||
public static func * (lhs: Self, rhs: Int.IntegerLiteralType) -> Int {
|
||||
Int(lhs) * rhs
|
||||
}
|
||||
}
|
||||
|
||||
extension Int {
|
||||
public static func + (lhs: Self, rhs: UInt32) -> Int {
|
||||
lhs + Int(rhs)
|
||||
}
|
||||
|
||||
public static func + (lhs: Self, rhs: UInt32) -> UInt32 {
|
||||
UInt32(lhs) + rhs
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt16 {
|
||||
func isDir() -> Bool {
|
||||
self & EXT4.FileModeFlag.TypeMask.rawValue == EXT4.FileModeFlag.S_IFDIR.rawValue
|
||||
}
|
||||
|
||||
func isLink() -> Bool {
|
||||
self & EXT4.FileModeFlag.TypeMask.rawValue == EXT4.FileModeFlag.S_IFLNK.rawValue
|
||||
}
|
||||
|
||||
func isReg() -> Bool {
|
||||
self & EXT4.FileModeFlag.TypeMask.rawValue == EXT4.FileModeFlag.S_IFREG.rawValue
|
||||
}
|
||||
|
||||
func fileType() -> UInt8 {
|
||||
typealias FMode = EXT4.FileModeFlag
|
||||
typealias FileType = EXT4.FileType
|
||||
switch self & FMode.TypeMask.rawValue {
|
||||
case FMode.S_IFREG.rawValue:
|
||||
return FileType.regular.rawValue
|
||||
case FMode.S_IFDIR.rawValue:
|
||||
return FileType.directory.rawValue
|
||||
case FMode.S_IFCHR.rawValue:
|
||||
return FileType.character.rawValue
|
||||
case FMode.S_IFBLK.rawValue:
|
||||
return FileType.block.rawValue
|
||||
case FMode.S_IFIFO.rawValue:
|
||||
return FileType.fifo.rawValue
|
||||
case FMode.S_IFSOCK.rawValue:
|
||||
return FileType.socket.rawValue
|
||||
case FMode.S_IFLNK.rawValue:
|
||||
return FileType.symbolicLink.rawValue
|
||||
default:
|
||||
return FileType.unknown.rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension [UInt8] {
|
||||
var allZeros: Bool {
|
||||
for num in self where num != 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# ``ContainerizationEXT4``
|
||||
|
||||
`ContainerizationEXT4` provides functionality to read the superblock of an existing ext4 block device and format a new block device with
|
||||
the ext4 file system.
|
||||
@@ -0,0 +1,78 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 CoreFoundation
|
||||
|
||||
// takes a pointer and converts its contents to native endian bytes
|
||||
public func withUnsafeLittleEndianBytes<T, Result>(of value: T, body: (UnsafeRawBufferPointer) throws -> Result)
|
||||
rethrows -> Result
|
||||
{
|
||||
switch Endian {
|
||||
case .little:
|
||||
return try withUnsafeBytes(of: value) { bytes in
|
||||
try body(bytes)
|
||||
}
|
||||
case .big:
|
||||
return try withUnsafeBytes(of: value) { buffer in
|
||||
let reversedBuffer = Array(buffer.reversed())
|
||||
return try reversedBuffer.withUnsafeBytes { buf in
|
||||
try body(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func withUnsafeLittleEndianBuffer<T>(
|
||||
of value: UnsafeRawBufferPointer, body: (UnsafeRawBufferPointer) throws -> T
|
||||
) rethrows -> T {
|
||||
switch Endian {
|
||||
case .little:
|
||||
return try body(value)
|
||||
case .big:
|
||||
let reversed = Array(value.reversed())
|
||||
return try reversed.withUnsafeBytes { buf in
|
||||
try body(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UnsafeRawBufferPointer {
|
||||
// loads littleEndian raw data, converts it native endian format and calls UnsafeRawBufferPointer.load
|
||||
public func loadLittleEndian<T>(as type: T.Type) -> T {
|
||||
switch Endian {
|
||||
case .little:
|
||||
return self.load(as: T.self)
|
||||
case .big:
|
||||
let buffer = Array(self.reversed())
|
||||
return buffer.withUnsafeBytes { ptr in
|
||||
ptr.load(as: T.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Endianness {
|
||||
case little
|
||||
case big
|
||||
}
|
||||
|
||||
// returns current endianness
|
||||
public var Endian: Endianness {
|
||||
var value: UInt32 = 0x0102_0304
|
||||
return withUnsafeBytes(of: &value) { buffer in
|
||||
buffer.first == 0x04 ? .little : .big
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user