Skip to content

Commit 4153326

Browse files
authoredNov 23, 2024··
Merge pull request #146 from p-x9/feature/trie-tree-search
2 parents 94fe4fa + 4620021 commit 4153326

22 files changed

+694
-402
lines changed
 

‎Sources/MachOKit/DyldCache.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,12 @@ extension DyldCache {
216216
}
217217

218218
extension DyldCache {
219-
public typealias DylibsTrieEntries = DataTrieTree<DylibsTrieNodeContent>
219+
public typealias DylibsTrie = DataTrieTree<DylibsTrieNodeContent>
220220

221221
/// Dylibs trie is for searching by dylib name.
222222
///
223223
/// The ``dylibIndices`` are retrieved from this trie tree.
224-
public var dylibsTrieEntries: DylibsTrieEntries? {
224+
public var dylibsTrie: DylibsTrie? {
225225
guard mainCacheHeader.dylibsTrieAddr > 0,
226226
mainCacheHeader.hasProperty(\.dylibsTrieSize) else {
227227
return nil
@@ -246,20 +246,20 @@ extension DyldCache {
246246
/// 0 /usr/lib/libobjc.dylib
247247
/// ```
248248
public var dylibIndices: [DylibIndex] {
249-
guard let dylibsTrieEntries else {
249+
guard let dylibsTrie else {
250250
return []
251251
}
252-
return dylibsTrieEntries.dylibIndices
252+
return dylibsTrie.dylibIndices
253253
}
254254
}
255255

256256
extension DyldCache {
257-
public typealias ProgramsTrieEntries = DataTrieTree<ProgramsTrieNodeContent>
257+
public typealias ProgramsTrie = DataTrieTree<ProgramsTrieNodeContent>
258258

259259
/// Pair of program name/cdhash and offset to prebuiltLoaderSet
260260
///
261261
/// The ``programOffsets`` are retrieved from this trie tree.
262-
public var programsTrieEntries: ProgramsTrieEntries? {
262+
public var programsTrie: ProgramsTrie? {
263263
guard mainCacheHeader.programTrieAddr > 0,
264264
mainCacheHeader.hasProperty(\.programTrieSize) else {
265265
return nil
@@ -269,7 +269,7 @@ extension DyldCache {
269269
}
270270
let size = mainCacheHeader.programTrieSize
271271

272-
return ProgramsTrieEntries(
272+
return ProgramsTrie(
273273
data: fileHandle.readData(offset: offset, size: Int(size))
274274
)
275275
}
@@ -284,10 +284,10 @@ extension DyldCache {
284284
/// 131776 /cdhash/fed26a75645fed2a674b5c4d01001bfa69b9dbea
285285
/// ```
286286
public var programOffsets: [ProgramOffset] {
287-
guard let programsTrieEntries else {
287+
guard let programsTrie else {
288288
return []
289289
}
290-
return programsTrieEntries.programOffsets
290+
return programsTrie.programOffsets
291291
}
292292

293293
/// Get the prebuiltLoaderSet indicated by programOffset.

‎Sources/MachOKit/DyldCacheLoaded.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,12 @@ extension DyldCacheLoaded {
207207
}
208208

209209
extension DyldCacheLoaded {
210-
public typealias DylibsTrieEntries = MemoryTrieTree<DylibsTrieNodeContent>
210+
public typealias DylibsTrie = MemoryTrieTree<DylibsTrieNodeContent>
211211

212212
/// Dylibs trie is for searching by dylib name.
213213
///
214214
/// The ``dylibIndices`` are retrieved from this trie tree.
215-
public var dylibsTrieEntries: DylibsTrieEntries? {
215+
public var dylibsTrie: DylibsTrie? {
216216
guard header.dylibsTrieAddr > 0,
217217
header.hasProperty(\.dylibsTrieSize),
218218
let slide else {
@@ -242,20 +242,20 @@ extension DyldCacheLoaded {
242242
/// 0 /usr/lib/libobjc.dylib
243243
/// ```
244244
public var dylibIndices: [DylibIndex] {
245-
guard let dylibsTrieEntries else {
245+
guard let dylibsTrie else {
246246
return []
247247
}
248-
return dylibsTrieEntries.dylibIndices
248+
return dylibsTrie.dylibIndices
249249
}
250250
}
251251

252252
extension DyldCacheLoaded {
253-
public typealias ProgramsTrieEntries = MemoryTrieTree<ProgramsTrieNodeContent>
253+
public typealias ProgramsTrie = MemoryTrieTree<ProgramsTrieNodeContent>
254254

255255
/// Pair of program name/cdhash and offset to prebuiltLoaderSet
256256
///
257257
/// The ``programOffsets`` are retrieved from this trie tree.
258-
public var programsTrieEntries: ProgramsTrieEntries? {
258+
public var programsTrie: ProgramsTrie? {
259259
guard header.programTrieAddr > 0,
260260
header.hasProperty(\.programTrieSize),
261261
let slide else {
@@ -268,7 +268,7 @@ extension DyldCacheLoaded {
268268
return nil
269269
}
270270

271-
return ProgramsTrieEntries(
271+
return ProgramsTrie(
272272
basePointer: basePointer,
273273
size: numericCast(size)
274274
)
@@ -284,10 +284,10 @@ extension DyldCacheLoaded {
284284
/// 131776 /cdhash/fed26a75645fed2a674b5c4d01001bfa69b9dbea
285285
/// ```
286286
public var programOffsets: [ProgramOffset] {
287-
guard let programsTrieEntries else {
287+
guard let programsTrie else {
288288
return []
289289
}
290-
return programsTrieEntries.programOffsets
290+
return programsTrie.programOffsets
291291
}
292292

293293
/// Get the prebuiltLoaderSet indicated by programOffset.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// FixedWidthInteger+.swift
3+
// MachOKit
4+
//
5+
// Created by p-x9 on 2024/11/20
6+
//
7+
//
8+
9+
import Foundation
10+
11+
extension FixedWidthInteger {
12+
var uleb128Size: Int {
13+
var value = self
14+
var result = 0
15+
16+
repeat {
17+
value = value >> 7
18+
result += 1
19+
} while value != 0
20+
21+
return result
22+
}
23+
}

‎Sources/MachOKit/Extension/Sequence+.swift

-178
Original file line numberDiff line numberDiff line change
@@ -8,74 +8,6 @@
88

99
import Foundation
1010

11-
extension Sequence<ExportTrieEntry> {
12-
public var exportedSymbols: [ExportedSymbol] {
13-
let entries = Array(self)
14-
guard !entries.isEmpty else { return [] }
15-
16-
let map: [Int: Element] = Dictionary(
17-
uniqueKeysWithValues: entries.map {
18-
($0.offset, $0)
19-
}
20-
)
21-
return extractExportedSymbols(
22-
currentName: "",
23-
currentOffset: 0,
24-
entry: entries[0],
25-
map: map
26-
)
27-
}
28-
29-
/// https://opensource.apple.com/source/dyld/dyld-421.1/interlinked-dylibs/Trie.hpp.auto.html
30-
private func extractExportedSymbols(
31-
currentName: String,
32-
currentOffset: Int,
33-
entry: Element,
34-
map: [Int: Element]
35-
) -> [ExportedSymbol] {
36-
var currentOffset = currentOffset
37-
if let offset = entry.symbolOffset {
38-
currentOffset += Int(bitPattern: offset)
39-
}
40-
41-
guard !entry.children.isEmpty else {
42-
return [
43-
ExportedSymbol(
44-
name: currentName,
45-
offset: currentOffset,
46-
flags: entry.flags ?? [],
47-
ordinal: entry.ordinal,
48-
importedName: entry.importedName,
49-
stub: entry.stub,
50-
resolver: entry.resolver
51-
)
52-
]
53-
}
54-
return entry.children.map {
55-
if let entry = map[Int($0.offset)] {
56-
return extractExportedSymbols(
57-
currentName: currentName + $0.label,
58-
currentOffset: currentOffset,
59-
entry: entry,
60-
map: map
61-
)
62-
} else {
63-
return [
64-
ExportedSymbol(
65-
name: currentName + $0.label,
66-
offset: currentOffset,
67-
flags: entry.flags ?? [],
68-
ordinal: entry.ordinal,
69-
importedName: entry.importedName,
70-
stub: entry.stub,
71-
resolver: entry.resolver
72-
)
73-
]
74-
}
75-
}.flatMap { $0 }
76-
}
77-
}
78-
7911
// https://opensource.apple.com/source/ld64/ld64-253.9/src/other/dyldinfo.cpp.auto.html
8012
extension Sequence<BindOperation> {
8113
func bindings(
@@ -348,113 +280,3 @@ extension Sequence where Element == CodeSignCodeDirectory {
348280
}
349281
}
350282
}
351-
352-
extension Sequence<DylibsTrieEntry> {
353-
public var dylibIndices: [DylibIndex] {
354-
let entries = Array(self)
355-
guard !entries.isEmpty else { return [] }
356-
357-
let map: [Int: Element] = Dictionary(
358-
uniqueKeysWithValues: entries.map {
359-
($0.offset, $0)
360-
}
361-
)
362-
return extractDylibIndices(
363-
currentName: "",
364-
currentOffset: 0,
365-
entry: entries[0],
366-
map: map
367-
)
368-
}
369-
370-
private func extractDylibIndices(
371-
currentName: String,
372-
currentOffset: Int,
373-
entry: Element,
374-
map: [Int: Element]
375-
) -> [DylibIndex] {
376-
guard !entry.children.isEmpty else {
377-
if let content = entry.content {
378-
return [
379-
.init(name: currentName, index: content.index)
380-
]
381-
}
382-
return []
383-
}
384-
return entry.children.map {
385-
if let entry = map[Int($0.offset)] {
386-
return extractDylibIndices(
387-
currentName: currentName + $0.label,
388-
currentOffset: currentOffset,
389-
entry: entry,
390-
map: map
391-
)
392-
} else {
393-
if let content = entry.content {
394-
return [
395-
.init(
396-
name: currentName + $0.label,
397-
index: content.index
398-
)
399-
]
400-
}
401-
return []
402-
}
403-
}.flatMap { $0 }
404-
}
405-
}
406-
407-
extension Sequence<ProgramsTrieEntry> {
408-
public var programOffsets: [ProgramOffset] {
409-
let entries = Array(self)
410-
guard !entries.isEmpty else { return [] }
411-
412-
let map: [Int: Element] = Dictionary(
413-
uniqueKeysWithValues: entries.map {
414-
($0.offset, $0)
415-
}
416-
)
417-
return extractProgramOffsets(
418-
currentName: "",
419-
currentOffset: 0,
420-
entry: entries[0],
421-
map: map
422-
)
423-
}
424-
425-
private func extractProgramOffsets(
426-
currentName: String,
427-
currentOffset: Int,
428-
entry: Element,
429-
map: [Int: Element]
430-
) -> [ProgramOffset] {
431-
guard !entry.children.isEmpty else {
432-
if let content = entry.content {
433-
return [
434-
.init(name: currentName, offset: content.offset)
435-
]
436-
}
437-
return []
438-
}
439-
return entry.children.map {
440-
if let entry = map[Int($0.offset)] {
441-
return extractProgramOffsets(
442-
currentName: currentName + $0.label,
443-
currentOffset: currentOffset,
444-
entry: entry,
445-
map: map
446-
)
447-
} else {
448-
if let content = entry.content {
449-
return [
450-
.init(
451-
name: currentName + $0.label,
452-
offset: content.offset
453-
)
454-
]
455-
}
456-
return []
457-
}
458-
}.flatMap { $0 }
459-
}
460-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// TrieTreeProtocol+.swift
3+
// MachOKit
4+
//
5+
// Created by p-x9 on 2024/11/20
6+
//
7+
//
8+
9+
import Foundation
10+
11+
extension TrieTreeProtocol where Content == ExportTrieNodeContent {
12+
public var exportedSymbols: [ExportedSymbol] {
13+
guard let root = first(where: { _ in true }) else {
14+
return []
15+
}
16+
var result: [(String, Content)] = []
17+
_recurseTrie(currentName: "", entry: root, result: &result)
18+
return result
19+
.map {
20+
name, content in
21+
let symbolOffset: Int? = if let symbolOffset = content.symbolOffset {
22+
.init(bitPattern: symbolOffset)
23+
} else { nil }
24+
return .init(
25+
name: name,
26+
offset: symbolOffset,
27+
flags: content.flags ?? [],
28+
ordinal: content.ordinal,
29+
importedName: content.importedName,
30+
stub: content.stub,
31+
resolver: content.resolver
32+
)
33+
}
34+
}
35+
36+
public func search(for key: String) -> ExportedSymbol? {
37+
guard let (_, content) = _search(for: key) else {
38+
return nil
39+
}
40+
let symbolOffset: Int? = if let symbolOffset = content.symbolOffset {
41+
.init(bitPattern: symbolOffset)
42+
} else { nil }
43+
44+
return .init(
45+
name: key,
46+
offset: symbolOffset,
47+
flags: content.flags ?? [],
48+
ordinal: content.ordinal,
49+
importedName: content.importedName,
50+
stub: content.stub,
51+
resolver: content.stub
52+
)
53+
}
54+
}
55+
56+
extension TrieTreeProtocol where Content == DylibsTrieNodeContent {
57+
public var dylibIndices: [DylibIndex] {
58+
guard let root = first(where: { _ in true }) else {
59+
return []
60+
}
61+
var result: [(String, Content)] = []
62+
_recurseTrie(currentName: "", entry: root, result: &result)
63+
return result.map {
64+
.init(name: $0, index: $1.index)
65+
}
66+
}
67+
68+
public func search(for key: String) -> DylibIndex? {
69+
guard let (_, content) = _search(for: key) else {
70+
return nil
71+
}
72+
return.init(name: key, index: content.index)
73+
}
74+
}
75+
76+
extension TrieTreeProtocol where Content == ProgramsTrieNodeContent {
77+
public var programOffsets: [ProgramOffset] {
78+
guard let root = first(where: { _ in true }) else {
79+
return []
80+
}
81+
var result: [(String, Content)] = []
82+
_recurseTrie(currentName: "", entry: root, result: &result)
83+
return result.map {
84+
.init(name: $0, offset: $1.offset)
85+
}
86+
}
87+
88+
public func search(for key: String) -> ProgramOffset? {
89+
guard let (_, content) = _search(for: key) else {
90+
return nil
91+
}
92+
return.init(name: key, offset: content.offset)
93+
}
94+
}

‎Sources/MachOKit/LoadCommand/Model/Version.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import Foundation
1010

11-
public struct Version {
11+
public struct Version: Equatable {
1212
public let major: Int
1313
public let minor: Int
1414
public let patch: Int
@@ -30,6 +30,15 @@ extension Version: CustomStringConvertible {
3030
}
3131
}
3232

33+
extension Version: Comparable {
34+
public static func < (lhs: Version, rhs: Version) -> Bool {
35+
if lhs.major != rhs.major { return lhs.major < rhs.major }
36+
if lhs.minor != rhs.minor { return lhs.minor < rhs.minor }
37+
if lhs.patch != rhs.patch { return lhs.patch < rhs.patch }
38+
return false
39+
}
40+
}
41+
3342
public struct SourceVersion {
3443
public let a: Int
3544
public let b: Int
+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//
2+
// MachOFile+ExportTrie.swift
3+
//
4+
//
5+
// Created by p-x9 on 2023/12/09.
6+
//
7+
//
8+
9+
import Foundation
10+
11+
extension MachOFile {
12+
// https://github.com/apple-oss-distributions/dyld/blob/65bbeed63cec73f313b1d636e63f243964725a9d/mach_o/ExportsTrie.cpp
13+
// https://github.com/apple-oss-distributions/ld64/blob/47f477cb721755419018f7530038b272e9d0cdea/src/mach_o/ExportsTrie.cpp
14+
public struct ExportTrie: Sequence {
15+
public typealias Wrapped = DataTrieTree<ExportTrieNodeContent>
16+
17+
public let exportOffset: Int
18+
public let exportSize: Int
19+
let ldVersion: Version?
20+
21+
let wrapped: Wrapped
22+
23+
var isPreDyld_1008: Bool {
24+
if let ldVersion {
25+
// Xcode 15.0 beta 1 (15A5160n)
26+
return ldVersion < .init(major: 1008, minor: 7, patch: 0)
27+
}
28+
return false // fallback
29+
}
30+
31+
public var data: Data {
32+
wrapped.data
33+
}
34+
35+
public func makeIterator() -> Iterator {
36+
.init(
37+
wrapped: wrapped.makeIterator(),
38+
isPreDyld_1008: isPreDyld_1008
39+
)
40+
}
41+
}
42+
}
43+
44+
extension MachOFile.ExportTrie {
45+
/// All exported symbols from the trie tree
46+
public var exportedSymbols: [ExportedSymbol] {
47+
wrapped.exportedSymbols
48+
}
49+
50+
/// Elements of each of the nodes that make up the trie tree
51+
///
52+
/// It is obtained by traversing the nodes of the trie tree.It is obtained by traversing a trie tree.
53+
/// Slower than using `ExportTrie` iterator, but compatible with all Linker(ld) versions
54+
public var entries: [ExportTrieEntry] {
55+
wrapped.entries
56+
}
57+
58+
/// Search the trie tree by symbol name to get the expoted symbol
59+
/// - Parameter key: symbol name
60+
/// - Returns: If found, retruns exported symbol
61+
public func search(for key: String) -> ExportedSymbol? {
62+
wrapped.search(for: key)
63+
}
64+
}
65+
66+
extension MachOFile.ExportTrie {
67+
public struct Iterator: IteratorProtocol {
68+
public typealias Element = Wrapped.Element
69+
70+
private var wrapped: Wrapped.Iterator
71+
let isPreDyld_1008: Bool
72+
73+
@_spi(Support)
74+
public init(wrapped: Wrapped.Iterator, isPreDyld_1008: Bool) {
75+
self.wrapped = wrapped
76+
self.isPreDyld_1008 = isPreDyld_1008
77+
}
78+
79+
public mutating func next() -> Element? {
80+
let isRoot = wrapped.nextOffset == 0
81+
82+
guard let next = wrapped.next() else {
83+
return nil
84+
}
85+
86+
// HACK: for after dyld-1008.7
87+
if isRoot && !isPreDyld_1008 {
88+
// ref: https://github.com/apple-oss-distributions/dyld/blob/main/mach_o/ExportsTrie.cpp#L669-L674
89+
// root is allocated the size that `UINT_MAX` can represent
90+
// 32 / 7
91+
wrapped.nextOffset -= next.children
92+
.map(\.offset.uleb128Size)
93+
.reduce(0, +)
94+
wrapped.nextOffset += 5 * next.children.count
95+
}
96+
97+
return next
98+
}
99+
}
100+
}
101+
102+
extension MachOFile.ExportTrie {
103+
private init(
104+
machO: MachOFile,
105+
exportOffset: Int,
106+
exportSize: Int,
107+
ldVersion: Version?
108+
) {
109+
let offset = machO.headerStartOffset + exportOffset
110+
let data = machO.fileHandle.readData(
111+
offset: numericCast(offset),
112+
size: exportSize
113+
)
114+
115+
self.init(
116+
exportOffset: exportOffset,
117+
exportSize: exportSize,
118+
ldVersion: ldVersion,
119+
wrapped: .init(data: data)
120+
)
121+
}
122+
123+
init(
124+
machO: MachOFile,
125+
info: dyld_info_command,
126+
ldVersion: Version?
127+
) {
128+
self.init(
129+
machO: machO,
130+
exportOffset: numericCast(info.export_off),
131+
exportSize: numericCast(info.export_size),
132+
ldVersion: ldVersion
133+
)
134+
}
135+
136+
init(
137+
machO: MachOFile,
138+
export: linkedit_data_command,
139+
ldVersion: Version?
140+
) {
141+
self.init(
142+
machO: machO,
143+
exportOffset: numericCast(export.dataoff),
144+
exportSize: numericCast(export.datasize),
145+
ldVersion: ldVersion
146+
)
147+
}
148+
}

‎Sources/MachOKit/MachOFile+ExportTrieEntries.swift

-70
This file was deleted.

‎Sources/MachOKit/MachOFile.swift

+22-3
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,22 @@ extension MachOFile {
313313
}
314314

315315
extension MachOFile {
316-
public var exportTrieEntries: ExportTrieEntries? {
316+
public var exportTrie: ExportTrie? {
317+
let ldVersion: Version? = {
318+
loadCommands.info(of: LoadCommand.buildVersion)?
319+
.tools(in: self)
320+
.first(where: { $0.tool == .ld })?
321+
.version
322+
}()
323+
317324
let info = loadCommands.info(of: LoadCommand.dyldInfo) ?? loadCommands.info(of: LoadCommand.dyldInfoOnly)
318325

319326
if let info {
320-
return .init(machO: self, info: info.layout)
327+
return .init(
328+
machO: self,
329+
info: info.layout,
330+
ldVersion: ldVersion
331+
)
321332
}
322333

323334
guard let export = loadCommands.info(of: LoadCommand.dyldExportsTrie) else {
@@ -326,9 +337,17 @@ extension MachOFile {
326337

327338
return .init(
328339
machO: self,
329-
export: export.layout
340+
export: export.layout,
341+
ldVersion: ldVersion
330342
)
331343
}
344+
345+
public var exportedSymbols: [ExportedSymbol] {
346+
guard let exportTrie else {
347+
return []
348+
}
349+
return exportTrie.exportedSymbols
350+
}
332351
}
333352

334353
extension MachOFile {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//
2+
// MachOImage+ExportTrie.swift
3+
//
4+
//
5+
// Created by p-x9 on 2023/12/04.
6+
//
7+
//
8+
9+
import Foundation
10+
11+
extension MachOImage {
12+
public struct ExportTrie: Sequence {
13+
public typealias Wrapped = MemoryTrieTree<ExportTrieNodeContent>
14+
15+
private let wrapped: MemoryTrieTree<ExportTrieNodeContent>
16+
let ldVersion: Version?
17+
18+
var isPreDyld_1008: Bool {
19+
if let ldVersion {
20+
// Initial version of ld-prime
21+
// Xcode 15.0 beta 1 (15A5160n)
22+
return ldVersion < .init(major: 1008, minor: 7, patch: 0)
23+
}
24+
return false // fallback
25+
}
26+
27+
public var basePointer: UnsafeRawPointer {
28+
wrapped.basePointer
29+
}
30+
public var exportSize: Int {
31+
wrapped.size
32+
}
33+
34+
init(
35+
wrapped: MemoryTrieTree<ExportTrieNodeContent>,
36+
ldVersion: Version?
37+
) {
38+
self.wrapped = wrapped
39+
self.ldVersion = ldVersion
40+
}
41+
42+
public func makeIterator() -> Iterator {
43+
.init(
44+
wrapped: wrapped.makeIterator(),
45+
isPreDyld_1008: isPreDyld_1008
46+
)
47+
}
48+
}
49+
}
50+
51+
extension MachOImage.ExportTrie {
52+
/// All exported symbols from the trie tree
53+
public var exportedSymbols: [ExportedSymbol] {
54+
wrapped.exportedSymbols
55+
}
56+
57+
/// Elements of each of the nodes that make up the trie tree
58+
///
59+
/// It is obtained by traversing the nodes of the trie tree.It is obtained by traversing a trie tree.
60+
/// Slower than using `ExportTrie` iterator, but compatible with all Linker(ld) versions
61+
public var entries: [ExportTrieEntry] {
62+
wrapped.entries
63+
}
64+
65+
/// Search the trie tree by symbol name to get the expoted symbol
66+
/// - Parameter key: symbol name
67+
/// - Returns: If found, retruns exported symbol
68+
public func search(for key: String) -> ExportedSymbol? {
69+
wrapped.search(for: key)
70+
}
71+
}
72+
73+
extension MachOImage.ExportTrie {
74+
public struct Iterator: IteratorProtocol {
75+
public typealias Element = Wrapped.Element
76+
77+
private var wrapped: Wrapped.Iterator
78+
let isPreDyld_1008: Bool
79+
80+
@_spi(Support)
81+
public init(wrapped: Wrapped.Iterator, isPreDyld_1008: Bool) {
82+
self.wrapped = wrapped
83+
self.isPreDyld_1008 = isPreDyld_1008
84+
}
85+
86+
public mutating func next() -> Element? {
87+
let isRoot = wrapped.nextOffset == 0
88+
89+
guard let next = wrapped.next() else {
90+
return nil
91+
}
92+
93+
// HACK: for after dyld-1008.7
94+
if isRoot && !isPreDyld_1008 {
95+
// ref: https://github.com/apple-oss-distributions/dyld/blob/main/mach_o/ExportsTrie.cpp#L669-L674
96+
// root is allocated the size that `UINT_MAX` can represent
97+
// 32 / 7
98+
wrapped.nextOffset -= next.children
99+
.map(\.offset.uleb128Size)
100+
.reduce(0, +)
101+
wrapped.nextOffset += 5 * next.children.count
102+
}
103+
104+
return next
105+
}
106+
}
107+
}
108+
109+
extension MachOImage.ExportTrie {
110+
init(
111+
ptr: UnsafeRawPointer,
112+
text: SegmentCommand64,
113+
linkedit: SegmentCommand64,
114+
info: dyld_info_command,
115+
ldVersion: Version?
116+
) {
117+
let fileSlide = Int(linkedit.vmaddr) - Int(text.vmaddr) - Int(linkedit.fileoff)
118+
let ptr = ptr
119+
.advanced(by: Int(info.export_off))
120+
.advanced(by: Int(fileSlide))
121+
.assumingMemoryBound(to: UInt8.self)
122+
123+
self.init(
124+
wrapped: .init(basePointer: ptr, size: Int(info.export_size)),
125+
ldVersion: ldVersion
126+
)
127+
}
128+
129+
init(
130+
ptr: UnsafeRawPointer,
131+
text: SegmentCommand,
132+
linkedit: SegmentCommand,
133+
info: dyld_info_command,
134+
ldVersion: Version?
135+
) {
136+
let fileSlide = Int(linkedit.vmaddr) - Int(text.vmaddr) - Int(linkedit.fileoff)
137+
let ptr = ptr
138+
.advanced(by: Int(info.export_off))
139+
.advanced(by: Int(fileSlide))
140+
.assumingMemoryBound(to: UInt8.self)
141+
142+
self.init(
143+
wrapped: .init(basePointer: ptr, size: Int(info.export_size)),
144+
ldVersion: ldVersion
145+
)
146+
}
147+
148+
init(
149+
linkedit: SegmentCommand64,
150+
export: linkedit_data_command,
151+
vmaddrSlide: Int,
152+
ldVersion: Version?
153+
) {
154+
155+
let linkeditStart = vmaddrSlide + Int(linkedit.layout.vmaddr - linkedit.layout.fileoff)
156+
let ptr = UnsafeRawPointer(bitPattern: linkeditStart)! // swiftlint:disable:this force_unwrapping
157+
.advanced(by: Int(export.dataoff))
158+
.assumingMemoryBound(to: UInt8.self)
159+
160+
self.init(
161+
wrapped: .init(basePointer: ptr, size: Int(export.datasize)),
162+
ldVersion: ldVersion
163+
)
164+
}
165+
166+
init(
167+
linkedit: SegmentCommand,
168+
export: linkedit_data_command,
169+
vmaddrSlide: Int,
170+
ldVersion: Version?
171+
) {
172+
let linkeditStart = vmaddrSlide + Int(linkedit.layout.vmaddr - linkedit.layout.fileoff)
173+
let ptr = UnsafeRawPointer(bitPattern: linkeditStart)! // swiftlint:disable:this force_unwrapping
174+
.advanced(by: Int(export.dataoff))
175+
.assumingMemoryBound(to: UInt8.self)
176+
177+
self.init(
178+
wrapped: .init(basePointer: ptr, size: Int(export.datasize)),
179+
ldVersion: ldVersion
180+
)
181+
}
182+
}

‎Sources/MachOKit/MachOImage+ExportTrieEntries.swift

-99
This file was deleted.

‎Sources/MachOKit/MachOImage.swift

+27-9
Original file line numberDiff line numberDiff line change
@@ -426,26 +426,35 @@ extension MachOImage {
426426
}
427427

428428
extension MachOImage {
429-
public var exportTrieEntries: ExportTrieEntries? {
429+
public var exportTrie: ExportTrie? {
430+
let ldVersion: Version? = {
431+
loadCommands.info(of: LoadCommand.buildVersion)?
432+
.tools(cmdsStart: cmdsStartPtr)
433+
.first(where: { $0.tool == .ld })?
434+
.version
435+
}()
436+
430437
let info = loadCommands.info(of: LoadCommand.dyldInfo) ?? loadCommands.info(of: LoadCommand.dyldInfoOnly)
431438

432439
if let info {
433440
if is64Bit,
434441
let text = loadCommands.text64,
435442
let linkedit = loadCommands.linkedit64 {
436-
return ExportTrieEntries(
443+
return ExportTrie(
437444
ptr: ptr,
438445
text: text,
439446
linkedit: linkedit,
440-
info: info.layout
447+
info: info.layout,
448+
ldVersion: ldVersion
441449
)
442450
} else if let text = loadCommands.text,
443451
let linkedit = loadCommands.linkedit {
444-
return ExportTrieEntries(
452+
return ExportTrie(
445453
ptr: ptr,
446454
text: text,
447455
linkedit: linkedit,
448-
info: info.layout
456+
info: info.layout,
457+
ldVersion: ldVersion
449458
)
450459
}
451460
}
@@ -457,20 +466,29 @@ extension MachOImage {
457466

458467
if is64Bit,
459468
let linkedit = loadCommands.linkedit64 {
460-
return ExportTrieEntries(
469+
return ExportTrie(
461470
linkedit: linkedit,
462471
export: export.layout,
463-
vmaddrSlide: vmaddrSlide
472+
vmaddrSlide: vmaddrSlide,
473+
ldVersion: ldVersion
464474
)
465475
} else if let linkedit = loadCommands.linkedit {
466-
return ExportTrieEntries(
476+
return ExportTrie(
467477
linkedit: linkedit,
468478
export: export.layout,
469-
vmaddrSlide: vmaddrSlide
479+
vmaddrSlide: vmaddrSlide,
480+
ldVersion: ldVersion
470481
)
471482
}
472483
return nil
473484
}
485+
486+
public var exportedSymbols: [ExportedSymbol] {
487+
guard let exportTrie else {
488+
return []
489+
}
490+
return exportTrie.exportedSymbols
491+
}
474492
}
475493

476494
extension MachOImage {

‎Sources/MachOKit/Model/ExportedSymbol.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public struct ExportedSymbol {
1212
public var name: String
1313
/// Symbol offset from start of mach header (`MachO`)
1414
/// Symbol offset from start of file (`MachOFile`)
15-
public var offset: Int
15+
public var offset: Int?
1616

1717
var flags: ExportSymbolFlags
1818

‎Sources/MachOKit/Protocol/DyldCacheRepresentable.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ public protocol DyldCacheRepresentable {
1414
associatedtype ImageInfos: RandomAccessCollection<DyldCacheImageInfo>
1515
associatedtype ImageTextInfos: RandomAccessCollection<DyldCacheImageTextInfo>
1616
associatedtype SubCaches: RandomAccessCollection<DyldSubCacheEntry>
17-
associatedtype DylibsTrieEntries: TrieTreeProtocol<DylibsTrieNodeContent>
18-
associatedtype ProgramsTrieEntries: TrieTreeProtocol<ProgramsTrieNodeContent>
17+
associatedtype DylibsTrie: TrieTreeProtocol<DylibsTrieNodeContent>
18+
associatedtype ProgramsTrie: TrieTreeProtocol<ProgramsTrieNodeContent>
1919

2020
/// Byte size of header
2121
var headerSize: Int { get }
@@ -55,7 +55,7 @@ public protocol DyldCacheRepresentable {
5555
/// Dylibs trie is for searching by dylib name.
5656
///
5757
/// The ``dylibIndices`` are retrieved from this trie tree.
58-
var dylibsTrieEntries: DylibsTrieEntries? { get }
58+
var dylibsTrie: DylibsTrie? { get }
5959
/// Array of Dylib name-index pairs
6060
///
6161
/// This index matches the index in the dylib image list that can be retrieved from imagesOffset.
@@ -70,7 +70,7 @@ public protocol DyldCacheRepresentable {
7070
/// Pair of program name/cdhash and offset to prebuiltLoaderSet
7171
///
7272
/// The ``programOffsets`` are retrieved from this trie tree.
73-
var programsTrieEntries: ProgramsTrieEntries? { get }
73+
var programsTrie: ProgramsTrie? { get }
7474
/// Pair of program name/cdhash and offset to prebuiltLoaderSet
7575
///
7676
/// Example:

‎Sources/MachOKit/Protocol/MachORepresentable.swift

+3-10
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public protocol MachORepresentable {
1616
associatedtype IndirectSymbols: RandomAccessCollection<IndirectSymbol>
1717
associatedtype RebaseOperations: Sequence<RebaseOperation>
1818
associatedtype BindOperations: Sequence<BindOperation>
19-
associatedtype ExportTrieEntries: Sequence<ExportTrieEntry>
19+
associatedtype ExportTrie: Sequence<ExportTrieEntry>
2020
associatedtype Strings: Sequence<StringTableEntry>
2121
associatedtype FunctionStarts: Sequence<FunctionStart>
2222
associatedtype DataInCode: RandomAccessCollection<DataInCodeEntry>
@@ -97,11 +97,11 @@ public protocol MachORepresentable {
9797
/// Sequence of export tries
9898
///
9999
/// If LC_DYLD_INFO(LC_DYLD_INFO_ONLY) does not exist, look for LC_DYLD_EXPORTS_TRIE
100-
var exportTrieEntries: ExportTrieEntries? { get }
100+
var exportTrie: ExportTrie? { get }
101101

102102
/// List of export symbols
103103
///
104-
/// It is obtained by parsing ``exportTrieEntries``
104+
/// It is obtained by parsing ``exportTrie``
105105
var exportedSymbols: [ExportedSymbol] { get }
106106

107107
/// List of binding symbols
@@ -252,13 +252,6 @@ extension MachORepresentable {
252252
}
253253

254254
extension MachORepresentable {
255-
public var exportedSymbols: [ExportedSymbol] {
256-
guard let exportTrieEntries else {
257-
return []
258-
}
259-
return exportTrieEntries.exportedSymbols
260-
}
261-
262255
public var bindingSymbols: [BindingSymbol] {
263256
guard let bindOperations else {
264257
return []

‎Sources/MachOKit/Util/TrieTree/DataTrieTree.swift

+17-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ public struct DataTrieTree<Content: TrieNodeContent>: TrieTreeProtocol {
1919
}
2020
}
2121

22+
extension DataTrieTree {
23+
public func element(atOffset offset: Int) -> TrieNode<Content>? {
24+
var nextOffset: Int = offset
25+
26+
return data.withUnsafeBytes {
27+
guard let basePointer = $0.baseAddress else { return nil }
28+
29+
return .readNext(
30+
basePointer: basePointer.assumingMemoryBound(to: UInt8.self),
31+
trieSize: data.count,
32+
nextOffset: &nextOffset
33+
)
34+
}
35+
}
36+
}
37+
2238
extension DataTrieTree: Sequence {
2339
public typealias Element = TrieNode<Content>
2440

@@ -30,7 +46,7 @@ extension DataTrieTree: Sequence {
3046
extension DataTrieTree {
3147
public struct Iterator: IteratorProtocol {
3248
private let data: Data
33-
private var nextOffset: Int = 0
49+
internal var nextOffset: Int = 0
3450

3551
@_spi(Support)
3652
public init(data: Data) {

‎Sources/MachOKit/Util/TrieTree/MemoryTrieTree.swift

+12-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ public struct MemoryTrieTree<Content: TrieNodeContent>: TrieTreeProtocol {
1919
}
2020
}
2121

22+
extension MemoryTrieTree {
23+
public func element(atOffset offset: Int) -> TrieNode<Content>? {
24+
var nextOffset: Int = offset
25+
return .readNext(
26+
basePointer: basePointer.assumingMemoryBound(to: UInt8.self),
27+
trieSize: size,
28+
nextOffset: &nextOffset
29+
)
30+
}
31+
}
32+
2233
extension MemoryTrieTree: Sequence {
2334
public typealias Element = TrieNode<Content>
2435

@@ -32,7 +43,7 @@ extension MemoryTrieTree {
3243
public let basePointer: UnsafeRawPointer
3344
public let size: Int
3445

35-
private var nextOffset: Int = 0
46+
internal var nextOffset: Int = 0
3647

3748
@_spi(Support)
3849
public init(basePointer: UnsafeRawPointer, size: Int) {

‎Sources/MachOKit/Util/TrieTree/Protocol/TrieTreeProtocol.swift

+100-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,110 @@
33
//
44
//
55
// Created by p-x9 on 2024/10/06
6-
//
6+
//
77
//
88

99
import Foundation
1010

11+
/// Protocol for structures representing Trie Tree
12+
/// - ``DataTrieTree``: Handles the trie tree contained in ``Data``
13+
/// - ``MemoryTrieTree``: Handles the trie tree exsisted on memory
14+
///
15+
/// It conforms to Sequence and sequentially retrieves the elements of the tree
16+
/// that exist contiguously in memory space.
17+
///
18+
/// To retrieve all elements, it is more accurate to use the `entries` parameter, which traverses each node.
19+
/// This is because some trie trees contain meaningless spaces between elements, which may not be contiguous in memory space.
1120
public protocol TrieTreeProtocol<Content>: Sequence where Element == TrieNode<Content> {
1221
associatedtype Content: TrieNodeContent
22+
23+
func element(atOffset offset: Int) -> Element?
24+
}
25+
26+
extension TrieTreeProtocol {
27+
/// Elements of each of the nodes that make up the trie tree
28+
///
29+
/// It is obtained by traversing the nodes of the trie tree.It is obtained by traversing a trie tree.
30+
/// In the case of traversal by the `Self` iterator, elements of contiguous memory space are retrieved sequentially.
31+
public var entries: [Element] {
32+
guard let root = first(where: { _ in true}) else {
33+
return []
34+
}
35+
var result: [Element] = []
36+
recurseTrie(entry: root, result: &result)
37+
return result
38+
}
39+
40+
private func recurseTrie(
41+
entry: Element,
42+
result: inout [Element]
43+
) {
44+
result.append(entry)
45+
for child in entry.children {
46+
guard let entry = element(atOffset: Int(child.offset)) else {
47+
continue
48+
}
49+
recurseTrie(
50+
entry: entry,
51+
result: &result
52+
)
53+
}
54+
}
55+
}
56+
57+
extension TrieTreeProtocol {
58+
/// Traverses the trie tree to obtain the names and contents of all the terminals.
59+
/// - Parameters:
60+
/// - currentName: current name
61+
/// - entry: Node element that is the root to start scanning
62+
/// - result: All terminal names and contents of the `entry`.
63+
public func _recurseTrie(
64+
currentName: String,
65+
entry: Element,
66+
result: inout [(String, Content)]
67+
) {
68+
if let content = entry.content {
69+
result.append((currentName, content))
70+
}
71+
for child in entry.children {
72+
guard let entry = element(atOffset: Int(child.offset)) else {
73+
continue
74+
}
75+
_recurseTrie(
76+
currentName: currentName + child.label,
77+
entry: entry,
78+
result: &result
79+
)
80+
}
81+
}
82+
}
83+
84+
extension TrieTreeProtocol {
85+
/// Search the trie tree by name to get terminal content and node offset
86+
/// - Parameter key: name
87+
/// - Returns: If found, retruns terminal content and node offset
88+
public func _search(for key: String) -> (offset: Int, content: Content)? {
89+
guard !key.isEmpty else { return nil }
90+
91+
var currentLabel = ""
92+
var current = self.first(where: { _ in true })
93+
94+
while true {
95+
guard let child = current?.children.first(
96+
where: { child in
97+
key.starts(with: currentLabel + child.label)
98+
}
99+
) else { break }
100+
currentLabel += child.label
101+
current = element(atOffset: numericCast(child.offset))
102+
103+
if key == currentLabel {
104+
guard let content = current?.content else {
105+
return nil
106+
}
107+
return (numericCast(child.offset), content)
108+
}
109+
}
110+
return nil
111+
}
13112
}

‎Tests/MachOKitTests/DyldCacheLoadedPrintTests.swift

+10
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,25 @@ final class DyldCacheLoadedPrintTests: XCTestCase {
161161
.sorted(by: { lhs, rhs in
162162
lhs.index < rhs.index
163163
})
164+
let trie = cache.dylibsTrie
164165
for index in indices {
166+
let found = trie?.search(for: index.name)
167+
XCTAssertNotNil(found)
168+
XCTAssertEqual(found?.index, index.index)
169+
165170
print(index.index, index.name)
166171
}
167172
}
168173

169174
func testProgramOffsets() {
170175
let cache = cache!
171176
let programOffsets = cache.programOffsets
177+
let trie = cache.programsTrie
172178
for programOffset in programOffsets {
179+
let found = trie?.search(for: programOffset.name)
180+
XCTAssertNotNil(found)
181+
XCTAssertEqual(found?.offset, programOffset.offset)
182+
173183
print(programOffset.offset, programOffset.name)
174184
}
175185
}

‎Tests/MachOKitTests/DyldCachePrintTests.swift

+9
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,24 @@ final class DyldCachePrintTests: XCTestCase {
168168
.sorted(by: { lhs, rhs in
169169
lhs.index < rhs.index
170170
})
171+
let trie = cache.dylibsTrie
171172
for index in indices {
173+
let found = trie?.search(for: index.name)
174+
XCTAssertNotNil(found)
175+
XCTAssertEqual(found?.index, index.index)
176+
172177
print(index.index, index.name)
173178
}
174179
}
175180

176181
func testProgramOffsets() {
177182
let cache = self.cache1!
178183
let programOffsets = cache.programOffsets
184+
let trie = cache.programsTrie
179185
for programOffset in programOffsets {
186+
let found = trie?.search(for: programOffset.name)
187+
XCTAssertNotNil(found)
188+
XCTAssertEqual(found?.offset, programOffset.offset)
180189
print(programOffset.offset, programOffset.name)
181190
}
182191
}

‎Tests/MachOKitTests/MachOFilePrintTests.swift

+7-3
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@ extension MachOFilePrintTests {
314314
}
315315

316316
func testExportTries() throws {
317-
guard let exportTrieEntries = machO.exportTrieEntries else { return }
318-
for entry in exportTrieEntries {
317+
guard let exportTrie = machO.exportTrie else { return }
318+
for entry in exportTrie.entries {
319319
print(entry)
320320
}
321321
}
@@ -360,7 +360,11 @@ extension MachOFilePrintTests {
360360
func testExportedSymbols() throws {
361361
for symbol in machO.exportedSymbols {
362362
print("----")
363-
print("0x" + String(symbol.offset, radix: 16), symbol.name)
363+
print("0x" + String(symbol.offset ?? 0, radix: 16), symbol.name)
364+
365+
let found = machO.exportTrie?.search(for: symbol.name)
366+
XCTAssertNotNil(found)
367+
XCTAssertEqual(found?.offset, symbol.offset)
364368

365369
print("Flags:", symbol.flags.bits)
366370
if let kind = symbol.flags.kind {

‎Tests/MachOKitTests/MachOPrintTests.swift

+7-3
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,8 @@ extension MachOPrintTests {
245245
}
246246

247247
func testExportTries() throws {
248-
guard let exportTrieEntries = machO.exportTrieEntries else { return }
249-
for entry in exportTrieEntries {
248+
guard let exportTrie = machO.exportTrie else { return }
249+
for entry in exportTrie.entries {
250250
print(entry)
251251
}
252252
}
@@ -291,7 +291,11 @@ extension MachOPrintTests {
291291
func testExportedSymbols() throws {
292292
for symbol in machO.exportedSymbols {
293293
print("----")
294-
print("0x" + String(symbol.offset, radix: 16), symbol.name)
294+
print("0x" + String(symbol.offset ?? 0, radix: 16), symbol.name)
295+
296+
let found = machO.exportTrie?.search(for: symbol.name)
297+
XCTAssertNotNil(found)
298+
XCTAssertEqual(found?.offset, symbol.offset)
295299

296300
print("Flags:", symbol.flags.bits)
297301
if let kind = symbol.flags.kind {

0 commit comments

Comments
 (0)
Please sign in to comment.