From 9a6f48018a4b4079bdc876c56acc18bb762edd08 Mon Sep 17 00:00:00 2001 From: Tristan Labelle Date: Sun, 29 Oct 2023 17:36:13 -0400 Subject: [PATCH] Implemented parameterized interface ID computation. --- Sources/WindowsMetadata/InterfaceID.swift | 125 +++++++++++++++ Sources/WindowsMetadata/SHA1.swift | 149 ++++++++++++++++++ Sources/WindowsMetadata/WinMDError.swift | 5 + .../WinRTTypeName+fromType.swift | 15 +- .../UnitTests/WindowsMetadata/SHA1Tests.swift | 74 +++++++++ .../WindowsMetadata/WinMetadataTests.swift | 19 ++- 6 files changed, 374 insertions(+), 13 deletions(-) create mode 100644 Sources/WindowsMetadata/InterfaceID.swift create mode 100644 Sources/WindowsMetadata/SHA1.swift create mode 100644 Sources/WindowsMetadata/WinMDError.swift create mode 100644 Tests/UnitTests/WindowsMetadata/SHA1Tests.swift diff --git a/Sources/WindowsMetadata/InterfaceID.swift b/Sources/WindowsMetadata/InterfaceID.swift new file mode 100644 index 0000000..41ee117 --- /dev/null +++ b/Sources/WindowsMetadata/InterfaceID.swift @@ -0,0 +1,125 @@ +import DotNetMetadata +import struct Foundation.UUID + +public func getInterfaceID(_ type: InterfaceDefinition, genericArgs: [TypeNode]? = nil) throws -> UUID { + if let genericArgs = genericArgs, genericArgs.count > 0 { + return try getParameterizedInterfaceID(type.bindType(genericArgs: genericArgs)) + } + else { + guard let guid = try type.findAttribute(GuidAttribute.self) else { throw WinMDError.missingAttribute } + return guid + } +} + +public func getInterfaceID(_ type: DelegateDefinition, genericArgs: [TypeNode]? = nil) throws -> UUID { + if let genericArgs = genericArgs, genericArgs.count > 0 { + return try getParameterizedInterfaceID(type.bindType(genericArgs: genericArgs)) + } + else { + guard let guid = try type.findAttribute(GuidAttribute.self) else { throw WinMDError.missingAttribute } + return guid + } +} + +// https://learn.microsoft.com/en-us/uwp/winrt-cref/winrt-type-system#guid-generation-for-parameterized-types +fileprivate let parameterizedInterfaceGuidBytes: [UInt8] = [ + 0x11, 0xf4, 0x7a, 0xd5, + 0x7b, 0x73, + 0x42, 0xc0, + 0xab, 0xae, 0x87, 0x8b, 0x1e, 0x16, 0xad, 0xee +]; + +private func getParameterizedInterfaceID(_ type: BoundType) throws -> UUID { + var signature: String = "" + try appendSignature(type, to: &signature) + + var sha1 = SHA1() + sha1.process(parameterizedInterfaceGuidBytes) + sha1.process(Array(signature.utf8)) + let hash = sha1.finalize() + return UUID(uuid: ( + hash[0], hash[1], hash[2], hash[3], + hash[4], hash[5], + (hash[6] & 0x0F) | 0x50, hash[7], + (hash[8] & 0x3F) | 0x80, hash[9], + hash[10], hash[11], hash[12], hash[13], hash[14], hash[15])) +} + +fileprivate func appendGuid(_ guid: UUID, to signature: inout String) { + signature.append("{") + signature.append(guid.uuidString.lowercased()) + signature.append("}") +} + +fileprivate func appendSignature(_ type: BoundType, to signature: inout String) throws { + if type.genericArgs.count > 0 { + signature.append("pinterface(") + appendGuid(try type.definition.findAttribute(GuidAttribute.self)!, to: &signature) + signature.append(";") + for (index, arg) in type.genericArgs.enumerated() { + if index > 0 { signature.append(";") } + guard case .bound(let arg) = arg else { throw WinMDError.unexpectedType } + try appendSignature(arg, to: &signature) + } + signature.append(")") + return + } + + let typeDefinition = type.definition + if typeDefinition.namespace == "System" { + switch typeDefinition.name { + case "Boolean": signature.append("b1") + case "Byte": signature.append("u1") + case "SByte": signature.append("i1") + case "Int16": signature.append("i2") + case "UInt16": signature.append("u2") + case "Int32": signature.append("i4") + case "UInt32": signature.append("u4") + case "Int64": signature.append("i8") + case "UInt64": signature.append("u8") + case "Single": signature.append("f4") + case "Double": signature.append("f8") + case "Char": signature.append("c2") + case "String": signature.append("string") + case "Guid": signature.append("g16") + case "Object": signature.append("cinterface(IInspectable)") + default: throw WinMDError.unexpectedType + } + + return + } + + switch typeDefinition { + case let structDefinition as StructDefinition: + signature.append("struct(") + signature.append(structDefinition.fullName) + signature.append(";") + for (index, field) in structDefinition.fields.enumerated() { + guard field.isInstance else { continue } + if index > 0 { signature.append(";") } + guard case .bound(let fieldType) = try field.type else { throw WinMDError.unexpectedType } + try appendSignature(fieldType, to: &signature) + } + signature.append(")") + case let enumDefinition as EnumDefinition: + signature.append("enum(") + signature.append(enumDefinition.fullName) + signature.append(";") + try appendSignature(enumDefinition.underlyingType.bindType(), to: &signature) + signature.append(")") + case let delegateDefinition as DelegateDefinition: + signature.append("delegate(") + appendGuid(try delegateDefinition.findAttribute(GuidAttribute.self)!, to: &signature) + signature.append(")") + case let interfaceDefinition as InterfaceDefinition: + appendGuid(try interfaceDefinition.findAttribute(GuidAttribute.self)!, to: &signature) + case let classDefinition as ClassDefinition: + signature.append("rc(") + signature.append(classDefinition.fullName) + signature.append(";") + try appendSignature(DefaultAttribute.getDefaultInterface(classDefinition)!.asBoundType, to: &signature) + signature.append(")") + default: + fatalError("Unexpected type definition: \(typeDefinition)") + } +} \ No newline at end of file diff --git a/Sources/WindowsMetadata/SHA1.swift b/Sources/WindowsMetadata/SHA1.swift new file mode 100644 index 0000000..377c7e1 --- /dev/null +++ b/Sources/WindowsMetadata/SHA1.swift @@ -0,0 +1,149 @@ +// Adapted from https://github.com/CommanderBubble/sha1 @ 356ab4f3df3c1573a3a7a56f7181f63616e495a6 + +// The MIT License (MIT) +// +// Copyright (c) 2015 Michael Lloyd +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +internal struct SHA1 { + public static let blockSize = 64 + public static let digestSize = 20 + + var h = (UInt32.zero, UInt32.zero, UInt32.zero, UInt32.zero, UInt32.zero) + /// Accumulates bytes until having a complete block to process + var block = [UInt8]() + var messageLength: UInt64 = 0 + + public init() { + block.reserveCapacity(Self.blockSize) + reset() + } + + public static func get(_ input: [UInt8]) -> [UInt8] { + var sha1 = SHA1() + sha1.process(input) + return sha1.finalize() + } + + public mutating func process(_ input: Bytes) where Bytes.Element == UInt8 { + for byte in input { + block.append(byte) + messageLength += 1 + if block.count == Self.blockSize { + processBlock() + block.removeAll() + } + } + } + + public mutating func reset() { + h = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0) + block.removeAll() + messageLength = 0 + } + + public mutating func finalize() -> [UInt8] { + assert(block.count < Self.blockSize) + block.append(0x80) // Begin padding + + // Pad until we have exactly 8 bytes left to write the message length + while block.count != Self.blockSize - 8 { + if block.count == Self.blockSize { + processBlock() + block.removeAll() + } + block.append(0x00) + } + + // Append message length + Self.appendBigEndianUInt32(&block, UInt32((messageLength >> 29) & 0xFFFFFFFF)) + Self.appendBigEndianUInt32(&block, UInt32((messageLength & 0x1FFFFFFF) << 3)) + processBlock() + block.removeAll() + + // copy the digest bytes + var digest = [UInt8]() + Self.appendBigEndianUInt32(&digest, h.0) + Self.appendBigEndianUInt32(&digest, h.1) + Self.appendBigEndianUInt32(&digest, h.2) + Self.appendBigEndianUInt32(&digest, h.3) + Self.appendBigEndianUInt32(&digest, h.4) + + reset() + + return digest + } + + private mutating func processBlock() { + // copy and expand the message block + var W = [UInt32](repeating: 0, count: 80) + for t in 0..<16 { + W[t] = (UInt32(block[t * 4]) << 24) + | (UInt32(block[t * 4 + 1]) << 16) + | (UInt32(block[t * 4 + 2]) << 8) + | UInt32(block[t * 4 + 3]) + } + + for t in 16..<80 { + W[t] = Self.rotateBitsLeft(W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16], 1) + } + + // main loop + var a = h.0, b = h.1, c = h.2, d = h.3, e = h.4 + for t in 0..<80 { + let K: UInt32, f: UInt32 + if (t < 20) { + K = 0x5a827999 + f = (b & c) | ((b ^ 0xFFFFFFFF) & d) //TODO: try using ~ + } else if (t < 40) { + K = 0x6ed9eba1 + f = b ^ c ^ d + } else if (t < 60) { + K = 0x8f1bbcdc + f = (b & c) | (b & d) | (c & d) + } else { + K = 0xca62c1d6 + f = b ^ c ^ d + } + + let temp = Self.rotateBitsLeft(a, 5) &+ f &+ e &+ W[t] &+ K + e = d + d = c + c = Self.rotateBitsLeft(b, 30) + b = a + a = temp + } + + // add variables + h = (h.0 &+ a, h.1 &+ b, h.2 &+ c, h.3 &+ d, h.4 &+ e) + } + + private static func rotateBitsLeft(_ data: UInt32, _ shift_bits: UInt32) -> UInt32 { + (data << shift_bits) | (data >> (32 - shift_bits)) + } + + // Save a 32-bit unsigned integer to memory, in big-endian order + private static func appendBigEndianUInt32(_ bytes: inout [UInt8], _ num: UInt32) { + bytes.append(UInt8((num >> 24) & 0xFF)) + bytes.append(UInt8((num >> 16) & 0xFF)) + bytes.append(UInt8((num >> 8) & 0xFF)) + bytes.append(UInt8((num >> 0) & 0xFF)) + } +} \ No newline at end of file diff --git a/Sources/WindowsMetadata/WinMDError.swift b/Sources/WindowsMetadata/WinMDError.swift new file mode 100644 index 0000000..f5d0b19 --- /dev/null +++ b/Sources/WindowsMetadata/WinMDError.swift @@ -0,0 +1,5 @@ +public enum WinMDError: Hashable, Error { + case missingAttribute + /// A type was used from the System namespace which is not a WinRT base type + case unexpectedType +} \ No newline at end of file diff --git a/Sources/WindowsMetadata/WinRTTypeName+fromType.swift b/Sources/WindowsMetadata/WinRTTypeName+fromType.swift index 2b2419b..6e49647 100644 --- a/Sources/WindowsMetadata/WinRTTypeName+fromType.swift +++ b/Sources/WindowsMetadata/WinRTTypeName+fromType.swift @@ -1,27 +1,26 @@ import DotNetMetadata extension WinRTTypeName { - public static func from(type: BoundType) -> WinRTTypeName? { + public static func from(type: BoundType) throws -> WinRTTypeName { if type.definition.namespace == "System" { - guard type.genericArgs.isEmpty else { return nil } - guard let primitiveType = WinRTPrimitiveType.fromSystemType(name: type.definition.name) else { return nil } + guard type.genericArgs.isEmpty else { throw WinMDError.unexpectedType } + guard let primitiveType = WinRTPrimitiveType.fromSystemType(name: type.definition.name) else { throw WinMDError.unexpectedType } return .primitive(primitiveType) } // https://learn.microsoft.com/en-us/uwp/winrt-cref/winrt-type-system // > All types—except for the fundamental types—must be contained within a namespace. // > It's not valid for a type to be in the global namespace. - guard let namespace = type.definition.namespace else { return nil } + guard let namespace = type.definition.namespace else { throw WinMDError.unexpectedType } if type.definition.genericArity > 0 { guard let parameterizedType = WinRTParameterizedType.from( - namespace: namespace, name: type.definition.name) else { return nil } + namespace: namespace, name: type.definition.name) else { throw WinMDError.unexpectedType } var genericArgs = [WinRTTypeName]() for genericArg in type.genericArgs { - guard case .bound(let genericArgBoundType) = genericArg, - let genericArgWinRTTypeName = from(type: genericArgBoundType) else { return nil } - genericArgs.append(genericArgWinRTTypeName) + guard case .bound(let genericArgBoundType) = genericArg else { throw WinMDError.unexpectedType } + genericArgs.append(try from(type: genericArgBoundType)) } return .parameterized(parameterizedType, args: genericArgs) } diff --git a/Tests/UnitTests/WindowsMetadata/SHA1Tests.swift b/Tests/UnitTests/WindowsMetadata/SHA1Tests.swift new file mode 100644 index 0000000..2814c36 --- /dev/null +++ b/Tests/UnitTests/WindowsMetadata/SHA1Tests.swift @@ -0,0 +1,74 @@ +@testable import WindowsMetadata +import XCTest + +final class SHA1Tests: XCTestCase { + func toHex(_ bytes: [UInt8]) -> String { + bytes.map { String(format: "%02x", $0) }.joined() + } + + func testValueOfEmpty() throws { + XCTAssertEqual(toHex(SHA1.get([])), "da39a3ee5e6b4b0d3255bfef95601890afd80709") + } + + func testValueOfSingleByte() throws { + XCTAssertEqual(toHex(SHA1.get([0x42])), "ae4f281df5a5d0ff3cad6371f76d5c29b6d953ec") + } + + func testValuesAroundPaddingLength() throws { + // Pad with 0x80 0x00, then message length + XCTAssertEqual( + toHex(SHA1.get([UInt8](repeating: 0x42, count: SHA1.blockSize - 10))), + "9f577f3425985e9b9ec5b11c4ed76675eb4a2aeb") + // Pad with 0x80, then message length + XCTAssertEqual( + toHex(SHA1.get([UInt8](repeating: 0x42, count: SHA1.blockSize - 9))), + "f42fc57c149118d6307f96b17acc00f19b4c8de7") + // Pad with 0x80 + 0x00*64, then message length + XCTAssertEqual( + toHex(SHA1.get([UInt8](repeating: 0x42, count: SHA1.blockSize - 8))), + "021f99328a6a79566f055914466ae1654d16ab01") + } + + func testValueOfOneBlockPlusOneByte() throws { + XCTAssertEqual( + toHex(SHA1.get([UInt8](repeating: 0x42, count: 65))), + "550fdc7cb0c34885cf8632c33c7057947578142b") + } + + func testIntraBlockAppending() throws { + let oneThenOneByte = { + var sha1 = SHA1() + sha1.process([0x00]) + sha1.process([0x01]) + return sha1.finalize() + }() + + let twoBytes = SHA1.get([0x00, 0x01]) + XCTAssertEqual(oneThenOneByte, twoBytes) + } + + func testBlockSplitting() throws { + let oneAndAHalfBlock = { + var sha1 = SHA1() + sha1.process([UInt8](repeating: 0, count: SHA1.blockSize * 3 / 2)) + return sha1.finalize() + }() + + let oneThenHalfBlock = { + var sha1 = SHA1() + sha1.process([UInt8](repeating: 0, count: SHA1.blockSize)) + sha1.process([UInt8](repeating: 0, count: SHA1.blockSize / 2)) + return sha1.finalize() + }() + + let halfThenOneBlock = { + var sha1 = SHA1() + sha1.process([UInt8](repeating: 0, count: SHA1.blockSize / 2)) + sha1.process([UInt8](repeating: 0, count: SHA1.blockSize)) + return sha1.finalize() + }() + + XCTAssertEqual(oneAndAHalfBlock, oneThenHalfBlock) + XCTAssertEqual(oneAndAHalfBlock, halfThenOneBlock) + } +} diff --git a/Tests/UnitTests/WindowsMetadata/WinMetadataTests.swift b/Tests/UnitTests/WindowsMetadata/WinMetadataTests.swift index be9795f..fe40dad 100644 --- a/Tests/UnitTests/WindowsMetadata/WinMetadataTests.swift +++ b/Tests/UnitTests/WindowsMetadata/WinMetadataTests.swift @@ -1,21 +1,23 @@ @testable import DotNetMetadata +import WindowsMetadata import DotNetMetadataFormat +import struct Foundation.UUID import XCTest final class WinMetadataTests: XCTestCase { internal static var context: AssemblyLoadContext! + internal static var mscorlib: Mscorlib! internal static var assembly: Assembly! override class func setUp() { guard let windowsFoundationPath = SystemAssemblies.WinMetadata.windowsFoundationPath else { return } let url = URL(fileURLWithPath: windowsFoundationPath) + context = AssemblyLoadContext(resolver: { _ in throw AssemblyLoadError.notFound() }) // Resolve the mscorlib dependency from the .NET Framework 4 machine installation - context = AssemblyLoadContext(resolver: { - guard $0.name == Mscorlib.name, let mscorlibPath = SystemAssemblies.DotNetFramework4.mscorlibPath else { throw AssemblyLoadError.notFound() } - return try ModuleFile(path: mscorlibPath) - }) - + if let mscorlibPath = SystemAssemblies.DotNetFramework4.mscorlibPath { + mscorlib = try? context.load(path: mscorlibPath) as? Mscorlib + } assembly = try? context.load(url: url) } @@ -28,4 +30,11 @@ final class WinMetadataTests: XCTestCase { try Self.assembly.findDefinedType(fullName: "Windows.Foundation.Point")?.base?.definition.fullName, "System.ValueType") } + + func testParameterizedInterfaceID() throws { + let iasyncOperation = try XCTUnwrap(Self.assembly.findDefinedType(fullName: "Windows.Foundation.IAsyncOperation`1") as? InterfaceDefinition) + XCTAssertEqual( + try WindowsMetadata.getInterfaceID(iasyncOperation, genericArgs: [Self.mscorlib.specialTypes.boolean.bindNode()]), + UUID(uuidString: "cdb5efb3-5788-509d-9be1-71ccb8a3362a")) + } }