diff --git a/Package.resolved b/Package.resolved index d9de795b36..e9ef1cdf94 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" : "68deedb6b98837564cf0231fa1df48de35881993", + "version" : "0.2.1" + } + }, { "identity" : "swift-lmdb", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 331def189f..2f3a810f10 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: [ @@ -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.2.1")), ] } 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/Indexing/Navigator/NavigatorIndex+Ext.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift index f4ed35223e..510d86721e 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift @@ -47,8 +47,10 @@ 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) - renderNode = try RenderNode.decode(fromJSON: data) + let fileHandle = try FileHandle(forReadingFrom: file.url) + 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 4da3981430..80720b003f 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift @@ -61,8 +61,10 @@ public class NavigatorIndex { /// A specific error to describe issues when processing a `NavigatorIndex`. public enum Error: Swift.Error, DescribedError { - /// Missing bundle identifier. + case missingBundleIdentifier + + @available(*, deprecated, renamed: "missingBundleIdentifier") case missingBundleIndentifier /// A RenderNode has no title and won't be indexed. @@ -72,8 +74,8 @@ public class NavigatorIndex { case navigatorIndexIsNil public var errorDescription: String { - switch self { - case .missingBundleIndentifier: + switch self { + case .missingBundleIdentifier, .missingBundleIndentifier: return "A navigator index requires a bundle identifier, which is missing." case .missingTitle: return "The page has no valid title available." @@ -169,13 +171,17 @@ public class NavigatorIndex { let information = try environment.openDatabase(named: "information", flags: []) - let data = try Data(contentsOf: url.appendingPathComponent("availability.index", isDirectory: false)) + let availabilityIndexFileURL = url.appendingPathComponent("availability.index", isDirectory: false) + let availabilityIndexFileHandle = try FileHandle(forReadingFrom: availabilityIndexFileURL) + guard let data = try availabilityIndexFileHandle.readToEnd() else { + throw CocoaError(.fileReadUnknown, userInfo: ["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. @@ -282,7 +288,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 ee328fc14e..2a51e956a6 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift @@ -129,8 +129,20 @@ 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) + guard let data = try fileHandle.readToEnd() else { + throw Error.cannotOpenFile(path: url.path) + } let readingCursor = ReadingCursor(data: data) self.readingCursor = readingCursor @@ -312,9 +324,14 @@ 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) + } + + guard let data = try fileHandle.readToEnd() else { + throw CocoaError(.fileReadUnknown, userInfo: ["path": path]) + } + 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..9d22980da0 100644 --- a/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift +++ b/Sources/SwiftDocC/Infrastructure/Bundle Assets/SVGIDExtractor.swift @@ -40,7 +40,10 @@ 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), + let data = try? fileHandle.readToEnd() + else { return nil } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift index f368125635..578ddaa54c 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift @@ -48,6 +48,11 @@ 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) + guard let data = try fileHandle.readToEnd() else { + 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 f3eca3b0ee..f0569ee4c0 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift @@ -28,7 +28,12 @@ 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) + guard let data = try fileHandle.readToEnd() else { + throw CocoaError(.fileReadUnknown, userInfo: ["path": url.path]) + } + + return data } } 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 fef56ca19d..a4f87e67a4 100644 --- a/Sources/SwiftDocC/Servers/FileServer.swift +++ b/Sources/SwiftDocC/Servers/FileServer.swift @@ -9,10 +9,8 @@ */ import Foundation +import HTTPTypes import SymbolKit -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif fileprivate let slashCharSet = CharacterSet(charactersIn: "/") @@ -56,16 +54,21 @@ 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 - }.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)) } /** @@ -73,32 +76,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: "/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: "application/octet-stream"]) } return (response, data) @@ -175,7 +178,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 try? fileHandle?.readToEnd() } } @@ -230,7 +234,12 @@ 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 } + let fileURL = URL(fileURLWithPath: path).appendingPathComponent(file) + guard + let fileHandle = try? FileHandle(forReadingFrom: fileURL), + let data = try? fileHandle.readToEnd() + else { continue } + 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..90234f89ee 100644 --- a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift +++ b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift @@ -200,7 +200,11 @@ 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) + guard let data = try fileHandle.readToEnd() else { + throw CocoaError(.fileReadUnknown, userInfo: ["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 68d4259605..37aa398f4e 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,8 +29,12 @@ struct DefaultRequestHandler: RequestHandlerFactory { where ChannelHandler.OutboundOut == HTTPServerResponsePart { return { context, head in - let response = try Data(contentsOf: self.rootURL.appendingPathComponent("index.html")) - + let fileURL = self.rootURL.appendingPathComponent("index.html") + let fileHandle = try FileHandle(forReadingFrom: fileURL) + guard let response = try fileHandle.readToEnd() else { + throw CocoaError(.fileReadUnknown, userInfo: ["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 ddbd443a17..8291f18019 100644 --- a/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift +++ b/Sources/SwiftDocCUtilities/PreviewServer/RequestHandler/FileRequestHandler.swift @@ -157,13 +157,17 @@ struct FileRequestHandler: RequestHandlerFactory { // Read the file contents do { - data = try Data(contentsOf: fileURL, options: .mappedIfSafe) + let fileHandle = try FileHandle(forReadingFrom: fileURL) + guard let readData = try fileHandle.readToEnd() else { + throw CocoaError(.fileReadUnknown, userInfo: ["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 2090336344..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 @@ -206,7 +210,10 @@ func generateSwiftDocCFrameworkSymbolGraph() throws -> SymbolGraph { isDirectory: false ) - let symbolGraphData = try Data(contentsOf: symbolGraphURL) + let symbolGraphFileHandle = try FileHandle(forReadingFrom: symbolGraphURL) + guard let symbolGraphData = try symbolGraphFileHandle.readToEnd() else { + throw SymbolGraphError.noDataReadFromFile(path: symbolGraphURL.path) + } return try JSONDecoder().decode(SymbolGraph.self, from: symbolGraphData) } diff --git a/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift b/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift index 9f1e9e3823..2053e78770 100644 --- a/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift +++ b/Tests/SwiftDocCTests/Servers/DocumentationSchemeHandlerTests.swift @@ -9,9 +9,7 @@ */ import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif +import HTTPTypes import XCTest @testable import SwiftDocC @@ -24,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)! @@ -58,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 ) @@ -67,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 8e1baab7d4..40d7b17428 100644 --- a/Tests/SwiftDocCTests/Servers/FileServerTests.swift +++ b/Tests/SwiftDocCTests/Servers/FileServerTests.swift @@ -9,9 +9,7 @@ */ import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif +import HTTPTypes import XCTest @testable import SwiftDocC @@ -20,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() @@ -36,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() { @@ -75,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() { @@ -89,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() { @@ -108,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() { @@ -131,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) } @@ -154,58 +173,51 @@ 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 - #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() { - 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) 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#./}"