Repository: pointfreeco/swift-case-paths
Pull request #13 (2020-07-26 19:27:30)

Swift 5.3 crash
# Contents

A fix for #11 

# Framework Target

A check is added before instantiating a value with a memory layout size of `0`. 

This new check ensures we do not attempt to instantiate a value from an uninhabited enum, which is what was causing the crash.

# Test Target

A new test is added which attempts to extract associated values from both `Never` and an equivalent, uninhabited custom type.

This test will trigger a Swift 5.3 crash without the framework changes, but passes with the changes applied.


Patch:
diff --git a/Sources/CasePaths/EnumReflection.swift b/Sources/CasePaths/EnumReflection.swift
index 959b2ac..ab36c61 100644
--- a/Sources/CasePaths/EnumReflection.swift
+++ b/Sources/CasePaths/EnumReflection.swift
@@ -66,7 +66,7 @@ public func extract<Root, Value>(case embed: (Value) -> Root, from root: Root) -
       }
       any = anyChild
     }
-    if MemoryLayout<Value>.size == 0 {
+    if MemoryLayout<Value>.size == 0, !isUninhabitedEnum(Value.self) {
       return (["\(root)"], unsafeBitCast((), to: Value.self))
     }
     return nil
@@ -97,3 +97,42 @@ public func extract<Root, Value>(_ case: @escaping (Value) -> Root) -> (Root) ->
     return extract(case: `case`, from: root)
   }
 }
+
+// MARK: - Private Helpers
+
+private struct EnumMetadata {
+  let kind: Int
+  let typeDescriptor: UnsafePointer<EnumTypeDescriptor>
+}
+
+private struct EnumTypeDescriptor {
+  // These fields are not modeled because we don't need them.
+  // They are the type descriptor flags and various pointer offsets.
+  let flags, p1, p2, p3, p4: Int32
+  
+  let numPayloadCasesAndPayloadSizeOffset: Int32
+  let numEmptyCases: Int32
+  
+  var numPayloadCases: Int32 {
+    numPayloadCasesAndPayloadSizeOffset & 0xFFFFFF
+  }
+}
+
+private func isUninhabitedEnum(_ type: Any.Type) -> Bool {
+    // Load the type kind from the common type metadata area. Memory layout reference:
+    // https://github.com/apple/swift/blob/master/docs/ABI/TypeMetadata.rst
+    let metadataPtr = unsafeBitCast(type, to: UnsafeRawPointer.self)
+    let metadataKind = metadataPtr.load(as: Int.self)
+    
+    // Check that this is an enum. Value reference:
+    // https://github.com/apple/swift/blob/master/stdlib/public/core/ReflectionMirror.swift
+    let isEnum = metadataKind == 0x201
+    guard isEnum else { return false }
+    
+    // Access enum type descriptor
+    let enumMetadata = metadataPtr.load(as: EnumMetadata.self)
+    let enumTypeDescriptor = enumMetadata.typeDescriptor.pointee
+    
+    let numCases = enumTypeDescriptor.numPayloadCases + enumTypeDescriptor.numEmptyCases
+    return numCases == 0
+}


Test patch:
diff --git a/Tests/CasePathsTests/CasePathsTests.swift b/Tests/CasePathsTests/CasePathsTests.swift
index 53f9661..91777ae 100644
--- a/Tests/CasePathsTests/CasePathsTests.swift
+++ b/Tests/CasePathsTests/CasePathsTests.swift
@@ -192,7 +192,27 @@ final class CasePathsTests: XCTestCase {
         .extract(from: .bar(.baz))
     )
   }
+  
+  func testNestedUninhabitedTypes() {
+    enum Uninhabited {}
+    
+    enum Foo {
+      case foo
+      case bar(Uninhabited)
+      case baz(Never)
+    }
+
+    XCTAssertNil(
+      (/Foo.bar)
+        .extract(from: Foo.foo)
+    )
 
+    XCTAssertNil(
+      (/Foo.baz)
+        .extract(from: Foo.foo)
+    )
+  }
+  
   func testEnumsWithoutAssociatedValues() {
     enum Foo: Equatable {
       case bar