From 3e233366222fb4514b84e61ed9a9253abd221924 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 7 Jul 2021 18:54:35 -0400 Subject: [PATCH] Update Gemini Network.framework --- .../2020-07-22-gemini-network-framework.md | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/site/posts/2020-07-22-gemini-network-framework.md b/site/posts/2020-07-22-gemini-network-framework.md index 8da6490..7ab2c90 100644 --- a/site/posts/2020-07-22-gemini-network-framework.md +++ b/site/posts/2020-07-22-gemini-network-framework.md @@ -135,7 +135,7 @@ Parsing input (i.e., the response from the server) is somewhat more complicated. First off is the status code (and the following space character). In the protocol implementation, there's a optional `Int` property used as temporary storage for the status code. If the `tempStatusCode` property is `nil`, the `parseInput` method is called on the framer. The length is always going to be 3 bytes (1 for each character of the status code, and 1 for the space). Inside the `parseInput` closure, if the buffer is not present or it's not of the expected length, the closure returns zero to indicate that no bytes were consumed. Otherwise, the contents of the buffer are converted to a String and then parsed into an integer^[If you were really building an implementation of the Gemini protocol, you would probably want to wrap the raw integer status code in something else to avoid dealing with magic numbers throughout your codebase. An enum backed by integer values, perhaps.] and stored in the temporary property (this is okay because the closure passed to `parseInput` is non-escaping, meaning it will be called before `parseInput` returns). Finally, the closure returns `3` to indicate that three bytes were consumed and should not be provided again as input. -Outside the `if`, there's a `guard` that checks that there is a status code present, either from immediately prior or potentially from a previous iteration of the loop/method. If not, it returns `3` from the `handleInput` method, telling the framework that that it expects there to be at least 3 bytes available before it's called again. The reason the status code is stored in a class property, and why the code ensures that it's `nil` before trying to parse, is so that if some subsequent parse step fails and the method returns and has to be invoked again in the future, it doesn't try to re-parse the status code because the actual data for it has already been consumed. +Outside the `if`, there's a `guard` that checks that there is a status code present, either from immediately prior or potentially from a previous invocation of the method. If not, it returns `3` from the `handleInput` method, telling the framework that that it expects there to be at least 3 bytes available before it's called again. The reason the status code is stored in a class property, and why the code ensures that it's `nil` before trying to parse, is so that if some subsequent parse step fails and the method returns and has to be invoked again in the future, it doesn't try to re-parse the status code because the actual data for it has already been consumed. ```swift class GeminiProtocol: NWProtocolFramerImplementation { @@ -214,17 +214,15 @@ class GeminiProtocol: NWProtocolFramerImplementation { } ``` -With the entire header parsed, an object can be constructed to represent the response metadata and an `NWProtocolFramer.Message` created to contain it. Actually delivering the +With the entire header parsed, an object can be constructed to represent the response metadata and an `NWProtocolFramer.Message` created to contain it. ```swift class GeminiProtocol: NWProtocolFramerImplementation { // ... func handleInput(framer: NWProtocolFramer.Instance) -> Int { - while true { - // ... - let header = GeminiResponseHeader(status: statusCode, meta: meta) - let message = NWProtocolFramer.Message(geminiResponseHeader: header) - } + // ... + let header = GeminiResponseHeader(status: statusCode, meta: meta) + let message = NWProtocolFramer.Message(geminiResponseHeader: header) } } ``` @@ -255,22 +253,21 @@ extension NWProtocolFramer.Message { } ``` -To actually pass the message off to the client of the protocol implementation, the `deliverInputNoCopy` method is used. Since the `handleInput` method has already parsed all of the data it needs to, and the response body is defined by the protocol to just be the rest of the response data, the `deliverInputNoCopy` method is a useful way of passing the data straight through to the protocol client, avoiding an extra memory copy. Since the Gemini protocol doesn't define any specific way of finding the end of a response, using `.max` as the length and specifying that the request is complete just delivers as much data to the protocol client as possible, stopping only when the connection closes. +To actually pass the message off to the client of the protocol implementation, the `deliverInputNoCopy` method is used. Since the `handleInput` method has already parsed all of the data it needs to, and the response body is defined by the protocol to just be the rest of the response data, the `deliverInputNoCopy` method is a useful way of passing the data straight through to the protocol client, avoiding an extra memory copy. If the protocol had to transform the body of the response somehow, it could be read as above and then delivered to the protocol client with the `deliverInput(data:message:isComplete:)` method. -If the protocol had to transform the body of the response somehow, it could be read as above and then delivered to the protocol client with the `deliverInput(data:message:isComplete:)` method. +If the request was successful (i.e., the status code was in the 2x range), we try to receive as many bytes as possible, because the protocol doesn't specify a way of determining the length of a response. All other response codes are defined to never have response bodies, so we don't need to deliver any data. Using `.max` is a little bit weird, since we don't actually _need_ to receive that many bytes. But it seems to work perfectly fine in practice: once all the input is received and the other side closes the connection, the input is delivered without error. -Here, it's also wrapped in an infinite loop, so that if the call to `deliverInputNoCopy` returns `true`, it loops and attempts it again. Unfortunately, the specific meaning of the deliver methods' return values as well the exact reason for the infinite retries is not specified in the docs. But this is what the sample project does and it doesn't seem to cause problems in practice, so ¯\\_(ツ)_/¯ +Annoyingly, the return value of the Swift function is entirely undocumented (even in the generated headers, where the parameters are). Fortunately, the C equivalent (`nw_framer_deliver_input_no_copy`) is more thoroughly documented and provides an answer: the function returns a boolean indicating whether the input was delivered immediately or whether the framework will wait for more bytes before delivering it. We don't care at all about this, so we just discard the return value. + +Finally, we return 0 from `handleInput`. Ordinarily, this would mean that there must be zero or more bytes available before the framework calls us again. But, because we've delivered all the available input, that will never happen. ```swift class GeminiProtocol: NWProtocolFramerImplementation { // ... func handleInput(framer: NWProtocolFramer.Instance) -> Int { // ... - while true { - if !framer.deliverInputNoCopy(length: .max, message: message, isComplete: true) { - return 0 - } - } + _ = framer.deliverInputNoCopy(length: statsCode.isSuccess ? .max : 0, message: message, isComplete: true) + return 0 } } ```