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

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)
}
}
}