680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
278 lines
12 KiB
Swift
278 lines
12 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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)
|
|
}
|
|
}
|
|
}
|