diff --git a/Sources/HttpParser.swift b/Sources/HttpParser.swift index 9a614bfe..392b2f0f 100644 --- a/Sources/HttpParser.swift +++ b/Sources/HttpParser.swift @@ -64,11 +64,9 @@ public class HttpParser { return c + [(name, value)] } } - + private func readBody(_ socket: Socket, size: Int) throws -> [UInt8] { - var body = [UInt8]() - for _ in 0.. [String: String] { diff --git a/Sources/Socket.swift b/Sources/Socket.swift index e2309eae..3027ba22 100644 --- a/Sources/Socket.swift +++ b/Sources/Socket.swift @@ -112,21 +112,65 @@ open class Socket: Hashable, Equatable { } } + /// Read a single byte off the socket. This method is optimized for reading + /// a single byte. For reading multiple bytes, use read(length:), which will + /// pre-allocate heap space and read directly into it. + /// + /// - Returns: A single byte + /// - Throws: SocketError.recvFailed if unable to read from the socket open func read() throws -> UInt8 { - var buffer = [UInt8](repeating: 0, count: 1) - #if os(Linux) - let next = recv(self.socketFileDescriptor as Int32, &buffer, Int(buffer.count), Int32(MSG_NOSIGNAL)) - #else - let next = recv(self.socketFileDescriptor as Int32, &buffer, Int(buffer.count), 0) - #endif - if next <= 0 { + var byte: UInt8 = 0 + let count = Darwin.read(self.socketFileDescriptor as Int32, &byte, 1) + guard count > 0 else { throw SocketError.recvFailed(Errno.description()) } - return buffer[0] + return byte + } + + /// Read up to `length` bytes from this socket + /// + /// - Parameter length: The maximum bytes to read + /// - Returns: A buffer containing the bytes read + /// - Throws: SocketError.recvFailed if unable to read bytes from the socket + open func read(length: Int) throws -> [UInt8] { + var buffer = UnsafeMutableBufferPointer.allocate(capacity: length) + + let bytesRead = try read(into: &buffer, length: length) + + let rv = [UInt8](buffer[0.., length: Int) throws -> Int { + var offset = 0 + guard let baseAddress = buffer.baseAddress else { return 0 } + + while offset < length { + // Compute next read length in bytes. The bytes read is never more than kBufferLength at once. + let readLength = offset + Socket.kBufferLength < length ? Socket.kBufferLength : length - offset + + let bytesRead = Darwin.read(self.socketFileDescriptor as Int32, baseAddress + offset, readLength) + guard bytesRead > 0 else { + throw SocketError.recvFailed(Errno.description()) + } + + offset += bytesRead + } + + return offset } - private static let CR = UInt8(13) - private static let NL = UInt8(10) + private static let CR: UInt8 = 13 + private static let NL: UInt8 = 10 public func readLine() throws -> String { var characters: String = "" diff --git a/XCode/SwifterTestsCommon/SwifterTestsHttpParser.swift b/XCode/SwifterTestsCommon/SwifterTestsHttpParser.swift index 02fb91a7..8b7cd895 100644 --- a/XCode/SwifterTestsCommon/SwifterTestsHttpParser.swift +++ b/XCode/SwifterTestsCommon/SwifterTestsHttpParser.swift @@ -6,29 +6,46 @@ // import XCTest -import Swifter +@testable import Swifter class SwifterTestsHttpParser: XCTestCase { + /// A specialized Socket which creates a linked socket pair with a pipe, and + /// immediately writes in fixed data. This enables tests to static fixture + /// data into the regular Socket flow. class TestSocket: Socket { - var content = [UInt8]() - var offset = 0 - init(_ content: String) { - super.init(socketFileDescriptor: -1) - self.content.append(contentsOf: [UInt8](content.utf8)) - } - - override func read() throws -> UInt8 { - if offset < content.count { - let value = self.content[offset] - offset = offset + 1 - return value + /// Create an array to hold the read and write sockets that pipe creates + var fds = [Int32](repeating: 0, count: 2) + fds.withUnsafeMutableBufferPointer { ptr in + let rv = pipe(ptr.baseAddress!) + guard rv >= 0 else { fatalError("Pipe error!") } + } + + // Extract the read and write handles into friendly variables + let fdRead = fds[0] + let fdWrite = fds[1] + + // Set non-blocking I/O on both sockets. This is required! + _ = fcntl(fdWrite, F_SETFL, O_NONBLOCK) + _ = fcntl(fdRead, F_SETFL, O_NONBLOCK) + + // Push the content bytes into the write socket. + _ = content.withCString { stringPointer in + // Count will be either >=0 to indicate bytes written, or -1 + // if the bytes will be written later (non-blocking). + let count = write(fdWrite, stringPointer, content.lengthOfBytes(using: .utf8) + 1) + guard count != -1 || errno == EAGAIN else { fatalError("Write error!") } } - throw SocketError.recvFailed("") + + // Close the write socket immediately. The OS will add an EOF byte + // and the read socket will remain open. + Darwin.close(fdWrite) // the super instance will close fdRead in deinit! + + super.init(socketFileDescriptor: fdRead) } } - + func testParser() { let parser = HttpParser() @@ -89,6 +106,43 @@ class SwifterTestsHttpParser: XCTestCase { let _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0\nContent-Length: 10\r\n\n")) XCTAssert(false, "Parser should throw an error if request' body is too short.") } catch { } + + do { // test payload less than 1 read segmant + let contentLength = Socket.kBufferLength - 128 + let bodyString = [String](repeating: "A", count: contentLength).joined(separator: "") + + let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString) + let request = try parser.readHttpRequest(TestSocket(payload)) + + XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size") + + let unicodeBytes = bodyString.utf8.map { return $0 } + XCTAssert(request.body == unicodeBytes, "Request body must be correct") + } catch { } + + do { // test payload equal to 1 read segmant + let contentLength = Socket.kBufferLength + let bodyString = [String](repeating: "B", count: contentLength).joined(separator: "") + let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString) + let request = try parser.readHttpRequest(TestSocket(payload)) + + XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size") + + let unicodeBytes = bodyString.utf8.map { return $0 } + XCTAssert(request.body == unicodeBytes, "Request body must be correct") + } catch { } + + do { // test very large multi-segment payload + let contentLength = Socket.kBufferLength * 4 + let bodyString = [String](repeating: "C", count: contentLength).joined(separator: "") + let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString) + let request = try parser.readHttpRequest(TestSocket(payload)) + + XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size") + + let unicodeBytes = bodyString.utf8.map { return $0 } + XCTAssert(request.body == unicodeBytes, "Request body must be correct") + } catch { } var r = try? parser.readHttpRequest(TestSocket("GET /open?link=https://www.youtube.com/watch?v=D2cUBG4PnOA HTTP/1.0\nContent-Length: 10\n\n1234567890"))