Repository: apple/swift-docc (swift)

Issue:
Better handling for invalid Info.plist files
When building documentation for a catalog that contains an invalid `Info.plist` file, the error message `docc` outputs is not user-friendly:

```
$ docc convert IncompleteInfoPlist.docc

Error: dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not a valid property list.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Value missing for key inside <dict> at line 26" UserInfo={NSDebugDescription=Value missing for key inside <dict> at line 26, kCFPropertyListOldStyleParsingError=Error Domain=NSCocoaErrorDomain Code=3840 "Malformed data byte group at line 1; invalid hex" UserInfo={NSDebugDescription=Malformed data byte group at line 1; invalid hex}})))
```

[IncompleteInfoPlist.docc.zip](https://github.com/apple/swift-docc/files/8714618/IncompleteInfoPlist.docc.zip)

We should display a more actionable user-facing diagnostic.

### Checklist
- [x] If possible, I've reproduced the issue using the `main` branch of this package.
- [x] This issue hasn't been addressed in an [existing GitHub issue](https://github.com/apple/swift-docc/issues).

### Swift-DocC Version Information

**Swift-DocC version:** `5.7`, `main`

Radar: rdar://93376709

Pull request description:
Bug/issue #, if applicable: #259 

## Summary

Show a more user-friendly diagnostic for Info.plist decode error

## Detail

According to Apple's Foundation doc:
https://developer.apple.com/documentation/foundation/propertylistdecoder/2895397-decode

The decode method on PropertyListDecoder will throw the [DecodingError.dataCorrupted(_:)](https://developer.apple.com/documentation/swift/decodingerror/datacorrupted)

But the default errorDescription on DecodingError or debugDescription DecodingError.Context will be verbose containing too much irrelevant info. Thus we can convert it to NSError and retrieve the core info we want from NSError.userInfo.

## Discuss

Currently, the diagnostic shows “Info.plist decode error.” + the inner debugMessage.
eg. “ Error: Info.plist decode failed. Value missing for key inside <dict> at line 26”

What's the expected diagnostic our user expects?
Please comment below if you have some suggestions on the diagnostic info.

## Testing

Use IncompleteInfoPlist.docc.zip in #259 

```
$ docc convert IncompleteInfoPlist.docc
Error: Info.plist decode failed. Value missing for key inside <dict> at line 26
```

## Checklist

Make sure you check off the following items. If they cannot be completed, provide a reason.

- [x] Added tests
- [x] Ran the `./bin/test` script and it succeeded
- [x] Updated documentation if necessary

Patch:
diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DataProviderBundleDiscovery.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DataProviderBundleDiscovery.swift
index 4f12b76469..c202237c4e 100644
--- a/Sources/SwiftDocC/Infrastructure/Workspace/DataProviderBundleDiscovery.swift
+++ b/Sources/SwiftDocC/Infrastructure/Workspace/DataProviderBundleDiscovery.swift
@@ -52,13 +52,13 @@ extension DocumentationWorkspaceDataProvider where Self: FileSystemProvider {
     ///           a ``DocumentationBundle/PropertyListError`` error if the bundle's Info.plist file is invalid.
     /// - Returns: The new documentation bundle.
     private func createBundle(_ directory: FSNode.Directory, _ bundleChildren: [FSNode], options: BundleDiscoveryOptions) throws -> DocumentationBundle {
-        let info: DocumentationBundle.Info
-        
-        var infoPlistData: Data?
+        let infoPlistData: Data?
         if let infoPlistRef = findInfoPlist(bundleChildren) {
             infoPlistData = try contentsOfURL(infoPlistRef.url)
+        } else {
+            infoPlistData = nil
         }
-        info = try DocumentationBundle.Info(from: infoPlistData, bundleDiscoveryOptions: options)
+        let info = try DocumentationBundle.Info(from: infoPlistData, bundleDiscoveryOptions: options)
         
         let markupFiles = findMarkupFiles(bundleChildren, recursive: true).map { $0.url }
         let miscResources = findNonMarkupFiles(bundleChildren, recursive: true).map { $0.url }
diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift
index 4f5f0dffac..f07000e91d 100644
--- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift
+++ b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift
@@ -64,11 +64,16 @@ extension DocumentationBundle {
         
         enum Error: DescribedError {
             case wrongType(expected: Any.Type, actual: Any.Type)
+            case plistDecodingError(_ context: DecodingError.Context)
             
             var errorDescription: String {
                 switch self {
                 case .wrongType(let expected, let actual):
                     return "Expected '\(expected)', but found '\(actual)'."
+                case .plistDecodingError(let context):
+                    let message = "Unable to decode Info.plist file. Verify that it is correctly formed."
+                    let verboseMessage = ((context.underlyingError as? NSError)?.userInfo["NSDebugDescription"] as? String) ?? context.debugDescription
+                    return [message, verboseMessage].joined(separator: " ")
                 }
             }
         }
@@ -86,10 +91,15 @@ extension DocumentationBundle {
                     propertyListDecoder.userInfo[.bundleDiscoveryOptions] = options
                 }
                 
-                self = try propertyListDecoder.decode(
-                    DocumentationBundle.Info.self,
-                    from: infoPlist
-                )
+                do {
+                    self = try propertyListDecoder.decode(
+                        DocumentationBundle.Info.self,
+                        from: infoPlist
+                    )
+                } catch DecodingError.dataCorrupted(let context) {
+                    throw Error.plistDecodingError(context)
+                }
+                
             } else {
                 try self.init(with: nil, bundleDiscoveryOptions: options)
             }


Test patch:
diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationBundleInfoTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationBundleInfoTests.swift
index b95706a39d..d77b1dd291 100644
--- a/Tests/SwiftDocCTests/Infrastructure/DocumentationBundleInfoTests.swift
+++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationBundleInfoTests.swift
@@ -284,4 +284,50 @@ class DocumentationBundleInfoTests: XCTestCase {
             try DocumentationBundle.Info(bundleDiscoveryOptions: bundleDiscoveryOptions)
         )
     }
+    
+    func testDataCorruptedPlist() throws {
+        let valueMissingInvaildPlist = """
+        <plist version="1.0">
+        <dict>
+          <key>CDDefaultCodeListingLanguage</key>
+          <string>swift</string>
+          <key>CFBundleName</key>
+          <string>Example</string>
+          <key>CFBundleDisplayName</key>
+          <string>Example</string>
+          <key>CFBundleIdentifier</key>
+          <string>org.swift.docc.example</string>
+          <key>CFBundleDevelopmentRegion</key>
+          <string>en</string>
+          <key>CFBundleIconName</key>
+          <string>DocumentationIcon</string>
+          <key>CFBundleIconFile</key>
+          <string>DocumentationIcon</string>
+          <key>CFBundlePackageType</key>
+          <string>DOCS</string>
+          <key>CFBundleShortVersionString</key>
+          <string>0.1.0</string>
+          <key>CFBundleVersion</key>
+          <string>0.1.0</string>
+          <key>CDAppleDefaultAvailability</key>
+        </dict>
+        </plist>
+        """
+        
+        let valueMissingInvaildPlistData = Data(valueMissingInvaildPlist.utf8)
+        XCTAssertThrowsError(
+            try DocumentationBundle.Info(from: valueMissingInvaildPlistData),
+            "Info.plist decode didn't throw as expected"
+        ) { error in
+            XCTAssertTrue(error is DocumentationBundle.Info.Error)
+            let errorTypeChecking: Bool
+            if case DocumentationBundle.Info.Error.plistDecodingError(_) = error {
+                errorTypeChecking = true
+            } else {
+                errorTypeChecking = false
+            }
+            XCTAssertTrue(errorTypeChecking)
+            XCTAssertEqual(error.localizedDescription, "Unable to decode Info.plist file. Verify that it is correctly formed. Value missing for key inside <dict> at line 24")
+        }
+    }
 }