Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 89 additions & 42 deletions Sources/MachOObjCSection/Extension/DyldCache+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,89 +28,136 @@ extension DyldCache {
}
}

// MARK: - locate value
extension DyldCache {
var headerOptimizationRO64: ObjCHeaderOptimizationRO64? {
guard cpu.is64Bit else {
return nil
}
if let objcOptimization {
return objcOptimization.headerOptimizationRO64(in: self)
}
if let oldObjcOptimization {
return oldObjcOptimization.headerOptimizationRO64(in: self)
}
return nil
/// A tuple containing the DyldCache where the value was found and the resolved value itself.
/// Useful because values may be located either in the current cache, the main cache,
/// or one of its subcaches.
typealias LocatedValue<V> = (cache: DyldCache, value: V)

/// Locate a value for a given optional KeyPath within this cache hierarchy.
///
/// This resolves the value by checking:
/// 1. This cache
/// 2. The main cache
/// 3. Any subcaches derived from `mainCache`
///
/// - Parameter keyPath: A keyPath returning an optional value.
/// - Returns: A tuple of `(cache, value)` if resolved, or `nil` if not found.
func locateValue<V>(
_ keyPath: KeyPath<DyldCache, V?>
) -> LocatedValue<V>? {
locateValue({ $0[keyPath: keyPath] })
}

var headerOptimizationRO32: ObjCHeaderOptimizationRO32? {
guard cpu.is64Bit else {
/// Locate a value using a custom resolver function running against each cache in the hierarchy.
///
/// Resolution order:
/// 1. This cache
/// 2. The main cache
/// 3. Each subcache of the main cache
///
/// - Parameter resolver: A closure returning an optional value for a given DyldCache.
/// - Returns: A tuple of `(cache, value)` if resolution is successful; otherwise `nil`.
func locateValue<V>(
_ resolver: (DyldCache) -> V?
) -> LocatedValue<V>? {
if let value = resolver(self) { return (self, value) }

guard let mainCache else { return nil }
Comment on lines +65 to +67
Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using a guard statement pattern to align with the existing code style in this file. Change if let value = resolver(self) { return (self, value) } to guard let value = resolver(self) else { ... } for consistency with lines 74-76 and 77-79.

Suggested change
if let value = resolver(self) { return (self, value) }
guard let mainCache else { return nil }
guard let value = resolver(self) else { return nil }
return (self, value)

Copilot uses AI. Check for mistakes.
if let value = resolver(mainCache) { return (mainCache, value) }

guard let subCaches = mainCache.subCaches else {
return nil
}
if let objcOptimization {
return objcOptimization.headerOptimizationRO32(in: self)
}
if let oldObjcOptimization {
return oldObjcOptimization.headerOptimizationRO32(in: self)
for subCache in subCaches {
guard let cache = try? subCache.subcache(for: mainCache) else {
continue
}
if let value = resolver(cache) {
return (cache, value)
}
}
return nil
}
}

var headerOptimizationRW64: ObjCHeaderOptimizationRW64? {
guard cpu.is64Bit else {
return nil
}
if let objcOptimization {
return objcOptimization.headerOptimizationRW64(in: self)
// MARK: - Objective-C
extension DyldCache {
var _objcOptimization: LocatedValue<ObjCOptimization>? {
locateValue(\.objcOptimization)
}

var _oldObjcOptimization: LocatedValue<OldObjCOptimization>? {
locateValue(\.oldObjcOptimization)
}
}

extension DyldCache {
var headerOptimizationRO64: ObjCHeaderOptimizationRO64? {
guard cpu.is64Bit else { return nil }
if let _objcOptimization {
return _objcOptimization.value.headerOptimizationRO64(in: self)
}
if let oldObjcOptimization {
return oldObjcOptimization.headerOptimizationRW64(in: self)
if let _oldObjcOptimization {
return _oldObjcOptimization.value.headerOptimizationRO64(in: self)
}
return nil
}

var headerOptimizationRW32: ObjCHeaderOptimizationRW32? {
guard cpu.is64Bit else {
return nil
}
if let objcOptimization {
return objcOptimization.headerOptimizationRW32(in: self)
var headerOptimizationRO32: ObjCHeaderOptimizationRO32? {
guard !cpu.is64Bit else { return nil }
if let _objcOptimization {
return _objcOptimization.value.headerOptimizationRO32(in: self)
}
if let oldObjcOptimization {
return oldObjcOptimization.headerOptimizationRW32(in: self)
if let _oldObjcOptimization {
return _oldObjcOptimization.value.headerOptimizationRO32(in: self)
}
return nil
}
}

extension DyldCache {
func machO(at index: Int) -> MachOFile? {
guard let mainCache else { return nil }
if let ro = mainCache.headerOptimizationRO64,
var _headerOptimizationRO64: LocatedValue<ObjCHeaderOptimizationRO64>? {
locateValue(\.headerOptimizationRO64)
}

var _headerOptimizationRO32: LocatedValue<ObjCHeaderOptimizationRO32>? {
locateValue(\.headerOptimizationRO32)
}
}

extension DyldCache {
func _machO(at index: Int) -> LocatedValue<MachOFile>? {
if let ro = _headerOptimizationRO64?.value,
ro.contains(index: index) {
guard let header = ro.headerInfos(in: mainCache)?.first(
let headers = locateValue({ ro.headerInfos(in: $0) })?.value
guard let header = headers?.first(
where: {
$0.index == index
}
) else {
return nil
}
return header._machO(mainCache: mainCache)
return locateValue { header.machO(in: $0) }
Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The nested locateValue call on line 142 (and 154) could fail to locate the MachO file even when a header is found. Consider adding a comment explaining why this additional search is necessary, or logging when the header exists but the machO cannot be located.

Copilot uses AI. Check for mistakes.
}
if let ro = mainCache.headerOptimizationRO32,
if let ro = _headerOptimizationRO32?.value,
ro.contains(index: index) {
guard let header = ro.headerInfos(in: mainCache)?.first(
let headers = locateValue({ ro.headerInfos(in: $0) })?.value
guard let header = headers?.first(
where: {
$0.index == index
}
) else {
return nil
}
return header._machO(mainCache: mainCache)
return locateValue { header.machO(in: $0) }
}
return nil
}
}

// MARK: - mach-o
extension DyldCache {
func machO(containing unslidAddress: UInt64) -> MachOFile? {
for machO in self.machOFiles() {
Expand Down
20 changes: 0 additions & 20 deletions Sources/MachOObjCSection/Extension/ObjCHeaderOptimizationRO+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,3 @@ extension ObjCHeaderOptimizationROProtocol {
(0 ..< count).contains(index)
}
}

extension ObjCHeaderInfoROProtocol {
func _machO(mainCache: DyldCache) -> MachOFile? {
if let machO = machO(in: mainCache) {
return machO
}
guard let subCaches = mainCache.subCaches else {
return nil
}
for subCache in subCaches {
guard let cache = try? subCache.subcache(for: mainCache) else {
continue
}
if let machO = machO(in: cache) {
return machO
}
}
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public struct ObjCMethodRelativeListList: RelativeListListProtocol {

#if canImport(MachO)
guard let cache: DyldCacheLoaded = .current else { return nil }
guard let machO = entry.machO(in: cache) else { return nil }
guard let machO = cache.machO(at: entry.imageIndex) else { return nil }

let list = List(
ptr: ptr,
Expand All @@ -52,7 +52,7 @@ extension ObjCMethodRelativeListList {
return nil
}

guard let machO = entry.machO(in: cache) else { return nil }
guard let machO = cache._machO(at: entry.imageIndex)?.value else { return nil }

let data = try! cache.fileHandle.readData(
offset: numericCast(resolvedOffset),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public struct ObjCPropertyRelativeListList: RelativeListListProtocol {

#if canImport(MachO)
guard let cache: DyldCacheLoaded = .current else { return nil }
guard let machO = entry.machO(in: cache) else { return nil }
guard let machO = cache.machO(at: entry.imageIndex) else { return nil }

let list = List(
ptr: ptr,
Expand All @@ -50,7 +50,7 @@ public struct ObjCPropertyRelativeListList: RelativeListListProtocol {
return nil
}

guard let machO = entry.machO(in: cache) else { return nil }
guard let machO = cache._machO(at: entry.imageIndex)?.value else { return nil }

let data = try! cache.fileHandle.readData(
offset: numericCast(resolvedOffset),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct ObjCProtocolRelativeListList64: ObjCProtocolRelativeListListProtoc

#if canImport(MachO)
guard let cache: DyldCacheLoaded = .current else { return nil }
guard let machO = entry.machO(in: cache) else { return nil }
guard let machO = cache.machO(at: entry.imageIndex) else { return nil }

let list = List(
ptr: ptr,
Expand All @@ -50,7 +50,7 @@ public struct ObjCProtocolRelativeListList64: ObjCProtocolRelativeListListProtoc
return nil
}

guard let machO = entry.machO(in: cache) else { return nil }
guard let machO = cache._machO(at: entry.imageIndex)?.value else { return nil }

let data = try! cache.fileHandle.readData(
offset: numericCast(resolvedOffset),
Expand Down Expand Up @@ -123,7 +123,7 @@ public struct ObjCProtocolRelativeListList32: ObjCProtocolRelativeListListProtoc
return nil
}

guard let listMachO = cache.machO(at: entry.imageIndex) else {
guard let listMachO = cache._machO(at: entry.imageIndex)?.value else {
return nil
}

Expand Down
27 changes: 4 additions & 23 deletions Sources/MachOObjCSection/Model/Util/RelativeListList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,16 @@ extension RelativeListListProtocol {

extension RelativeListListProtocol {
public func entries(in machO: MachOFile) -> [Entry] {
Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactored code assumes machO.fileHandleAndOffset(forOffset:) exists, but this method is not visible in the diff or context. Add a comment explaining this dependency or ensure the method is documented, especially since this is a critical change from the previous manual offset resolution logic.

Suggested change
public func entries(in machO: MachOFile) -> [Entry] {
public func entries(in machO: MachOFile) -> [Entry] {
// NOTE: This code depends on `MachOFile.fileHandleAndOffset(forOffset:)` existing.
// This method is expected to resolve a file handle and file offset for a given virtual offset.
// Ensure that `fileHandleAndOffset(forOffset:)` is implemented and documented on `MachOFile`.

Copilot uses AI. Check for mistakes.
let offset = offset + machO.headerStartOffset

var resolvedOffset: UInt64 = numericCast(offset)

var fileHandle = machO.fileHandle

if let (_cache, _offset) = machO.cacheAndFileOffset(
fromStart: UInt64(offset)
) {
resolvedOffset = _offset
fileHandle = _cache.fileHandle
guard let (fileHandle, fileOffset) = machO.fileHandleAndOffset(forOffset: numericCast(offset)) else {
return []
}

let sequence: DataSequence<Entry.Layout> = fileHandle.readDataSequence(
offset: resolvedOffset + numericCast(MemoryLayout<Header>.size),
offset: fileOffset + numericCast(MemoryLayout<Header>.size),
numberOfElements: numericCast(header.count)
)

let baseOffset = offset + MemoryLayout<Header>.size - machO.headerStartOffset
let baseOffset = offset + MemoryLayout<Header>.size
let entrySize = MemoryLayout<Entry.Layout>.size
return sequence.enumerated()
.map { i, layout in
Expand All @@ -105,13 +96,3 @@ extension RelativeListListProtocol {
}
}
}

extension RelativeListListEntry {
public func machO(in cache: DyldCacheLoaded) -> MachOImage? {
cache.machO(at: imageIndex)
}

public func machO(in cache: DyldCache) -> MachOFile? {
cache.machO(at: imageIndex)
}
}
9 changes: 9 additions & 0 deletions Sources/MachOObjCSection/Support/ObjCDump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import Foundation
import MachOKit
import ObjCDump

extension MachOFile {
// WORKAROUND: Due to a bug in MachOKit, the imagePath of a machO obtained from ObjCHeaderInfoRO may sometimes be an empty string.
fileprivate var imagePath: String {
loadCommands.info(of: LoadCommand.idDylib)?
.dylib(in: self)
.name ?? ""
}
}

// MARK: - IVar
extension ObjCIvarProtocol {
public func info(in machO: MachOFile) -> ObjCIvarInfo? {
Expand Down