Skip to content
This repository has been archived by the owner on Apr 7, 2022. It is now read-only.

chttp refactor #211

Merged
merged 16 commits into from Jan 29, 2018
1 change: 1 addition & 0 deletions Package.swift
Expand Up @@ -36,6 +36,7 @@ let package = Package(
.testTarget(name: "FormURLEncodedTests", dependencies: ["FormURLEncoded"]),
.target(name: "HTTP", dependencies: ["CHTTP", "TCP"]),
.testTarget(name: "HTTPTests", dependencies: ["HTTP"]),
.target(name: "Performance", dependencies: ["HTTP", "TCP"]),
// .target(name: "HTTP2", dependencies: ["HTTP", "TLS", "Pufferfish"]),
// .testTarget(name: "HTTP2Tests", dependencies: ["HTTP2"]),
.target(name: "Multipart", dependencies: ["Debugging", "HTTP"]),
Expand Down
1 change: 1 addition & 0 deletions Sources/HTTP/Client/HTTPClient+TCP.swift
Expand Up @@ -10,3 +10,4 @@ extension HTTPClient {
return HTTPClient(stream: tcpSocket.stream(on: worker), on: worker)
}
}

5 changes: 3 additions & 2 deletions Sources/HTTP/Client/HTTPClient.swift
Expand Up @@ -22,8 +22,8 @@ public final class HTTPClient {
{
let queueStream = QueueStream<HTTPResponse, HTTPRequest>()

let serializerStream = HTTPRequestSerializer().stream(on: worker)
let parserStream = HTTPResponseParser().stream(on: worker)
let serializerStream = HTTPRequestSerializer()
let parserStream = HTTPResponseParser()

stream.stream(to: parserStream)
.stream(to: queueStream)
Expand All @@ -47,3 +47,4 @@ public final class HTTPClient {
}
}
}

