Skip to content

Commit 25e56cb

Browse files
authored
Merge pull request #267 from p-x9/feature/archive
Support for unix archive (.a) files
2 parents eb30537 + f0f3e6e commit 25e56cb

File tree

8 files changed

+224
-5
lines changed

8 files changed

+224
-5
lines changed

Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ let package = Package(
1515
name: "MachOKit",
1616
targets: ["MachOKit"]
1717
),
18+
.library(
19+
name: "MachOArchiveKit",
20+
targets: ["MachOArchiveKit"]
21+
),
1822
.library(
1923
name: "MachOKitC",
2024
targets: ["MachOKitC"]
@@ -29,6 +33,10 @@ let package = Package(
2933
url: "https://github.com/p-x9/swift-fileio-extra.git",
3034
from: "0.2.2"
3135
),
36+
.package(
37+
url: "https://github.com/p-x9/ObjectArchiveKit.git",
38+
from: "0.3.0"
39+
),
3240
],
3341
targets: [
3442
.target(
@@ -42,13 +50,20 @@ let package = Package(
4250
.enableExperimentalFeature("AccessLevelOnImport", .when(configuration: .debug))
4351
]
4452
),
53+
.target(
54+
name: "MachOArchiveKit",
55+
dependencies: [
56+
"MachOKit",
57+
.product(name: "ObjectArchiveKit", package: "ObjectArchiveKit"),
58+
]
59+
),
4560
.target(
4661
name: "MachOKitC",
4762
publicHeadersPath: "include"
4863
),
4964
.testTarget(
5065
name: "MachOKitTests",
51-
dependencies: ["MachOKit"]
66+
dependencies: ["MachOKit", "MachOArchiveKit"]
5267
)
5368
]
5469
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// ArchiveFile+MachOFile.swift
3+
// MachOKit
4+
//
5+
// Created by p-x9 on 2026/03/17
6+
//
7+
//
8+
9+
import MachOKit
10+
import ObjectArchiveKit
11+
12+
extension ArchiveFile {
13+
/// Creates `MachOFile` instances for all Mach-O members contained in the archive.
14+
///
15+
/// Non-Mach-O members such as symbol tables are skipped automatically.
16+
///
17+
/// - Returns: An array of `MachOFile` instances for members whose payload starts with a Mach-O magic.
18+
/// - Throws: Any error thrown while initializing a `MachOFile`.
19+
public func machOFiles() throws -> [MachOFile] {
20+
try members.compactMap { member in
21+
guard let dataOffset = member.dataOffset(in: self) else {
22+
throw ObjectArchiveKitError.invalidHeader
23+
}
24+
return try? MachOFile(
25+
url: url,
26+
imagePath: member.name(in: self),
27+
headerStartOffset: dataOffset + headerStartOffset,
28+
headerStartOffsetInCache: 0
29+
)
30+
}
31+
}
32+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// FatFile+Archive.swift
3+
// MachOKit
4+
//
5+
// Created by p-x9 on 2026/03/17
6+
//
7+
//
8+
9+
import MachOKit
10+
import ObjectArchiveKit
11+
12+
extension FatFile {
13+
/// Creates `ArchiveFile` instances for all architectures
14+
/// whose slice payload is a Unix `ar` archive.
15+
///
16+
/// This is useful for fat static libraries where each architecture
17+
/// slice contains a separate archive.
18+
///
19+
/// - Returns: An array of `ArchiveFile` instances for archive slices.
20+
/// - Throws: Any error thrown while initializing an `ArchiveFile`.
21+
public func archiveFiles() throws -> [ArchiveFile] {
22+
try arches.compactMap { arch in
23+
try ArchiveFile(
24+
url: url,
25+
headerStartOffset: numericCast(arch.offset),
26+
size: Int(arch.size)
27+
)
28+
}
29+
}
30+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//
2+
// exported.swift
3+
// MachOKit
4+
//
5+
// Created by p-x9 on 2026/03/17
6+
//
7+
//
8+
9+
@_exported import MachOKit
10+
@_exported import ObjectArchiveKit

Sources/MachOKit/FatFile.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Foundation
2020
public class FatFile {
2121

2222
/// The file URL of the fat binary.
23-
let url: URL
23+
public let url: URL
2424

2525
/// File handle used for reading the binary contents.
2626
let fileHandle: FileHandle

Sources/MachOKit/MachOFile.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public class MachOFile: MachORepresentable {
106106
)
107107
}
108108

109-
private init(
109+
package init(
110110
url: URL,
111111
imagePath: String?,
112112
headerStartOffset: Int,
@@ -127,7 +127,11 @@ public class MachOFile: MachORepresentable {
127127
offset: UInt64(headerStartOffset + headerStartOffsetInCache)
128128
)
129129

130-
let isSwapped = header.magic.isSwapped
130+
guard let magic = header.magic else {
131+
throw MachOKitError.invalidMagic
132+
}
133+
134+
let isSwapped = magic.isSwapped
131135
if isSwapped {
132136
swap_mach_header(&header.layout, NXHostByteOrder())
133137
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//
2+
// ArchiveFilePrintTests.swift
3+
// MachOKit
4+
//
5+
// Created by p-x9 on 2026/03/16
6+
//
7+
//
8+
9+
import Foundation
10+
import XCTest
11+
@testable import MachOKit
12+
import ObjectArchiveKit
13+
import MachOArchiveKit
14+
15+
final class ArchiveFileTests: XCTestCase {
16+
var fat: FatFile!
17+
var archive: ArchiveFile!
18+
19+
override func setUp() async throws {
20+
let developerDirectoryURL = try developerDirectoryURL()
21+
let url = developerDirectoryURL
22+
.appendingPathComponent("Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/17/lib/darwin/libclang_rt.osx.a")
23+
let file = try MachOKit.loadFromFile(url: url)
24+
switch file {
25+
case let .fat(fat):
26+
self.fat = fat
27+
let archives = try fat.archiveFiles()
28+
self.archive = archives[0]
29+
case .machO:
30+
XCTFail("Expected archive or fat archive file")
31+
return
32+
}
33+
}
34+
35+
func testDumpFileStructure() throws {
36+
func dump(_ machO: MachOFile, level: Int) {
37+
let path = machO.imagePath
38+
let name = path.split(separator: "/").last ?? ""
39+
print(String(repeating: " ", count: level) + name)
40+
}
41+
42+
func dump(_ fat: FatFile, level: Int) {
43+
print(String(repeating: " ", count: level) + "Fat", fat.url.lastPathComponent)
44+
do {
45+
let machOs = try fat.machOFiles()
46+
for machO in machOs {
47+
dump(machO, level: level + 1)
48+
}
49+
} catch {}
50+
do {
51+
let archives = try fat.archiveFiles()
52+
let archs = fat.arches
53+
for (archive, arch) in zip(archives, archs) {
54+
dump(archive, level: level + 1, cpu: arch.cpu)
55+
}
56+
} catch {}
57+
}
58+
59+
func dump(_ archive: ArchiveFile, level: Int, cpu: CPU) {
60+
print(String(repeating: " ", count: level) + "Archive", cpu)
61+
do {
62+
let machOs = try archive.machOFiles()
63+
for machO in machOs.prefix(5) {
64+
dump(machO, level: level + 1)
65+
}
66+
print(String(repeating: " ", count: level + 1) + "...")
67+
} catch {}
68+
}
69+
70+
dump(fat, level: 0)
71+
}
72+
}
73+
74+
extension ArchiveFileTests {
75+
func testBSDSymbols() throws {
76+
guard let symbolTable = archive.bsdSymbolTable else {
77+
return
78+
}
79+
print("count: \(symbolTable.count)")
80+
print("isSorted: \(symbolTable.isSorted(in: archive))")
81+
for symbol in try symbolTable.entries(in: archive) {
82+
let name = try symbolTable.name(for: symbol, in: archive)
83+
print(name ?? "unknown", symbol.stringOffset, symbol.headerOffset)
84+
}
85+
}
86+
87+
func testDarwin64Symbols() throws {
88+
guard let symbolTable = archive.darwin64SymbolTable else {
89+
return
90+
}
91+
print("count: \(symbolTable.count)")
92+
print("isSorted: \(symbolTable.isSorted(in: archive))")
93+
for symbol in try symbolTable.entries(in: archive) {
94+
let name = try symbolTable.name(for: symbol, in: archive)
95+
print(name ?? "unknown", symbol.stringOffset, symbol.headerOffset)
96+
}
97+
}
98+
}
99+
100+
extension ArchiveFileTests {
101+
private func developerDirectoryURL() throws -> URL {
102+
let process = Process()
103+
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcode-select")
104+
process.arguments = ["-p"]
105+
106+
let pipe = Pipe()
107+
process.standardOutput = pipe
108+
109+
try process.run()
110+
process.waitUntilExit()
111+
112+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
113+
if var path = String(data: data, encoding: .utf8) {
114+
path.removeLast()
115+
return URL(fileURLWithPath: path)
116+
}
117+
fatalError("Failed to read Xcode install path")
118+
}
119+
}

0 commit comments

Comments
 (0)