Skip to content

Commit

Permalink
Implemented parameterized interface ID computation.
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanlabelle committed Oct 29, 2023
1 parent 861ee0e commit 9a6f480
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 13 deletions.
125 changes: 125 additions & 0 deletions Sources/WindowsMetadata/InterfaceID.swift
Original file line number Diff line number Diff line change
@@ -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)")
}
}
149 changes: 149 additions & 0 deletions Sources/WindowsMetadata/SHA1.swift
Original file line number Diff line number Diff line change
@@ -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<Bytes: Sequence>(_ 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))
}
}
5 changes: 5 additions & 0 deletions Sources/WindowsMetadata/WinMDError.swift
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 7 additions & 8 deletions Sources/WindowsMetadata/WinRTTypeName+fromType.swift
Original file line number Diff line number Diff line change
@@ -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)
}
Expand Down
74 changes: 74 additions & 0 deletions Tests/UnitTests/WindowsMetadata/SHA1Tests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 9a6f480

Please sign in to comment.