├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── ObjCDump │ ├── Extension │ │ └── ObjCType+.swift │ ├── Model │ │ ├── ObjCCategoryInfo.swift │ │ ├── ObjCClassInfo.swift │ │ ├── ObjCIvarInfo.swift │ │ ├── ObjCMethodInfo.swift │ │ ├── ObjCPropertyInfo.swift │ │ └── ObjCProtocolInfo.swift │ └── ObjCDump.swift └── ObjCTypeDecodeKit │ ├── Extension │ └── String+.swift │ ├── Model │ ├── Node.swift │ ├── ObjCField.swift │ ├── ObjCMethodType.swift │ ├── ObjCModifier.swift │ ├── ObjCPropertyAttribute.swift │ ├── ObjCType.swift │ └── types.swift │ ├── ObjCMethodTypeDecoder.swift │ ├── ObjCPropertyTypeDecoder.swift │ ├── ObjCTypeDecoder.swift │ └── Protocol │ └── ObjCTypeCodable.swift └── Tests ├── ObjCDumpTests └── ObjCDumpTests.swift └── ObjCTypeDecodeKitTests ├── ObjCMethodTypeDecodeTests.swift ├── ObjCTypeDecodeKitTests.swift └── ObjCTypeEncodeTests.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - README.md 9 | - LICENSE 10 | pull_request: 11 | paths-ignore: 12 | - README.md 13 | - LICENSE 14 | workflow_dispatch: 15 | 16 | permissions: 17 | contents: read 18 | 19 | env: 20 | DEVELOPER_DIR: /Applications/Xcode_15.4.app 21 | 22 | jobs: 23 | build: 24 | name: Build & Test 25 | runs-on: macos-14 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Select Xcode 15 31 | run: sudo xcode-select -s /Applications/Xcode_15.4.app 32 | 33 | - name: Build 34 | run: swift build 35 | 36 | - name: Test 37 | run: swift test 38 | 39 | linux-build: 40 | name: Linux Test 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Install Swift 44 | # WORKAROUND:https://github.com/swift-actions/setup-swift/pull/680 45 | uses: swift-actions/setup-swift@bb83339d1e8577741bdc6c65ba551ce7dc0fb854 46 | with: 47 | swift-version: '5.10.1' 48 | 49 | - uses: actions/checkout@v4 50 | 51 | - name: Test 52 | run: swift test 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 p-x9 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.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ObjCDump", 7 | products: [ 8 | .library( 9 | name: "ObjCDump", 10 | targets: ["ObjCDump"] 11 | ), 12 | ], 13 | targets: [ 14 | .target( 15 | name: "ObjCDump", 16 | dependencies: [ 17 | "ObjCTypeDecodeKit" 18 | ] 19 | ), 20 | .target( 21 | name: "ObjCTypeDecodeKit" 22 | ), 23 | .testTarget( 24 | name: "ObjCDumpTests", 25 | dependencies: ["ObjCDump"] 26 | ), 27 | .testTarget( 28 | name: "ObjCTypeDecodeKitTests", 29 | dependencies: ["ObjCTypeDecodeKit"] 30 | ), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-objc-dump 2 | 3 | A Swift Library for dumping objective-c class/protocol/method/property/ivar informations. 4 | 5 | 6 | 7 | [![Github issues](https://img.shields.io/github/issues/p-x9/swift-objc-dump)](https://github.com/p-x9/swift-objc-dump/issues) 8 | [![Github forks](https://img.shields.io/github/forks/p-x9/swift-objc-dump)](https://github.com/p-x9/swift-objc-dump/network/members) 9 | [![Github stars](https://img.shields.io/github/stars/p-x9/swift-objc-dump)](https://github.com/p-x9/swift-objc-dump/stargazers) 10 | [![Github top language](https://img.shields.io/github/languages/top/p-x9/swift-objc-dump)](https://github.com/p-x9/swift-objc-dump/) 11 | 12 | ## Usage 13 | 14 | Information on Objective-C class/protocol/method/property/ivar can be obtained as follows. 15 | 16 | ```swift 17 | import ObjCDump 18 | 19 | // Class 20 | let info = classInfo(of: NSObject.self) 21 | let methods = info.methods 22 | /* or */ 23 | let methods = methods(of: NSObject.self, isInstance: true) 24 | 25 | // Protocol 26 | let `prtocol` = NSProtocolFromString("NSCoding")! 27 | let info = protocolInfo(of: `protocol`) 28 | let methods = info.optionalMethods 29 | /* or */ 30 | let methods = methods(of: `protocol`, isRequired: false, isInstance: true) 31 | 32 | // Method 33 | let method = class_getInstanceMethod(NSObject.self, NSSelectorFromString("init"))! 34 | let info = ObjCMethodInfo(method) 35 | 36 | // Property 37 | let property = class_getProperty(NSObject.self, "classDescription")! 38 | let info = ObjCPropertyInfo(method) 39 | 40 | // Ivar 41 | let ivar: Ivar = /**/ 42 | let info = ObjCIvarInfo(ivar) 43 | ``` 44 | 45 | ### header string 46 | 47 | It is possible to output Objective-C headers as follows. 48 | 49 | ```swift 50 | import ObjCDump 51 | 52 | let info = classInfo(of: NSObject.self) 53 | print(info.headerString) 54 | 55 | /* 56 | @interface NSObject { 57 | Class isa; 58 | } 59 | 60 | @property(class, readonly) BOOL accessInstanceVariablesDirectly; 61 | 62 | @property(readonly, copy) NSClassDescription *classDescription; 63 | @property(readonly, copy) NSArray *attributeKeys; 64 | 65 | /* ...... */ 66 | 67 | + (BOOL)xr_object:(id)arg0 isEqual:(id)arg1; 68 | + (void)dealloc; 69 | + (id)description; 70 | + (void)doesNotRecognizeSelector:(SEL)arg0; 71 | + (id)init; 72 | + (id)instanceMethodSignatureForSelector:(SEL)arg0; 73 | + (void)load; 74 | + (id)methodSignatureForSelector:(SEL)arg0; 75 | + (id)__allocWithZone_OA:(struct _NSZone { } *)arg0; 76 | 77 | /* ...... */ 78 | 79 | - (id)performSelector:(SEL)arg0 withObject:(id)arg1 withObject:(id)arg2; 80 | - (BOOL)respondsToSelector:(SEL)arg0; 81 | - (id)retain; 82 | - (unsigned long long)retainCount; 83 | - (BOOL)retainWeakReference; 84 | - (id)self; 85 | - (Class)superclass; 86 | - (struct _NSZone { } *)zone; 87 | 88 | @end 89 | */ 90 | ``` 91 | 92 | ### Objective-C type encoding 93 | 94 | The type information obtained at ObjC line time is encoded. 95 | This is the same conversion that can be obtained with the @encode directive in Objective-C. 96 | 97 | [Reference](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100) 98 | 99 | As there are no published functions for decoding this encoded type information, a new decoder has been implemented. 100 | It can be used as follows 101 | 102 | ```swift 103 | import ObjCTypeDecodeKit 104 | 105 | let type = ObjCTypeDecoder.decode("(?=iI{?=i{CGRect={CGPoint=dd}{CGSize=dd}}})") 106 | 107 | print(type.decoded()) 108 | /* 109 | union { 110 | int x0; 111 | unsigned int x1; 112 | struct { 113 | int x0; 114 | struct CGRect { 115 | struct CGPoint { 116 | double x0; 117 | double x1; 118 | } x0; 119 | struct CGSize { 120 | double x0; 121 | double x1; 122 | } x1; 123 | } x1; 124 | } x2; 125 | } 126 | */ 127 | ``` 128 | 129 | ## License 130 | 131 | swift-objc-dump is released under the MIT License. See [LICENSE](./LICENSE) 132 | -------------------------------------------------------------------------------- /Sources/ObjCDump/Extension/ObjCType+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCType+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/23 6 | // 7 | // 8 | 9 | import Foundation 10 | import ObjCTypeDecodeKit 11 | 12 | extension ObjCType { 13 | var decodedStringForArgument: String { 14 | switch self { 15 | case .struct(let name, let field): 16 | if let name { 17 | return name 18 | } else { 19 | let type: ObjCType = .struct(name: nil, fields: field) 20 | return type.decoded(tab: "") 21 | .components(separatedBy: .newlines) 22 | .joined(separator: " ") 23 | } 24 | case .union(let name, let field): 25 | if let name { 26 | return name 27 | } else { 28 | let type: ObjCType = .union(name: nil, fields: field) 29 | return type.decoded(tab: "") 30 | .components(separatedBy: .newlines) 31 | .joined(separator: " ") 32 | } 33 | // Objective-C BOOL types may be represented by signed char or by C/C++ bool types. 34 | // This means that the type encoding may be represented as C(c) or as B. 35 | // [reference](https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/objc.h#L61-L86) 36 | case .char: fallthrough 37 | case .uchar: 38 | return "BOOL" 39 | default: 40 | break 41 | } 42 | 43 | return decoded(tab: "") 44 | .components(separatedBy: .newlines) 45 | .joined(separator: " ") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ObjCDump/Model/ObjCCategoryInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCCategoryInfo.swift 3 | // ObjCDump 4 | // 5 | // Created by p-x9 on 2024/12/11 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// Structure for representing objc category information. 12 | public struct ObjCCategoryInfo { 13 | /// Name of the category 14 | public let name: String 15 | 16 | /// Name of the target class name 17 | public let className: String 18 | 19 | /// List of protocols to which the class conforms. 20 | public let protocols: [ObjCProtocolInfo] 21 | 22 | /// List of class properties held by the category. 23 | public let classProperties: [ObjCPropertyInfo] 24 | /// List of instance properties held by the category. 25 | public let properties: [ObjCPropertyInfo] 26 | 27 | /// List of class methods held by the category. 28 | public let classMethods: [ObjCMethodInfo] 29 | /// List of instance methods held by the category. 30 | public let methods: [ObjCMethodInfo] 31 | 32 | /// Initializes a new instance of `ObjCCategoryInfo`. 33 | /// - Parameters: 34 | /// - name: Name of the category 35 | /// - className: Name of the target class name 36 | /// - protocols: List of protocols to which the class conforms. 37 | /// - classProperties: List of class properties held by the category. 38 | /// - properties: List of instance properties held by the category. 39 | /// - classMethods: List of class methods held by the category. 40 | /// - methods: List of instance methods held by the category. 41 | public init( 42 | name: String, 43 | className: String, 44 | protocols: [ObjCProtocolInfo], 45 | classProperties: [ObjCPropertyInfo], 46 | properties: [ObjCPropertyInfo], 47 | classMethods: [ObjCMethodInfo], 48 | methods: [ObjCMethodInfo] 49 | ) { 50 | self.name = name 51 | self.className = className 52 | self.protocols = protocols 53 | self.classProperties = classProperties 54 | self.properties = properties 55 | self.classMethods = classMethods 56 | self.methods = methods 57 | } 58 | } 59 | 60 | extension ObjCCategoryInfo { 61 | public var headerString: String { 62 | var decl = "@interface \(className) (\(name))" 63 | 64 | if !protocols.isEmpty { 65 | let protocols = protocols.map(\.name) 66 | .joined(separator: ", ") 67 | decl += " <\(protocols)>" 68 | } 69 | 70 | var lines = [decl] 71 | 72 | if !classProperties.isEmpty { 73 | lines.append("") 74 | lines += classProperties.map(\.headerString) 75 | } 76 | if !properties.isEmpty { 77 | lines.append("") 78 | lines += properties.map(\.headerString) 79 | } 80 | 81 | if !classMethods.isEmpty { 82 | lines.append("") 83 | lines += classMethods.map(\.headerString) 84 | } 85 | if !methods.isEmpty { 86 | lines.append("") 87 | lines += methods.map(\.headerString) 88 | } 89 | 90 | lines += ["", "@end"] 91 | 92 | return lines.joined(separator: "\n") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/ObjCDump/Model/ObjCClassInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCClassInfo.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/24 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// Structure for representing objc class information. 12 | public struct ObjCClassInfo { 13 | /// Name of the class 14 | public let name: String 15 | /// Version of the class 16 | public let version: Int32 17 | /// Name of the dynamic library the class originated from. 18 | public let imageName: String? 19 | 20 | /// Size of instances of the class. 21 | public let instanceSize: Int 22 | 23 | /// Super class name of the class 24 | public let superClassName: String? 25 | 26 | /// List of protocols to which the class conforms. 27 | public let protocols: [ObjCProtocolInfo] 28 | 29 | /// List of instance variables held by the class. 30 | public let ivars: [ObjCIvarInfo] 31 | 32 | /// List of class properties held by the class. 33 | public let classProperties: [ObjCPropertyInfo] 34 | /// List of instance properties held by the class. 35 | public let properties: [ObjCPropertyInfo] 36 | 37 | /// List of class methods held by the class. 38 | public let classMethods: [ObjCMethodInfo] 39 | /// List of instance methods held by the class. 40 | public let methods: [ObjCMethodInfo] 41 | 42 | 43 | /// Initializes a new instance of `ObjCClassInfo`. 44 | /// - Parameters: 45 | /// - name: Name of the class 46 | /// - version: Version of the class 47 | /// - imageName: Name of the dynamic library the class originated from. 48 | /// - instanceSize: Size of instances of the class. 49 | /// - superClassName: Super class name of the class 50 | /// - protocols: List of protocols to which the class conforms. 51 | /// - ivars: List of instance variables held by the class. 52 | /// - classProperties: List of class properties held by the class. 53 | /// - properties: List of instance properties held by the class. 54 | /// - classMethods: List of class methods held by the class. 55 | /// - methods: List of instance methods held by the class. 56 | public init( 57 | name: String, 58 | version: Int32, 59 | imageName: String?, 60 | instanceSize: Int, 61 | superClassName: String?, 62 | protocols: [ObjCProtocolInfo], 63 | ivars: [ObjCIvarInfo], 64 | classProperties: [ObjCPropertyInfo], 65 | properties: [ObjCPropertyInfo], 66 | classMethods: [ObjCMethodInfo], 67 | methods: [ObjCMethodInfo] 68 | ) { 69 | self.name = name 70 | self.version = version 71 | self.imageName = imageName 72 | self.instanceSize = instanceSize 73 | self.superClassName = superClassName 74 | self.protocols = protocols 75 | self.ivars = ivars 76 | self.classProperties = classProperties 77 | self.properties = properties 78 | self.classMethods = classMethods 79 | self.methods = methods 80 | } 81 | 82 | #if canImport(ObjectiveC) 83 | /// Initializes a new instance of `ObjCClassInfo`. 84 | /// - Parameter cls: Class of the target for which information is to be obtained. 85 | public init(_ cls: AnyClass) { 86 | // Name 87 | let name = NSStringFromClass(cls) 88 | 89 | // Version 90 | let version = class_getVersion(cls) 91 | 92 | // imageName 93 | let _imageName = class_getImageName(cls) 94 | let imageName: String? = if let _imageName { 95 | String(cString: _imageName) 96 | } else { nil } 97 | 98 | // instance Size 99 | let instanceSize = class_getInstanceSize(cls) 100 | 101 | // super class 102 | let superCls: AnyClass? = class_getSuperclass(cls) 103 | let superClassName: String? = if let superCls { 104 | NSStringFromClass(superCls) 105 | } else { nil } 106 | 107 | self.init( 108 | name: name, 109 | version: version, 110 | imageName: imageName, 111 | instanceSize: instanceSize, 112 | superClassName: superClassName, 113 | protocols: Self.protocols(of: cls), 114 | ivars: Self.ivars(of: cls), 115 | classProperties: Self.properties(of: cls, isInstance: false), 116 | properties: Self.properties(of: cls, isInstance: true), 117 | classMethods: Self.methods(of: cls, isInstance: false), 118 | methods: Self.methods(of: cls, isInstance: true) 119 | ) 120 | } 121 | #endif 122 | } 123 | 124 | extension ObjCClassInfo { 125 | public var headerString: String { 126 | var decl = "@interface \(name)" 127 | if let superClassName { 128 | decl += " : \(superClassName)" 129 | } 130 | 131 | if !protocols.isEmpty { 132 | let protocols = protocols.map(\.name) 133 | .joined(separator: ", ") 134 | decl += " <\(protocols)>" 135 | } 136 | 137 | var lines = [decl] 138 | 139 | if !ivars.isEmpty { 140 | lines[0] += " {" 141 | lines += ivars 142 | .map(\.headerString) 143 | .map { 144 | $0.components(separatedBy: .newlines) 145 | .map { " \($0)" } 146 | .joined(separator: "\n") 147 | } 148 | lines.append("}") 149 | } 150 | 151 | if !classProperties.isEmpty { 152 | lines.append("") 153 | lines += classProperties.map(\.headerString) 154 | } 155 | if !properties.isEmpty { 156 | lines.append("") 157 | lines += properties.map(\.headerString) 158 | } 159 | 160 | if !classMethods.isEmpty { 161 | lines.append("") 162 | lines += classMethods.map(\.headerString) 163 | } 164 | if !methods.isEmpty { 165 | lines.append("") 166 | lines += methods.map(\.headerString) 167 | } 168 | 169 | lines += ["", "@end"] 170 | 171 | return lines.joined(separator: "\n") 172 | } 173 | } 174 | 175 | #if canImport(ObjectiveC) 176 | extension ObjCClassInfo { 177 | public static func protocols( 178 | of cls: AnyClass 179 | ) -> [ObjCProtocolInfo] { 180 | var count: UInt32 = 0 181 | let start = class_copyProtocolList(cls, &count) 182 | guard let start else { return [] } 183 | 184 | defer { 185 | free(.init(start)) 186 | } 187 | return UnsafeBufferPointer(start: start, count: Int(count)) 188 | .compactMap { 189 | ObjCProtocolInfo($0) 190 | } 191 | } 192 | 193 | public static func ivars( 194 | of cls: AnyClass 195 | ) -> [ObjCIvarInfo] { 196 | var count: UInt32 = 0 197 | let start = class_copyIvarList(cls, &count) 198 | guard let start else { return [] } 199 | 200 | defer { 201 | free(.init(start)) 202 | } 203 | return UnsafeBufferPointer(start: start, count: Int(count)) 204 | .compactMap { 205 | ObjCIvarInfo($0) 206 | } 207 | } 208 | 209 | public static func properties( 210 | of cls: AnyClass, 211 | isInstance: Bool 212 | ) -> [ObjCPropertyInfo] { 213 | var cls: AnyClass = cls 214 | if !isInstance { 215 | cls = objc_getMetaClass(NSStringFromClass(cls)) as! AnyClass 216 | } 217 | 218 | var count: UInt32 = 0 219 | let start = class_copyPropertyList(cls, &count) 220 | guard let start else { return [] } 221 | 222 | defer { 223 | free(.init(start)) 224 | } 225 | return UnsafeBufferPointer(start: start, count: Int(count)) 226 | .compactMap { 227 | ObjCPropertyInfo($0, isClassProperty: !isInstance) 228 | } 229 | } 230 | 231 | public static func methods( 232 | of cls: AnyClass, 233 | isInstance: Bool 234 | ) -> [ObjCMethodInfo] { 235 | var cls: AnyClass = cls 236 | if !isInstance { 237 | cls = objc_getMetaClass(NSStringFromClass(cls)) as! AnyClass 238 | } 239 | 240 | var count: UInt32 = 0 241 | let start = class_copyMethodList(cls, &count) 242 | guard let start else { return [] } 243 | 244 | defer { 245 | free(.init(start)) 246 | } 247 | return UnsafeBufferPointer(start: start, count: Int(count)) 248 | .compactMap { 249 | ObjCMethodInfo($0, isClassMethod: !isInstance) 250 | } 251 | } 252 | } 253 | #endif 254 | -------------------------------------------------------------------------------- /Sources/ObjCDump/Model/ObjCIvarInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCIvarInfo.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/23 6 | // 7 | // 8 | 9 | import Foundation 10 | import ObjCTypeDecodeKit 11 | 12 | /// Structure for representing objc instance variable information. 13 | public struct ObjCIvarInfo { 14 | /// Name of the Ivar 15 | public let name: String 16 | /// Encoded type of the Ivar 17 | public let typeEncoding: String 18 | /// Offset of Ivar 19 | public let offset: Int 20 | 21 | /// Initializes a new instance of `ObjCIvarInfo`. 22 | /// - Parameters: 23 | /// - name: Name of the Ivar 24 | /// - typeEncoding: Encoded type of the Ivar 25 | /// - offset: Offset of Ivar 26 | public init( 27 | name: String, 28 | typeEncoding: String, 29 | offset: Int 30 | ) { 31 | self.name = name 32 | self.typeEncoding = typeEncoding 33 | self.offset = offset 34 | } 35 | 36 | #if canImport(ObjectiveC) 37 | /// Initializes a new instance of `ObjCIvarInfo`. 38 | /// - Parameter ivar: Ivar of the target for which information is to be obtained. 39 | public init?(_ ivar: Ivar) { 40 | guard let _name = ivar_getName(ivar), 41 | let _typeEncoding = ivar_getTypeEncoding(ivar) else { 42 | return nil 43 | } 44 | 45 | self.init( 46 | name: String(cString: _name), 47 | typeEncoding: String(cString: _typeEncoding), 48 | offset: ivar_getOffset(ivar) 49 | ) 50 | } 51 | #endif 52 | } 53 | 54 | extension ObjCIvarInfo { 55 | /// Type of Ivar 56 | public var type: ObjCType? { 57 | ObjCTypeDecoder.decode(typeEncoding) 58 | } 59 | } 60 | 61 | extension ObjCIvarInfo { 62 | public var headerString: String { 63 | if let type, case let .bitField(width) = type { 64 | let field = ObjCField( 65 | type: .int, 66 | name: name, 67 | bitWidth: width 68 | ) 69 | return field.decodedForHeader(fallbackName: name) 70 | } else { 71 | if [.char, .uchar].contains(type) { 72 | return "BOOL \(name)" 73 | } 74 | let type = type?.decoded() 75 | if let type, type.last == "*" { 76 | return "\(type)\(name);" 77 | } 78 | return "\(type ?? "unknown") \(name);" 79 | } 80 | } 81 | } 82 | 83 | extension ObjCField { 84 | func decodedForHeader( 85 | fallbackName: String, tab: String = " " 86 | ) -> String { 87 | if [.char, .uchar].contains(type) { 88 | return "BOOL \(name ?? fallbackName);" 89 | } 90 | return decoded(fallbackName: fallbackName, tab: tab) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/ObjCDump/Model/ObjCMethodInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCMethodInfo.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/23 6 | // 7 | // 8 | 9 | import Foundation 10 | import ObjCTypeDecodeKit 11 | 12 | /// Structure for representing objc method information. 13 | @dynamicMemberLookup 14 | public struct ObjCMethodInfo { 15 | /// Name of the method 16 | public let name: String 17 | /// Encoded method type of the method 18 | public let typeEncoding: String 19 | /// A boolean value that indicates whatever the method is class method or not. 20 | public let isClassMethod: Bool 21 | 22 | /// Initializes a new instance of `ObjCMethodInfo`. 23 | /// - Parameters: 24 | /// - name: Name of the method 25 | /// - typeEncoding: Encoded method type of the method 26 | /// - isClassMethod: A boolean value that indicates whatever the method is class method or not. 27 | public init( 28 | name: String, 29 | typeEncoding: String, 30 | isClassMethod: Bool 31 | ) { 32 | self.name = name 33 | self.typeEncoding = typeEncoding 34 | self.isClassMethod = isClassMethod 35 | } 36 | 37 | #if canImport(ObjectiveC) 38 | /// Initializes a new instance of `ObjCMethodInfo`. 39 | /// - Parameters: 40 | /// - method: Method of the target for which information is to be obtained. 41 | /// - isClassMethod: A boolean value that indicates whatever the method is class method or not. 42 | public init?( 43 | _ method: Method, 44 | isClassMethod: Bool 45 | ) { 46 | guard let _typeEncoding = method_getTypeEncoding(method) else { 47 | return nil 48 | } 49 | let _name = method_getName(method) 50 | self.init( 51 | name: NSStringFromSelector(_name), 52 | typeEncoding: String(cString: _typeEncoding), 53 | isClassMethod: isClassMethod 54 | ) 55 | } 56 | 57 | /// Initializes a new instance of `ObjCMethodInfo`. 58 | /// - Parameters: 59 | /// - description: Method description of the target for which information is to be obtained. 60 | /// - isClassMethod: A boolean value that indicates whatever the method is class method or not. 61 | public init?( 62 | _ description: objc_method_description, 63 | isClassMethod: Bool 64 | ) { 65 | guard let _name = description.name, 66 | let _typeEncoding = description.types else { 67 | return nil 68 | } 69 | 70 | self.init( 71 | name: NSStringFromSelector(_name), 72 | typeEncoding: String(cString: _typeEncoding), 73 | isClassMethod: isClassMethod 74 | ) 75 | } 76 | #endif 77 | } 78 | 79 | extension ObjCMethodInfo { 80 | /// Method type of the method 81 | /// 82 | /// It includes the type of the arguments and the type of the return value. 83 | public var type: ObjCMethodType? { 84 | ObjCMethodTypeDecoder.decode(typeEncoding) 85 | } 86 | 87 | public subscript(dynamicMember keyPath: KeyPath) -> V? { 88 | type?[keyPath: keyPath] 89 | } 90 | } 91 | 92 | extension ObjCMethodInfo { 93 | public var headerString: String { 94 | let prefix = isClassMethod ? "+" : "-" 95 | let type: ObjCMethodType? = self.type 96 | 97 | // return type 98 | let returnType = type?.returnType.decodedStringForArgument ?? "unknown" 99 | 100 | // arguments 101 | let numberOfArguments = name.filter({ $0 == ":" }).count 102 | guard numberOfArguments > 0 else { 103 | return "\(prefix) (\(returnType))\(name);" 104 | } 105 | 106 | let nameAndLabels = name.split(separator: ":") 107 | 108 | let argumentInfos = type?.argumentInfos ?? [] 109 | let argumentTypes = argumentInfos.map(\.type.decodedStringForArgument) 110 | 111 | var result = "\(prefix) (\(returnType))" 112 | 113 | zip(nameAndLabels, argumentTypes).enumerated() 114 | .forEach { (i, nameWithType) in 115 | let (name, type) = nameWithType 116 | var entry = "\(name):(\(type))arg\(i)" 117 | if i != 0 { entry = " \(entry)" } 118 | result += entry 119 | } 120 | 121 | result += ";" 122 | 123 | return result 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/ObjCDump/Model/ObjCPropertyInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCPropertyInfo.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/24 6 | // 7 | // 8 | 9 | import Foundation 10 | import ObjCTypeDecodeKit 11 | 12 | /// Structure for representing objc property information. 13 | public struct ObjCPropertyInfo { 14 | /// Name of the property 15 | public let name: String 16 | /// Attributes string of the property 17 | public let attributesString: String 18 | /// A boolean value that indicates whatever the property is class property or not. 19 | public let isClassProperty: Bool 20 | 21 | /// Initializes a new instance of `ObjCPropertyInfo`. 22 | /// - Parameters: 23 | /// - name: Name of the property 24 | /// - attributes: Attributes string of the property 25 | /// - isClassProperty: A boolean value that indicates whatever the property is class property or not. 26 | public init( 27 | name: String, 28 | attributes: String, 29 | isClassProperty: Bool 30 | ) { 31 | self.name = name 32 | self.attributesString = attributes 33 | self.isClassProperty = isClassProperty 34 | } 35 | 36 | #if canImport(ObjectiveC) 37 | /// Initializes a new instance of `ObjCPropertyInfo`. 38 | /// - Parameters: 39 | /// - property: Property of the target for which information is to be obtained. 40 | /// - isClassProperty: A boolean value that indicates whatever the property is class property or not. 41 | public init?( 42 | _ property: objc_property_t, 43 | isClassProperty: Bool 44 | ) { 45 | guard let _attributes = property_getAttributes(property) else { 46 | return nil 47 | } 48 | let _name = property_getName(property) 49 | self.init( 50 | name: String(cString: _name), 51 | attributes: String(cString: _attributes), 52 | isClassProperty: isClassProperty 53 | ) 54 | } 55 | #endif 56 | } 57 | 58 | extension ObjCPropertyInfo { 59 | /// Attribute list of the property 60 | public var attributes: [ObjCPropertyAttribute] { 61 | ObjCPropertyTypeDecoder.decode(attributesString) 62 | } 63 | } 64 | 65 | extension ObjCPropertyInfo { 66 | public var headerString: String { 67 | let attributes = self.attributes 68 | // Type 69 | let type = attributes.compactMap { 70 | if case let .type(type) = $0, let type { return type } 71 | return nil 72 | }.first 73 | let typeString = type?.decodedStringForArgument ?? "unknown" 74 | 75 | // Attributes 76 | var _attributes: [String] = [] 77 | // class 78 | if isClassProperty { 79 | _attributes.append("class") 80 | } 81 | 82 | // getter 83 | if let getter = attributes.compactMap({ 84 | if case let .getter(name) = $0 { return name } 85 | return nil 86 | }).first { 87 | _attributes.append("getter=\(getter)") 88 | } 89 | // setter 90 | if let setter = attributes.compactMap({ 91 | if case let .setter(name) = $0 { return name } 92 | return nil 93 | }).first { 94 | _attributes.append("setter=\(setter)") 95 | } 96 | 97 | // readonly 98 | if attributes.contains(.readonly) { 99 | _attributes.append("readonly") 100 | } 101 | 102 | // weak 103 | if attributes.contains(.weak) { 104 | _attributes.append("weak") 105 | } 106 | // copy 107 | if attributes.contains(.copy) { 108 | _attributes.append("copy") 109 | } 110 | // retain 111 | if attributes.contains(.retain) { 112 | _attributes.append("retain") 113 | } 114 | 115 | // nonatomic 116 | if attributes.contains(.nonatomic) { 117 | _attributes.append("nonatomic") 118 | } 119 | 120 | // Comments 121 | var comments: [String] = [] 122 | if attributes.contains(.dynamic) { 123 | comments.append("@dynamic \(name)") 124 | } 125 | if let ivar = attributes.compactMap({ 126 | if case let .ivar(name) = $0 { return name } 127 | return nil 128 | }).first { 129 | if ivar == name { 130 | comments.append("@synthesize \(ivar)") 131 | } else { 132 | comments.append("@synthesize \(name)=\(ivar)") 133 | } 134 | } 135 | 136 | var result = "@property" 137 | if !_attributes.isEmpty { 138 | result += "(" 139 | result += _attributes.joined(separator: ", ") 140 | result += ")" 141 | } 142 | result += " \(typeString)" 143 | if typeString.last == "*" { 144 | result += "\(name);" 145 | } else { 146 | result += " \(name);" 147 | } 148 | if !comments.isEmpty { 149 | for comment in comments { 150 | result += " // \(comment)" 151 | } 152 | } 153 | 154 | return result 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Sources/ObjCDump/Model/ObjCProtocolInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCProtocolInfo.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/26 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// Structure for representing objc protocol information. 12 | public struct ObjCProtocolInfo { 13 | /// Name of the protocol 14 | public let name: String 15 | 16 | /// List of protocols to which the protocol conforms. 17 | public let protocols: [ObjCProtocolInfo] 18 | 19 | /// List of required class properties. 20 | public let classProperties: [ObjCPropertyInfo] 21 | /// List of required instance properties. 22 | public let properties: [ObjCPropertyInfo] 23 | /// List of required class methods. 24 | public let classMethods: [ObjCMethodInfo] 25 | /// List of required instance methods. 26 | public let methods: [ObjCMethodInfo] 27 | 28 | /// List of optional class properties. 29 | public let optionalClassProperties: [ObjCPropertyInfo] 30 | /// List of optional instance properties. 31 | public let optionalProperties: [ObjCPropertyInfo] 32 | /// List of optional class methods. 33 | public let optionalClassMethods: [ObjCMethodInfo] 34 | /// List of optional instance methods. 35 | public let optionalMethods: [ObjCMethodInfo] 36 | 37 | /// Initializes a new instance of `ObjCProtocolInfo`. 38 | /// - Parameters: 39 | /// - name: Name of the protocol 40 | /// - protocols: List of protocols to which the protocol conforms. 41 | /// - classProperties: List of required class properties. 42 | /// - properties: List of required instance properties. 43 | /// - classMethods: List of required class methods. 44 | /// - methods: List of required instance methods. 45 | /// - optionalClassProperties: List of optional class properties. 46 | /// - optionalProperties: List of optional instance properties. 47 | /// - optionalClassMethods: List of optional class methods. 48 | /// - optionalMethods: List of optional instance methods. 49 | /// 50 | /// Objective-C protocol does not currently support optional properties. 51 | /// [reference](https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/objc-runtime-new.mm#L5255) 52 | public init( 53 | name: String, 54 | protocols: [ObjCProtocolInfo], 55 | classProperties: [ObjCPropertyInfo], 56 | properties: [ObjCPropertyInfo], 57 | classMethods: [ObjCMethodInfo], 58 | methods: [ObjCMethodInfo], 59 | optionalClassProperties: [ObjCPropertyInfo] = [], 60 | optionalProperties: [ObjCPropertyInfo] = [], 61 | optionalClassMethods: [ObjCMethodInfo], 62 | optionalMethods: [ObjCMethodInfo] 63 | ) { 64 | self.name = name 65 | self.protocols = protocols 66 | self.classProperties = classProperties 67 | self.properties = properties 68 | self.classMethods = classMethods 69 | self.methods = methods 70 | self.optionalClassProperties = optionalClassProperties 71 | self.optionalProperties = optionalProperties 72 | self.optionalClassMethods = optionalClassMethods 73 | self.optionalMethods = optionalMethods 74 | } 75 | 76 | #if canImport(ObjectiveC) 77 | /// Initializes a new instance of `ObjCProtocolInfo`. 78 | /// - Parameter `protocol`: Protocol of the target for which information is to be obtained. 79 | public init(_ `protocol`: Protocol) { 80 | // Name 81 | let _name = protocol_getName(`protocol`) 82 | let name = String(cString: _name) 83 | 84 | self.init( 85 | name: name, 86 | protocols: Self.protocols( 87 | of: `protocol` 88 | ), 89 | classProperties: Self.properties( 90 | of: `protocol`, 91 | isRequired: true, isInstance: false 92 | ), 93 | properties: Self.properties( 94 | of: `protocol`, 95 | isRequired: true, isInstance: true 96 | ), 97 | classMethods: Self.methods( 98 | of: `protocol`, 99 | isRequired: true, isInstance: false 100 | ), 101 | methods: Self.methods( 102 | of: `protocol`, 103 | isRequired: true, isInstance: true 104 | ), 105 | optionalClassProperties: Self.properties( 106 | of: `protocol`, 107 | isRequired: false, isInstance: false 108 | ), 109 | optionalProperties: Self.properties( 110 | of: `protocol`, 111 | isRequired: false, 112 | isInstance: true) 113 | , 114 | optionalClassMethods: Self.methods( 115 | of: `protocol`, 116 | isRequired: false, isInstance: false 117 | ), 118 | optionalMethods: Self.methods( 119 | of: `protocol`, 120 | isRequired: false, isInstance: true 121 | ) 122 | ) 123 | } 124 | #endif 125 | } 126 | 127 | extension ObjCProtocolInfo { 128 | public var headerString: String { 129 | var decl = "@protocol \(name)" 130 | if !protocols.isEmpty { 131 | let protocols = protocols.map(\.name) 132 | .joined(separator: ", ") 133 | decl += " <\(protocols)>" 134 | } 135 | 136 | var lines = [decl] 137 | 138 | if !classProperties.isEmpty || 139 | !properties.isEmpty || 140 | !classMethods.isEmpty || 141 | !methods.isEmpty { 142 | lines += ["", "@required"] 143 | } 144 | 145 | if !classProperties.isEmpty { 146 | lines.append("") 147 | lines += classProperties.map(\.headerString) 148 | } 149 | if !properties.isEmpty { 150 | lines.append("") 151 | lines += properties.map(\.headerString) 152 | } 153 | 154 | if !classMethods.isEmpty { 155 | lines.append("") 156 | lines += classMethods.map(\.headerString) 157 | } 158 | if !methods.isEmpty { 159 | lines.append("") 160 | lines += methods.map(\.headerString) 161 | } 162 | 163 | if !optionalClassProperties.isEmpty || 164 | !optionalProperties.isEmpty || 165 | !optionalClassMethods.isEmpty || 166 | !optionalMethods.isEmpty { 167 | lines += ["", "@optional"] 168 | } 169 | 170 | if !optionalClassProperties.isEmpty { 171 | lines.append("") 172 | lines += optionalClassProperties.map(\.headerString) 173 | } 174 | if !optionalProperties.isEmpty { 175 | lines.append("") 176 | lines += optionalProperties.map(\.headerString) 177 | } 178 | if !optionalClassMethods.isEmpty { 179 | lines.append("") 180 | lines += optionalClassMethods.map(\.headerString) 181 | } 182 | if !optionalMethods.isEmpty { 183 | lines.append("") 184 | lines += optionalMethods.map(\.headerString) 185 | } 186 | 187 | lines += ["", "@end"] 188 | 189 | return lines.joined(separator: "\n") 190 | } 191 | } 192 | 193 | #if canImport(ObjectiveC) 194 | extension ObjCProtocolInfo { 195 | public static func protocols( 196 | of `protocol`: Protocol 197 | ) -> [ObjCProtocolInfo] { 198 | var count: UInt32 = 0 199 | let start = protocol_copyProtocolList(`protocol`, &count) 200 | guard let start else { return [] } 201 | 202 | defer { 203 | free(.init(start)) 204 | } 205 | return UnsafeBufferPointer(start: start, count: Int(count)) 206 | .compactMap { 207 | ObjCProtocolInfo($0) 208 | } 209 | } 210 | 211 | public static func properties( 212 | of `protocol`: Protocol, 213 | isRequired: Bool, 214 | isInstance: Bool 215 | ) -> [ObjCPropertyInfo] { 216 | var count: UInt32 = 0 217 | let start = protocol_copyPropertyList2(`protocol`, &count, isRequired, isInstance) 218 | guard let start else { return [] } 219 | 220 | defer { 221 | free(.init(start)) 222 | } 223 | return UnsafeBufferPointer(start: start, count: Int(count)) 224 | .compactMap { 225 | ObjCPropertyInfo($0, isClassProperty: !isInstance) 226 | } 227 | } 228 | 229 | public static func methods( 230 | of `protocol`: Protocol, 231 | isRequired: Bool, 232 | isInstance: Bool 233 | ) -> [ObjCMethodInfo] { 234 | var count: UInt32 = 0 235 | let start = protocol_copyMethodDescriptionList(`protocol`, isRequired, isInstance, &count) 236 | guard let start else { return [] } 237 | 238 | defer { 239 | free(.init(start)) 240 | } 241 | return UnsafeBufferPointer(start: start, count: Int(count)) 242 | .compactMap { 243 | ObjCMethodInfo($0, isClassMethod: !isInstance) 244 | } 245 | } 246 | } 247 | #endif 248 | -------------------------------------------------------------------------------- /Sources/ObjCDump/ObjCDump.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(ObjectiveC) 4 | // MARK: - Functions for Class 5 | 6 | /// Obtain informations about the class. 7 | /// - Parameter cls: Target class. 8 | /// - Returns: Information about the class, e.g. property lists, method lists, etc. 9 | @inlinable 10 | public func classInfo(for cls: AnyClass) -> ObjCClassInfo? { 11 | ObjCClassInfo(cls) 12 | } 13 | 14 | /// Obtain a list of information on the protocols to which the class conforms. 15 | /// - Parameter cls: Target class 16 | /// - Returns: List of protocol infos 17 | @inlinable 18 | public func protocols(of cls: AnyClass) -> [ObjCProtocolInfo] { 19 | ObjCClassInfo.protocols(of: cls) 20 | } 21 | 22 | /// Obtain a list of Instance variables held by the class. 23 | /// - Parameter cls: Target class 24 | /// - Returns: List of ivar infos 25 | @inlinable 26 | public func ivars(of cls: AnyClass) -> [ObjCIvarInfo] { 27 | ObjCClassInfo.ivars(of: cls) 28 | } 29 | 30 | /// Obtain a list of properties held by the class. 31 | /// - Parameters: 32 | /// - cls: Target class 33 | /// - isInstance: A Boolean value that indicates whether the instance property is targeted. 34 | /// - Returns: List of property infos 35 | @inlinable 36 | public func properties(of cls: AnyClass, isInstance: Bool) -> [ObjCPropertyInfo] { 37 | ObjCClassInfo.properties(of: cls, isInstance: isInstance) 38 | } 39 | 40 | /// Obtain a list of method held by the class. 41 | /// - Parameters: 42 | /// - cls: Target class 43 | /// - isInstance: A Boolean value that indicates whether the instance methods is targeted. 44 | /// - Returns: List of method infos 45 | @inlinable 46 | public func methods(of cls: AnyClass, isInstance: Bool) -> [ObjCMethodInfo] { 47 | ObjCClassInfo.methods(of: cls, isInstance: isInstance) 48 | } 49 | 50 | // MARK: - Functions for Protocol 51 | 52 | /// Obtain informations about the protocol. 53 | /// - Parameter `protocol`: Target protocol 54 | /// - Returns: Information about the protocol, e.g. property lists, method lists, etc. 55 | @inlinable 56 | public func protocolInfo(for `protocol`: Protocol) -> ObjCProtocolInfo? { 57 | ObjCProtocolInfo(`protocol`) 58 | } 59 | 60 | /// Obtain a list of information on the protocols to which the protocol conforms. 61 | /// - Parameter `protocol`: Target class 62 | /// - Returns: List of protocol infos 63 | @inlinable 64 | public func protocols(of `protocol`: Protocol) -> [ObjCProtocolInfo] { 65 | ObjCProtocolInfo.protocols(of: `protocol`) 66 | } 67 | 68 | /// Obtain a list of (optionally) required properties by the protocol. 69 | /// - Parameters: 70 | /// - `protocol`: Target protocol 71 | /// - isRequired: A Boolean value that indicates whether it is required or optional 72 | /// - isInstance: A Boolean value that indicates whether the instance properties is targeted. 73 | /// - Returns: List of (optionally) required property infos 74 | @inlinable 75 | public func properties( 76 | of `protocol`: Protocol, 77 | isRequired: Bool, 78 | isInstance: Bool 79 | ) -> [ObjCPropertyInfo] { 80 | ObjCProtocolInfo.properties( 81 | of: `protocol`, isRequired: isRequired, isInstance: isInstance 82 | ) 83 | } 84 | 85 | /// Obtain a list of (optionally) required method by the protocol. 86 | /// - Parameters: 87 | /// - `protocol`: Target protocol 88 | /// - isRequired: A Boolean value that indicates whether it is required or optional 89 | /// - isInstance: A Boolean value that indicates whether the instance method is targeted. 90 | /// - Returns: List of (optionally) required method infos 91 | @inlinable 92 | public func methods( 93 | of `protocol`: Protocol, 94 | isRequired: Bool, 95 | isInstance: Bool 96 | ) -> [ObjCMethodInfo] { 97 | ObjCProtocolInfo.methods( 98 | of: `protocol`, isRequired: isRequired, isInstance: isInstance 99 | ) 100 | } 101 | #endif 102 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/Extension/String+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/19 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | /// Finds the index of the matching closing bracket for a given opening bracket. 13 | /// - Parameters: 14 | /// - open: The opening bracket character. 15 | /// - close: The closing bracket character. 16 | /// - Returns: The index of the matching closing bracket if found, otherwise `nil`. 17 | /// - Complexity: O(n), where n is the length of the string. 18 | func indexForMatchingBracket( 19 | open: Character, 20 | close: Character 21 | ) -> Int? { 22 | var depth = 0 23 | for (index, char) in enumerated() { 24 | depth += (char == open) ? 1 : (char == close) ? -1 : 0 25 | if depth == 0 { 26 | return index 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | /// Finds the index of the first closing double quote that matches the given opening double quote in the string. 33 | /// - Parameter openIndex: The index of the opening double quote. 34 | /// - Returns: The index of the matching closing double quote, or `nil` if not found. 35 | func indexForFirstMatchingQuote( 36 | openIndex: String.Index 37 | ) -> Int? { 38 | var inQuote = false 39 | var index = openIndex 40 | while index < endIndex { 41 | if self[index] == "\"" { 42 | if inQuote { 43 | return distance(from: startIndex, to: index) 44 | } else { 45 | inQuote = true 46 | } 47 | } 48 | index = self.index(after: index) 49 | } 50 | return nil 51 | } 52 | 53 | /// Extracts the initial sequence of numeric characters from the string. 54 | /// - Returns: A string containing the initial sequence of numeric characters if any, otherwise `nil`. 55 | /// - Complexity: O(n), where n is the length of the string. 56 | func readInitialDigits() -> String? { 57 | var digits = "" 58 | for (i, char) in self.enumerated() { 59 | if i == 0 && char == "-" { 60 | digits.append("-") 61 | } else if char.isNumber { 62 | digits.append(char) 63 | } else { 64 | break 65 | } 66 | } 67 | if digits == "-" { return nil } 68 | return digits.isEmpty ? nil : digits 69 | } 70 | } 71 | 72 | extension String { 73 | func trailing(after index: Index) -> String? { 74 | if distance(from: index, to: endIndex) > 0 { 75 | return String(self[self.index(after: index) ..< endIndex]) 76 | } 77 | return nil 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/Model/Node.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Node.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/19 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Node { 12 | public var decoded: ObjCType? 13 | public var trailing: String? 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/Model/ObjCField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCField.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/21 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ObjCField: Equatable { 12 | public let type: ObjCType 13 | public var name: String? 14 | public var bitWidth: Int? 15 | 16 | package init( 17 | type: ObjCType, 18 | name: String? = nil, 19 | bitWidth: Int? = nil 20 | ) { 21 | self.type = type 22 | self.name = name 23 | self.bitWidth = bitWidth 24 | } 25 | } 26 | 27 | extension ObjCField: ObjCTypeDecodable { 28 | public func decoded(tab: String = " ") -> String { 29 | if let bitWidth { 30 | "\(type.decoded(tab: tab)) \(name ?? "x") : \(bitWidth);" 31 | } else { 32 | "\(type.decoded(tab: tab)) \(name ?? "x");" 33 | } 34 | } 35 | 36 | public func decoded(fallbackName: String, tab: String = " ") -> String { 37 | if let bitWidth { 38 | "\(type.decoded(tab: tab)) \(name ?? fallbackName) : \(bitWidth);" 39 | } else { 40 | "\(type.decoded(tab: tab)) \(name ?? fallbackName);" 41 | } 42 | } 43 | } 44 | 45 | extension ObjCField: ObjCTypeEncodable { 46 | public func encoded() -> String { 47 | if let bitWidth { 48 | if let name { 49 | return "\"\(name)\"b\(bitWidth)" 50 | } else { 51 | return "b\(bitWidth)" 52 | } 53 | } else { 54 | if let name { 55 | return "\"\(name)\"\(type.encoded())" 56 | } else { 57 | return "\(type.encoded())" 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/Model/ObjCMethodType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCMethodType.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/22 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ObjCMethodType { 12 | public struct ArgumentInfo { 13 | public let type: ObjCType 14 | public let offset: Int 15 | } 16 | 17 | public let returnType: ObjCType 18 | public let stackSize: Int 19 | 20 | public let selfInfo: ArgumentInfo 21 | public let selectorInfo: ArgumentInfo 22 | public let argumentInfos: [ArgumentInfo] 23 | } 24 | 25 | extension ObjCMethodType: ObjCTypeEncodable { 26 | public func encoded() -> String { 27 | let arguments = argumentInfos 28 | .map({ $0.encoded() }) 29 | .joined() 30 | return "\(returnType.encoded())\(stackSize)\(selfInfo.encoded())\(selectorInfo.encoded())\(arguments)" 31 | } 32 | } 33 | 34 | extension ObjCMethodType.ArgumentInfo: ObjCTypeEncodable { 35 | public func encoded() -> String { 36 | "\(type.encoded())\(offset)" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/Model/ObjCModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCModifier.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/21 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ObjCModifier: CaseIterable, Equatable { 12 | case complex 13 | case atomic 14 | case const 15 | case `in` 16 | case `inout` 17 | case out 18 | case bycopy 19 | case byref 20 | case oneway 21 | case register 22 | } 23 | 24 | extension ObjCModifier: ObjCTypeEncodable { 25 | public func encoded() -> String { 26 | switch self { 27 | case .complex: "j" // #include 28 | case .atomic: "A" // #include 29 | case .const: "r" 30 | case .in: "n" 31 | case .inout: "N" 32 | case .out: "o" 33 | case .bycopy: "O" 34 | case .byref: "R" 35 | case .oneway: "V" 36 | case .register: "+" // FIXME: ???? 37 | } 38 | } 39 | } 40 | 41 | extension ObjCModifier: ObjCTypeDecodable { 42 | public func decoded(tab: String = " ") -> String { 43 | switch self { 44 | case .complex: "_Complex" // #include 45 | case .atomic: "_Atomic" // #include 46 | case .const: "const" 47 | case .in: "in" 48 | case .inout: "inout" 49 | case .out: "out" 50 | case .bycopy: "bycopy" 51 | case .byref: "byref" 52 | case .oneway: "oneway" 53 | case .register: "register" // FIXME: ???? 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/Model/ObjCPropertyAttribute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCPropertyAttribute.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/23 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW1 12 | public enum ObjCPropertyAttribute: Equatable { 13 | case type(ObjCType?) // T 14 | case readonly // R 15 | case copy // C 16 | case retain // & 17 | case nonatomic // N 18 | case getter(name: String) // G 19 | case setter(name: String) // S 20 | case dynamic // D 21 | case weak // W 22 | case ivar(name: String) // V 23 | // case p // P The property is eligible for garbage collection. 24 | // case t // t Specifies the type using old-style encoding. 25 | case other(String) 26 | } 27 | 28 | extension ObjCPropertyAttribute: ObjCTypeEncodable { 29 | public func encoded() -> String { 30 | switch self { 31 | case .type(let type): 32 | if let type { 33 | "T\(type.encoded())" 34 | } else { 35 | "T" 36 | } 37 | case .readonly: "R" 38 | case .copy: "C" 39 | case .retain: "&" 40 | case .nonatomic: "N" 41 | case .getter(let name): "G\(name)" 42 | case .setter(let name): "S\(name)" 43 | case .dynamic: "D" 44 | case .weak: "W" 45 | case .ivar(let name): "V\(name)" 46 | case .other(let string): "\(string)" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/Model/ObjCType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCType.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/21 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public indirect enum ObjCType: Equatable { 12 | case `class` 13 | case selector 14 | 15 | case char 16 | case uchar 17 | 18 | case short 19 | case ushort 20 | 21 | case int 22 | case uint 23 | 24 | case long 25 | case ulong 26 | 27 | case longLong 28 | case ulongLong 29 | 30 | case int128 31 | case uint128 32 | 33 | case float 34 | case double 35 | case longDouble 36 | 37 | case bool 38 | case void 39 | case unknown 40 | 41 | case charPtr 42 | 43 | case atom 44 | 45 | case object(name: String?) 46 | case block(return: ObjCType?, args: [ObjCType]?) 47 | case functionPointer 48 | 49 | case array(type: ObjCType, size: Int?) 50 | case pointer(type: ObjCType) 51 | 52 | case bitField(width: Int) 53 | 54 | case union(name: String?, fields: [ObjCField]?) 55 | case `struct`(name: String?, fields: [ObjCField]?) 56 | 57 | case modified(_ modifier: ObjCModifier, type: ObjCType) 58 | 59 | case other(String) 60 | } 61 | 62 | extension ObjCType: ObjCTypeDecodable { 63 | public func decoded(tab: String = " ") -> String { 64 | switch self { 65 | case .class: return "Class" 66 | case .selector: return "SEL" 67 | case .char: return "char" 68 | case .uchar: return "unsigned char" 69 | case .short: return "short" 70 | case .ushort: return "unsigned short" 71 | case .int: return "int" 72 | case .uint: return "unsigned int" 73 | case .long: return "long" 74 | case .ulong: return "unsigned long" 75 | case .longLong: return "long long" 76 | case .ulongLong: return "unsigned long long" 77 | case .int128: return "__int128_t" 78 | case .uint128: return "__uint128_t" 79 | case .float: return "float" 80 | case .double: return "double" 81 | case .longDouble: return "long double" 82 | case .bool: return "BOOL" 83 | case .void: return "void" 84 | case .unknown: return "unknown" 85 | case .charPtr: return "char *" 86 | case .atom: return "atom" 87 | case .object(let name): 88 | if let name { 89 | if name.first == "<" && name.last == ">" { 90 | return "id \(name)" 91 | } 92 | return "\(name) *" 93 | } else { 94 | return "id" 95 | } 96 | case .block(let ret, let args): 97 | guard let ret, let args else { 98 | return "id /* block */" 99 | } 100 | let argTypes = args 101 | .map({ $0.decoded(tab: tab) }) 102 | .joined(separator: ", ") 103 | return "\(ret.decoded(tab: tab)) (^)(\(argTypes))" 104 | case .functionPointer: 105 | return "void * /* function pointer */" 106 | case .array(let type, let size): 107 | if let size { 108 | return "\(type.decoded(tab: tab))[\(size)]" 109 | } else { 110 | return "\(type.decoded(tab: tab))[]" 111 | } 112 | case .pointer(let type): 113 | return "\(type.decoded(tab: tab)) *" 114 | case .bitField(let width): 115 | return "int x : \(width)" 116 | case .union(let name, let fields): 117 | guard let fields, !fields.isEmpty else { 118 | if let name { 119 | return "union \(name)" 120 | } else { 121 | return "union {}" 122 | } 123 | } 124 | let fieldDefs = fields 125 | .enumerated() 126 | .map { 127 | $1.decoded(fallbackName: "x\($0)", tab: tab) 128 | .components(separatedBy: .newlines) 129 | .map { tab + $0 } 130 | .joined(separator: "\n") 131 | } 132 | .joined(separator: "\n") 133 | if let name { 134 | return """ 135 | union \(name) { 136 | \(fieldDefs) 137 | } 138 | """ 139 | } else { 140 | return """ 141 | union { 142 | \(fieldDefs) 143 | } 144 | """ 145 | } 146 | case .struct(let name, let fields): 147 | guard let fields, !fields.isEmpty else { 148 | if let name { 149 | return "struct \(name)" 150 | } else { 151 | return "struct {}" 152 | } 153 | } 154 | let fieldDefs = fields 155 | .enumerated() 156 | .map { 157 | $1.decoded(fallbackName: "x\($0)", tab: tab) 158 | .components(separatedBy: .newlines) 159 | .map { tab + $0 } 160 | .joined(separator: "\n") 161 | } 162 | .joined(separator: "\n") 163 | if let name { 164 | return """ 165 | struct \(name) { 166 | \(fieldDefs) 167 | } 168 | """ 169 | } else { 170 | return """ 171 | struct { 172 | \(fieldDefs) 173 | } 174 | """ 175 | } 176 | case .modified(let modifier, let type): 177 | return "\(modifier.decoded(tab: tab)) \(type.decoded(tab: tab))" 178 | 179 | case .other(let string): 180 | return string 181 | } 182 | } 183 | } 184 | 185 | extension ObjCType: ObjCTypeEncodable { 186 | public func encoded() -> String { 187 | switch self { 188 | case .class: return "#" 189 | case .selector: return ":" 190 | case .char: return "c" 191 | case .uchar: return "C" 192 | case .short: return "s" 193 | case .ushort: return "S" 194 | case .int: return "i" 195 | case .uint: return "I" 196 | case .long: return "l" 197 | case .ulong: return "L" 198 | case .longLong: return "q" 199 | case .ulongLong: return "Q" 200 | case .int128: return "t" 201 | case .uint128: return "T" 202 | case .float: return "f" 203 | case .double: return "d" 204 | case .longDouble: return "D" 205 | case .bool: return "B" 206 | case .void: return "v" 207 | case .unknown: return "?" 208 | case .charPtr: return "*" 209 | case .atom: return "%" 210 | case .object(let name): 211 | if let name { 212 | return "@\"\(name)\"" 213 | } else { 214 | return "@" 215 | } 216 | case let .block(ret, args): 217 | guard let ret, let args else { 218 | return "@?" 219 | } 220 | let argTypes = args.map({ $0.encoded() }).joined() 221 | return "@?<\(ret.encoded())@?\(argTypes)>" 222 | case .functionPointer: return "^?" 223 | case .array(let type, let size): 224 | if let size { 225 | return "[\(size)\(type.encoded())]" 226 | } else { 227 | return "[\(type.encoded())]" 228 | } 229 | case .pointer(let type): 230 | return "^\(type.encoded())" 231 | case .bitField(let width): 232 | return "b\(width)" 233 | case .union(let name, let fields): 234 | guard let fields else { 235 | if let name { return "(\(name))" } 236 | else { return "()" } 237 | } 238 | let fieldDefs = fields.map({ $0.encoded() }) 239 | .joined() 240 | if let name { 241 | return "(\(name)=\(fieldDefs))" 242 | } else { 243 | return "(?=\(fieldDefs))" 244 | } 245 | case .struct(let name, let fields): 246 | guard let fields else { 247 | if let name { return "{\(name)}" } 248 | else { return "{}" } 249 | } 250 | let fieldDefs = fields.map({ $0.encoded() }) 251 | .joined() 252 | if let name { 253 | return "{\(name)=\(fieldDefs)}" 254 | } else { 255 | return "{?=\(fieldDefs)}" 256 | } 257 | case .modified(let modifier, let type): 258 | return "\(modifier.encoded())\(type.encoded())" 259 | 260 | case .other(let string): 261 | return string 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/Model/types.swift: -------------------------------------------------------------------------------- 1 | // 2 | // types.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/19 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | // ref: https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/runtime.h#L1856 12 | 13 | let simpleTypes: [Character: ObjCType] = [ 14 | "@": .object(name: nil), 15 | "#": .class, 16 | ":": .selector, 17 | "c": .char, 18 | "C": .uchar, 19 | "s": .short, 20 | "S": .ushort, 21 | "i": .int, 22 | "I": .uint, 23 | "l": .long, 24 | "L": .ulong, 25 | "q": .longLong, 26 | "Q": .ulongLong, 27 | "t": .int128, 28 | "T": .uint128, 29 | "f": .float, 30 | "d": .double, 31 | "D": .longDouble, 32 | "B": .bool, 33 | "v": .void, 34 | "?": .unknown, 35 | "*": .charPtr, 36 | "%": .atom // FIXME: ????? 37 | ] 38 | 39 | let modifiers: [Character: ObjCModifier] = [ 40 | "A": .atomic, // #include 41 | "j": .complex, // #include 42 | "r": .const, 43 | "n": .in, 44 | "N": .`inout`, 45 | "o": .out, 46 | "O": .bycopy, 47 | "R": .byref, 48 | "V": .oneway, 49 | "+": .register, // FIXME: ???? 50 | ] 51 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/ObjCMethodTypeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCMethodTypeDecoder.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/22 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ObjCMethodTypeDecoder { 12 | // ref: https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/objc-typeencoding.mm#L171 13 | public static func decode(_ type: String) -> ObjCMethodType? { 14 | guard let node = ObjCTypeDecoder._decode(type), 15 | let returnType = node.decoded, 16 | var trailing = node.trailing else { 17 | return nil 18 | } 19 | 20 | if trailing.first == "+" { trailing.removeFirst() } 21 | 22 | // Stack Size 23 | guard let _stackSize = trailing.readInitialDigits(), 24 | let stackSize = Int(_stackSize) else { 25 | return nil 26 | } 27 | trailing.removeFirst(_stackSize.count) 28 | 29 | // self 30 | guard let node = ObjCTypeDecoder._decode(trailing), 31 | let selfType = node.decoded, 32 | var trailing = node.trailing else { 33 | return nil 34 | } 35 | guard let _selfOffset = trailing.readInitialDigits(), 36 | let selfOffset = Int(_selfOffset) else { 37 | return nil 38 | } 39 | trailing.removeFirst(_selfOffset.count) 40 | let selfInfo = ObjCMethodType.ArgumentInfo( 41 | type: selfType, 42 | offset: selfOffset 43 | ) 44 | 45 | // arguments 46 | var arguments: [ObjCMethodType.ArgumentInfo] = [] 47 | while !trailing.isEmpty { 48 | guard let node = ObjCTypeDecoder._decode(trailing), 49 | let type = node.decoded, 50 | var _trailing = node.trailing else { 51 | return nil 52 | } 53 | guard let _offset = _trailing.readInitialDigits(), 54 | let offset = Int(_offset) else { 55 | return nil 56 | } 57 | _trailing.removeFirst(_offset.count) 58 | trailing = _trailing 59 | arguments.append( 60 | ObjCMethodType.ArgumentInfo( 61 | type: type, 62 | offset: offset 63 | ) 64 | ) 65 | } 66 | 67 | guard let selectorInfo = arguments.first else { return nil } 68 | if arguments.count == 1 { arguments = [] } 69 | else { arguments = Array(arguments[1...]) } 70 | 71 | return .init( 72 | returnType: returnType, 73 | stackSize: stackSize, 74 | selfInfo: selfInfo, 75 | selectorInfo: selectorInfo, 76 | argumentInfos: arguments 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/ObjCPropertyTypeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCPropertyTypeDecoder.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/23 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ObjCPropertyTypeDecoder { 12 | public static func decode(_ type: String) -> [ObjCPropertyAttribute] { 13 | let _attributes = type.split(separator: ",").map { String($0) } 14 | var attributes: [ObjCPropertyAttribute] = [] 15 | 16 | for _attribute in _attributes { 17 | guard let first = _attribute.first else { 18 | attributes.append(.other(_attribute)) 19 | continue 20 | } 21 | switch first { 22 | case "T": 23 | var type = _attribute 24 | type.removeFirst() 25 | attributes.append( 26 | .type(ObjCTypeDecoder.decode(type)) 27 | ) 28 | case "R": attributes.append(.readonly) 29 | case "C": attributes.append(.copy) 30 | case "&": attributes.append(.retain) 31 | case "N": attributes.append(.nonatomic) 32 | case "G": 33 | var name = _attribute 34 | name.removeFirst() 35 | attributes.append( 36 | .getter(name: name) 37 | ) 38 | case "S": 39 | var name = _attribute 40 | name.removeFirst() 41 | attributes.append( 42 | .setter(name: name) 43 | ) 44 | case "D": attributes.append(.dynamic) 45 | case "W": attributes.append(.weak) 46 | case "V": 47 | var name = _attribute 48 | name.removeFirst() 49 | attributes.append( 50 | .ivar(name: name) 51 | ) 52 | default: 53 | attributes.append(.other(_attribute)) 54 | } 55 | } 56 | 57 | return attributes 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/ObjCTypeDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCTypeDecoder.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/19 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ObjCTypeDecoder { 12 | 13 | public static func decode(_ type: String) -> ObjCType? { 14 | _decode(type)?.decoded 15 | } 16 | 17 | public static func _decode(_ type: String) -> Node? { 18 | guard let first = type.first else { return nil } 19 | 20 | switch first { 21 | // decode `id` 22 | // ref: https://github.com/gnustep/libobjc2/blob/2855d1771478e1e368fcfeb4d56aecbb4d9429ca/encoding2.c#L159 23 | case _ where type.starts(with: "@?"): 24 | var trailing = type 25 | trailing.removeFirst(2) 26 | 27 | if trailing.starts(with: "<") { 28 | guard let closingIndex = trailing.indexForMatchingBracket( 29 | open: "<", close: ">" 30 | ) else { return nil } 31 | let startInex = trailing.index(trailing.startIndex, offsetBy: 1) 32 | let endIndex = trailing.index(trailing.startIndex, offsetBy: closingIndex) 33 | let content = String(trailing[startInex ..< endIndex]) // 34 | guard let retNode = _decode(content), 35 | let retType = retNode.decoded else { 36 | return nil 37 | } 38 | guard var args = retNode.trailing else { return nil } 39 | guard args.starts(with: "@?") else { return nil } 40 | args.removeFirst(2) 41 | var argTypes: [ObjCType] = [] 42 | while !args.isEmpty { 43 | guard let node = _decode(args), 44 | let argType = node.decoded else { 45 | return nil 46 | } 47 | argTypes.append(argType) 48 | if let trailing = node.trailing { 49 | args = trailing 50 | } else { 51 | break 52 | } 53 | } 54 | let trailing = trailing.trailing(after: endIndex) 55 | return .init( 56 | decoded: .block(return: retType, args: argTypes), 57 | trailing: trailing 58 | ) 59 | } 60 | return .init( 61 | decoded: .block(return: nil, args: nil), 62 | trailing: trailing 63 | ) 64 | 65 | case _ where type.starts(with: #"@""#): 66 | guard let closingIndex = type.indexForFirstMatchingQuote( 67 | openIndex: type.index(after: type.startIndex) 68 | ) else { return nil } 69 | let startIndex = type.index(type.startIndex, offsetBy: 2) 70 | let endIndex = type.index(type.startIndex, offsetBy: closingIndex) 71 | let name = String(type[startIndex ..< endIndex]) 72 | 73 | let trailing = type.trailing(after: endIndex) 74 | return .init(decoded: .object(name: name), trailing: trailing) 75 | 76 | case _ where type.starts(with: "^?"): 77 | var trailing = type 78 | trailing.removeFirst(2) 79 | return .init(decoded: .functionPointer, trailing: trailing) 80 | 81 | case _ where simpleTypes.keys.contains(first): 82 | var trailing = type 83 | trailing.removeFirst() 84 | return .init(decoded: simpleTypes[first], trailing: trailing) 85 | 86 | case _ where modifiers.keys.contains(first): 87 | var content = type 88 | content.removeFirst() 89 | guard let modifier = modifiers[first], 90 | let content = _decode(content), 91 | let contentType = content.decoded else { 92 | return nil 93 | } 94 | return .init( 95 | decoded: .modified(modifier, type: contentType), 96 | trailing: content.trailing 97 | ) 98 | 99 | case "b": 100 | var content = type 101 | content.removeFirst() 102 | 103 | guard let _length = content.readInitialDigits(), 104 | let length = Int(_length) else { 105 | return nil 106 | } 107 | let trailing = type.trailing( 108 | after: type.index(type.startIndex, offsetBy: _length.count) 109 | ) 110 | return .init( 111 | decoded: .bitField(width: length), 112 | trailing: trailing 113 | ) 114 | 115 | case "[": 116 | return _decodeArray(type) 117 | case "^": 118 | return _decodePointer(type) 119 | case "(": 120 | return _decodeUnion(type) 121 | case "{": 122 | return _decodeStruct(type) 123 | 124 | default: 125 | break 126 | } 127 | return nil 128 | } 129 | } 130 | 131 | extension ObjCTypeDecoder { 132 | 133 | // MARK: - Pointer ^ 134 | private static func _decodePointer(_ type: String) -> Node? { 135 | var content = type 136 | content.removeFirst() // ^ 137 | 138 | guard let node = _decode(content), 139 | let contentType = node.decoded else { 140 | return nil 141 | } 142 | 143 | return .init(decoded: .pointer(type: contentType), trailing: node.trailing) 144 | } 145 | 146 | // MARK: - Bit Field b 147 | private static func _decodeBitField(_ type: String, name: String?) -> (field: ObjCField, trailing: String?)? { 148 | var content = type 149 | content.removeFirst() // b 150 | 151 | guard let _length = content.readInitialDigits(), 152 | let length = Int(_length) else { 153 | return nil 154 | } 155 | 156 | let endInex = content.index(content.startIndex, offsetBy: _length.count) 157 | let trailing = type.trailing(after: endInex) 158 | 159 | return ( 160 | .init(type: .int, name: name, bitWidth: length), 161 | trailing 162 | ) 163 | } 164 | 165 | // MARK: - Array [] 166 | private static func _decodeArray(_ type: String) -> Node? { 167 | 168 | guard let closingIndex = type.indexForMatchingBracket(open: "[", close: "]") else { 169 | return nil 170 | } 171 | 172 | let startInex = type.index(type.startIndex, offsetBy: 1) 173 | let endIndex = type.index(type.startIndex, offsetBy: closingIndex) 174 | 175 | var content = String(type[startInex ..< endIndex]) // [content] 176 | 177 | let length = content.readInitialDigits() 178 | 179 | if let _length = length, 180 | let length = Int(_length) { 181 | content.removeFirst(_length.count) 182 | guard let node = _decode(content), 183 | let contentType = node.decoded else { 184 | return nil 185 | } 186 | 187 | // TODO: `node.trailing` must be empty 188 | let trailing = type.trailing(after: endIndex) 189 | 190 | return .init( 191 | decoded: .array(type: contentType, size: length), 192 | trailing: trailing 193 | ) 194 | } 195 | 196 | guard let node = _decode(content), 197 | let contentType = node.decoded else { 198 | return nil 199 | } 200 | 201 | // TODO: `node.trailing` must be empty 202 | let trailing = type.trailing(after: endIndex) 203 | 204 | return .init( 205 | decoded: .array(type: contentType, size: nil), 206 | trailing: trailing 207 | ) 208 | } 209 | 210 | // MARK: - Union () 211 | private static func _decodeUnion(_ type: String) -> Node? { 212 | return _decodeFields(type, for: .union) 213 | } 214 | 215 | // MARK: - Struct {} 216 | private static func _decodeStruct(_ type: String) -> Node? { 217 | return _decodeFields(type, for: .struct) 218 | } 219 | 220 | // MARK: - Union or Struct 221 | enum _TypeKind: String { 222 | case `struct` 223 | case union 224 | } 225 | 226 | private static func _decodeFields(_ type: String, for kind: _TypeKind) -> Node? { 227 | let open, close: Character 228 | switch kind { 229 | case .struct: 230 | open = "{"; close = "}" 231 | case .union: 232 | open = "("; close = ")" 233 | } 234 | 235 | guard let closingIndex = type.indexForMatchingBracket(open: open, close: close) else { 236 | return nil 237 | } 238 | 239 | let startInex = type.index(type.startIndex, offsetBy: 1) 240 | let endIndex = type.index(type.startIndex, offsetBy: closingIndex) 241 | 242 | let content = String(type[startInex ..< endIndex]) // (content) 243 | guard !content.isEmpty else { return nil } 244 | 245 | guard let equalIndex = content.firstIndex(of: "=") else { 246 | let trailing = type.trailing(after: endIndex) 247 | switch kind { 248 | case .struct: 249 | return .init(decoded: .struct(name: content, fields: nil), trailing: trailing) 250 | case .union: 251 | return .init(decoded: .union(name: content, fields: nil), trailing: trailing) 252 | } 253 | } 254 | 255 | var typeName: String? = String(content[content.startIndex ..< equalIndex]) 256 | if typeName == "?" { typeName = nil } 257 | 258 | var _fields = String(content[content.index(equalIndex, offsetBy: 1) ..< content.endIndex]) 259 | var fields: [ObjCField] = [] 260 | 261 | var number = 0 262 | while !_fields.isEmpty { 263 | guard let (field, trailing) = _decodeField(_fields) else { break } 264 | fields.append(field) 265 | if let trailing { 266 | _fields = trailing 267 | number += 1 268 | } else { 269 | break 270 | } 271 | } 272 | 273 | let trailing = type.trailing(after: endIndex) 274 | 275 | switch kind { 276 | case .struct: 277 | return .init( 278 | decoded: .struct(name: typeName, fields: fields), 279 | trailing: trailing 280 | ) 281 | case .union: 282 | return .init( 283 | decoded: .union(name: typeName, fields: fields), 284 | trailing: trailing) 285 | 286 | } 287 | } 288 | 289 | private static func _decodeField(_ type: String) -> (field: ObjCField, trailing: String?)? { 290 | guard let first = type.first else { return nil } 291 | switch first { 292 | case "b": 293 | return _decodeBitField(type, name: nil) 294 | case "\"": 295 | guard let closingIndex = type.indexForFirstMatchingQuote( 296 | openIndex: type.startIndex 297 | ) else { return nil } 298 | let startIndex = type.index(type.startIndex, offsetBy: 1) 299 | let endIndex = type.index(type.startIndex, offsetBy: closingIndex) 300 | let name = String(type[startIndex ..< endIndex]) 301 | 302 | let contentType = String(type[type.index(after: endIndex) ..< type.endIndex]) 303 | if contentType.starts(with: "b"), 304 | let (field, trailing) = _decodeBitField(contentType, name: name) { 305 | return (field, trailing) 306 | } else if let node = _decode(contentType), 307 | let contentType = node.decoded { 308 | return ( 309 | .init(type: contentType, name: name), 310 | node.trailing 311 | ) 312 | } else { return nil } 313 | 314 | default: 315 | guard let node = _decode(type), 316 | let decoded = node.decoded else { 317 | return nil 318 | } 319 | return ( 320 | .init(type: decoded), 321 | node.trailing 322 | ) 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /Sources/ObjCTypeDecodeKit/Protocol/ObjCTypeCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCTypeCodable.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/21 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias ObjCTypeCodable = ObjCTypeDecodable & ObjCTypeEncodable 12 | 13 | public protocol ObjCTypeDecodable { 14 | func decoded(tab: String) -> String 15 | } 16 | 17 | public protocol ObjCTypeEncodable { 18 | func encoded() -> String 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ObjCDumpTests/ObjCDumpTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(ObjectiveC) 2 | 3 | import XCTest 4 | @testable import ObjCDump 5 | 6 | final class ObjCDumpTests: XCTestCase { 7 | func testClass() { 8 | guard let info = classInfo(for: ObjCDumpTests.self) else { 9 | XCTAssert(true) 10 | return 11 | } 12 | print(info.headerString) 13 | } 14 | 15 | func testProtocol() { 16 | guard let `protocol` = NSProtocolFromString("NSCoding"), 17 | let info = protocolInfo(for: `protocol`) else { 18 | XCTAssert(true) 19 | return 20 | } 21 | print(info.headerString) 22 | } 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /Tests/ObjCTypeDecodeKitTests/ObjCMethodTypeDecodeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCMethodTypeDecodeTests.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/22 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import ObjCTypeDecodeKit 11 | 12 | final class ObjCMethodTypeDecodeTests: XCTestCase { 13 | func test() { 14 | // Instance Methods 15 | checkEncode("v32@0:8@16^{CGContext=}24") 16 | checkEncode("v40@0:8@16^{CGContext=}24@?32") 17 | checkEncode("v52@0:8B16{CGRect={CGPoint=dd}{CGSize=dd}}20") 18 | checkEncode("@32@0:8#16:24") 19 | checkEncode("@40@0:8{CGPoint=dd}16q32") 20 | checkEncode("@48@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16") 21 | checkEncode("^{CGSRegionObject=}56@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16B48B52") 22 | checkEncode("@24@0:8q16") 23 | checkEncode("f24@0:8q16") 24 | checkEncode("v40@0:8@16@24@32") 25 | checkEncode("@56@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48") 26 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}64@0:8{CGPoint=dd}16{CGRect={CGPoint=dd}{CGSize=dd}}32") 27 | checkEncode("B24@0:8@16") 28 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}36@0:8@16@24B32") 29 | checkEncode("v16@0:8") 30 | checkEncode("v40@0:8Q16@24@32") 31 | checkEncode("@24@0:8^{CGPoint=dd}16") 32 | checkEncode("B76@0:8@16{CGRect={CGPoint=dd}{CGSize=dd}}24@56B64@68") 33 | checkEncode("@16@0:8") 34 | checkEncode("v36@0:8@16B24@?28") 35 | checkEncode("B16@0:8") 36 | checkEncode("v60@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48B56") 37 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}16@0:8") 38 | checkEncode("@20@0:8B16") 39 | checkEncode("{CGSize=dd}16@0:8") 40 | checkEncode("v64@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16{CGSize=dd}48") 41 | checkEncode("v24@0:8d16") 42 | checkEncode("v56@0:8@16{NSEdgeInsets=dddd}24") 43 | checkEncode("^{_CAView=}16@0:8") 44 | checkEncode("B28@0:8@16B24") 45 | checkEncode("i16@0:8") 46 | checkEncode("B24@0:8B16B20") 47 | checkEncode("@24@0:8@?16") 48 | checkEncode("v40@0:8^{CGSize=dd}16^{CGSize=dd}24^{CGSize=dd}32") 49 | checkEncode("B32@0:8r^{CGRect={CGPoint=dd}{CGSize=dd}}16@24") 50 | checkEncode("@?16@0:8") 51 | checkEncode("v44@0:8^{CGSize=dd}16^{CGSize=dd}24^{CGSize=dd}32f40") 52 | checkEncode("q64@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48@56") 53 | checkEncode("@48@0:8@16q24@32d40") 54 | checkEncode("v56@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48") 55 | checkEncode("@32@0:8q16@24") 56 | checkEncode("d32@0:8^{CGSize=dd}16@24") 57 | checkEncode("v56@0:8@16{_NSRange=QQ}24@40@?48") 58 | checkEncode("v20@0:8I16") 59 | checkEncode("v32@0:8^?16^v24") 60 | checkEncode("v64@0:8{CGAffineTransform=dddddd}16") 61 | checkEncode("@24@0:8^{CGRect={CGPoint=dd}{CGSize=dd}}16") 62 | checkEncode("B40@0:8@16d24^@32") 63 | checkEncode("v28@0:8@16B24") 64 | checkEncode("B68@0:8@16{CGRect={CGPoint=dd}{CGSize=dd}}24B56@60") 65 | checkEncode("B40@0:8@16q24@32") 66 | checkEncode("d16@0:8") 67 | checkEncode("B56@0:8@16{CGRect={CGPoint=dd}{CGSize=dd}}24") 68 | checkEncode("v64@0:8B16{CGRect={CGPoint=dd}{CGSize=dd}}20@52B60") 69 | checkEncode("B52@0:8i16@20d28@36@44") 70 | checkEncode("B48@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16") 71 | checkEncode("{NSEdgeInsets=dddd}56@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48") 72 | checkEncode("B64@0:8{CGPoint=dd}16{CGRect={CGPoint=dd}{CGSize=dd}}32") 73 | checkEncode("B24@0:8^v16") 74 | checkEncode("@24@0:8^{CGSize=dd}16") 75 | checkEncode("{CGPoint=dd}24@0:8@16") 76 | checkEncode("v40@0:8@16{CGPoint=dd}24") 77 | checkEncode("v32@0:8{CGSize=dd}16") 78 | checkEncode("@28@0:8q16B24") 79 | checkEncode("Q24@0:8@16") 80 | checkEncode("v48@0:8Q16d24@32@?40") 81 | checkEncode("B32@0:8^q16^q24") 82 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}52@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16B48") 83 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}24@0:8q16") 84 | checkEncode("v96@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48@56@64@72q80@88") 85 | checkEncode("{NSEdgeInsets=dddd}48@0:8{NSEdgeInsets=dddd}16") 86 | checkEncode("q64@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48^v56") 87 | checkEncode("v84@0:8@16{CGPoint=dd}24{CGSize=dd}40@56@64@72B80") 88 | checkEncode("{CGAffineTransform=dddddd}24@0:8@16") 89 | checkEncode("v24@0:8^@16") 90 | checkEncode("v52@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16B48") 91 | checkEncode("v40@0:8@16q24@32") 92 | checkEncode("B56@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48") 93 | checkEncode("B68@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48B56@?60") 94 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}40@0:8@16q24@32") 95 | checkEncode("v32@0:8@16@24") 96 | checkEncode("B32@0:8@16@24") 97 | checkEncode("@56@0:8@16{CGRect={CGPoint=dd}{CGSize=dd}}24") 98 | checkEncode("v32@0:8[4{CGRect={CGPoint=dd}{CGSize=dd}}]16^q24") 99 | checkEncode("v20@0:8B16") 100 | checkEncode("B28@0:8^{CGPoint=dd}16B24") 101 | checkEncode("{NSEdgeInsets=dddd}16@0:8") 102 | checkEncode("{CGPoint=dd}48@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16") 103 | checkEncode("v64@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16B48@52B60") 104 | checkEncode("v40@0:8@16@24^@32") 105 | checkEncode("{CGPoint=dd}32@0:8{CGPoint=dd}16") 106 | checkEncode("v40@0:8^{CGRect={CGPoint=dd}{CGSize=dd}}16^{CGRect={CGPoint=dd}{CGSize=dd}}24Q32") 107 | checkEncode("v32@0:8r^{CGPoint=dd}16@24") 108 | checkEncode("v32@0:8@16d24") 109 | checkEncode("@40@0:8@16{_NSRange=QQ}24") 110 | checkEncode("@24@0:8Q16") 111 | checkEncode("v32@0:8r^{CGSize=dd}16@24") 112 | checkEncode("@24@0:8:16") 113 | checkEncode("#16@0:8") 114 | checkEncode("v48@0:8^{CGRect={CGPoint=dd}{CGSize=dd}}16^{CGRect={CGPoint=dd}{CGSize=dd}}24{CGSize=dd}32") 115 | checkEncode("v60@0:8@16{CGRect={CGPoint=dd}{CGSize=dd}}24B56") 116 | checkEncode("v32@0:8@16^@24") 117 | checkEncode("Vv16@0:8") 118 | checkEncode("d32@0:8{CGPoint=dd}16") 119 | checkEncode("@32@0:8@16:24") 120 | checkEncode("q16@0:8") 121 | checkEncode("v60@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16B48@52") 122 | checkEncode("v28@0:8B16@?20") 123 | checkEncode("v48@0:8@16q24@32@40") 124 | checkEncode("v32@0:8{CGPoint=dd}16") 125 | checkEncode("{CGSize=dd}40@0:8{CGSize=dd}16@32") 126 | checkEncode("v24@0:8^{?=ddd{?=b1b31}}16") 127 | checkEncode("v40@0:8@16@24B32B36") 128 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}56@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16Q48") 129 | checkEncode("B44@0:8i16@20d28@36") 130 | checkEncode("d24@0:8@16") 131 | checkEncode("@44@0:8Q16@24@32B40") 132 | checkEncode("{CGAffineTransform=dddddd}16@0:8") 133 | checkEncode("B24@0:8^{_NSRange=QQ}16") 134 | checkEncode("@32@0:8{CGPoint=dd}16") 135 | checkEncode("@32@0:8@16@24") 136 | checkEncode("v32@0:8r^^{CGRect}16^q24") 137 | checkEncode("v32@0:8^@16^@24") 138 | checkEncode("{CGSize=dd}40@0:8{CGSize=dd}16f32f36") 139 | checkEncode("v48@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16") 140 | checkEncode("@36@0:8{CGPoint=dd}16B32") 141 | checkEncode("@88@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16{CGRect={CGPoint=dd}{CGSize=dd}}48@80") 142 | checkEncode("Q16@0:8") 143 | checkEncode("i24@0:8@16") 144 | checkEncode("{CGSize=dd}24@0:8@16") 145 | checkEncode("@32@0:8^{CGPoint=dd}16@24") 146 | checkEncode("v24@0:8^{_CAView=}16") 147 | checkEncode("v64@0:8^{CGRect={CGPoint=dd}{CGSize=dd}}16@24^^v32^B40^q48q56") 148 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}48@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16") 149 | checkEncode("I16@0:8") 150 | checkEncode("v24@0:8@16") 151 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}64@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16{CGSize=dd}48") 152 | checkEncode("v24@0:8B16B20") 153 | checkEncode("v56@0:8@16{CGRect={CGPoint=dd}{CGSize=dd}}24") 154 | checkEncode("B32@0:8{CGPoint=dd}16") 155 | checkEncode("q68@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48^v56B64") 156 | checkEncode("v48@0:8@16@24Q32@?40") 157 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}24@0:8@16") 158 | checkEncode("v72@0:8q16@24{CGRect={CGPoint=dd}{CGSize=dd}}32@64") 159 | checkEncode("f16@0:8") 160 | checkEncode("q24@0:8@16") 161 | checkEncode("{CGPoint=dd}16@0:8") 162 | checkEncode("v24@0:8Q16") 163 | checkEncode("^{CGSRegionObject=}52@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16B48") 164 | checkEncode("v32@0:8@16Q24") 165 | checkEncode("@24@0:8#16") 166 | checkEncode("@40@0:8@16@24@32") 167 | checkEncode("q76@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48^v56B64q68") 168 | checkEncode("@52@0:8@16B24@28{CGPoint=dd}36") 169 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}56@0:8@16{CGRect={CGPoint=dd}{CGSize=dd}}24") 170 | checkEncode("v48@0:8^d16d24d32d40") 171 | checkEncode("v28@0:8f16q20") 172 | checkEncode("v32@0:8^q16q24") 173 | checkEncode("v64@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16{CGPoint=dd}48") 174 | checkEncode("{CGSize=dd}40@0:8{CGSize=dd}16Q32") 175 | checkEncode("B32@0:8@16^@24") 176 | checkEncode("{CGSize=dd}32@0:8{CGSize=dd}16") 177 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}56@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16@48") 178 | checkEncode("v24@0:8q16") 179 | checkEncode("{CGPoint=dd}40@0:8{CGPoint=dd}16@32") 180 | checkEncode("@24@0:8@16") 181 | checkEncode("q20@0:8B16") 182 | checkEncode("@28@0:8@16B24") 183 | checkEncode("v24@0:8@?16") 184 | checkEncode("{?=dd}32@0:8{CGSize=dd}16") 185 | checkEncode("B48@0:8^d16@24@32^@40") 186 | 187 | // Class Methods 188 | checkEncode("v24@0:8@16") 189 | checkEncode("q16@0:8") 190 | checkEncode("v24@0:8@?16") 191 | checkEncode("v16@0:8") 192 | checkEncode("@24@0:8@16") 193 | checkEncode("@16@0:8") 194 | checkEncode("i16@0:8") 195 | checkEncode("@32@0:8Q16@24") 196 | checkEncode("Q16@0:8") 197 | checkEncode("B16@0:8") 198 | checkEncode("v20@0:8B16") 199 | checkEncode("#16@0:8") 200 | } 201 | } 202 | 203 | extension ObjCMethodTypeDecodeTests { 204 | func checkEncode(_ encoded: String) { 205 | let decoded = decoded(encoded) 206 | XCTAssertEqual(decoded?.encoded(), encoded) 207 | XCTAssertEqual(decoded?.selectorInfo.type.decoded(), "SEL") 208 | } 209 | } 210 | 211 | extension ObjCMethodTypeDecodeTests { 212 | func decoded(_ type: String) -> ObjCMethodType? { 213 | ObjCMethodTypeDecoder.decode(type) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Tests/ObjCTypeDecodeKitTests/ObjCTypeDecodeKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCTypeDecodeKitTests.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/19 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import ObjCTypeDecodeKit 11 | 12 | final class ObjCTypeDecodeKitTests: XCTestCase { 13 | func testSimpleTypes() { 14 | XCTAssertEqual(decoded("@"), "id") 15 | XCTAssertEqual(decoded("#"), "Class") 16 | XCTAssertEqual(decoded(":"), "SEL") 17 | 18 | XCTAssertEqual(decoded("c"), "char") 19 | XCTAssertEqual(decoded("C"), "unsigned char") 20 | 21 | XCTAssertEqual(decoded("s"), "short") 22 | XCTAssertEqual(decoded("S"), "unsigned short") 23 | 24 | XCTAssertEqual(decoded("i"), "int") 25 | XCTAssertEqual(decoded("I"), "unsigned int") 26 | 27 | XCTAssertEqual(decoded("l"), "long") 28 | XCTAssertEqual(decoded("L"), "unsigned long") 29 | 30 | XCTAssertEqual(decoded("q"), "long long") 31 | XCTAssertEqual(decoded("Q"), "unsigned long long") 32 | 33 | XCTAssertEqual(decoded("t"), "__int128_t") 34 | XCTAssertEqual(decoded("T"), "__uint128_t") 35 | 36 | XCTAssertEqual(decoded("f"), "float") 37 | 38 | XCTAssertEqual(decoded("d"), "double") 39 | XCTAssertEqual(decoded("D"), "long double") 40 | 41 | XCTAssertEqual(decoded("B"), "BOOL") 42 | XCTAssertEqual(decoded("v"), "void") 43 | XCTAssertEqual(decoded("?"), "unknown") 44 | XCTAssertEqual(decoded("*"), "char *") 45 | 46 | XCTAssertEqual(decoded("%"), "atom") // FIXME: ????? 47 | } 48 | 49 | func testNamedId() { 50 | XCTAssertEqual( 51 | decoded(#"@"NSLayoutManager""#), 52 | "NSLayoutManager *" 53 | ) 54 | XCTAssertEqual( 55 | decoded(#"@?"#), 56 | "id /* block */" 57 | ) 58 | } 59 | 60 | func testPointers() { 61 | XCTAssertEqual(decoded("^i"), "int *") 62 | XCTAssertEqual(decoded("^v"), "void *") 63 | } 64 | 65 | func testArray() { 66 | XCTAssertEqual(decoded("[i]"), "int[]") 67 | XCTAssertEqual(decoded("[128i]"), "int[128]") 68 | XCTAssertEqual(decoded("[128^i]"), "int *[128]") 69 | } 70 | 71 | func testUnion() { 72 | XCTAssertEqual( 73 | decoded("(?=iQ)"), 74 | """ 75 | union { 76 | int x0; 77 | unsigned long long x1; 78 | } 79 | """ 80 | ) 81 | XCTAssertEqual( 82 | decoded("(?=iQ(?=*B))"), 83 | """ 84 | union { 85 | int x0; 86 | unsigned long long x1; 87 | union { 88 | char * x0; 89 | BOOL x1; 90 | } x2; 91 | } 92 | """ 93 | ) 94 | } 95 | 96 | func testStruct() { 97 | XCTAssertEqual( 98 | decoded("{CGRect={CGPoint=dd}{CGSize=dd}}"), 99 | """ 100 | struct CGRect { 101 | struct CGPoint { 102 | double x0; 103 | double x1; 104 | } x0; 105 | struct CGSize { 106 | double x0; 107 | double x1; 108 | } x1; 109 | } 110 | """ 111 | ) 112 | XCTAssertEqual( 113 | decoded(#"{CGSize="width"d"height"d}"#), 114 | """ 115 | struct CGSize { 116 | double width; 117 | double height; 118 | } 119 | """ 120 | ) 121 | XCTAssertEqual( 122 | decoded(#"{CGRect="origin"{CGPoint="x"d"y"d}"size"{CGSize="width"d"height"d}}"#), 123 | """ 124 | struct CGRect { 125 | struct CGPoint { 126 | double x; 127 | double y; 128 | } origin; 129 | struct CGSize { 130 | double width; 131 | double height; 132 | } size; 133 | } 134 | """ 135 | ) 136 | XCTAssertEqual( 137 | decoded(#"{_tvFlags="horizontallyResizable"b1"verticallyResizable"b1"viewOwnsTextStorage"b1"displayWithoutLayout"b1"settingMarkedRange"b1"containerOriginInvalid"b1"registeredForDragging"b1"superviewIsClipView"b1"forceRulerUpdate"b1"typingText"b1"wasPostingFrameNotifications"b1"wasRotatedOrScaledFromBase"b1"settingNeedsDisplay"b1"mouseInside"b1"verticalLayout"b2"diagonallyRotatedOrScaled"b1"hasScaledBacking"b1"shouldCloseQL"b1"dragUpdateRequstOwner"b1"genericDragRemoveSource"b1"isAttributedPlaceholder"b1"isDDAction"b1"showingFindIndicator"b1"isDrawingLayer"b1"touchBarInstantiated"b1"calculatingContainerOrigin"b1"doesOverrideDrawInsertionPointInRect"b1"darkEffectiveAppearance"b1"isPresentingReveal"b1"isDrawingFindIndicatorContent"b1"isAutoscrollingForTextLayoutManager"b1"_downgradeState"b2"isWatchingUnspecifiedClipView"b1}"#), 138 | """ 139 | struct _tvFlags { 140 | int horizontallyResizable : 1; 141 | int verticallyResizable : 1; 142 | int viewOwnsTextStorage : 1; 143 | int displayWithoutLayout : 1; 144 | int settingMarkedRange : 1; 145 | int containerOriginInvalid : 1; 146 | int registeredForDragging : 1; 147 | int superviewIsClipView : 1; 148 | int forceRulerUpdate : 1; 149 | int typingText : 1; 150 | int wasPostingFrameNotifications : 1; 151 | int wasRotatedOrScaledFromBase : 1; 152 | int settingNeedsDisplay : 1; 153 | int mouseInside : 1; 154 | int verticalLayout : 2; 155 | int diagonallyRotatedOrScaled : 1; 156 | int hasScaledBacking : 1; 157 | int shouldCloseQL : 1; 158 | int dragUpdateRequstOwner : 1; 159 | int genericDragRemoveSource : 1; 160 | int isAttributedPlaceholder : 1; 161 | int isDDAction : 1; 162 | int showingFindIndicator : 1; 163 | int isDrawingLayer : 1; 164 | int touchBarInstantiated : 1; 165 | int calculatingContainerOrigin : 1; 166 | int doesOverrideDrawInsertionPointInRect : 1; 167 | int darkEffectiveAppearance : 1; 168 | int isPresentingReveal : 1; 169 | int isDrawingFindIndicatorContent : 1; 170 | int isAutoscrollingForTextLayoutManager : 1; 171 | int _downgradeState : 2; 172 | int isWatchingUnspecifiedClipView : 1; 173 | } 174 | """ 175 | ) 176 | } 177 | 178 | func testAtomic() { 179 | XCTAssertEqual(decoded("Ai"), "_Atomic int") 180 | XCTAssertEqual(decoded("A*"), "_Atomic char *") 181 | XCTAssertEqual( 182 | decoded("^AQ"), 183 | "_Atomic unsigned long long *" 184 | ) 185 | XCTAssertEqual(decoded("^A{CGPoint}"), "_Atomic struct CGPoint *") 186 | } 187 | 188 | func testComplex() { 189 | XCTAssertEqual(decoded("ji"), "_Complex int") 190 | XCTAssertEqual( 191 | decoded("^jQ"), 192 | "_Complex unsigned long long *" 193 | ) 194 | } 195 | 196 | func test() { 197 | XCTAssertEqual( 198 | decoded("(?=iI{?=i{CGRect={CGPoint=dd}{CGSize=dd}}})"), 199 | """ 200 | union { 201 | int x0; 202 | unsigned int x1; 203 | struct { 204 | int x0; 205 | struct CGRect { 206 | struct CGPoint { 207 | double x0; 208 | double x1; 209 | } x0; 210 | struct CGSize { 211 | double x0; 212 | double x1; 213 | } x1; 214 | } x1; 215 | } x2; 216 | } 217 | """ 218 | ) 219 | } 220 | } 221 | 222 | extension ObjCTypeDecodeKitTests { 223 | @_disfavoredOverload 224 | func decoded(_ type: String) -> String? { 225 | ObjCTypeDecoder.decode(type)?.decoded() 226 | } 227 | 228 | func decoded(_ type: String) -> ObjCType? { 229 | ObjCTypeDecoder.decode(type) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Tests/ObjCTypeDecodeKitTests/ObjCTypeEncodeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCTypeEncodeTests.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/21 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import ObjCTypeDecodeKit 11 | 12 | final class ObjCTypeEncodeTests: XCTestCase { 13 | func testSimpleTypes() { 14 | checkEncode("@") 15 | checkEncode("#") 16 | checkEncode(":") 17 | 18 | checkEncode("c") 19 | checkEncode("C") 20 | 21 | checkEncode("s") 22 | checkEncode("S") 23 | 24 | checkEncode("i") 25 | checkEncode("I") 26 | 27 | checkEncode("l") 28 | checkEncode("L") 29 | 30 | checkEncode("q") 31 | checkEncode("Q") 32 | 33 | checkEncode("t") 34 | checkEncode("T") 35 | 36 | checkEncode("f") 37 | 38 | checkEncode("d") 39 | checkEncode("D") 40 | 41 | checkEncode("B") 42 | checkEncode("v") 43 | checkEncode("?") 44 | checkEncode("*") 45 | 46 | checkEncode("%") // FIXME: ????? 47 | } 48 | 49 | func testNamedId() { 50 | checkEncode(#"@"NSLayoutManager""#) 51 | checkEncode(#"@?"#) 52 | } 53 | 54 | func testPointers() { 55 | checkEncode("^i") 56 | checkEncode("^v") 57 | } 58 | 59 | func testArray() { 60 | checkEncode("[i]") 61 | checkEncode("[128i]") 62 | checkEncode("[128^i]") 63 | } 64 | 65 | func testUnion() { 66 | checkEncode("(?=iQ)") 67 | checkEncode("(?=iQ(?=*B))") 68 | } 69 | 70 | func testStruct() { 71 | checkEncode("{CGRect={CGPoint=dd}{CGSize=dd}}") 72 | checkEncode(#"{CGSize="width"d"height"d}"#) 73 | checkEncode(#"{CGRect="origin"{CGPoint="x"d"y"d}"size"{CGSize="width"d"height"d}}"#) 74 | checkEncode(#"{_tvFlags="horizontallyResizable"b1"verticallyResizable"b1"viewOwnsTextStorage"b1"displayWithoutLayout"b1"settingMarkedRange"b1"containerOriginInvalid"b1"registeredForDragging"b1"superviewIsClipView"b1"forceRulerUpdate"b1"typingText"b1"wasPostingFrameNotifications"b1"wasRotatedOrScaledFromBase"b1"settingNeedsDisplay"b1"mouseInside"b1"verticalLayout"b2"diagonallyRotatedOrScaled"b1"hasScaledBacking"b1"shouldCloseQL"b1"dragUpdateRequstOwner"b1"genericDragRemoveSource"b1"isAttributedPlaceholder"b1"isDDAction"b1"showingFindIndicator"b1"isDrawingLayer"b1"touchBarInstantiated"b1"calculatingContainerOrigin"b1"doesOverrideDrawInsertionPointInRect"b1"darkEffectiveAppearance"b1"isPresentingReveal"b1"isDrawingFindIndicatorContent"b1"isAutoscrollingForTextLayoutManager"b1"_downgradeState"b2"isWatchingUnspecifiedClipView"b1}"#) 75 | } 76 | 77 | func testBlock() { 78 | checkEncode("@?") 79 | checkEncode("@?") 80 | checkEncode(#"@?"#) 81 | checkEncode(#"@?"#) 82 | checkEncode(#"@?">"#) 83 | } 84 | 85 | func testAtomic() { 86 | checkEncode("Ai") 87 | checkEncode("A*") 88 | checkEncode("^AQ") 89 | checkEncode("^A{CGPoint}") 90 | } 91 | 92 | func testComplex() { 93 | checkEncode("ji") 94 | checkEncode("^jQ") 95 | } 96 | 97 | func test() { 98 | checkEncode("(?=iI{?=i{CGRect={CGPoint=dd}{CGSize=dd}}})") 99 | } 100 | } 101 | 102 | extension ObjCTypeEncodeTests { 103 | func checkEncode(_ encoded: String) { 104 | XCTAssertEqual(decoded(encoded)?.encoded(), encoded) 105 | } 106 | } 107 | 108 | extension ObjCTypeEncodeTests { 109 | @_disfavoredOverload 110 | func decoded(_ type: String) -> String? { 111 | ObjCTypeDecoder.decode(type)?.decoded() 112 | } 113 | 114 | func decoded(_ type: String) -> ObjCType? { 115 | ObjCTypeDecoder.decode(type) 116 | } 117 | } 118 | --------------------------------------------------------------------------------