6 changes: 1 addition & 5 deletions Sources/HTTP/Content/MediaType.swift
Expand Up @@ -197,11 +197,7 @@ extension HTTPMessage {
return MediaType(string: contentType)
}
set {
if let newValue = newValue {
headers.appendValue(newValue.bytes, forName: .contentType)
} else {
headers.removeValues(forName: .contentType)
}
headers[.contentType] = newValue?.description // FIXME: performance
}
}
}
Expand Down
258 changes: 138 additions & 120 deletions Sources/HTTP/Message/HTTPBody.swift
Expand Up @@ -10,79 +10,14 @@ import TCP
///
/// [Learn More →](https://docs.vapor.codes/3.0/http/body/)
public struct HTTPBody: Codable {
/// The internal storage medium.
///
/// NOTE: This is an implementation detail
enum Storage: Codable {
case none
case data(Data)
case staticString(StaticString)
case dispatchData(DispatchData)
case string(String)
case chunkedOutputStream(OutputChunkedStreamClosure)
case binaryOutputStream(size: Int?, stream: AnyOutputStream<ByteBuffer>)

func encode(to encoder: Encoder) throws {
switch self {
case .none: return
case .data(let data):
try data.encode(to: encoder)
case .dispatchData(let data):
try Data(data).encode(to: encoder)
case .staticString(let string):
try Data(bytes: string.utf8Start, count: string.utf8CodeUnitCount).encode(to: encoder)
case .string(let string):
try string.encode(to: encoder)
case .chunkedOutputStream(_):
/// FIXME: properly encode stream
return
case .binaryOutputStream(_):
/// FIXME: properly encode stream
return
}
}

init(from decoder: Decoder) throws {
self = .data(try Data(from: decoder))
}

/// The size of this buffer
var count: Int {
switch self {
case .data(let data): return data.count
case .dispatchData(let data): return data.count
case .staticString(let staticString): return staticString.utf8CodeUnitCount
case .string(let string): return string.utf8.count
case .chunkedOutputStream, .none:
/// FIXME: convert to data then return count?
return 0
case .binaryOutputStream(let size, _):
return size ?? 0
}
}

/// Accesses the bytes of this data
func withUnsafeBytes<Return>(_ run: ((BytesPointer) throws -> (Return))) throws -> Return {
switch self {
case .data(let data):
return try data.withUnsafeBytes(run)
case .dispatchData(let data):
return try data.withUnsafeBytes(body: run)
case .staticString(let staticString):
return try run(staticString.utf8Start)
case .string(let string):
return try string.withCString { pointer in
return try pointer.withMemoryRebound(to: UInt8.self, capacity: self.count, run)
}
case .none, .chunkedOutputStream(_), .binaryOutputStream(_):
throw HTTPError(identifier: "invalid-stream-acccess", reason: "A BodyStream was being accessed as a sequential byte buffer, which is impossible.")
}
}
}

/// The underlying storage type
var storage: Storage

var storage: HTTPBodyStorage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot cleaner


/// Internal HTTPBody init with underlying storage type.
internal init(storage: HTTPBodyStorage) {
self.storage = storage
}

/// Creates an empty body
public init() {
storage = .none
Expand All @@ -108,77 +43,35 @@ public struct HTTPBody: Codable {
self.storage = .string(string)
}

/// Output the body stream to the chunk encoding stream
/// When supplied in this closure
typealias OutputChunkedStreamClosure = (HTTPChunkEncodingStream) -> (HTTPChunkEncodingStream)

/// A chunked body stream
public init<S>(chunked stream: S) where S: Async.OutputStream, S.Output == ByteBuffer {
self.storage = .chunkedOutputStream(stream.stream)
}

/// A chunked body stream
public init(size: Int?, stream: AnyOutputStream<ByteBuffer>) {
self.storage = .binaryOutputStream(size: size, stream: stream)
public init(stream: AnyOutputStream<ByteBuffer>, count: @escaping () -> Int?) {
self.storage = .binaryOutputStream(count: count, stream: stream)
}

/// Decodes a body from from a Decoder
public init(from decoder: Decoder) throws {
self.storage = try Storage(from: decoder)
self.storage = try HTTPBodyStorage(from: decoder)
}

/// Executes a closure with a pointer to the start of the data
///
/// Can be used to read data from this buffer until the `count`.
public func withUnsafeBytes<Return>(_ run: ((BytesPointer) throws -> (Return))) throws -> Return {
public func withUnsafeBytes<Return>(_ run: (ByteBuffer) throws -> Return) throws -> Return {
return try self.storage.withUnsafeBytes(run)
}

/// Get body data.
public func makeData(max: Int) -> Future<Data> {
switch storage {
case .none:
return Future(Data())
case .data(let data):
return Future(data)
case .dispatchData(let dispatch):
return Future(Data(dispatch))
case .staticString(let string):
return Future(Data(bytes: string.utf8Start, count: string.utf8CodeUnitCount))
case .string(let string):
return Future(Data(string.utf8))
case .chunkedOutputStream(_):
return Future(error: HTTPError(identifier: "chunked-output-stream", reason: "Cannot convert a chunked output stream to a `Data` buffer"))
case .binaryOutputStream(let size, let stream):
let promise = Promise<Data>()
var data = Data()

if let size = size {
data.reserveCapacity(size)
}

let drain = DrainStream(ByteBuffer.self, onInput: { buffer in
if let size = size {
guard data.count + buffer.count <= size else {
throw HTTPError(identifier: "body-size", reason: "The body was larger than the request.")
}
}

guard data.count + buffer.count <= max else {
throw HTTPError(identifier: "body-size", reason: "The body was larger than the limit")
}

data.append(Data(buffer: buffer))
}, onError: promise.fail, onClose: {
promise.complete(data)
})
stream.output(to: drain)
return promise.future
}
return storage.makeData(max: max)
}

/// The size of the data buffer
public var count: Int {
public var count: Int? {
return self.storage.count
}
}
Expand All @@ -199,3 +92,128 @@ extension String: HTTPBodyRepresentable {
}
}


/// Output the body stream to the chunk encoding stream
/// When supplied in this closure
typealias OutputChunkedStreamClosure = (HTTPChunkEncodingStream) -> (HTTPChunkEncodingStream)

/// The internal storage medium.
///
/// NOTE: This is an implementation detail
enum HTTPBodyStorage: Codable {
case none
case buffer(ByteBuffer)
case data(Data)
case staticString(StaticString)
case dispatchData(DispatchData)
case string(String)
case chunkedOutputStream(OutputChunkedStreamClosure)
case binaryOutputStream(count: () -> Int?, stream: AnyOutputStream<ByteBuffer>)

/// See `Encodable.encode(to:)`
func encode(to encoder: Encoder) throws {
switch self {
case .none: return
case .data(let data): try data.encode(to: encoder)
case .buffer(let buffer): try Data(buffer).encode(to: encoder)
case .dispatchData(let data): try Data(data).encode(to: encoder)
case .staticString(let string): try Data(bytes: string.utf8Start, count: string.utf8CodeUnitCount).encode(to: encoder)
case .string(let string): try string.encode(to: encoder)
case .chunkedOutputStream, .binaryOutputStream:
throw HTTPError(
identifier: "streamingBody",
reason: "A BodyStream cannot be encoded with `encode(to:)`."
)
}
}

/// See `Decodable.init(from:)`
init(from decoder: Decoder) throws {
self = .data(try Data(from: decoder))
}

/// The size of the HTTP body's data.
/// `nil` of the body is a non-determinate stream.
var count: Int? {
switch self {
case .data(let data): return data.count
case .dispatchData(let data): return data.count
case .staticString(let staticString): return staticString.utf8CodeUnitCount
case .string(let string): return string.utf8.count
case .buffer(let buffer): return buffer.count
case .none: return 0
case .chunkedOutputStream: return nil
case .binaryOutputStream(let size, _): return size()
}
}

/// Accesses the bytes of this data
func withUnsafeBytes<Return>(_ run: (ByteBuffer) throws -> Return) throws -> Return {
switch self {
case .data(let data):
return try data.withByteBuffer(run)
case .dispatchData(let data):
let data = Data(data)
return try data.withByteBuffer(run)
case .staticString(let staticString):
return staticString.withUTF8Buffer { buffer in
return try! run(buffer) // FIXME: throwing
}
case .string(let string):
let buffer = string.withCString { pointer in
return ByteBuffer(
start: pointer.withMemoryRebound(to: UInt8.self, capacity: string.utf8.count) { $0 },
count: string.utf8.count
)
}
return try run(buffer)
case .buffer(let buffer):
return try run(buffer)
case .none:
return try run(ByteBuffer(start: nil, count: 0))
case .chunkedOutputStream(_), .binaryOutputStream(_):
throw HTTPError(
identifier: "streamingBody",
reason: "A BodyStream was being accessed as a sequential byte buffer, which is impossible."
)
}
}

func makeData(max: Int) -> Future<Data> {
switch self {
case .none: return Future(Data())
case .buffer(let buffer): return Future(Data(buffer))
case .data(let data): return Future(data)
case .dispatchData(let dispatch): return Future(Data(dispatch))
case .staticString(let string): return Future(Data(bytes: string.utf8Start, count: string.utf8CodeUnitCount))
case .string(let string): return Future(Data(string.utf8))
case .chunkedOutputStream(_):
let error = HTTPError(
identifier: "chunkedBody",
reason: "Cannot use `makeData(max:)` on a chunk-encoded body."
)
return Future(error: error)
case .binaryOutputStream(let size, let stream):
let promise = Promise<Data>()
let size = size() ?? 0
var data = Data()
data.reserveCapacity(size)

let drain = DrainStream(ByteBuffer.self, onInput: { buffer in
guard data.count + buffer.count <= size else {
throw HTTPError(identifier: "bodySize", reason: "The body was larger than the request.")
}

guard data.count + buffer.count <= max else {
throw HTTPError(identifier: "bodySize", reason: "The body was larger than the limit")
}

data.append(Data(buffer: buffer))
}, onError: promise.fail, onClose: {
promise.complete(data)
})
stream.output(to: drain)
return promise.future
}
}
}
37 changes: 37 additions & 0 deletions Sources/HTTP/Message/HTTPHeaderIndex.swift
@@ -0,0 +1,37 @@
/// The location of a `Name: Value` pair in the raw HTTP headers data.
struct HTTPHeaderIndex {
/// Start offset of the header's name field.
var nameStartIndex: Int
/// End offset of the header's name field
var nameEndIndex: Int

/// Start offset of the header's value field.
var valueStartIndex: Int
/// End offset of the header's value field.
var valueEndIndex: Int
}

extension HTTPHeaderIndex {
/// The lowest index of this header.
var startIndex: Int {
return nameStartIndex
}

/// The highest index of this header.
var endIndex: Int {
return valueEndIndex + 2 // include trailing \r\n
}

/// The length of this header.
var size: Int {
return endIndex - startIndex
}
}

extension HTTPHeaderIndex: CustomStringConvertible {
/// See `CustomStringConvertible.description`
var description: String {
return "[\(nameStartIndex)..<\(nameEndIndex):\(valueStartIndex)..<\(valueEndIndex)]"
}

}