├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── EmbeddedPropertyList │ ├── BundleVersion.swift │ ├── EmbeddedPropertyList.docc │ └── EmbeddedPropertyList.md │ ├── EmbeddedPropertyListReader.swift │ └── ReadError.swift └── Tests └── EmbeddedPropertyListTests ├── BundleVersionTests.swift ├── ExecutableHelpers.swift └── ReadExternalTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Josh Kaplan (trilemma.dev) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "EmbeddedPropertyList", 8 | platforms: [.macOS(.v10_10)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "EmbeddedPropertyList", 13 | targets: ["EmbeddedPropertyList"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "EmbeddedPropertyList", 24 | dependencies: []), 25 | .testTarget( 26 | name: "EmbeddedPropertyListTests", 27 | dependencies: ["EmbeddedPropertyList"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Using this framework you can read property lists embedded inside of a running executable as well as those of executables 2 | stored on disk. These types of executables are often Command Line Tools. Built-in support is provided for reading both 3 | embedded info and launchd property lists. Custom property list types can also be specified. 4 | 5 | To see a runnable sample app using this framework, check out 6 | [SwiftAuthorizationSample](https://github.com/trilemma-dev/SwiftAuthorizationSample). 7 | 8 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ftrilemma-dev%2FEmbeddedPropertyList%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/trilemma-dev/EmbeddedPropertyList) 9 | 10 | # Usage 11 | Property lists are returned as [`Data`](https://developer.apple.com/documentation/foundation/data) instances. Usually 12 | you'll want to deserialize using one of: 13 | * `ProperyListDecoder`'s 14 | [`decode(_:from:)`](https://developer.apple.com/documentation/foundation/propertylistdecoder/2895397-decode) 15 | to deserialize the `Data` into a [`Decodable`](https://developer.apple.com/documentation/swift/decodable) 16 | * `PropertyListSerialization`'s 17 | [`propertyList(from:options:format:)`](https://developer.apple.com/documentation/foundation/propertylistserialization/1409678-propertylist) 18 | to deserialize the `Data` into an [`NSDictionary`](https://developer.apple.com/documentation/foundation/nsdictionary) 19 | 20 | ### Example — Read Internal, Create `Decodable` 21 | When running inside an executable, decode a launchd property list into a custom `Decodable` struct: 22 | ```swift 23 | struct LaunchdPropertyList: Decodable { 24 | let machServices: [String : Bool] 25 | let label: String 26 | 27 | private enum CodingKeys: String, CodingKey { 28 | case machServices = "MachServices" 29 | case label = "Label" 30 | } 31 | } 32 | 33 | let data = try EmbeddedPropertyListReader.launchd.readInternal() 34 | let plist = try PropertyListDecoder().decode(LaunchdPropertyList.self, from: data) 35 | ``` 36 | 37 | ### Example — Read External, Create `NSDictionary` 38 | For an external executable, deserialize an info property list as an `NSDictionary`: 39 | ```swift 40 | let executableURL = URL(fileUrlWithPath: <# path here #>) 41 | let data = try EmbeddedPropertyListReader.info.readExternal(from: executableURL) 42 | let plist = try PropertyListSerialization.propertyList(from: data, 43 | options: .mutableContainersAndLeaves, 44 | format: nil) as? NSDictionary 45 | ``` 46 | 47 | ### Example — Create `Decodable` Using `BundleVersion` 48 | Decode an info property list, using `BundleVersion` to decode the 49 | [`CFBundleVersion`](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion) 50 | entry: 51 | 52 | ```swift 53 | struct InfoPropertyList: Decodable { 54 | let bundleVersion: BundleVersion 55 | let bundleIdentifier: String 56 | 57 | private enum CodingKeys: String, CodingKey { 58 | case bundleVersion = "CFBundleVersion" 59 | case bundleIdentifier = "CFBundleIdentifier" 60 | } 61 | } 62 | 63 | let data = try EmbeddedPropertyListReader.info.readInternal() 64 | let plist = try PropertyListDecoder().decode(InfoPropertyList.self, from: data) 65 | ``` 66 | -------------------------------------------------------------------------------- /Sources/EmbeddedPropertyList/BundleVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BundleVersion.swift 3 | // EmbeddedPropertyList 4 | // 5 | // Created by Josh Kaplan on 2021-10-13 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents a 11 | /// [`CFBundleVersion`](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion) 12 | /// value as found in info property lists. 13 | /// 14 | /// This struct is capable of representing any version with a format that matches one of: 15 | /// - `major` 16 | /// - `major.minor` 17 | /// - `major.minor.patch` 18 | /// - More values after `patch` may be provided, but will be ignored in comparison and equality checks. 19 | /// 20 | /// `major`, `minor`, and `patch` and any additional values must be representable as `UInt`s. Any values not provided will be represented as `0`. For 21 | /// example if this represents `1.2` then `patch` will be `0`. This matches `CFBundleVersion` semantics. 22 | /// 23 | /// > Note: `CFBundleVersion` does not exclusively represent a **bundle's** version. A Mach-O executable's info property list often contains this key. 24 | public struct BundleVersion: RawRepresentable { 25 | 26 | public typealias RawValue = String 27 | 28 | /// The raw string representation of this version. 29 | public let rawValue: String 30 | 31 | /// The major version. 32 | public let major: UInt 33 | 34 | /// The minor version. 35 | /// 36 | /// `0` if not specified. 37 | public let minor: UInt 38 | 39 | /// The patch version. 40 | /// 41 | /// `0` if not specified. 42 | public let patch: UInt 43 | 44 | /// Initializes from a raw `String` representation. 45 | /// 46 | /// - Parameters: 47 | /// - rawValue: To successfully initialize, the `rawValue` must match one of: 48 | /// - `major` 49 | /// - `major.minor` 50 | /// - `major.minor.patch` 51 | /// - More period-separated values after `patch` may be provided, but will be ignored in comparison and equality checks. 52 | /// 53 | /// Where `major`, `minor`, `patch` and any additional values are representable as `UInt`s. 54 | public init?(rawValue: String) { 55 | self.rawValue = rawValue 56 | 57 | // MARK: Validation 58 | 59 | // Must start with an unsigned integer, not a separator 60 | if rawValue.starts(with: ".") { 61 | return nil 62 | } 63 | 64 | // Must not end with a separator 65 | if rawValue.hasSuffix(".") { 66 | return nil 67 | } 68 | 69 | let versionParts = rawValue.split(separator: ".") 70 | 71 | // At least one part must exist 72 | if versionParts.isEmpty { 73 | return nil 74 | } 75 | 76 | // All parts must be unsigned integers 77 | var uintParts = [UInt]() 78 | for versionPart in versionParts { 79 | if let part = UInt(versionPart) { 80 | uintParts.append(part) 81 | } else { 82 | return nil 83 | } 84 | } 85 | 86 | // MARK: Initialization 87 | 88 | if uintParts.count == 1 { 89 | self.major = uintParts[0] 90 | self.minor = 0 91 | self.patch = 0 92 | } else if uintParts.count == 2 { 93 | self.major = uintParts[0] 94 | self.minor = uintParts[1] 95 | self.patch = 0 96 | } else { 97 | self.major = uintParts[0] 98 | self.minor = uintParts[1] 99 | self.patch = uintParts[2] 100 | } 101 | } 102 | } 103 | 104 | extension BundleVersion: CustomStringConvertible { 105 | /// A textual representation of this version. 106 | public var description: String { 107 | return "\(self.major).\(self.minor).\(self.patch) (\(self.rawValue))" 108 | } 109 | } 110 | 111 | extension BundleVersion: Hashable { 112 | /// Hashes this version. 113 | /// 114 | /// Hashing does not take ``rawValue-swift.property`` into account, this means the following will hash identically: 115 | /// - `1` and `1.0.0` 116 | /// - `1.2` and `1.2.0` 117 | /// - `1.2.3` and `1.2.3.4` 118 | public func hash(into hasher: inout Hasher) { 119 | hasher.combine(self.major) 120 | hasher.combine(self.minor) 121 | hasher.combine(self.patch) 122 | } 123 | 124 | /// Determines equality of two `Version` instances. 125 | /// 126 | /// The ``rawValue-swift.property`` is not considered, this means the following will evaluate as equal: 127 | /// - `1` and `1.0.0` 128 | /// - `1.2` and `1.2.0` 129 | /// - `1.2.3` and `1.2.3.4` 130 | public static func == (lhs: BundleVersion, rhs: BundleVersion) -> Bool { 131 | return (lhs.major == rhs.major) && (lhs.minor == rhs.minor) && (lhs.patch == rhs.patch) 132 | } 133 | } 134 | 135 | extension BundleVersion: Comparable { 136 | /// Semantically compares two `Version` instances. 137 | /// 138 | /// When comparing versions, any values beyond ``patch`` will not be taken into account. 139 | public static func < (lhs: BundleVersion, rhs: BundleVersion) -> Bool { 140 | var lessThan = false 141 | if lhs.major < rhs.major { 142 | lessThan = true 143 | } else if lhs.major == rhs.major, lhs.minor < rhs.minor { 144 | lessThan = true 145 | } else if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch < rhs.patch { 146 | lessThan = true 147 | } 148 | 149 | return lessThan 150 | } 151 | } 152 | 153 | extension BundleVersion: Decodable { 154 | /// Initializes from an encoded representation. 155 | /// 156 | /// - Parameter decoder: Decoder containing an encoded representation of a version. 157 | public init(from decoder: Decoder) throws { 158 | let container = try decoder.singleValueContainer() 159 | let rawValue = try container.decode(String.self) 160 | if let bundleVersion = BundleVersion(rawValue: rawValue) { 161 | self = bundleVersion 162 | } else { 163 | let context = DecodingError.Context(codingPath: container.codingPath, 164 | debugDescription: "\(rawValue) is not a valid build number", 165 | underlyingError: nil) 166 | throw DecodingError.dataCorrupted(context) 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Sources/EmbeddedPropertyList/EmbeddedPropertyList.docc/EmbeddedPropertyList.md: -------------------------------------------------------------------------------- 1 | # ``EmbeddedPropertyList`` 2 | 3 | Read property lists embedded inside of Mach-O executables. 4 | 5 | ## Overview 6 | Using this framework you can read property lists embedded inside of a running executable as well as those of 7 | executables stored on disk. These types of executables are often Command Line Tools. Built-in support is provided for 8 | reading both embedded info and launchd property lists. Custom property list types can also be specified. 9 | 10 | > Note: Only 64-bit Intel and ARM executables (or universal binary slices) are supported. Mac OS X 10.6 11 | Snow Leopard was the last 32-bit OS. macOS 10.14 Mojave was the last to run 32-bit binaries. 12 | 13 | ## Usage 14 | Property lists are returned as [`Data`](https://developer.apple.com/documentation/foundation/data) instances. Usually 15 | you'll want to deserialize using one of: 16 | * `ProperyListDecoder`'s 17 | [`decode(_:from:)`](https://developer.apple.com/documentation/foundation/propertylistdecoder/2895397-decode) 18 | to deserialize the `Data` into a [`Decodable`](https://developer.apple.com/documentation/swift/decodable) 19 | * `PropertyListSerialization`'s 20 | [`propertyList(from:options:format:)`](https://developer.apple.com/documentation/foundation/propertylistserialization/1409678-propertylist) 21 | to deserialize the `Data` into an [`NSDictionary`](https://developer.apple.com/documentation/foundation/nsdictionary) 22 | 23 | #### Example — Read Internal, Create Decodable 24 | When running inside an executable, decode a launchd property list into a custom `Decodable` struct: 25 | ```swift 26 | struct LaunchdPropertyList: Decodable { 27 | let machServices: [String : Bool] 28 | let label: String 29 | 30 | private enum CodingKeys: String, CodingKey { 31 | case machServices = "MachServices" 32 | case label = "Label" 33 | } 34 | } 35 | 36 | let data = try EmbeddedPropertyListReader.launchd.readInternal() 37 | let plist = try PropertyListDecoder().decode(LaunchdPropertyList.self, 38 | from: data) 39 | ``` 40 | 41 | #### Example — Read External, Create NSDictionary 42 | For an external executable, deserialize an info property list as an `NSDictionary`: 43 | ```swift 44 | let executableURL = URL(fileUrlWithPath: <# path here #>) 45 | let data = try EmbeddedPropertyListReader.info.readExternal(from: executableURL) 46 | let plist = try PropertyListSerialization.propertyList(from: data, 47 | options: .mutableContainersAndLeaves, 48 | format: nil) as? NSDictionary 49 | ``` 50 | 51 | #### Example — Create Decodable Using BundleVersion 52 | Decode an info property list, using ``BundleVersion`` to decode the 53 | [`CFBundleVersion`](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion) 54 | entry: 55 | 56 | ```swift 57 | struct InfoPropertyList: Decodable { 58 | let bundleVersion: BundleVersion 59 | let bundleIdentifier: String 60 | 61 | private enum CodingKeys: String, CodingKey { 62 | case bundleVersion = "CFBundleVersion" 63 | case bundleIdentifier = "CFBundleIdentifier" 64 | } 65 | } 66 | 67 | let data = try EmbeddedPropertyListReader.info.readInternal() 68 | let plist = try PropertyListDecoder().decode(InfoPropertyList.self, from: data) 69 | ``` 70 | 71 | #### Comparing Property Lists 72 | In some circumstances you may want to directly compare two property lists. If you want to compare their true on disk 73 | representations, you can 74 | [compare them as `Data` instances](https://developer.apple.com/documentation/foundation/data/2293245). However, because 75 | there are multiple encoding formats for property lists in most cases you should first deserialize them before performing 76 | a comparison. 77 | 78 | ## Topics 79 | 80 | ### Reader 81 | - ``EmbeddedPropertyListReader`` 82 | ### Property List Types 83 | - ``BundleVersion`` 84 | ### Errors 85 | - ``ReadError`` 86 | -------------------------------------------------------------------------------- /Sources/EmbeddedPropertyList/EmbeddedPropertyListReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmbeddedPropertyListReader.swift 3 | // EmbeddedPropertyList 4 | // 5 | // Created by Josh Kaplan on 2021-10-13 6 | // 7 | 8 | import Foundation 9 | 10 | /// Read a property list embedded in a Mach-O executable. 11 | public enum EmbeddedPropertyListReader { 12 | /// An embedded info property list. 13 | case info 14 | /// An embedded launchd property list. 15 | case launchd 16 | /// A custom embedded property list type. 17 | /// 18 | /// The associated value must be the name of the `__TEXT` section within Mach-O header of the executable (or executable slice in the case of a universal 19 | /// binary). 20 | case other(String) 21 | 22 | /// The name of `__TEXT` section within Mach-O header. 23 | private var sectionName: String { 24 | switch self { 25 | case .info: 26 | return "__info_plist" 27 | case .launchd: 28 | return "__launchd_plist" 29 | case .other(let userProvided): 30 | return userProvided 31 | } 32 | } 33 | 34 | /// Read the property list embedded within this executable. 35 | /// 36 | /// - Returns: The property list as data. 37 | public func readInternal() throws -> Data { 38 | // By passing in nil, this returns a handle for the dynamic shared object (shared library) for this executable 39 | guard let handle = dlopen(nil, RTLD_LAZY) else { 40 | throw ReadError.machHeaderExecuteSymbolUnretrievable 41 | } 42 | defer { dlclose(handle) } 43 | 44 | guard let mhExecutePointer = dlsym(handle, MH_EXECUTE_SYM) else { 45 | throw ReadError.machHeaderExecuteSymbolUnretrievable 46 | } 47 | let mhExecuteBoundPointer = mhExecutePointer.assumingMemoryBound(to: mach_header_64.self) 48 | 49 | var size: UInt = 0 50 | guard let section = getsectiondata(mhExecuteBoundPointer, "__TEXT", self.sectionName, &size) else { 51 | throw ReadError.sectionNotFound 52 | } 53 | 54 | return Data(bytes: section, count: Int(size)) 55 | } 56 | 57 | /// Read the property list embedded in an on disk executable. 58 | /// 59 | /// - Parameters: 60 | /// - from: Location of the executable to be read. 61 | /// - forSlice: _Optional_ If this is a univeral binary with multiple architectures supported by this framework, then this slice's property list will be 62 | /// returned. Otherwise this parameter is ignored. By default the property list for the slice corresponding to the CPU type of the Mac running 63 | /// this code will be returned. 64 | /// - Throws: Only 64-bit executables (or 64-bit slices of universal binaries) are supported; if the executable only contains unsupported architectures then 65 | /// ``ReadError/unsupportedArchitecture`` will be thrown. 66 | /// - Returns: The property list as data. 67 | public func readExternal(from executableURL: URL, 68 | forSlice slice: UniversalBinarySliceType = .system) throws -> Data { 69 | try readExternal(from: Data(contentsOf: executableURL), forSlice: slice) 70 | } 71 | 72 | // Actually does the reading, split into a seperate function to improve testability 73 | internal func readExternal(from data: Data, forSlice slice: UniversalBinarySliceType) throws -> Data { 74 | // Determine if this is a Mach-O executable. If it's not, then trying to parse it is very likely to result in 75 | // bad memory access that will crash this process. 76 | let magic = readMagic(data, fromByteOffset: 0) 77 | if !isMagicFat(magic) && !isMagic32(magic) && !isMagic64(magic) { 78 | throw ReadError.notMachOExecutable 79 | } 80 | 81 | // Determine if this is a fat (universal) or single architecture executable. If it's a fat executable we'll need 82 | // to determine the offset of one or more of the architecture "slices" within it so that we can find the plist 83 | // data within those slices. 84 | let machHeaderOffset: UInt32 85 | if isMagicFat(magic) { 86 | let mustSwap = mustSwapEndianness(magic: magic) 87 | let offsets = machHeaderOffsetsForFatExecutable(data: data, mustSwap: mustSwap) 88 | if offsets.values.isEmpty { 89 | throw ReadError.unsupportedArchitecture 90 | } else if offsets.values.count == 1, let offset = offsets.values.first { 91 | machHeaderOffset = offset 92 | } else if let offset = offsets[try slice.asDarwinType()] { 93 | machHeaderOffset = offset 94 | } else { 95 | throw ReadError.universalBinarySliceUnavailable 96 | } 97 | } else { 98 | if !isMagic64(magic) { 99 | // This implementation only supports 64-bit architectures. 100 | throw ReadError.unsupportedArchitecture 101 | } 102 | 103 | // When not a fat executable, the mach header starts at the beginning of the executable 104 | machHeaderOffset = 0 105 | } 106 | 107 | // The getsectbynamefromheader_64 function expects the mach_header_64 pointer to be part of a contiguous block 108 | // of memory that contains (at least) the entire mach-o data structure; passing in a pointer to just the 109 | // mach_header_64 struct which is disassociated from the rest of the header will result in bad memory access and 110 | // therefore crash this process. 111 | // Function source code: https://opensource.apple.com/source/cctools/cctools-895/libmacho/getsecbyname.c.auto.html 112 | let offsetData = data[Data.Index(machHeaderOffset)..(_ data: Data, as type: T.Type, fromByteOffset offset: Int) -> T { 138 | data.withUnsafeBytes { pointer in 139 | pointer.load(fromByteOffset: offset, as: type) 140 | } 141 | } 142 | 143 | /// Reads the magic value from the start of the executable as well as any architecture slices within in it (when a universal binary) 144 | /// 145 | /// - Parameters: 146 | /// - _: The executable's bytes. 147 | /// - fromByteOffset: Relative to the start of `data` where to start reading the magic value. 148 | /// - Returns: The magic value. 149 | private func readMagic(_ data: Data, fromByteOffset offset: Int) -> UInt32 { 150 | return read(data, as: UInt32.self, fromByteOffset: offset) 151 | } 152 | 153 | /// Whether the magic value represents an executable for a 32-bit architecture. 154 | /// 155 | /// If the magic value passed in doesn't represent a executable header and instead represents a fat header, then false will be returned. As such, using this 156 | /// function cannot allow you to distinguish between a fat executable and a 64-bit executable or slice. Use `isMagicFat()` for this purpose. 157 | /// 158 | /// - Parameters: 159 | /// - _: The magic value. 160 | /// - Returns: Whether the magic value represents a 32-bit architecture executable. 161 | private func isMagic32(_ magic: UInt32) -> Bool { 162 | return (magic == MH_MAGIC) || (magic == MH_CIGAM) 163 | } 164 | 165 | /// Whether the magic value represents a executable for a 64-bit architecture. 166 | /// 167 | /// If the magic value passed in doesn't represent an executable header and instead represents a fat header, then false will be returned. As such, using this 168 | /// function cannot allow you to distinguish between a fat executable and a 32-bit executable or slice. Use `isMagicFat()` for this purpose. 169 | /// 170 | /// - Parameters: 171 | /// - _: The magic value. 172 | /// - Returns: Whether the magic value represents a 64-bit architecture executable. 173 | private func isMagic64(_ magic: UInt32) -> Bool { 174 | return (magic == MH_MAGIC_64) || (magic == MH_CIGAM_64) 175 | } 176 | 177 | /// Whether the magic value represents a fat executable (universal binary). 178 | /// 179 | /// - Parameters: 180 | /// - _: The magic value. 181 | /// - Returns: Whether the magic value represents a fat executable (universal binary). 182 | private func isMagicFat(_ magic: UInt32) -> Bool { 183 | return (magic == FAT_MAGIC) || (magic == FAT_CIGAM) 184 | } 185 | 186 | /// Whether endianness of the fat or mach header that proceeds this magic value must be swapped. 187 | /// 188 | /// - Parameters: 189 | /// - magic: The magic value 190 | /// - Returns: Whether the magic value represents the oppositie endianness. 191 | private func mustSwapEndianness(magic: UInt32) -> Bool { 192 | return (magic == MH_CIGAM) || (magic == MH_CIGAM_64) || (magic == FAT_CIGAM) 193 | } 194 | 195 | /// Finds the offsets within the executable of where the mach header for each slice of the fat executable (universal binary) is located. 196 | /// 197 | /// - Parameters: 198 | /// - data: Data representing the fat executable (universal binary). This function assumes, and performs no error checking, that the data passed in 199 | /// represents a fat executable. If it does not, behavior is undefined and it's likely that a fatal bad memory access will occur. 200 | /// - mustSwap: Whether the data representing the fat header must have it endianness swapped. 201 | /// - Returns: A dictionary of CPU types to mach headers within the executable. Only 64-bit CPU types are included. 202 | private func machHeaderOffsetsForFatExecutable(data: Data, mustSwap: Bool) -> [cpu_type_t : UInt32] { 203 | // To populate with offsets 204 | var archOffsets = [cpu_type_t : UInt32]() 205 | 206 | // In practice the fat header and fat arch data is always in big-endian byte order while x86_64 (Intel) and 207 | // arm64 (Apple Silicon) are little-endian. So the byte orders are always going to need to be swapped. The code 208 | // here does not assume this to be true, but it's helpful to keep in mind if ever debugging this code. 209 | var header = read(data, as: fat_header.self, fromByteOffset: 0) 210 | if mustSwap { 211 | swap_fat_header(&header, NXHostByteOrder()) 212 | } 213 | 214 | // Loop through all of the architecture descriptions in the fat executable (in practice there will typically be 215 | // 2). These descriptions start immediately after the fat header, so start the offset there. 216 | var archOffset = MemoryLayout.size 217 | for _ in 0...size 230 | } 231 | 232 | return archOffsets 233 | } 234 | 235 | /// A universal binary slice's CPU type. 236 | public enum UniversalBinarySliceType { 237 | /// Slice type for an Intel 64-bit CPU. 238 | case x86_64 239 | /// Slice type for an ARM 64-bit CPU (also known as Apple Silicon). 240 | case arm64 241 | /// Slice type for the CPU type of the Mac executing this code. 242 | case system 243 | 244 | /// Returns the corresponding Darwin `cpu_type_t` for this enum value. 245 | fileprivate func asDarwinType() throws -> cpu_type_t { 246 | switch self { 247 | case .x86_64: 248 | return CPU_TYPE_X86_64 249 | case .arm64: 250 | return CPU_TYPE_ARM64 251 | case .system: 252 | return try cpuType() 253 | } 254 | } 255 | 256 | /// Determines the CPU type of this Mac. 257 | /// 258 | /// This relies on two `sysctl` calls. More information can be found via `man sysctl`. In particular `sysctl -a` will list all available commands on 259 | /// the system which should include both `hw.cputype` and `hw.cpu64bit_capable`. 260 | private func cpuType() throws -> cpu_type_t { 261 | // Retrieve CPU type 262 | var cpuType = cpu_type_t() 263 | var size = MemoryLayout.size 264 | guard sysctlbyname("hw.cputype", &cpuType, &size, nil, 0) == 0 else { 265 | throw ReadError.architectureNotDetermined 266 | } 267 | 268 | // Determine if this is a 64-bit CPU 269 | var capable64bit = Int32() 270 | size = MemoryLayout.size 271 | guard sysctlbyname("hw.cpu64bit_capable", &capable64bit, &size, nil, 0) == 0 else { 272 | throw ReadError.architectureNotDetermined 273 | } 274 | 275 | // If this 64-bit then adjust accordingly to match the definitions for CPU_TYPE_X86_64 and CPU_TYPE_ARM64 276 | cpuType = (capable64bit == 1) ? (cpuType | CPU_ARCH_ABI64) : cpuType 277 | 278 | return cpuType 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /Sources/EmbeddedPropertyList/ReadError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadError.swift 3 | // EmbeddedPropertyList 4 | // 5 | // Created by Josh Kaplan on 2021-10-13 6 | // 7 | 8 | import Foundation 9 | 10 | /// Errors that may occur while trying to read embedded property lists. 11 | public enum ReadError: Error { 12 | /// The `__TEXT` section describing where the property list is stored was not found in the Mach-O header. 13 | case sectionNotFound 14 | /// The file is not an executable with a Mach-O header. 15 | case notMachOExecutable 16 | /// None of the Mach-O header architectures in the executable are supported. 17 | /// 18 | /// `x86_64` (Intel) and `arm64` (Apple Silicon) architectures are supported. 19 | case unsupportedArchitecture 20 | /// The requested universal binary slice type was not present in the Mach-O executable. 21 | case universalBinarySliceUnavailable 22 | /// The mach header execute symbol for the Mach-O executable could not be retrieved. 23 | /// 24 | /// This is an internal error. 25 | case machHeaderExecuteSymbolUnretrievable 26 | /// The architecture of this Mac could not be determiend. 27 | /// 28 | /// This is an internal error. 29 | case architectureNotDetermined 30 | } 31 | -------------------------------------------------------------------------------- /Tests/EmbeddedPropertyListTests/BundleVersionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BundleVersionTests.swift 3 | // EmbeddedPropertyList 4 | // 5 | // Created by Josh Kaplan on 2021-11-18 6 | // 7 | 8 | import XCTest 9 | @testable import EmbeddedPropertyList 10 | 11 | final class BundleVersionTests: XCTestCase { 12 | func testMajorVersion() { 13 | XCTAssertNotNil(BundleVersion(rawValue: "1")) 14 | XCTAssertNil(BundleVersion(rawValue: "1.")) 15 | XCTAssertNil(BundleVersion(rawValue: "A")) 16 | XCTAssertNil(BundleVersion(rawValue: "")) 17 | XCTAssertNil(BundleVersion(rawValue: ".")) 18 | XCTAssertNil(BundleVersion(rawValue: ".1")) 19 | XCTAssertNil(BundleVersion(rawValue: " .1")) 20 | } 21 | 22 | func testMinorVersion() { 23 | XCTAssertNotNil(BundleVersion(rawValue: "1.2")) 24 | XCTAssertNil(BundleVersion(rawValue: "1.2.")) 25 | XCTAssertNil(BundleVersion(rawValue: "1.B")) 26 | XCTAssertNil(BundleVersion(rawValue: "A.B")) 27 | } 28 | 29 | func testPatchVersion() { 30 | XCTAssertNotNil(BundleVersion(rawValue: "1.2.3")) 31 | XCTAssertNil(BundleVersion(rawValue: "1.2.3.")) 32 | XCTAssertNil(BundleVersion(rawValue: "1.2.C")) 33 | XCTAssertNil(BundleVersion(rawValue: "1.B.C")) 34 | XCTAssertNil(BundleVersion(rawValue: "A.B.C")) 35 | } 36 | 37 | func testPostPatchVersion() { 38 | XCTAssertNotNil(BundleVersion(rawValue: "1.2.3.4")) 39 | XCTAssertNil(BundleVersion(rawValue: "1.2.3.4.")) 40 | XCTAssertNil(BundleVersion(rawValue: "1.2.3.D")) 41 | XCTAssertNil(BundleVersion(rawValue: "1.2.C.D")) 42 | XCTAssertNil(BundleVersion(rawValue: "1.B.C.D")) 43 | XCTAssertNil(BundleVersion(rawValue: "A.B.C.D")) 44 | 45 | XCTAssertNotNil(BundleVersion(rawValue: "1.2.3.4.5")) 46 | XCTAssertNil(BundleVersion(rawValue: "1.2.3.4.5.")) 47 | XCTAssertNil(BundleVersion(rawValue: "1.2.3.4.E")) 48 | XCTAssertNil(BundleVersion(rawValue: "1.2.3.D.E")) 49 | XCTAssertNil(BundleVersion(rawValue: "1.2.C.D.E")) 50 | XCTAssertNil(BundleVersion(rawValue: "1.B.C.D.E")) 51 | XCTAssertNil(BundleVersion(rawValue: "A.B.C.D.E")) 52 | } 53 | 54 | struct Container: Decodable { 55 | let version: BundleVersion 56 | let other: Int 57 | } 58 | 59 | func testDecodeValid() throws { 60 | let versionRawValue = "1.0.6" 61 | let serialized = try PropertyListSerialization.data(fromPropertyList: ["version": versionRawValue, 62 | "other": 5], 63 | format: .xml, 64 | options: 0) 65 | let container = try PropertyListDecoder().decode(Container.self, from: serialized) 66 | XCTAssertEqual(container.version.rawValue, versionRawValue) 67 | } 68 | 69 | func testDecodeInvalid() throws { 70 | let versionRawValue = "1.0.a" 71 | let serialized = try PropertyListSerialization.data(fromPropertyList: ["version": versionRawValue, 72 | "other": 5], 73 | format: .xml, 74 | options: 0) 75 | do { 76 | _ = try PropertyListDecoder().decode(Container.self, from: serialized) 77 | XCTFail("Error expected to be thrown, but was not") 78 | } catch DecodingError.dataCorrupted(_) { 79 | // Expected 80 | } catch { 81 | XCTFail("Unexpected error was thrown: \(error)") 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/EmbeddedPropertyListTests/ReadExternalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadExternalTests.swift 3 | // EmbeddedPropertyList 4 | // 5 | // Created by Josh Kaplan on 2021-11-18 6 | // 7 | 8 | import XCTest 9 | @testable import EmbeddedPropertyList 10 | 11 | final class ReadExternalTests: XCTestCase { 12 | 13 | // MARK: Info 14 | 15 | func testInfoReadIntelx86_64__for_x86_64Slice_IgnoredParameter() throws { 16 | let plistData = try EmbeddedPropertyListReader.info.readExternal(from: TestExecutables.intelx86_64, 17 | forSlice: .x86_64) 18 | let plist = try PropertyListDecoder().decode(InfoPropertyList.self, from: plistData) 19 | XCTAssertEqual(plist.bundleIdentifier, InfoPropertyList.bundleIdentifierValue) 20 | XCTAssertEqual(plist.bundleVersion.rawValue, InfoPropertyList.bundleVersionValue) 21 | } 22 | 23 | func testInfoReadIntelx86_64__for_arm64Slice_IgnoredParameter() throws { 24 | let plistData = try EmbeddedPropertyListReader.info.readExternal(from: TestExecutables.intelx86_64, 25 | forSlice: .arm64) 26 | let plist = try PropertyListDecoder().decode(InfoPropertyList.self, from: plistData) 27 | XCTAssertEqual(plist.bundleIdentifier, InfoPropertyList.bundleIdentifierValue) 28 | XCTAssertEqual(plist.bundleVersion.rawValue, InfoPropertyList.bundleVersionValue) 29 | } 30 | 31 | func testInfoReadUniversalBinary__for_x86_64Slice() throws { 32 | let plistData = try EmbeddedPropertyListReader.info.readExternal(from: TestExecutables.universalBinary, 33 | forSlice: .x86_64) 34 | let plist = try PropertyListDecoder().decode(InfoPropertyList.self, from: plistData) 35 | XCTAssertEqual(plist.bundleIdentifier, InfoPropertyList.bundleIdentifierValue) 36 | XCTAssertEqual(plist.bundleVersion.rawValue, InfoPropertyList.bundleVersionValue) 37 | } 38 | 39 | func testInfoReadUniversalBinary__for_arm64Slice() throws { 40 | let plistData = try EmbeddedPropertyListReader.info.readExternal(from: TestExecutables.universalBinary, 41 | forSlice: .arm64) 42 | let plist = try PropertyListDecoder().decode(InfoPropertyList.self, from: plistData) 43 | XCTAssertEqual(plist.bundleIdentifier, InfoPropertyList.bundleIdentifierValue) 44 | XCTAssertEqual(plist.bundleVersion.rawValue, InfoPropertyList.bundleVersionValue) 45 | } 46 | 47 | // MARK: launchd 48 | 49 | func testLaunchdReadIntelx86_64__for_x86_64Slice_IgnoredParameter() throws { 50 | let plistData = try EmbeddedPropertyListReader.launchd.readExternal(from: TestExecutables.intelx86_64, 51 | forSlice: .x86_64) 52 | let plist = try PropertyListDecoder().decode(LaunchdPropertyList.self, from: plistData) 53 | XCTAssertEqual(plist.label, LaunchdPropertyList.labelValue) 54 | } 55 | 56 | func testLaunchdReadIntelx86_64__for_arm64Slice_IgnoredParameter() throws { 57 | let plistData = try EmbeddedPropertyListReader.launchd.readExternal(from: TestExecutables.intelx86_64, 58 | forSlice: .arm64) 59 | let plist = try PropertyListDecoder().decode(LaunchdPropertyList.self, from: plistData) 60 | XCTAssertEqual(plist.label, LaunchdPropertyList.labelValue) 61 | } 62 | 63 | func testLaunchdReadUniversalBinary__for_x86_64Slice() throws { 64 | let plistData = try EmbeddedPropertyListReader.launchd.readExternal(from: TestExecutables.universalBinary, 65 | forSlice: .x86_64) 66 | let plist = try PropertyListDecoder().decode(LaunchdPropertyList.self, from: plistData) 67 | XCTAssertEqual(plist.label, LaunchdPropertyList.labelValue) 68 | } 69 | 70 | func testLaunchdReadUniversalBinary__for_arm64Slice() throws { 71 | let plistData = try EmbeddedPropertyListReader.launchd.readExternal(from: TestExecutables.universalBinary, 72 | forSlice: .arm64) 73 | let plist = try PropertyListDecoder().decode(LaunchdPropertyList.self, from: plistData) 74 | XCTAssertEqual(plist.label, LaunchdPropertyList.labelValue) 75 | } 76 | } 77 | --------------------------------------------------------------------------------