From 66353a1cc0f51da81501baab2bf3d46a2cacc017 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 16:43:18 +0100 Subject: [PATCH 01/12] Replace uses of `Data(contentsOf:)` with `FileHandle` `Data(contentsOf:)` on non-Darwin platforms comes from the `FoundationNetworking` module, which pulls in a massive amount of transitive dependencies of libcurl. That makes it harder to redistribute a statically linked executable binary of DocC. --- .../Navigator/NavigatorIndex+Ext.swift | 3 ++- .../Indexing/Navigator/NavigatorIndex.swift | 5 ++++- .../Indexing/Navigator/NavigatorTree.swift | 22 ++++++++++++++----- .../Bundle Assets/SVGIDExtractor.swift | 4 ++-- .../LocalFileSystemDataProvider.swift | 3 ++- .../PrebuiltLocalFileSystemDataProvider.swift | 3 ++- Sources/SwiftDocC/Servers/FileServer.swift | 10 ++++----- .../FilesAndFolders.swift | 3 ++- .../DefaultRequestHandler.swift | 3 ++- .../RequestHandler/FileRequestHandler.swift | 3 ++- Sources/generate-symbol-graph/main.swift | 3 ++- 11 files changed, 42 insertions(+), 20 deletions(-) diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift index f4ed35223e..d88ac1607c 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift @@ -47,7 +47,8 @@ public class FileSystemRenderNodeProvider: RenderNodeProvider { // we need to process JSON files only if file.url.pathExtension.lowercased() == "json" { do { - let data = try Data(contentsOf: file.url) + let fileHandle = try FileHandle(forReadingFrom: file.url) + let data = fileHandle.readDataToEndOfFile() renderNode = try RenderNode.decode(fromJSON: data) } catch { let diagnostic = Diagnostic(source: file.url, diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift index 4da3981430..68e4398a11 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift @@ -169,7 +169,10 @@ public class NavigatorIndex { let information = try environment.openDatabase(named: "information", flags: []) - let data = try Data(contentsOf: url.appendingPathComponent("availability.index", isDirectory: false)) + let availabilityIndexFileHandle = try FileHandle( + forReadingFrom: url.appendingPathComponent("availability.index", isDirectory: false) + ) + let data = availabilityIndexFileHandle.readDataToEndOfFile() let plistDecoder = PropertyListDecoder() let availabilityIndex = try plistDecoder.decode(AvailabilityIndex.self, from: data) let bundleIdentifier = bundleIdentifier ?? information.get(type: String.self, forKey: NavigatorIndex.bundleKey) ?? NavigatorIndex.UnknownBundleIdentifier diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift index ee328fc14e..23d21d1de1 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift @@ -129,8 +129,18 @@ public class NavigatorTree { - presentationIdentifier: Defines if nodes should have a presentation identifier useful in presentation contexts. - broadcast: The callback to update get updates of the current process. */ - public func read(from url: URL, bundleIdentifier: String? = nil, interfaceLanguages: Set, timeout: TimeInterval, delay: TimeInterval = 0.01, queue: DispatchQueue, presentationIdentifier: String? = nil, broadcast: BroadcastCallback?) throws { - let data = try Data(contentsOf: url) + public func read( + from url: URL, + bundleIdentifier: String? = nil, + interfaceLanguages: Set, + timeout: TimeInterval, + delay: TimeInterval = 0.01, + queue: DispatchQueue, + presentationIdentifier: String? = nil, + broadcast: BroadcastCallback? + ) throws { + let fileHandle = try FileHandle(forReadingFrom: url) + let data = fileHandle.readDataToEndOfFile() let readingCursor = ReadingCursor(data: data) self.readingCursor = readingCursor @@ -312,9 +322,11 @@ public class NavigatorTree { presentationIdentifier: String? = nil, onNodeRead: ((NavigatorTree.Node) -> Void)? = nil ) throws -> NavigatorTree { - let fileUrl = URL(fileURLWithPath: path) - let data = try Data(contentsOf: fileUrl) - + guard let fileHandle = FileHandle(forReadingAtPath: path) else { + throw Error.cannotOpenFile(path: path) + } + let data = fileHandle.readDataToEndOfFile() + var map = [UInt32: Node]() var index: UInt32 = 0 var cursor = 0 diff --git a/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift b/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift index 02e50794ee..05c6dc0ede 100644 --- a/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift +++ b/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift @@ -40,11 +40,11 @@ enum SVGIDExtractor { /// Returns nil if any errors are encountered or if an `id` attribute is /// not found in the given SVG. static func extractID(from svg: URL) -> String? { - guard let data = try? Data(contentsOf: svg) else { + guard let fileHandle = try? FileHandle(forReadingFrom: svg) else { return nil } - return _extractID(from: data) + return _extractID(from: fileHandle.readDataToEndOfFile()) } } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift index f368125635..18d239c07a 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift @@ -48,6 +48,7 @@ public struct LocalFileSystemDataProvider: DocumentationWorkspaceDataProvider, F public func contentsOfURL(_ url: URL) throws -> Data { precondition(url.isFileURL, "Unexpected non-file url '\(url)'.") - return try Data(contentsOf: url) + let fileHandle = try FileHandle(forReadingFrom: url) + return fileHandle.readDataToEndOfFile() } } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift index f3eca3b0ee..ad6f36afa9 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift @@ -28,7 +28,8 @@ public struct PrebuiltLocalFileSystemDataProvider: DocumentationWorkspaceDataPro public func contentsOfURL(_ url: URL) throws -> Data { precondition(url.isFileURL, "Unexpected non-file url '\(url)'.") - return try Data(contentsOf: url) + let fileHandle = try FileHandle(forReadingFrom: url) + return fileHandle.readDataToEndOfFile() } } diff --git a/Sources/SwiftDocC/Servers/FileServer.swift b/Sources/SwiftDocC/Servers/FileServer.swift index fef56ca19d..f85dfae571 100644 --- a/Sources/SwiftDocC/Servers/FileServer.swift +++ b/Sources/SwiftDocC/Servers/FileServer.swift @@ -10,9 +10,6 @@ import Foundation import SymbolKit -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif fileprivate let slashCharSet = CharacterSet(charactersIn: "/") @@ -175,7 +172,8 @@ public class FileSystemServerProvider: FileServerProvider { public func data(for path: String) -> Data? { let finalURL = directoryURL.appendingPathComponent(path) - return try? Data(contentsOf: finalURL) + let fileHandle = try? FileHandle(forReadingFrom: finalURL) + return fileHandle?.readDataToEndOfFile() } } @@ -230,7 +228,9 @@ public class MemoryFileServerProvider: FileServerProvider { for file in enumerator { guard let file = file as? String else { fatalError("Enumerator returned an unexpected type.") } - guard let data = try? Data(contentsOf: URL(fileURLWithPath: path).appendingPathComponent(file)) else { continue } + guard let fileHandle = try? FileHandle(forReadingFrom: URL(fileURLWithPath: path).appendingPathComponent(file)) + else { continue } + let data = fileHandle.readDataToEndOfFile() if recursive == false && file.contains("/") { continue } // skip if subfolder and recursive is disabled addFile(path: "/\(trimmedSubPath)/\(file)", data: data) } diff --git a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift index 7656975393..4a350ddc52 100644 --- a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift +++ b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift @@ -200,7 +200,8 @@ public struct CopyOfFile: File, DataRepresentable { // to use `FileManager.default` directly here instead of `FileManagerProtocol`. var isDirectory: ObjCBool = false guard FileManager.default.fileExists(atPath: original.path, isDirectory: &isDirectory), !isDirectory.boolValue else { throw Error.notAFile(original) } - return try Data(contentsOf: original) + let fileHandle = try FileHandle(forReadingFrom: original) + return fileHandle.readDataToEndOfFile() } public func write(to url: URL) throws { diff --git a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift index 68d4259605..88f592544f 100644 --- a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift +++ b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift @@ -28,7 +28,8 @@ struct DefaultRequestHandler: RequestHandlerFactory { where ChannelHandler.OutboundOut == HTTPServerResponsePart { return { context, head in - let response = try Data(contentsOf: self.rootURL.appendingPathComponent("index.html")) + let fileHandle = try FileHandle(forReadingFrom: self.rootURL.appendingPathComponent("index.html")) + let response = fileHandle.readDataToEndOfFile() var content = context.channel.allocator.buffer(capacity: response.count) content.writeBytes(response) diff --git a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift index ddbd443a17..c52d483c4d 100644 --- a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift +++ b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift @@ -157,7 +157,8 @@ struct FileRequestHandler: RequestHandlerFactory { // Read the file contents do { - data = try Data(contentsOf: fileURL, options: .mappedIfSafe) + let fileHandle = try FileHandle(forReadingFrom: fileURL) + data = fileHandle.readDataToEndOfFile() totalLength = data.count } catch { throw RequestError(status: .notFound) diff --git a/Sources/generate-symbol-graph/main.swift b/Sources/generate-symbol-graph/main.swift index 2090336344..f2358e0bcc 100644 --- a/Sources/generate-symbol-graph/main.swift +++ b/Sources/generate-symbol-graph/main.swift @@ -206,7 +206,8 @@ func generateSwiftDocCFrameworkSymbolGraph() throws -> SymbolGraph { isDirectory: false ) - let symbolGraphData = try Data(contentsOf: symbolGraphURL) + let symbolGraphFileHandle = try FileHandle(forReadingFrom: symbolGraphURL) + let symbolGraphData = symbolGraphFileHandle.readDataToEndOfFile() return try JSONDecoder().decode(SymbolGraph.self, from: symbolGraphData) } From 0db65faf94af3bf60532198ec82a3d60db573c6f Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 16:44:08 +0100 Subject: [PATCH 02/12] Remove remaining `import FoundationNetworking` lines --- .../Servers/DocumentationSchemeHandlerTests.swift | 3 --- Tests/SwiftDocCTests/Servers/FileServerTests.swift | 4 ---- 2 files changed, 7 deletions(-) diff --git a/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift b/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift index 9f1e9e3823..f46975ed1d 100644 --- a/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift +++ b/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift @@ -9,9 +9,6 @@ */ import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif import XCTest @testable import SwiftDocC diff --git a/Tests/SwiftDocCTests/Servers/FileServerTests.swift b/Tests/SwiftDocCTests/Servers/FileServerTests.swift index 8e1baab7d4..9bc1bc2a62 100644 --- a/Tests/SwiftDocCTests/Servers/FileServerTests.swift +++ b/Tests/SwiftDocCTests/Servers/FileServerTests.swift @@ -9,10 +9,6 @@ */ import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - import XCTest @testable import SwiftDocC From c1b5529499d432f812937683a933177b62034cb2 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 19:28:56 +0100 Subject: [PATCH 03/12] Clean up uses of `URLRequest` and `URLResponse` --- Package.resolved | 9 ++ Package.swift | 3 + .../Servers/DocumentationSchemeHandler.swift | 53 +++++++++-- Sources/SwiftDocC/Servers/FileServer.swift | 35 +++---- .../DocumentationSchemeHandlerTests.swift | 27 ++---- .../Servers/FileServerTests.swift | 91 ++++++++++++------- 6 files changed, 142 insertions(+), 76 deletions(-) diff --git a/Package.resolved b/Package.resolved index d9de795b36..41d21cb0b2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -63,6 +63,15 @@ "revision" : "53e5cb9b18222f66cb8d6fb684d7383e705e0936" } }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "39661f4ca82db8ad050cc060a471c8e2c542b24f", + "version" : "0.1.1" + } + }, { "identity" : "swift-lmdb", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 331def189f..aab959de9f 100644 --- a/Package.swift +++ b/Package.swift @@ -45,6 +45,7 @@ let package = Package( .product(name: "SymbolKit", package: "swift-docc-symbolkit"), .product(name: "CLMDB", package: "swift-lmdb"), .product(name: "Crypto", package: "swift-crypto"), + .product(name: "HTTPTypes", package: "swift-http-types"), ], swiftSettings: swiftSettings ), @@ -139,6 +140,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(url: "https://github.com/apple/swift-docc-symbolkit", branch: "main"), .package(url: "https://github.com/apple/swift-crypto.git", from: "2.5.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.2.0"), + .package(url: "https://github.com/apple/swift-http-types.git", .upToNextMinor(from: "0.1.0")), ] } else { // Building in the Swift.org CI system, so rely on local versions of dependencies. @@ -149,5 +151,6 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(path: "../swift-argument-parser"), .package(path: "../swift-docc-symbolkit"), .package(path: "../swift-crypto"), + .package(path: "../swift-http-types"), ] } diff --git a/Sources/SwiftDocC/Servers/DocumentationSchemeHandler.swift b/Sources/SwiftDocC/Servers/DocumentationSchemeHandler.swift index bcef139c4c..270020f13d 100644 --- a/Sources/SwiftDocC/Servers/DocumentationSchemeHandler.swift +++ b/Sources/SwiftDocC/Servers/DocumentationSchemeHandler.swift @@ -9,16 +9,17 @@ */ import Foundation - -#if canImport(WebKit) -import WebKit +import HTTPTypes @available(*, deprecated, renamed: "DocumentationSchemeHandler") public typealias TopicReferenceSchemeHandler = DocumentationSchemeHandler public class DocumentationSchemeHandler: NSObject { - - public typealias FallbackResponseHandler = (URLRequest) -> (URLResponse, Data)? - + enum Error: Swift.Error { + case noURLProvided + } + + public typealias FallbackResponseHandler = (HTTPRequest) -> (HTTPTypes.HTTPResponse, Data)? + // The schema to support the documentation. public static let scheme = "doc" public static var fullScheme: String { @@ -71,7 +72,7 @@ public class DocumentationSchemeHandler: NSObject { } /// Returns a response to a given request. - public func response(to request: URLRequest) -> (URLResponse, Data?) { + public func response(to request: HTTPRequest) -> (HTTPTypes.HTTPResponse, Data?) { var (response, data) = fileServer.response(to: request) if data == nil, let fallbackHandler = fallbackHandler, let (fallbackResponse, fallbackData) = fallbackHandler(request) { @@ -82,11 +83,47 @@ public class DocumentationSchemeHandler: NSObject { } } +#if canImport(WebKit) +import WebKit + // MARK: WKURLSchemeHandler protocol extension DocumentationSchemeHandler: WKURLSchemeHandler { public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { - let (response, data) = self.response(to: urlSchemeTask.request) + let request = urlSchemeTask.request + + // Render authority pseudo-header in accordance with https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2 + let authority: String + guard let url = request.url else { + urlSchemeTask.didFailWithError(Error.noURLProvided) + return + } + + let userAuthority: String + if let user = url.user { + userAuthority = "\(user)@" + } else { + userAuthority = "" + } + + let portAuthority: String + if let port = url.port { + portAuthority = ":\(port)" + } else { + portAuthority = "" + } + + authority = "\(userAuthority)\(url.host ?? "")\(portAuthority)" + let httpRequest = HTTPRequest(method: .get, scheme: request.url?.scheme, authority: authority, path: request.url?.path) + + let (httpResponse, data) = self.response(to: httpRequest) + + let response = URLResponse( + url: url, + mimeType: httpResponse.headerFields[.contentType], + expectedContentLength: httpResponse.headerFields[.contentLength].flatMap(Int.init) ?? -1, + textEncodingName: httpResponse.headerFields[.contentEncoding] + ) urlSchemeTask.didReceive(response) if let data = data { urlSchemeTask.didReceive(data) diff --git a/Sources/SwiftDocC/Servers/FileServer.swift b/Sources/SwiftDocC/Servers/FileServer.swift index f85dfae571..3913170dc6 100644 --- a/Sources/SwiftDocC/Servers/FileServer.swift +++ b/Sources/SwiftDocC/Servers/FileServer.swift @@ -9,6 +9,7 @@ */ import Foundation +import HTTPTypes import SymbolKit fileprivate let slashCharSet = CharacterSet(charactersIn: "/") @@ -53,16 +54,16 @@ public class FileServer { /** Returns the data for a given URL. */ - public func data(for url: URL) -> Data? { + public func data(for path: String) -> Data? { let providerKey = providers.keys.sorted { (l, r) -> Bool in l.count > r.count - }.filter { (path) -> Bool in - return url.path.trimmingCharacters(in: slashCharSet).hasPrefix(path) + }.filter { (providerPath) -> Bool in + return path.trimmingCharacters(in: slashCharSet).hasPrefix(providerPath) }.first ?? "" //in case missing an exact match, get the root one guard let provider = providers[providerKey] else { fatalError("A provider has not been passed to a FileServer.") } - return provider.data(for: url.path.trimmingCharacters(in: slashCharSet).removingPrefix(providerKey)) + return provider.data(for: path.trimmingCharacters(in: slashCharSet).removingPrefix(providerKey)) } /** @@ -70,32 +71,32 @@ public class FileServer { - Parameter request: The request coming from a web client. - Returns: The response and data which are going to be served to the client. */ - public func response(to request: URLRequest) -> (URLResponse, Data?) { - guard let url = request.url else { - return (HTTPURLResponse(url: baseURL, statusCode: 400, httpVersion: "HTTP/1.1", headerFields: nil)!, nil) + public func response(to request: HTTPRequest) -> (HTTPTypes.HTTPResponse, Data?) { + guard let path = request.path as NSString? else { + return (.init(status: 400), nil) } var data: Data? = nil - let response: URLResponse - + let response: HTTPTypes.HTTPResponse + let mimeType: String // We need to make sure that the path extension is for an actual file and not a symbol name which is a false positive // like: "'...(_:)-6u3ic", that would be recognized as filename with the extension "(_:)-6u3ic". (rdar://71856738) - if url.pathExtension.isAlphanumeric && !url.lastPathComponent.isSwiftEntity { - data = self.data(for: url) - mimeType = FileServer.mimeType(for: url.pathExtension) + if path.pathExtension.isAlphanumeric && !path.lastPathComponent.isSwiftEntity { + data = self.data(for: path as String) + mimeType = FileServer.mimeType(for: path.pathExtension) } else { // request is for a path, we need to fake a redirect here - if url.pathComponents.isEmpty { - xlog("Tried to load an invalid URL: \(url.absoluteString).\nFalling back to serve index.html.") + if path.pathComponents.isEmpty { + xlog("Tried to load an invalid path: \(path).\nFalling back to serve index.html.") } mimeType = "text/html" - data = self.data(for: baseURL.appendingPathComponent("/index.html")) + data = self.data(for: path.appendingPathComponent("/index.html")) } if let data = data { - response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil) + response = .init(status: .ok, headerFields: [.contentType: mimeType, .contentLength: "\(data.count)"]) } else { - response = URLResponse(url: url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) + response = .init(status: .ok, headerFields: [.contentType: mimeType]) } return (response, data) diff --git a/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift b/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift index f46975ed1d..2053e78770 100644 --- a/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift +++ b/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift @@ -9,6 +9,7 @@ */ import Foundation +import HTTPTypes import XCTest @testable import SwiftDocC @@ -21,33 +22,29 @@ class DocumentationSchemeHandlerTests: XCTestCase { forResource: "TestBundle", withExtension: "docc", subdirectory: "Test Bundles")! func testDocumentationSchemeHandler() { - #if !os(Linux) && !os(Android) && !os(Windows) let topicSchemeHandler = DocumentationSchemeHandler(withTemplateURL: templateURL) - let request = URLRequest(url: baseURL.appendingPathComponent("/images/figure1.jpg")) - + let request = HTTPRequest(path: "/images/figure1.jpg") + var (response, data) = topicSchemeHandler.response(to: request) XCTAssertNotNil(data) XCTAssertEqual(response.mimeType, "image/jpeg") - let failingRequest = URLRequest(url: baseURL.appendingPathComponent("/not/found.jpg")) + let failingRequest = HTTPRequest(path: "/not/found.jpg") (response, data) = topicSchemeHandler.response(to: failingRequest) XCTAssertNil(data) - topicSchemeHandler.fallbackHandler = { (request: URLRequest) -> (URLResponse, Data)? in - guard let url = request.url else { return nil } - let response = URLResponse(url: url, mimeType: "text/html", expectedContentLength: helloWorldHTML.count, textEncodingName: nil) + topicSchemeHandler.fallbackHandler = { (request: HTTPRequest) -> (HTTPTypes.HTTPResponse, Data)? in + let response = HTTPResponse(mimeType: "text/html", expectedContentLength: helloWorldHTML.count) return (response, helloWorldHTML) } (response, data) = topicSchemeHandler.response(to: failingRequest) XCTAssertEqual(data, helloWorldHTML) XCTAssertEqual(response.mimeType, "text/html") - #endif } func testSetData() { - #if !os(Linux) && !os(Android) && !os(Windows) let topicSchemeHandler = DocumentationSchemeHandler(withTemplateURL: templateURL) let data = "hello!".data(using: .utf8)! @@ -55,7 +52,7 @@ class DocumentationSchemeHandlerTests: XCTestCase { XCTAssertEqual( topicSchemeHandler.response( - to: URLRequest(url: baseURL.appendingPathComponent("/data/a.txt")) + to: HTTPRequest(path: "/data/a.txt") ).1, data ) @@ -64,20 +61,16 @@ class DocumentationSchemeHandlerTests: XCTestCase { XCTAssertEqual( topicSchemeHandler.response( - to: URLRequest(url: baseURL.appendingPathComponent("/data/b.txt")) + to: HTTPRequest(path: "/data/b.txt") ).1, data ) XCTAssertNil( topicSchemeHandler.response( - to: URLRequest(url: baseURL.appendingPathComponent("/data/a.txt")) + to: HTTPRequest(path: "/data/a.txt") ).1, - "a.txt should have been deleted because we set the daata to b.txt." + "a.txt should have been deleted because we set the data to b.txt." ) - #endif } } - - - diff --git a/Tests/SwiftDocCTests/Servers/FileServerTests.swift b/Tests/SwiftDocCTests/Servers/FileServerTests.swift index 9bc1bc2a62..41af452b6c 100644 --- a/Tests/SwiftDocCTests/Servers/FileServerTests.swift +++ b/Tests/SwiftDocCTests/Servers/FileServerTests.swift @@ -9,6 +9,8 @@ */ import Foundation +import HTTPTypes + import XCTest @testable import SwiftDocC @@ -16,8 +18,29 @@ fileprivate let baseURL = URL(string: "test://")! fileprivate let helloWorldHTML = "
Hello Title
Hello world".data(using: .utf8)! fileprivate let jsFile = "var jsFile = true;".data(using: .utf8)! +extension HTTPRequest { + init(path: String) { + self.init(method: .get, scheme: nil, authority: nil, path: path) + } +} + +extension HTTPTypes.HTTPResponse { + init(mimeType: String? = nil, expectedContentLength: Int = -1) { + var headerFields = HTTPFields() + if let mimeType = mimeType { + headerFields[.contentType] = mimeType + } + if expectedContentLength >= 0 { + headerFields[.contentLength] = "\(expectedContentLength)" + } + self.init(status: .ok, headerFields: headerFields) + } + var mimeType: String? { + self.headerFields[.contentType] + } +} + class FileServerTests: XCTestCase { - var defaultFileServer: FileServer = { var fileServer = FileServer(baseURL: baseURL) var memoryFileProvider = MemoryFileServerProvider() @@ -32,33 +55,33 @@ class FileServerTests: XCTestCase { }() func testBasicURL() { - var retrieved = defaultFileServer.data(for: baseURL) + var retrieved = defaultFileServer.data(for: "/") XCTAssertEqual(helloWorldHTML, retrieved) - retrieved = defaultFileServer.data(for: baseURL.appendingPathComponent("index.html")) + retrieved = defaultFileServer.data(for: "index.html") XCTAssertEqual(helloWorldHTML, retrieved) - retrieved = defaultFileServer.data(for: baseURL.appendingPathComponent("/js/file.js")) + retrieved = defaultFileServer.data(for: "/js/file.js") XCTAssertEqual(jsFile, retrieved) } func testBasicPath() { - var retrieved = defaultFileServer.data(for: baseURL.appendingPathComponent("index.html")) + var retrieved = defaultFileServer.data(for: "index.html") XCTAssertEqual(helloWorldHTML, retrieved) - retrieved = defaultFileServer.data(for: baseURL.appendingPathComponent("/index.html")) + retrieved = defaultFileServer.data(for: "/index.html") XCTAssertEqual(helloWorldHTML, retrieved) - retrieved = defaultFileServer.data(for: baseURL.appendingPathComponent("/js/file.js")) + retrieved = defaultFileServer.data(for: "/js/file.js") XCTAssertEqual(jsFile, retrieved) } func testEmpty() { - var retrieved = defaultFileServer.data(for: baseURL.appendingPathComponent("/invalid.html")) - XCTAssertNil(retrieved, "\(baseURL.appendingPathComponent("/invalid.html").absoluteString) should return nil, but returned \(String(describing: retrieved))") - - retrieved = defaultFileServer.data(for: baseURL.appendingPathComponent("/invalid/")) - XCTAssertNil(retrieved, "\(baseURL.appendingPathComponent("/invalid/").absoluteString) should return nil, but returned \(String(describing: retrieved))") + var retrieved = defaultFileServer.data(for: "/invalid.html") + XCTAssertNil(retrieved, "`/invalid.html` should return nil, but returned \(String(describing: retrieved))") + + retrieved = defaultFileServer.data(for: "/invalid/") + XCTAssertNil(retrieved, "`/invalid/` should return nil, but returned \(String(describing: retrieved))") } func testAddingFilesInFolder() { @@ -71,8 +94,8 @@ class FileServerTests: XCTestCase { let fileServer = FileServer(baseURL: baseURL) fileServer.register(provider: memoryFileProvider) - XCTAssertNotNil(fileServer.data(for: baseURL.appendingPathComponent("/figure1.png"))) - XCTAssertNotNil(fileServer.data(for: baseURL.appendingPathComponent("/images/figure1.jpg"))) + XCTAssertNotNil(fileServer.data(for: "/figure1.png")) + XCTAssertNotNil(fileServer.data(for: "/images/figure1.jpg")) } func testAddingRemovingFromPath() { @@ -85,11 +108,11 @@ class FileServerTests: XCTestCase { let fileServer = FileServer(baseURL: baseURL) fileServer.register(provider: memoryFileProvider) - XCTAssertNotNil(fileServer.data(for: baseURL.appendingPathComponent("/images/figure1.jpg"))) - + XCTAssertNotNil(fileServer.data(for: "/images/figure1.jpg")) + memoryFileProvider.removeAllFiles(in: "/images") - XCTAssertNil(fileServer.data(for: baseURL.appendingPathComponent("/images/figure1.jpg"))) + XCTAssertNil(fileServer.data(for: "/images/figure1.jpg")) } func testDiskServerProvider() { @@ -104,8 +127,8 @@ class FileServerTests: XCTestCase { let fileServer = FileServer(baseURL: baseURL) fileServer.register(provider: fileSystemFileProvider) - XCTAssertNotNil(fileServer.data(for: baseURL.appendingPathComponent("/images/figure1.jpg"))) - XCTAssertNotNil(fileServer.data(for: baseURL.appendingPathComponent("/figure1.png"))) + XCTAssertNotNil(fileServer.data(for: "/images/figure1.jpg")) + XCTAssertNotNil(fileServer.data(for: "/figure1.png")) } func testSubPathProvider() { @@ -127,16 +150,16 @@ class FileServerTests: XCTestCase { fileServer.register(provider: memoryFileProvider, subPath: "/subPath") - XCTAssertNotNil(fileServer.data(for: baseURL.appendingPathComponent("/images/figure1.jpg"))) - XCTAssertNotNil(fileServer.data(for: baseURL.appendingPathComponent("/figure1.png"))) + XCTAssertNotNil(fileServer.data(for: "/images/figure1.jpg")) + XCTAssertNotNil(fileServer.data(for: "/figure1.png")) - var retrieved = fileServer.data(for: baseURL.appendingPathComponent("/subPath")) + var retrieved = fileServer.data(for: "/subPath") XCTAssertEqual(helloWorldHTML, retrieved) - retrieved = fileServer.data(for: baseURL.appendingPathComponent("/subPath/index.html")) + retrieved = fileServer.data(for: "/subPath/index.html") XCTAssertEqual(helloWorldHTML, retrieved) - retrieved = fileServer.data(for: baseURL.appendingPathComponent("/subPath/js/file.js")) + retrieved = fileServer.data(for: "/subPath/js/file.js") XCTAssertEqual(jsFile, retrieved) } @@ -150,13 +173,13 @@ class FileServerTests: XCTestCase { let fileServer = FileServer(baseURL: baseURL) fileServer.register(provider: memoryFileProvider) - let request = URLRequest(url: baseURL.appendingPathComponent("/images/figure1.jpg")) - + let request = HTTPRequest(path: "/images/figure1.jpg") + var (response, data) = fileServer.response(to: request) XCTAssertNotNil(data) XCTAssertEqual(response.mimeType, "image/jpeg") - let failingRequest = URLRequest(url: baseURL.appendingPathComponent("/not/found.jpg")) + let failingRequest = HTTPRequest(path: "/not/found.jpg") (response, data) = fileServer.response(to: failingRequest) XCTAssertNil(data) // Initializing a URLResponse with `nil` as MIME type in Linux returns nil @@ -169,39 +192,39 @@ class FileServerTests: XCTestCase { } func testRedirectToHome() { - var request = URLRequest(url: baseURL.appendingPathComponent("/home")) + var request = HTTPRequest(path: "/home") var (response, data) = defaultFileServer.response(to: request) XCTAssertEqual(helloWorldHTML, data) XCTAssertEqual("text/html", response.mimeType) - request = URLRequest(url: baseURL.appendingPathComponent("/foo///bar")) + request = HTTPRequest(path: "/foo///bar") (response, data) = defaultFileServer.response(to: request) XCTAssertEqual(helloWorldHTML, data) XCTAssertEqual("text/html", response.mimeType) - request = URLRequest(url: baseURL.appendingPathComponent("/project/Project")) + request = HTTPRequest(path: "/project/Project") (response, data) = defaultFileServer.response(to: request) XCTAssertEqual(helloWorldHTML, data) XCTAssertEqual("text/html", response.mimeType) - request = URLRequest(url: baseURL.appendingPathComponent("/project/subPath/'...(_:)-6u3ic")) + request = HTTPRequest(path: "/project/subPath/'...(_:)-6u3ic") (response, data) = defaultFileServer.response(to: request) XCTAssertEqual(helloWorldHTML, data) XCTAssertEqual("text/html", response.mimeType) - request = URLRequest(url: baseURL.appendingPathComponent("/project/subPath/body-swift.property")) + request = HTTPRequest(path: "/project/subPath/body-swift.property") (response, data) = defaultFileServer.response(to: request) XCTAssertEqual(helloWorldHTML, data) XCTAssertEqual("text/html", response.mimeType) - request = URLRequest(url: baseURL.appendingPathComponent("/theme/js/highlight-swift.js")) + request = HTTPRequest(path: "/theme/js/highlight-swift.js") (response, data) = defaultFileServer.response(to: request) XCTAssertNotEqual(helloWorldHTML, data) XCTAssertNotEqual("text/html", response.mimeType) } func testInvalidReference() { - let request = URLRequest(url: baseURL.appendingPathComponent("thisWontResolve")) + let request = HTTPRequest(path: "thisWontResolve") let (response, data) = defaultFileServer.response(to: request) XCTAssertEqual(helloWorldHTML, data) XCTAssertEqual("text/html", response.mimeType) From 992d2218db13027d0116f745600ef2f93168cfff Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 19:41:42 +0100 Subject: [PATCH 04/12] Fix needlessly prepending request path --- Sources/SwiftDocC/Servers/FileServer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftDocC/Servers/FileServer.swift b/Sources/SwiftDocC/Servers/FileServer.swift index 3913170dc6..524f1238fa 100644 --- a/Sources/SwiftDocC/Servers/FileServer.swift +++ b/Sources/SwiftDocC/Servers/FileServer.swift @@ -90,7 +90,7 @@ public class FileServer { xlog("Tried to load an invalid path: \(path).\nFalling back to serve index.html.") } mimeType = "text/html" - data = self.data(for: path.appendingPathComponent("/index.html")) + data = self.data(for: "/index.html") } if let data = data { From 58a5d0ee746095232f7e5a2c897f76f0ea82dfef Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 20:20:45 +0100 Subject: [PATCH 05/12] Fix default content type test failure --- Sources/SwiftDocC/Servers/FileServer.swift | 2 +- Tests/SwiftDocCTests/Servers/FileServerTests.swift | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Sources/SwiftDocC/Servers/FileServer.swift b/Sources/SwiftDocC/Servers/FileServer.swift index 524f1238fa..164dcbbe6f 100644 --- a/Sources/SwiftDocC/Servers/FileServer.swift +++ b/Sources/SwiftDocC/Servers/FileServer.swift @@ -96,7 +96,7 @@ public class FileServer { if let data = data { response = .init(status: .ok, headerFields: [.contentType: mimeType, .contentLength: "\(data.count)"]) } else { - response = .init(status: .ok, headerFields: [.contentType: mimeType]) + response = .init(status: .ok, headerFields: [.contentType: "application/octet-stream"]) } return (response, data) diff --git a/Tests/SwiftDocCTests/Servers/FileServerTests.swift b/Tests/SwiftDocCTests/Servers/FileServerTests.swift index 41af452b6c..40d7b17428 100644 --- a/Tests/SwiftDocCTests/Servers/FileServerTests.swift +++ b/Tests/SwiftDocCTests/Servers/FileServerTests.swift @@ -181,14 +181,7 @@ class FileServerTests: XCTestCase { let failingRequest = HTTPRequest(path: "/not/found.jpg") (response, data) = fileServer.response(to: failingRequest) - XCTAssertNil(data) - // Initializing a URLResponse with `nil` as MIME type in Linux returns nil - #if os(Linux) || os(Android) || os(Windows) - XCTAssertNil(response.mimeType) - #else - // Doing the same in macOS or iOS returns the default MIME type XCTAssertEqual(response.mimeType, "application/octet-stream") - #endif } func testRedirectToHome() { From f042a6849fc64a097046f06eb6015c72ac00785d Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 21:30:06 +0100 Subject: [PATCH 06/12] Use throwing `readToEnd` for debugging and error handling --- Package.swift | 3 ++- .../Navigator/NavigatorIndex+Ext.swift | 5 +++-- .../Indexing/Navigator/NavigatorIndex.swift | 20 +++++++++--------- .../Indexing/Navigator/NavigatorTree.swift | 10 ++++++--- .../Bundle Assets/SVGIDExtractor.swift | 7 +++++-- .../LocalFileSystemDataProvider.swift | 6 +++++- .../PrebuiltLocalFileSystemDataProvider.swift | 6 +++++- Sources/SwiftDocC/Servers/FileServer.swift | 9 +++++--- .../Utility/Errors/FileSystemError.swift | 21 +++++++++++++++++++ .../FilesAndFolders.swift | 6 +++++- .../DefaultRequestHandler.swift | 10 ++++++--- .../RequestHandler/FileRequestHandler.swift | 7 +++++-- Sources/generate-symbol-graph/main.swift | 8 ++++++- 13 files changed, 88 insertions(+), 30 deletions(-) create mode 100644 Sources/SwiftDocC/Utility/Errors/FileSystemError.swift diff --git a/Package.swift b/Package.swift index aab959de9f..38677925a6 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ let swiftSettings: [SwiftSetting] = [ let package = Package( name: "SwiftDocC", platforms: [ - .macOS(.v10_15), + .macOS(.v11), .iOS(.v13) ], products: [ @@ -91,6 +91,7 @@ let package = Package( .target( name: "SwiftDocCTestUtilities", dependencies: [ + "SwiftDocC", .product(name: "SymbolKit", package: "swift-docc-symbolkit"), ], swiftSettings: swiftSettings diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift index d88ac1607c..510d86721e 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift @@ -48,8 +48,9 @@ public class FileSystemRenderNodeProvider: RenderNodeProvider { if file.url.pathExtension.lowercased() == "json" { do { let fileHandle = try FileHandle(forReadingFrom: file.url) - let data = fileHandle.readDataToEndOfFile() - renderNode = try RenderNode.decode(fromJSON: data) + if let data = try fileHandle.readToEnd() { + renderNode = try RenderNode.decode(fromJSON: data) + } } catch { let diagnostic = Diagnostic(source: file.url, severity: .warning, diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift index 68e4398a11..08608f57a6 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift @@ -61,9 +61,8 @@ public class NavigatorIndex { /// A specific error to describe issues when processing a `NavigatorIndex`. public enum Error: Swift.Error, DescribedError { - /// Missing bundle identifier. - case missingBundleIndentifier + case missingBundleIdentifier /// A RenderNode has no title and won't be indexed. case missingTitle(description: String) @@ -72,8 +71,8 @@ public class NavigatorIndex { case navigatorIndexIsNil public var errorDescription: String { - switch self { - case .missingBundleIndentifier: + switch self { + case .missingBundleIdentifier: return "A navigator index requires a bundle identifier, which is missing." case .missingTitle: return "The page has no valid title available." @@ -169,16 +168,17 @@ public class NavigatorIndex { let information = try environment.openDatabase(named: "information", flags: []) - let availabilityIndexFileHandle = try FileHandle( - forReadingFrom: url.appendingPathComponent("availability.index", isDirectory: false) - ) - let data = availabilityIndexFileHandle.readDataToEndOfFile() + let availabilityIndexFileURL = url.appendingPathComponent("availability.index", isDirectory: false) + let availabilityIndexFileHandle = try FileHandle(forReadingFrom: availabilityIndexFileURL) + guard let data = try availabilityIndexFileHandle.readToEnd() else { + throw FileSystemError.noDataReadFromFile(path: availabilityIndexFileURL.path) + } let plistDecoder = PropertyListDecoder() let availabilityIndex = try plistDecoder.decode(AvailabilityIndex.self, from: data) let bundleIdentifier = bundleIdentifier ?? information.get(type: String.self, forKey: NavigatorIndex.bundleKey) ?? NavigatorIndex.UnknownBundleIdentifier guard bundleIdentifier != NavigatorIndex.UnknownBundleIdentifier else { - throw Error.missingBundleIndentifier + throw Error.missingBundleIdentifier } // Use `.fnv1` by default if no path hasher is set for compatibility reasons. @@ -285,7 +285,7 @@ public class NavigatorIndex { self.availabilityIndex = AvailabilityIndex() guard self.bundleIdentifier != NavigatorIndex.UnknownBundleIdentifier else { - throw Error.missingBundleIndentifier + throw Error.missingBundleIdentifier } } diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift index 23d21d1de1..094b5c27ba 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift @@ -140,7 +140,9 @@ public class NavigatorTree { broadcast: BroadcastCallback? ) throws { let fileHandle = try FileHandle(forReadingFrom: url) - let data = fileHandle.readDataToEndOfFile() + guard let data = try fileHandle.readToEnd() else { + throw Error.cannotOpenFile(path: url.path) + } let readingCursor = ReadingCursor(data: data) self.readingCursor = readingCursor @@ -322,10 +324,12 @@ public class NavigatorTree { presentationIdentifier: String? = nil, onNodeRead: ((NavigatorTree.Node) -> Void)? = nil ) throws -> NavigatorTree { - guard let fileHandle = FileHandle(forReadingAtPath: path) else { + guard + let fileHandle = FileHandle(forReadingAtPath: path), + let data = try fileHandle.readToEnd() + else { throw Error.cannotOpenFile(path: path) } - let data = fileHandle.readDataToEndOfFile() var map = [UInt32: Node]() var index: UInt32 = 0 diff --git a/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift b/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift index 05c6dc0ede..9d22980da0 100644 --- a/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift +++ b/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift @@ -40,11 +40,14 @@ enum SVGIDExtractor { /// Returns nil if any errors are encountered or if an `id` attribute is /// not found in the given SVG. static func extractID(from svg: URL) -> String? { - guard let fileHandle = try? FileHandle(forReadingFrom: svg) else { + guard + let fileHandle = try? FileHandle(forReadingFrom: svg), + let data = try? fileHandle.readToEnd() + else { return nil } - return _extractID(from: fileHandle.readDataToEndOfFile()) + return _extractID(from: data) } } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift index 18d239c07a..90959a8467 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift @@ -49,6 +49,10 @@ public struct LocalFileSystemDataProvider: DocumentationWorkspaceDataProvider, F public func contentsOfURL(_ url: URL) throws -> Data { precondition(url.isFileURL, "Unexpected non-file url '\(url)'.") let fileHandle = try FileHandle(forReadingFrom: url) - return fileHandle.readDataToEndOfFile() + guard let data = try fileHandle.readToEnd() else { + throw FileSystemError.noDataReadFromFile(path: url.path) + } + + return data } } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift index ad6f36afa9..66b213ba8b 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift @@ -29,7 +29,11 @@ public struct PrebuiltLocalFileSystemDataProvider: DocumentationWorkspaceDataPro public func contentsOfURL(_ url: URL) throws -> Data { precondition(url.isFileURL, "Unexpected non-file url '\(url)'.") let fileHandle = try FileHandle(forReadingFrom: url) - return fileHandle.readDataToEndOfFile() + guard let data = try fileHandle.readToEnd() else { + throw FileSystemError.noDataReadFromFile(path: url.path) + } + + return data } } diff --git a/Sources/SwiftDocC/Servers/FileServer.swift b/Sources/SwiftDocC/Servers/FileServer.swift index 164dcbbe6f..deb8009faf 100644 --- a/Sources/SwiftDocC/Servers/FileServer.swift +++ b/Sources/SwiftDocC/Servers/FileServer.swift @@ -174,7 +174,7 @@ public class FileSystemServerProvider: FileServerProvider { public func data(for path: String) -> Data? { let finalURL = directoryURL.appendingPathComponent(path) let fileHandle = try? FileHandle(forReadingFrom: finalURL) - return fileHandle?.readDataToEndOfFile() + return try? fileHandle?.readToEnd() } } @@ -229,9 +229,12 @@ public class MemoryFileServerProvider: FileServerProvider { for file in enumerator { guard let file = file as? String else { fatalError("Enumerator returned an unexpected type.") } - guard let fileHandle = try? FileHandle(forReadingFrom: URL(fileURLWithPath: path).appendingPathComponent(file)) + let fileURL = URL(fileURLWithPath: path).appendingPathComponent(file) + guard + let fileHandle = try? FileHandle(forReadingFrom: fileURL), + let data = try? fileHandle.readToEnd() else { continue } - let data = fileHandle.readDataToEndOfFile() + if recursive == false && file.contains("/") { continue } // skip if subfolder and recursive is disabled addFile(path: "/\(trimmedSubPath)/\(file)", data: data) } diff --git a/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift b/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift new file mode 100644 index 0000000000..9a75bbf96a --- /dev/null +++ b/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift @@ -0,0 +1,21 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +public enum FileSystemError: Error, DescribedError { + /// No data could be read from file at `path`. + case noDataReadFromFile(path: String) + + public var errorDescription: String { + switch self { + case .noDataReadFromFile(let path): + return "No data could be read from file at `\(path)`" + } + } +} diff --git a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift index 4a350ddc52..671024f044 100644 --- a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift +++ b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift @@ -9,6 +9,7 @@ */ import Foundation +import SwiftDocC import XCTest /* @@ -201,7 +202,10 @@ public struct CopyOfFile: File, DataRepresentable { var isDirectory: ObjCBool = false guard FileManager.default.fileExists(atPath: original.path, isDirectory: &isDirectory), !isDirectory.boolValue else { throw Error.notAFile(original) } let fileHandle = try FileHandle(forReadingFrom: original) - return fileHandle.readDataToEndOfFile() + guard let data = try fileHandle.readToEnd() else { + throw FileSystemError.noDataReadFromFile(path: original.path) + } + return data } public func write(to url: URL) throws { diff --git a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift index 88f592544f..d2337352e3 100644 --- a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift +++ b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift @@ -12,6 +12,7 @@ import Foundation import NIO import NIOHTTP1 +import SwiftDocC /// A request handler that serves the default app page to clients. /// @@ -28,9 +29,12 @@ struct DefaultRequestHandler: RequestHandlerFactory { where ChannelHandler.OutboundOut == HTTPServerResponsePart { return { context, head in - let fileHandle = try FileHandle(forReadingFrom: self.rootURL.appendingPathComponent("index.html")) - let response = fileHandle.readDataToEndOfFile() - + let fileURL = self.rootURL.appendingPathComponent("index.html") + let fileHandle = try FileHandle(forReadingFrom: fileURL) + guard let response = try fileHandle.readToEnd() else { + throw FileSystemError.noDataReadFromFile(path: fileURL.path) + } + var content = context.channel.allocator.buffer(capacity: response.count) content.writeBytes(response) diff --git a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift index c52d483c4d..3cbe77e03e 100644 --- a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift +++ b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift @@ -158,13 +158,16 @@ struct FileRequestHandler: RequestHandlerFactory { // Read the file contents do { let fileHandle = try FileHandle(forReadingFrom: fileURL) - data = fileHandle.readDataToEndOfFile() + guard let readData = try fileHandle.readToEnd() else { + throw FileSystemError.noDataReadFromFile(path: fileURL.path) + } + data = readData totalLength = data.count } catch { throw RequestError(status: .notFound) } - // Add Range header if neccessary + // Add Range header if necessary var headers = HTTPHeaders() let range = head.headers["Range"].first.flatMap(RangeHeader.init) if let range = range { diff --git a/Sources/generate-symbol-graph/main.swift b/Sources/generate-symbol-graph/main.swift index f2358e0bcc..92dd647738 100644 --- a/Sources/generate-symbol-graph/main.swift +++ b/Sources/generate-symbol-graph/main.swift @@ -164,6 +164,10 @@ let supportedDirectives: [Directive] = [ ) } +enum SymbolGraphError: Error { + case noDataReadFromFile(path: String) +} + func generateSwiftDocCFrameworkSymbolGraph() throws -> SymbolGraph { let packagePath = URL(fileURLWithPath: #file) .deletingLastPathComponent() // generate-symbol-graph @@ -207,7 +211,9 @@ func generateSwiftDocCFrameworkSymbolGraph() throws -> SymbolGraph { ) let symbolGraphFileHandle = try FileHandle(forReadingFrom: symbolGraphURL) - let symbolGraphData = symbolGraphFileHandle.readDataToEndOfFile() + guard let symbolGraphData = try symbolGraphFileHandle.readToEnd() else { + throw SymbolGraphError.noDataReadFromFile(path: symbolGraphURL.path) + } return try JSONDecoder().decode(SymbolGraph.self, from: symbolGraphData) } From 71fe7908cff027f564556d8acbe5cb9cadb9b61d Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 22:13:25 +0100 Subject: [PATCH 07/12] Bump swift-http-types to 0.2.1 --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 41d21cb0b2..e9ef1cdf94 100644 --- a/Package.resolved +++ b/Package.resolved @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types.git", "state" : { - "revision" : "39661f4ca82db8ad050cc060a471c8e2c542b24f", - "version" : "0.1.1" + "revision" : "68deedb6b98837564cf0231fa1df48de35881993", + "version" : "0.2.1" } }, { diff --git a/Package.swift b/Package.swift index 38677925a6..f5186a68ae 100644 --- a/Package.swift +++ b/Package.swift @@ -141,7 +141,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(url: "https://github.com/apple/swift-docc-symbolkit", branch: "main"), .package(url: "https://github.com/apple/swift-crypto.git", from: "2.5.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.2.0"), - .package(url: "https://github.com/apple/swift-http-types.git", .upToNextMinor(from: "0.1.0")), + .package(url: "https://github.com/apple/swift-http-types.git", .upToNextMinor(from: "0.2.1")), ] } else { // Building in the Swift.org CI system, so rely on local versions of dependencies. From 8c4e6d04ce2baefabe65629d4680aaf2d790f324 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 22:37:55 +0100 Subject: [PATCH 08/12] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Rönnqvist --- Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift | 3 +++ Sources/SwiftDocC/Servers/FileServer.swift | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift index 08608f57a6..000f6e2653 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift @@ -64,6 +64,9 @@ public class NavigatorIndex { /// Missing bundle identifier. case missingBundleIdentifier + @available(*, deprecated, renamed: "missingBundleIdentifier") + case missingBundleIndentifier + /// A RenderNode has no title and won't be indexed. case missingTitle(description: String) diff --git a/Sources/SwiftDocC/Servers/FileServer.swift b/Sources/SwiftDocC/Servers/FileServer.swift index deb8009faf..c25d9deee0 100644 --- a/Sources/SwiftDocC/Servers/FileServer.swift +++ b/Sources/SwiftDocC/Servers/FileServer.swift @@ -54,6 +54,11 @@ public class FileServer { /** Returns the data for a given URL. */ + @available(*, deprecated, message: "Use 'data(for path: String)' instead.") + public func data(for url: URL) -> Data? { + return data(for url.path) + } + public func data(for path: String) -> Data? { let providerKey = providers.keys.sorted { (l, r) -> Bool in l.count > r.count From fb38bef48bbdebbb0ad05409fdf3cdb8c95bde18 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 22:47:11 +0100 Subject: [PATCH 09/12] Update copyright year, refine error handling --- Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift | 9 +++++---- Sources/SwiftDocC/Utility/Errors/FileSystemError.swift | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift index 094b5c27ba..4859f36a54 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift @@ -324,13 +324,14 @@ public class NavigatorTree { presentationIdentifier: String? = nil, onNodeRead: ((NavigatorTree.Node) -> Void)? = nil ) throws -> NavigatorTree { - guard - let fileHandle = FileHandle(forReadingAtPath: path), - let data = try fileHandle.readToEnd() - else { + guard let fileHandle = FileHandle(forReadingAtPath: path) else { throw Error.cannotOpenFile(path: path) } + guard let data = try fileHandle.readToEnd() else { + throw FileSystemError.noDataReadFromFile(path: path) + } + var map = [UInt32: Node]() var index: UInt32 = 0 var cursor = 0 diff --git a/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift b/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift index 9a75bbf96a..0cdd3b23fb 100644 --- a/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift +++ b/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2023 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information From f2b590878ca7a19305d965572f722b26a39c8a58 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 22:59:02 +0100 Subject: [PATCH 10/12] Check for uses of `FoundationNetworking` in `./bin/test` --- bin/test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/test b/bin/test index c890307978..0569e80741 100755 --- a/bin/test +++ b/bin/test @@ -11,6 +11,8 @@ set -eu +grep -r FoundationNetworking Sources && echo "Replace uses of FoundationNetworking with alternatives" && exit 1 + # A `realpath` alternative using the default C implementation. filepath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" From ea5bccbda801e2f3155e99f26ef414fb2f4e5194 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 23:41:58 +0100 Subject: [PATCH 11/12] Replace `FileSystemError` with `CocoaError` --- Package.swift | 1 - .../Indexing/Navigator/NavigatorIndex.swift | 4 ++-- .../Indexing/Navigator/NavigatorTree.swift | 2 +- .../LocalFileSystemDataProvider.swift | 2 +- .../PrebuiltLocalFileSystemDataProvider.swift | 2 +- Sources/SwiftDocC/Servers/FileServer.swift | 2 +- .../Utility/Errors/FileSystemError.swift | 21 ------------------- .../FilesAndFolders.swift | 2 +- .../DefaultRequestHandler.swift | 2 +- .../RequestHandler/FileRequestHandler.swift | 2 +- 10 files changed, 9 insertions(+), 31 deletions(-) delete mode 100644 Sources/SwiftDocC/Utility/Errors/FileSystemError.swift diff --git a/Package.swift b/Package.swift index f5186a68ae..2f3a810f10 100644 --- a/Package.swift +++ b/Package.swift @@ -91,7 +91,6 @@ let package = Package( .target( name: "SwiftDocCTestUtilities", dependencies: [ - "SwiftDocC", .product(name: "SymbolKit", package: "swift-docc-symbolkit"), ], swiftSettings: swiftSettings diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift index 000f6e2653..80720b003f 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift @@ -75,7 +75,7 @@ public class NavigatorIndex { public var errorDescription: String { switch self { - case .missingBundleIdentifier: + case .missingBundleIdentifier, .missingBundleIndentifier: return "A navigator index requires a bundle identifier, which is missing." case .missingTitle: return "The page has no valid title available." @@ -174,7 +174,7 @@ public class NavigatorIndex { let availabilityIndexFileURL = url.appendingPathComponent("availability.index", isDirectory: false) let availabilityIndexFileHandle = try FileHandle(forReadingFrom: availabilityIndexFileURL) guard let data = try availabilityIndexFileHandle.readToEnd() else { - throw FileSystemError.noDataReadFromFile(path: availabilityIndexFileURL.path) + throw CocoaError(.fileReadUnknown, userInfo: ["path": availabilityIndexFileURL.path]) } let plistDecoder = PropertyListDecoder() let availabilityIndex = try plistDecoder.decode(AvailabilityIndex.self, from: data) diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift index 4859f36a54..2a51e956a6 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift @@ -329,7 +329,7 @@ public class NavigatorTree { } guard let data = try fileHandle.readToEnd() else { - throw FileSystemError.noDataReadFromFile(path: path) + throw CocoaError(.fileReadUnknown, userInfo: ["path": path]) } var map = [UInt32: Node]() diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift index 90959a8467..578ddaa54c 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift @@ -50,7 +50,7 @@ public struct LocalFileSystemDataProvider: DocumentationWorkspaceDataProvider, F precondition(url.isFileURL, "Unexpected non-file url '\(url)'.") let fileHandle = try FileHandle(forReadingFrom: url) guard let data = try fileHandle.readToEnd() else { - throw FileSystemError.noDataReadFromFile(path: url.path) + throw CocoaError(.fileReadUnknown, userInfo: ["path": url.path]) } return data diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift index 66b213ba8b..f0569ee4c0 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift @@ -30,7 +30,7 @@ public struct PrebuiltLocalFileSystemDataProvider: DocumentationWorkspaceDataPro precondition(url.isFileURL, "Unexpected non-file url '\(url)'.") let fileHandle = try FileHandle(forReadingFrom: url) guard let data = try fileHandle.readToEnd() else { - throw FileSystemError.noDataReadFromFile(path: url.path) + throw CocoaError(.fileReadUnknown, userInfo: ["path": url.path]) } return data diff --git a/Sources/SwiftDocC/Servers/FileServer.swift b/Sources/SwiftDocC/Servers/FileServer.swift index c25d9deee0..a4f87e67a4 100644 --- a/Sources/SwiftDocC/Servers/FileServer.swift +++ b/Sources/SwiftDocC/Servers/FileServer.swift @@ -56,7 +56,7 @@ public class FileServer { */ @available(*, deprecated, message: "Use 'data(for path: String)' instead.") public func data(for url: URL) -> Data? { - return data(for url.path) + return data(for: url.path) } public func data(for path: String) -> Data? { diff --git a/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift b/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift deleted file mode 100644 index 0cdd3b23fb..0000000000 --- a/Sources/SwiftDocC/Utility/Errors/FileSystemError.swift +++ /dev/null @@ -1,21 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2023 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See https://swift.org/LICENSE.txt for license information - See https://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ - -public enum FileSystemError: Error, DescribedError { - /// No data could be read from file at `path`. - case noDataReadFromFile(path: String) - - public var errorDescription: String { - switch self { - case .noDataReadFromFile(let path): - return "No data could be read from file at `\(path)`" - } - } -} diff --git a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift index 671024f044..5ab01188f2 100644 --- a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift +++ b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift @@ -203,7 +203,7 @@ public struct CopyOfFile: File, DataRepresentable { guard FileManager.default.fileExists(atPath: original.path, isDirectory: &isDirectory), !isDirectory.boolValue else { throw Error.notAFile(original) } let fileHandle = try FileHandle(forReadingFrom: original) guard let data = try fileHandle.readToEnd() else { - throw FileSystemError.noDataReadFromFile(path: original.path) + throw CocoaError(.fileReadUnknown, userInfo: ["path": original.path]) } return data } diff --git a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift index d2337352e3..37aa398f4e 100644 --- a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift +++ b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/DefaultRequestHandler.swift @@ -32,7 +32,7 @@ struct DefaultRequestHandler: RequestHandlerFactory { let fileURL = self.rootURL.appendingPathComponent("index.html") let fileHandle = try FileHandle(forReadingFrom: fileURL) guard let response = try fileHandle.readToEnd() else { - throw FileSystemError.noDataReadFromFile(path: fileURL.path) + throw CocoaError(.fileReadUnknown, userInfo: ["path": fileURL.path]) } var content = context.channel.allocator.buffer(capacity: response.count) diff --git a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift index 3cbe77e03e..8291f18019 100644 --- a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift +++ b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift @@ -159,7 +159,7 @@ struct FileRequestHandler: RequestHandlerFactory { do { let fileHandle = try FileHandle(forReadingFrom: fileURL) guard let readData = try fileHandle.readToEnd() else { - throw FileSystemError.noDataReadFromFile(path: fileURL.path) + throw CocoaError(.fileReadUnknown, userInfo: ["path": fileURL.path]) } data = readData totalLength = data.count From 98561ac9dcfc825e58c5d06ea505510350637902 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 2 Aug 2023 23:50:37 +0100 Subject: [PATCH 12/12] Remove broken import from `FilesAndFolders.swift` --- Sources/SwiftDocCTestUtilities/FilesAndFolders.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift index 5ab01188f2..90234f89ee 100644 --- a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift +++ b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift @@ -9,7 +9,6 @@ */ import Foundation -import SwiftDocC import XCTest /*