Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// ArchitectureValidating.swift

import Foundation

protocol ArchitectureValidating {
/// Validates that the binary at the given path is compatible with the host machine's architecture.
/// - Parameter binaryPath: The path to the binary file to validate.
/// - Throws: `ArchitectureValidatorError.incompatibleArchitecture` if the binary is not compatible.
func validate(binaryPath: String) throws

/// Detects the architecture of the binary at the given path.
/// - Parameter binaryPath: The path to the binary file.
/// - Returns: The detected `Architecture` of the binary.
/// - Throws: `ArchitectureValidatorError.unableToReadBinary` if the binary cannot be read.
func detectArchitecture(at binaryPath: String) throws -> Architecture
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// ArchitectureValidator.swift

import Foundation

struct ArchitectureValidator: ArchitectureValidating {

enum ArchitectureValidatorError: Error, LocalizedError, Equatable {
case incompatibleArchitecture(binary: String, binaryArch: Architecture, hostArch: Architecture)
case unableToReadBinary(path: String)
case unknownArchitecture(path: String)

var errorDescription: String? {
switch self {
case .incompatibleArchitecture(let binary, let binaryArch, let hostArch):
return "Binary '\(binary)' has architecture \(binaryArch.rawValue) but this machine requires \(hostArch.rawValue)."
case .unableToReadBinary(let path):
return "Unable to read binary at path: \(path)"
case .unknownArchitecture(let path):
return "Unable to determine architecture for binary at path: \(path)"
}
}
}

// MARK: - Mach-O Constants

/// Mach-O 64-bit magic number (little-endian)
private static let MH_MAGIC_64: UInt32 = 0xFEEDFACF
/// Mach-O 64-bit magic number (big-endian, reversed)
private static let MH_CIGAM_64: UInt32 = 0xCFFAEDFE
/// Mach-O 32-bit magic number (little-endian)
private static let MH_MAGIC: UInt32 = 0xFEEDFACE
/// Mach-O 32-bit magic number (big-endian, reversed)
private static let MH_CIGAM: UInt32 = 0xCEFAEDFE
/// Universal binary (fat) magic number (big-endian)
private static let FAT_MAGIC: UInt32 = 0xCAFEBABE
/// Universal binary (fat) magic number (little-endian, reversed)
private static let FAT_CIGAM: UInt32 = 0xBEBAFECA
/// Universal binary 64-bit magic number
private static let FAT_MAGIC_64: UInt32 = 0xCAFEBABF
/// Universal binary 64-bit magic number (reversed)
private static let FAT_CIGAM_64: UInt32 = 0xBFBAFECA

/// CPU type for x86_64 (includes CPU_ARCH_ABI64 flag)
private static let CPU_TYPE_X86_64: UInt32 = 0x01000007
/// CPU type for ARM64 (includes CPU_ARCH_ABI64 flag)
private static let CPU_TYPE_ARM64: UInt32 = 0x0100000C

private let fileManager: ArchitectureValidatorFileManaging

init(fileManager: ArchitectureValidatorFileManaging) {
self.fileManager = fileManager
}

// MARK: - ArchitectureValidating

func validate(binaryPath: String) throws {
let architecture = try detectArchitecture(at: binaryPath)

guard architecture.isCompatibleWithHost else {
let binaryName = URL(fileURLWithPath: binaryPath).lastPathComponent
throw ArchitectureValidatorError.incompatibleArchitecture(
binary: binaryName,
binaryArch: architecture,
hostArch: Architecture.host
)
}
}

func detectArchitecture(at binaryPath: String) throws -> Architecture {
guard fileManager.fileExists(atPath: binaryPath),
let data = fileManager.contents(atPath: binaryPath),
data.count >= 8 else {
throw ArchitectureValidatorError.unableToReadBinary(path: binaryPath)
}

let magic = data.withUnsafeBytes { $0.load(as: UInt32.self) }

// Check if it's a universal (fat) binary
if magic == Self.FAT_MAGIC || magic == Self.FAT_CIGAM ||
magic == Self.FAT_MAGIC_64 || magic == Self.FAT_CIGAM_64 {
return try detectFatBinaryArchitectures(data: data, magic: magic, path: binaryPath)
}

// Check if it's a single-architecture Mach-O
if magic == Self.MH_MAGIC_64 || magic == Self.MH_CIGAM_64 ||
magic == Self.MH_MAGIC || magic == Self.MH_CIGAM {
return try detectMachOArchitecture(data: data, magic: magic, path: binaryPath)
}

throw ArchitectureValidatorError.unknownArchitecture(path: binaryPath)
}

// MARK: - Private

private func detectMachOArchitecture(data: Data, magic: UInt32, path: String) throws -> Architecture {
// CPU type is at offset 4 in the Mach-O header
guard data.count >= 8 else {
throw ArchitectureValidatorError.unableToReadBinary(path: path)
}

let needsSwap = (magic == Self.MH_CIGAM_64 || magic == Self.MH_CIGAM)

let cpuType: UInt32 = data.withUnsafeBytes { buffer in
let value = buffer.load(fromByteOffset: 4, as: UInt32.self)
return needsSwap ? value.byteSwapped : value
}

return try architectureFromCPUType(cpuType, path: path)
}

private func detectFatBinaryArchitectures(data: Data, magic: UInt32, path: String) throws -> Architecture {
// Fat header: magic (4 bytes) + nfat_arch (4 bytes)
guard data.count >= 8 else {
throw ArchitectureValidatorError.unableToReadBinary(path: path)
}

let needsSwap = (magic == Self.FAT_CIGAM || magic == Self.FAT_CIGAM_64)
let is64BitFat = (magic == Self.FAT_MAGIC_64 || magic == Self.FAT_CIGAM_64)

let nfatArch: UInt32 = data.withUnsafeBytes { buffer in
let value = buffer.load(fromByteOffset: 4, as: UInt32.self)
return needsSwap ? value.byteSwapped : value
}

// fat_arch struct: cputype (4), cpusubtype (4), offset (4/8), size (4/8), align (4)
// For 32-bit fat: struct is 20 bytes
// For 64-bit fat: struct is 32 bytes
let fatArchSize = is64BitFat ? 32 : 20
let headerSize = 8

var architectures: [Architecture] = []

for i in 0..<Int(nfatArch) {
let archOffset = headerSize + (i * fatArchSize)
guard data.count >= archOffset + 4 else { continue }

let cpuType: UInt32 = data.withUnsafeBytes { buffer in
let value = buffer.load(fromByteOffset: archOffset, as: UInt32.self)
return needsSwap ? value.byteSwapped : value
}

if let arch = try? architectureFromCPUType(cpuType, path: path) {
architectures.append(arch)
}
}

// If we found multiple architectures, it's a universal binary
if architectures.count > 1 {
return .universal
}

// If we found exactly one, return it
if let first = architectures.first {
return first
}

throw ArchitectureValidatorError.unknownArchitecture(path: path)
}

private func architectureFromCPUType(_ cpuType: UInt32, path: String) throws -> Architecture {
switch cpuType {
case Self.CPU_TYPE_ARM64:
return .arm64
case Self.CPU_TYPE_X86_64:
return .x86_64
default:
throw ArchitectureValidatorError.unknownArchitecture(path: path)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// ArchitectureValidatorFileManaging.swift

import Foundation

public protocol ArchitectureValidatorFileManaging {
func contents(atPath path: String) -> Data?
func fileExists(atPath: String) -> Bool
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Foundation

public protocol FileManaging:
ArchitectureValidatorFileManaging,
BinaryFinderFileManaging,
ChecksumValidatorFileManaging,
FileTypeDetectorFileManaging,
Expand Down
10 changes: 10 additions & 0 deletions Sources/LucaCore/Core/Installer/Installer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public struct Installer {
private let printer: Printing
private let binaryFinder: BinaryFinding
private let checksumValidator: ChecksumValidating
private let architectureValidator: ArchitectureValidating
private let fileDownloader: FileDownloading
private let downloader: Downloading
private let permissionManager: PermissionManaging
Expand All @@ -29,6 +30,7 @@ public struct Installer {
self.printer = printer
self.binaryFinder = BinaryFinder(fileManager: fileManager)
self.checksumValidator = ChecksumValidator(fileManager: fileManager)
self.architectureValidator = ArchitectureValidator(fileManager: fileManager)
self.fileDownloader = FileDownloader(session: .shared)
self.downloader = Downloader(fileDownloader: fileDownloader)
self.permissionManager = PermissionManager(fileManager: fileManager)
Expand Down Expand Up @@ -126,6 +128,10 @@ public struct Installer {
return try binaryFinder.findBinary(atPath: installationDestination.path)
}()

let fullBinaryPath = installationDestination.appending(path: binaryPath).path
printer.printFormatted("\(.raw("🔍 Validating architecture for \(tool.name) version \(tool.version)..."))")
try architectureValidator.validate(binaryPath: fullBinaryPath)

let enrichedTool = EnrichedTool(
name: tool.name,
version: tool.version,
Expand All @@ -152,6 +158,10 @@ public struct Installer {
let destinationFile = installationDestination
.appending(components: binaryName)
try fileManager.moveItem(at: downloadedFile, to: destinationFile)

printer.printFormatted("\(.raw("🔍 Validating architecture for \(tool.name) version \(tool.version)..."))")
try architectureValidator.validate(binaryPath: destinationFile.path)

let enrichedTool = EnrichedTool(
name: tool.name,
version: tool.version,
Expand Down
32 changes: 32 additions & 0 deletions Sources/LucaCore/Models/Architecture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Architecture.swift

import Foundation

/// Represents CPU architectures for Mach-O binaries.
public enum Architecture: String, Codable, Equatable, Sendable {
case arm64
case x86_64
case universal

/// The architecture of the current host machine.
public static var host: Architecture {
#if arch(arm64)
return .arm64
#elseif arch(x86_64)
return .x86_64
#else
fatalError("Unsupported architecture")
#endif
}

/// Returns `true` if this architecture is compatible with the host machine.
/// Universal binaries are always compatible.
public var isCompatibleWithHost: Bool {
switch self {
case .universal:
return true
case .arm64, .x86_64:
return self == Architecture.host
}
}
}
Loading
Loading