From 3df2fb968022ed9fe9ee6529adb67adf7105464b Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Sat, 17 Sep 2022 12:43:59 +0200 Subject: [PATCH 01/12] Remove stubs --- Sources/Memcache/Memcache.swift | 0 Tests/MemcacheTests/MemcacheTests.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Sources/Memcache/Memcache.swift delete mode 100644 Tests/MemcacheTests/MemcacheTests.swift diff --git a/Sources/Memcache/Memcache.swift b/Sources/Memcache/Memcache.swift deleted file mode 100644 index e69de29..0000000 diff --git a/Tests/MemcacheTests/MemcacheTests.swift b/Tests/MemcacheTests/MemcacheTests.swift deleted file mode 100644 index e69de29..0000000 From f6eebff44802cc566e25ace11977fbd4f824a5ad Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Sat, 17 Sep 2022 16:16:02 +0200 Subject: [PATCH 02/12] Add MemcacheBackendMessage + tests --- Package.swift | 3 +- .../Messages/Backend+Flags.swift | 38 +++++++ .../Messages/Backend+Value.swift | 56 ++++++++++ .../Messages/MemcacheBackendMessage.swift | 103 ++++++++++++++++++ .../Extensions/UInt8+characters.swift | 5 + .../MemcacheBackendMessage+Equatable.swift | 22 ++++ .../MemcacheBackendMessageTests.swift | 36 ++++++ 7 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift create mode 100644 Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift create mode 100644 Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift create mode 100644 Sources/Memcache/Extensions/UInt8+characters.swift create mode 100644 Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift create mode 100644 Tests/MemcacheTests/MemcacheBackendMessageTests.swift diff --git a/Package.swift b/Package.swift index 6b7b76a..ec225a3 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,8 @@ let package = Package( .testTarget( name: "MemcacheTests", dependencies: [ - .target(name: "Memcache") + .target(name: "Memcache"), + .product(name: "NIOTestUtils", package: "swift-nio") ] ), diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift b/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift new file mode 100644 index 0000000..ba88cd2 --- /dev/null +++ b/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift @@ -0,0 +1,38 @@ +import NIOCore + +extension MemcacheBackendMessage { + struct Flags: MemcacheMessagePayloadDecodable, Equatable, ExpressibleByArrayLiteral { + let flags: [String] + + init(_ flags: [String]) { + self.flags = flags + } + + init(arrayLiteral elements: String...) { + flags = elements + } + + static func decode(from buffer: inout ByteBuffer) throws -> Self { + guard buffer.readableBytes > 2 else { return [] } + + let bytes = buffer.readableBytesView + guard let newlineIndex = bytes.firstIndex(of: .newline), + bytes[newlineIndex - 1] == .carriageReturn, + let flagsString = buffer.getString(at: buffer.readerIndex, length: newlineIndex - 2 - buffer.readerIndex) + else { + fatalError() // TODO: invalid string error + } + + // we read the flags but not the tailing \r\n + buffer.moveReaderIndex(to: newlineIndex - 1) + + return Flags(flagsString.components(separatedBy: " ")) + } + } +} + +extension MemcacheBackendMessage.Flags: CustomDebugStringConvertible { + var debugDescription: String { + "[\(flags.map({ "\"\($0)\"" }).joined(separator: ", "))]" + } +} diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift b/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift new file mode 100644 index 0000000..7426d44 --- /dev/null +++ b/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift @@ -0,0 +1,56 @@ +import NIOCore + +extension MemcacheBackendMessage { + struct Value: MemcacheMessagePayloadDecodable, Equatable { + var flags: Flags + var data: ByteBuffer + + static func decode(from buffer: inout ByteBuffer) throws -> Self { + let bytes = buffer.readableBytesView + + let nextSpaceIndex = bytes.firstIndex(of: .space) + let nextNewlineIndex = bytes.firstIndex(of: .newline) + let sizeStringEndIndex: Int + if let nextSpaceIndex = nextSpaceIndex, + let nextNewlineIndex = nextNewlineIndex, + nextSpaceIndex < nextNewlineIndex { + // Flags after the size string + sizeStringEndIndex = nextSpaceIndex - 1 + } else if let nextNewlineIndex = nextNewlineIndex { + // No flags after the size string + sizeStringEndIndex = nextNewlineIndex - 1 + } else { + fatalError() // TODO: unexpected message format error + } + + guard let sizeString = buffer.readString(length: sizeStringEndIndex - bytes.startIndex), + let size = Int(sizeString) + else { + fatalError() // TODO: unexpected message format error + } + + let flags: Flags + if let nextSpaceIndex = nextSpaceIndex, + let nextNewlineIndex = nextNewlineIndex, + nextSpaceIndex < nextNewlineIndex { + buffer.moveReaderIndex(forwardBy: 1) // + flags = try .decode(from: &buffer) + } else { + flags = Flags() + } + + buffer.moveReaderIndex(forwardBy: 2) // \r\n + guard let dataBlock = buffer.readSlice(length: size) else { + fatalError() // TODO: Not enough data error + } + + return Value(flags: flags, data: dataBlock) + } + } +} + +extension MemcacheBackendMessage.Value: CustomDebugStringConvertible { + var debugDescription: String { + return "flags: \(flags), data: \(data.readableBytes) bytes" + } +} diff --git a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift new file mode 100644 index 0000000..02deb66 --- /dev/null +++ b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift @@ -0,0 +1,103 @@ +import NIOCore + +protocol MemcacheMessagePayloadDecodable { + static func decode(from buffer: inout ByteBuffer) throws -> Self +} + +enum MemcacheBackendMessage { + case header(Flags) + case notFound(Flags) + case notStored(Flags) + case exists(Flags) + case value(Value) + case end + // TODO: error responses +} + +extension MemcacheBackendMessage { + enum Verb: RawRepresentable { + typealias RawValue = String + + case header + case notFound + case notStored + case exists + case value + case end + // TODO: error responses + + init?(rawValue: String) { + switch rawValue { + case "HD": + self = .header + case "NF": + self = .notFound + case "NS": + self = .notStored + case "EX": + self = .exists + case "VA": + self = .value + case "EN": + self = .end + default: + return nil + } + } + + var rawValue: String { + switch self { + case .header: + return "HD" + case .notFound: + return "NF" + case .notStored: + return "NS" + case .exists: + return "EX" + case .value: + return "VA" + case .end: + return "EN" + } + } + } +} + +extension MemcacheBackendMessage { + static func decode(from buffer: inout ByteBuffer, for verb: Verb) throws -> MemcacheBackendMessage { + switch verb { + case .header: + return try .header(.decode(from: &buffer)) + case .notFound: + return try .notFound(.decode(from: &buffer)) + case .notStored: + return try .notStored(.decode(from: &buffer)) + case .exists: + return try .exists(.decode(from: &buffer)) + case .value: + return try .value(.decode(from: &buffer)) + case .end: + return .end + } + } +} + +extension MemcacheBackendMessage: CustomDebugStringConvertible { + var debugDescription: String { + switch self { + case let .header(flags): + return ".header(\(String(reflecting: flags)))" + case let .notFound(flags): + return ".notFound(\(String(reflecting: flags)))" + case let .notStored(flags): + return ".notStored(\(String(reflecting: flags)))" + case let .exists(flags): + return ".exists(\(String(reflecting: flags)))" + case let .value(value): + return ".value(\(String(reflecting: value)))" + case .end: + return ".end" + } + } +} diff --git a/Sources/Memcache/Extensions/UInt8+characters.swift b/Sources/Memcache/Extensions/UInt8+characters.swift new file mode 100644 index 0000000..a0a056a --- /dev/null +++ b/Sources/Memcache/Extensions/UInt8+characters.swift @@ -0,0 +1,5 @@ +extension UInt8 { + static let newline = UInt8(ascii: "\n") + static let carriageReturn = UInt8(ascii: "\r") + static let space = UInt8(ascii: " ") +} diff --git a/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift b/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift new file mode 100644 index 0000000..f854421 --- /dev/null +++ b/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift @@ -0,0 +1,22 @@ +@testable import Memcache + +extension MemcacheBackendMessage: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.header(lhs), .header(rhs)): + return lhs == rhs + case let (.notFound(lhs), .notFound(rhs)): + return lhs == rhs + case let (.notStored(lhs), .notStored(rhs)): + return lhs == rhs + case let (.exists(lhs), .exists(rhs)): + return lhs == rhs + case let (.value(lhs), .value(rhs)): + return lhs == rhs + case (.end, .end): + return true + default: + return false + } + } +} diff --git a/Tests/MemcacheTests/MemcacheBackendMessageTests.swift b/Tests/MemcacheTests/MemcacheBackendMessageTests.swift new file mode 100644 index 0000000..97048f5 --- /dev/null +++ b/Tests/MemcacheTests/MemcacheBackendMessageTests.swift @@ -0,0 +1,36 @@ +import NIOCore +import NIOTestUtils +import XCTest + +@testable import Memcache + +final class MemcacheBackendMessageTests: XCTestCase { + func testInitVerbWithString() { + XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "HD"), .header) + XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "NF"), .notFound) + XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "NS"), .notStored) + XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "EX"), .exists) + XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "VA"), .value) + XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "EN"), .end) + + XCTAssertNil(MemcacheBackendMessage.Verb(rawValue: "")) + } + + func testVerbHasCorrectRawValue() { + XCTAssertEqual(MemcacheBackendMessage.Verb.header.rawValue, "HD") + XCTAssertEqual(MemcacheBackendMessage.Verb.notFound.rawValue, "NF") + XCTAssertEqual(MemcacheBackendMessage.Verb.notStored.rawValue, "NS") + XCTAssertEqual(MemcacheBackendMessage.Verb.exists.rawValue, "EX") + XCTAssertEqual(MemcacheBackendMessage.Verb.value.rawValue, "VA") + XCTAssertEqual(MemcacheBackendMessage.Verb.end.rawValue, "EN") + } + + func testDebugDescription() { + XCTAssertEqual("\(MemcacheBackendMessage.header(["T1", "v"]))", #".header(["T1", "v"])"#) + XCTAssertEqual("\(MemcacheBackendMessage.notFound(["T1", "v"]))", #".notFound(["T1", "v"])"#) + XCTAssertEqual("\(MemcacheBackendMessage.notStored(["T1", "v"]))", #".notStored(["T1", "v"])"#) + XCTAssertEqual("\(MemcacheBackendMessage.exists(["T1", "v"]))", #".exists(["T1", "v"])"#) + XCTAssertEqual("\(MemcacheBackendMessage.value(.init(flags: ["T1", "v"], data: ByteBuffer())))", #".value(flags: ["T1", "v"], data: 0 bytes)"#) + XCTAssertEqual("\(MemcacheBackendMessage.end)", ".end") + } +} From 1b8357f5a6f1cae28eb8a306ba96cfa29188fa54 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Sun, 18 Sep 2022 22:01:24 +0200 Subject: [PATCH 03/12] Add doc comments for messages --- .../Messages/MemcacheBackendMessage.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift index 02deb66..a892d32 100644 --- a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift +++ b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift @@ -5,12 +5,24 @@ protocol MemcacheMessagePayloadDecodable { } enum MemcacheBackendMessage { + /// Header (`HD *\r\n`) case header(Flags) + + /// Not found (`NF *\r\n`) case notFound(Flags) + + /// Not stored (`NS *\r\n`) case notStored(Flags) + + /// Exists (`EX *\r\n`) case exists(Flags) + + /// Value (`VA *\r\n\r\n`) case value(Value) + + /// End (`EN\r\n`) case end + // TODO: error responses } From 1058a5bc124ecad16b5b1576705b014c02785569 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Mon, 19 Sep 2022 00:26:52 +0200 Subject: [PATCH 04/12] Implement decoding of most common messages + tests --- .../MemcacheBackendMessageDecoder.swift | 57 +++++++++++++++++ .../MemcacheDecodingError.swift | 52 +++++++++++++++ .../MemcacheNeedMoreDataError.swift | 1 + .../MemcachePartialDecodingError.swift | 20 ++++++ .../Messages/Backend+Flags.swift | 29 +++++---- .../Messages/Backend+Value.swift | 62 +++++++++--------- .../Messages/MemcacheBackendMessage.swift | 1 + .../Extensions/ByteBuffer+Memcache.swift | 57 +++++++++++++++++ .../MemcacheBackendMessageDecoderTests.swift | 64 +++++++++++++++++++ 9 files changed, 303 insertions(+), 40 deletions(-) create mode 100644 Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift create mode 100644 Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift create mode 100644 Sources/Memcache/ChannelHandler/MemcacheNeedMoreDataError.swift create mode 100644 Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift create mode 100644 Sources/Memcache/Extensions/ByteBuffer+Memcache.swift create mode 100644 Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift diff --git a/Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift b/Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift new file mode 100644 index 0000000..7d6c005 --- /dev/null +++ b/Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift @@ -0,0 +1,57 @@ +import Foundation +import NIOCore + +struct MemcacheBackendMessageDecoder: NIOSingleStepByteToMessageDecoder { + typealias InboundOut = MemcacheBackendMessage + + func decode(buffer: inout ByteBuffer) throws -> MemcacheBackendMessage? { + // Keep track of the reader index in case we later notice that we need more data + let startReaderIndex = buffer.readerIndex + + // Peek at the message to read the verb. It is before the first \r\n and before the first if the message + // contains one. + guard let messageSlice = buffer.getCarriageReturnNewlineTerminatedSlice(at: buffer.readerIndex) else { + // reader index wasn't moved, wait for more bytes + return nil + } + + // The verb in messageSlice is either the entire slice or the part before the first + let endVerbIndex = messageSlice.readableBytesView.firstIndex(of: .space) ?? messageSlice.writerIndex + + guard let verbString = messageSlice.getString(at: messageSlice.readerIndex, length: endVerbIndex) else { + // If we can't read a string, the messageSlice must be empty (i.e. no characters before the first occurence of \r\n) + throw MemcacheDecodingError.emptyMessageReceived(bytes: buffer) + } + + guard let verb = MemcacheBackendMessage.Verb(rawValue: verbString) else { + throw MemcacheDecodingError.unknownVerbReceived(messageVerb: verbString, messageBytes: messageSlice) + } + + // Move the buffer's readerIndex to after the verb so we can continue reading flags and/or data. + buffer.moveReaderIndex(forwardBy: endVerbIndex) + + if buffer.readableBytesView.first == .space { + // Move the reader index to after the that is following the verb + buffer.moveReaderIndex(forwardBy: 1) + } + + do { + // Pass the buffer instead of messageSlice because .value messages continue after the first \r\n + let result = try MemcacheBackendMessage.decode(from: &buffer, for: verb) + // TODO: Can we make sure the message was read entirely? Difficult because we don't know the length of VA messages here. + return result + } catch _ as MemcacheNeedMoreDataError { + // A message decoder told us that it expects more data. Move the reader index back to the start and try again. + buffer.moveReaderIndex(to: startReaderIndex) + return nil + } catch let error as MemcachePartialDecodingError { + throw MemcacheDecodingError.withPartialError(error, messageVerb: verb.rawValue, messageBytes: messageSlice) + } catch { + preconditionFailure("Expected to only see `MemcachePartialDecodingError` here.") + } + } + + func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> MemcacheBackendMessage? { + try self.decode(buffer: &buffer) + } +} diff --git a/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift b/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift new file mode 100644 index 0000000..95860a4 --- /dev/null +++ b/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift @@ -0,0 +1,52 @@ +import NIOCore + +struct MemcacheDecodingError: Error { + let messageVerb: String + let payload: String + let description: String + let file: StaticString + let line: UInt + + static func withPartialError( + _ partialError: MemcachePartialDecodingError, + messageVerb: String, + messageBytes: ByteBuffer + ) -> Self { + MemcacheDecodingError( + messageVerb: messageVerb, + payload: "", // TODO: Can we get a base64 representation without Foundation? + description: partialError.description, + file: partialError.file, + line: partialError.line + ) + } + + static func emptyMessageReceived( + bytes: ByteBuffer, + file: StaticString = #file, + line: UInt = #line + ) -> Self { + MemcacheDecodingError( + messageVerb: "", + payload: "", // TODO: Can we get a base64 representation without Foundation? + description: "Received an empty message (i.e. no characters before the first occurence of \r\n). A valid message has to contain a messageVerb at least.", + file: file, + line: line + ) + } + + static func unknownVerbReceived( + messageVerb: String, + messageBytes: ByteBuffer, + file: StaticString = #file, + line: UInt = #line + ) -> Self { + MemcacheDecodingError( + messageVerb: messageVerb, + payload: "", // TODO: Can we get a base64 representation without Foundation? + description: "Received a message with messageVerb '\(messageVerb)'. There is no message type associated with this message identifier.", + file: file, + line: line + ) + } +} diff --git a/Sources/Memcache/ChannelHandler/MemcacheNeedMoreDataError.swift b/Sources/Memcache/ChannelHandler/MemcacheNeedMoreDataError.swift new file mode 100644 index 0000000..ff8a554 --- /dev/null +++ b/Sources/Memcache/ChannelHandler/MemcacheNeedMoreDataError.swift @@ -0,0 +1 @@ +struct MemcacheNeedMoreDataError: Error {} diff --git a/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift b/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift new file mode 100644 index 0000000..72d4f2f --- /dev/null +++ b/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift @@ -0,0 +1,20 @@ +import NIOCore + +struct MemcachePartialDecodingError: Error { + let description: String + let file: StaticString + let line: UInt + + static func fieldNotDecodable( + as type: Any.Type, + from string: String, + file: StaticString = #file, + line: UInt = #line + ) -> Self { + MemcachePartialDecodingError( + description: "Could not read '\(type)' from '\(string)' from the ByteBuffer.", + file: file, + line: line + ) + } +} diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift b/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift index ba88cd2..ce8b8e6 100644 --- a/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift +++ b/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift @@ -2,7 +2,7 @@ import NIOCore extension MemcacheBackendMessage { struct Flags: MemcacheMessagePayloadDecodable, Equatable, ExpressibleByArrayLiteral { - let flags: [String] + let flags: [String] // TODO: Do we want something like (Character, token: String?) instead? Or a struct? init(_ flags: [String]) { self.flags = flags @@ -12,19 +12,26 @@ extension MemcacheBackendMessage { flags = elements } + /// Decode flags from any backend message. + /// + /// The following formats can be decoded from the `buffer`: + /// - `\r\n`. Flags are space-separated strings. + /// - `\r\n`. No flags. static func decode(from buffer: inout ByteBuffer) throws -> Self { - guard buffer.readableBytes > 2 else { return [] } - - let bytes = buffer.readableBytesView - guard let newlineIndex = bytes.firstIndex(of: .newline), - bytes[newlineIndex - 1] == .carriageReturn, - let flagsString = buffer.getString(at: buffer.readerIndex, length: newlineIndex - 2 - buffer.readerIndex) - else { - fatalError() // TODO: invalid string error + // Flags are always the last part of a message, which is terminated by \r\n. + // Because we get passed a potentially longer buffer here, we parse until \r\n. + guard var flagsSlice = buffer.readCarriageReturnNewlineTerminatedSlice() else { + // No \r\n? Something went terribly wrong... + preconditionFailure("Expected to only see messages that contain \r\n here.") } - // we read the flags but not the tailing \r\n - buffer.moveReaderIndex(to: newlineIndex - 1) + // Flags can always be empty + guard flagsSlice.readableBytes > 0 else { return [] } + + // The slice now only contains the flags separated by + guard let flagsString = flagsSlice.readString(length: flagsSlice.readableBytes) else { + preconditionFailure("We have readable bytes so we should be able to read a string") + } return Flags(flagsString.components(separatedBy: " ")) } diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift b/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift index 7426d44..c50a458 100644 --- a/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift +++ b/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift @@ -5,43 +5,47 @@ extension MemcacheBackendMessage { var flags: Flags var data: ByteBuffer + /// Decode a `VA` backend message. + /// + /// The message can have the following formats: + /// - ` \r\n\r\n`. Flags are space-separated strings. + /// - `\r\n\r\n` static func decode(from buffer: inout ByteBuffer) throws -> Self { - let bytes = buffer.readableBytesView - - let nextSpaceIndex = bytes.firstIndex(of: .space) - let nextNewlineIndex = bytes.firstIndex(of: .newline) - let sizeStringEndIndex: Int - if let nextSpaceIndex = nextSpaceIndex, - let nextNewlineIndex = nextNewlineIndex, - nextSpaceIndex < nextNewlineIndex { - // Flags after the size string - sizeStringEndIndex = nextSpaceIndex - 1 - } else if let nextNewlineIndex = nextNewlineIndex { - // No flags after the size string - sizeStringEndIndex = nextNewlineIndex - 1 - } else { - fatalError() // TODO: unexpected message format error + // Decode the size of the data block, optional flags, and the data block itself + guard let valueMetaSlice = buffer.getCarriageReturnNewlineTerminatedSlice(at: buffer.readerIndex) else { + // No \r\n? Something went terribly wrong... + preconditionFailure("Expected to only see messages that contain \r\n here.") } - guard let sizeString = buffer.readString(length: sizeStringEndIndex - bytes.startIndex), - let size = Int(sizeString) - else { - fatalError() // TODO: unexpected message format error + // The size value in valueMetaSlice is either the entire slice or the part before the first + let endSizeIndex = valueMetaSlice.readableBytesView.firstIndex(of: .space) ?? valueMetaSlice.writerIndex + + guard let sizeString = valueMetaSlice.getString(at: valueMetaSlice.readerIndex, length: endSizeIndex) else { + preconditionFailure("We have readable bytes so we should be able to read a string") + } + + guard let size = Int(sizeString) else { + throw MemcachePartialDecodingError.fieldNotDecodable(as: Int.self, from: sizeString) } - let flags: Flags - if let nextSpaceIndex = nextSpaceIndex, - let nextNewlineIndex = nextNewlineIndex, - nextSpaceIndex < nextNewlineIndex { - buffer.moveReaderIndex(forwardBy: 1) // - flags = try .decode(from: &buffer) - } else { - flags = Flags() + // Move the buffer's readerIndex to after the size so we can continue reading flags and/or data. + buffer.moveReaderIndex(forwardBy: endSizeIndex) + + if buffer.readableBytesView.first == .space { + // Move the reader index to after the that is following the size + buffer.moveReaderIndex(forwardBy: 1) } - buffer.moveReaderIndex(forwardBy: 2) // \r\n + let flags = try Flags.decode(from: &buffer) + guard let dataBlock = buffer.readSlice(length: size) else { - fatalError() // TODO: Not enough data error + // Tell the decoder that we expect more data + throw MemcacheNeedMoreDataError() + } + + // Make sure we received the final terminating \r\n. + guard let _ = buffer.readCarriageReturnNewlineTerminatedSlice() else { + throw MemcacheNeedMoreDataError() } return Value(flags: flags, data: dataBlock) diff --git a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift index a892d32..ee76370 100644 --- a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift +++ b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift @@ -90,6 +90,7 @@ extension MemcacheBackendMessage { case .value: return try .value(.decode(from: &buffer)) case .end: + buffer.moveReaderIndex(forwardBy: 2) return .end } } diff --git a/Sources/Memcache/Extensions/ByteBuffer+Memcache.swift b/Sources/Memcache/Extensions/ByteBuffer+Memcache.swift new file mode 100644 index 0000000..f746990 --- /dev/null +++ b/Sources/Memcache/Extensions/ByteBuffer+Memcache.swift @@ -0,0 +1,57 @@ +import NIOCore + +extension ByteBuffer { + @inlinable + func _getCarriageReturnNewlineTerminatedSliceLength(at index: Int) -> Int? { + guard readerIndex <= index && index < writerIndex - 1 else { + return nil + } + + var subview = readableBytesView[index...] + repeat { + guard let carriageReturnIndex = subview.firstIndex(of: UInt8(ascii: "\r")) else { + return nil + } + let expectedNewlineIndex = carriageReturnIndex + 1 + + if subview.endIndex > expectedNewlineIndex, + subview[expectedNewlineIndex] == UInt8(ascii: "\n") { + return (expectedNewlineIndex - 1) &- index + } else { + subview = readableBytesView[expectedNewlineIndex...] + } + } while subview.count >= 2 + + return nil + } + + /// Read a slice off this `ByteBuffer` that is terminated with `\r\n`. Move the reader index forward by the slices length and + /// it's two terminator characters. + /// + /// - Returns: A `ByteBuffer` slice of this `ByteBuffer` or `nil` if there isn't a complete `\r\n`-terminated slice, including + /// terminators, in the readable bytes of the buffer. The returned slice does not include the terminators. + @inlinable + mutating func readCarriageReturnNewlineTerminatedSlice() -> ByteBuffer? { + guard let sliceLength = _getCarriageReturnNewlineTerminatedSliceLength(at: readerIndex) else { + return nil + } + let result = readSlice(length: sliceLength) + moveReaderIndex(forwardBy: 2) // move forward by \r\n + return result + } + + /// Get a slice at `index` from this `ByteBuffer` that is terminated with `\r\n`. Does not move the reader index. + /// The selected bytes must be readable or else `nil` will be returned. + /// + /// - Parameters: + /// - index: The starting index into `ByteBuffer` containing the `\r\n`-terminated slice of interest. + /// - Returns: A `ByteBuffer` slice of this `ByteBuffer` or `nil` if there isn't a complete `\r\n`-terminated slice, including + /// terminators, in the readable bytes after `index` in the buffer. The returned slice does not include the terminators. + @inlinable + func getCarriageReturnNewlineTerminatedSlice(at index: Int) -> ByteBuffer? { + guard let sliceLength = _getCarriageReturnNewlineTerminatedSliceLength(at: index) else { + return nil + } + return getSlice(at: index, length: sliceLength) + } +} diff --git a/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift b/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift new file mode 100644 index 0000000..75fba92 --- /dev/null +++ b/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift @@ -0,0 +1,64 @@ +import NIOCore +import NIOTestUtils +import XCTest + +@testable import Memcache + +final class MemcacheBackendMessageDecoderTests: XCTestCase { + func testDecodeMessageWithAndWithoutFlags() { + // TODO: Test decoding these messages also without flags. Test EN\r\n should fail when we receive it with flags. + let flags: MemcacheBackendMessage.Flags = ["B1", "a", "r"] + let expected: [MemcacheBackendMessage] = [ + .header(flags), + .notFound(flags), + .notStored(flags), + .exists(flags), + .end + ] + + let messageString = [MemcacheBackendMessage.Verb]([.header, .notFound, .notStored, .exists]) + .map { "\($0.rawValue) \(flags.flags.joined(separator: " "))\r\n" } + .joined() + + "EN\r\n" + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( + stringInputOutputPairs: [(messageString, expected)], + decoderFactory: { MemcacheBackendMessageDecoder() } + )) + } + + func testDecodeValueMessageWithFlags() { + var buffer = ByteBuffer() + buffer.writeString("foo") + let expected: [MemcacheBackendMessage] = [ + .value(.init(flags: ["b", "a", "r"], data: buffer)) + ] + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( + stringInputOutputPairs: [("VA 3 b a r\r\nfoo\r\n", expected)], + decoderFactory: { MemcacheBackendMessageDecoder() } + )) + } + + func testDecodeValueMessageWithoutFlags() { + var buffer = ByteBuffer() + buffer.writeString("foo") + let expected: [MemcacheBackendMessage] = [ + .value(.init(flags: [], data: buffer)) + ] + + XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( + stringInputOutputPairs: [("VA 3\r\nfoo\r\n", expected)], + decoderFactory: { MemcacheBackendMessageDecoder() } + )) + } + + func testDecodeMessageWithUnknownVerb() { + XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( + stringInputOutputPairs: [("XX T1 foo\r\n", [])], + decoderFactory: { MemcacheBackendMessageDecoder() } + )) { + XCTAssert($0 is MemcacheDecodingError) + } + } +} From e4ef67b24fbc3112e36382a130a96df7b81f7095 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Mon, 19 Sep 2022 18:36:44 +0200 Subject: [PATCH 05/12] Use #fileID instead of #file for errors --- .../ChannelHandler/MemcacheDecodingError.swift | 6 +++--- .../MemcachePartialDecodingError.swift | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift b/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift index 95860a4..5a9a802 100644 --- a/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift +++ b/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift @@ -4,7 +4,7 @@ struct MemcacheDecodingError: Error { let messageVerb: String let payload: String let description: String - let file: StaticString + let file: String let line: UInt static func withPartialError( @@ -23,7 +23,7 @@ struct MemcacheDecodingError: Error { static func emptyMessageReceived( bytes: ByteBuffer, - file: StaticString = #file, + file: String = #fileID, line: UInt = #line ) -> Self { MemcacheDecodingError( @@ -38,7 +38,7 @@ struct MemcacheDecodingError: Error { static func unknownVerbReceived( messageVerb: String, messageBytes: ByteBuffer, - file: StaticString = #file, + file: String = #fileID, line: UInt = #line ) -> Self { MemcacheDecodingError( diff --git a/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift b/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift index 72d4f2f..9a312d4 100644 --- a/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift +++ b/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift @@ -2,13 +2,26 @@ import NIOCore struct MemcachePartialDecodingError: Error { let description: String - let file: StaticString + let file: String let line: UInt + static func expectedExactlyNRemainingBytes( + _ expected: Int, + actual: Int, + file: String = #fileID, + line: UInt = #line + ) -> Self { + MemcachePartialDecodingError( + description: "Expected exactly '\(expected)' but found '\(actual)' remaining bytes", + file: file, + line: line + ) + } + static func fieldNotDecodable( as type: Any.Type, from string: String, - file: StaticString = #file, + file: String = #fileID, line: UInt = #line ) -> Self { MemcachePartialDecodingError( From ec9769cf7a7c7784f430a8509e9ad33b48a912e5 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Mon, 19 Sep 2022 19:31:53 +0200 Subject: [PATCH 06/12] Remove unnecessary @inlinable --- Sources/Memcache/Extensions/ByteBuffer+Memcache.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/Memcache/Extensions/ByteBuffer+Memcache.swift b/Sources/Memcache/Extensions/ByteBuffer+Memcache.swift index f746990..88086ac 100644 --- a/Sources/Memcache/Extensions/ByteBuffer+Memcache.swift +++ b/Sources/Memcache/Extensions/ByteBuffer+Memcache.swift @@ -1,7 +1,6 @@ import NIOCore extension ByteBuffer { - @inlinable func _getCarriageReturnNewlineTerminatedSliceLength(at index: Int) -> Int? { guard readerIndex <= index && index < writerIndex - 1 else { return nil @@ -30,7 +29,6 @@ extension ByteBuffer { /// /// - Returns: A `ByteBuffer` slice of this `ByteBuffer` or `nil` if there isn't a complete `\r\n`-terminated slice, including /// terminators, in the readable bytes of the buffer. The returned slice does not include the terminators. - @inlinable mutating func readCarriageReturnNewlineTerminatedSlice() -> ByteBuffer? { guard let sliceLength = _getCarriageReturnNewlineTerminatedSliceLength(at: readerIndex) else { return nil @@ -47,7 +45,6 @@ extension ByteBuffer { /// - index: The starting index into `ByteBuffer` containing the `\r\n`-terminated slice of interest. /// - Returns: A `ByteBuffer` slice of this `ByteBuffer` or `nil` if there isn't a complete `\r\n`-terminated slice, including /// terminators, in the readable bytes after `index` in the buffer. The returned slice does not include the terminators. - @inlinable func getCarriageReturnNewlineTerminatedSlice(at index: Int) -> ByteBuffer? { guard let sliceLength = _getCarriageReturnNewlineTerminatedSliceLength(at: index) else { return nil From 139909d2aa2f5de67d263753da8d12ce20aabe84 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Sun, 25 Sep 2022 20:24:08 +0200 Subject: [PATCH 07/12] Add swift-extras-base64 for data printing --- Package.swift | 6 ++++-- .../Memcache/ChannelHandler/MemcacheDecodingError.swift | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index ec225a3..9566fb6 100644 --- a/Package.swift +++ b/Package.swift @@ -9,13 +9,15 @@ let package = Package( .executable(name: "memcache-swift-example", targets: ["memcache-swift-example"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.40.0") + .package(url: "https://github.com/apple/swift-nio.git", from: "2.40.0"), + .package(url: "https://github.com/swift-extras/swift-extras-base64.git", from: "0.7.0") ], targets: [ .target( name: "Memcache", dependencies: [ - .product(name: "NIO", package: "swift-nio") + .product(name: "NIO", package: "swift-nio"), + .product(name: "ExtrasBase64", package: "swift-extras-base64") ] ), .testTarget( diff --git a/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift b/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift index 5a9a802..0376582 100644 --- a/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift +++ b/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift @@ -1,3 +1,4 @@ +import ExtrasBase64 import NIOCore struct MemcacheDecodingError: Error { @@ -14,7 +15,7 @@ struct MemcacheDecodingError: Error { ) -> Self { MemcacheDecodingError( messageVerb: messageVerb, - payload: "", // TODO: Can we get a base64 representation without Foundation? + payload: String(base64Encoding: messageBytes.readableBytesView), description: partialError.description, file: partialError.file, line: partialError.line @@ -28,7 +29,7 @@ struct MemcacheDecodingError: Error { ) -> Self { MemcacheDecodingError( messageVerb: "", - payload: "", // TODO: Can we get a base64 representation without Foundation? + payload: String(base64Encoding: bytes.readableBytesView), description: "Received an empty message (i.e. no characters before the first occurence of \r\n). A valid message has to contain a messageVerb at least.", file: file, line: line @@ -43,7 +44,7 @@ struct MemcacheDecodingError: Error { ) -> Self { MemcacheDecodingError( messageVerb: messageVerb, - payload: "", // TODO: Can we get a base64 representation without Foundation? + payload: String(base64Encoding: messageBytes.readableBytesView), description: "Received a message with messageVerb '\(messageVerb)'. There is no message type associated with this message identifier.", file: file, line: line From b37d880ec2e1d0865b00633fe5e1cf4885f3f1a3 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Sun, 25 Sep 2022 22:26:51 +0200 Subject: [PATCH 08/12] Decode flags --- .../MemcachePartialDecodingError.swift | 19 +- .../Messages/Backend+Flags.swift | 141 +++++++- .../Messages/Backend+Value.swift | 5 +- Sources/Memcache/Flags/MemcacheFlag.swift | 300 ++++++++++++++++++ .../Memcache/Flags/MemcacheFlags+Tokens.swift | 126 ++++++++ .../MemcacheBackendMessage+Equatable.swift | 59 ++++ .../MemcacheBackendMessageDecoderTests.swift | 9 +- .../MemcacheBackendMessageTests.swift | 10 +- 8 files changed, 646 insertions(+), 23 deletions(-) create mode 100644 Sources/Memcache/Flags/MemcacheFlag.swift create mode 100644 Sources/Memcache/Flags/MemcacheFlags+Tokens.swift diff --git a/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift b/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift index 9a312d4..f0a4d94 100644 --- a/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift +++ b/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift @@ -5,14 +5,27 @@ struct MemcachePartialDecodingError: Error { let file: String let line: UInt - static func expectedExactlyNRemainingBytes( + static func expectedExactlyNRemainingCharacters( _ expected: Int, actual: Int, file: String = #fileID, line: UInt = #line ) -> Self { MemcachePartialDecodingError( - description: "Expected exactly '\(expected)' but found '\(actual)' remaining bytes", + description: "Expected exactly '\(expected)' but found '\(actual)' remaining characters", + file: file, + line: line + ) + } + + static func expectedAtMostNRemainingCharacters( + _ expected: Int, + actual: Int, + file: String = #fileID, + line: UInt = #line + ) -> Self { + MemcachePartialDecodingError( + description: "Expected at most '\(expected)' but found '\(actual)' remaining characters", file: file, line: line ) @@ -25,7 +38,7 @@ struct MemcachePartialDecodingError: Error { line: UInt = #line ) -> Self { MemcachePartialDecodingError( - description: "Could not read '\(type)' from '\(string)' from the ByteBuffer.", + description: "Could not decode '\(type)' from '\(string)'", file: file, line: line ) diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift b/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift index ce8b8e6..1c78770 100644 --- a/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift +++ b/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift @@ -1,14 +1,14 @@ import NIOCore extension MemcacheBackendMessage { - struct Flags: MemcacheMessagePayloadDecodable, Equatable, ExpressibleByArrayLiteral { - let flags: [String] // TODO: Do we want something like (Character, token: String?) instead? Or a struct? + struct Flags: MemcacheMessagePayloadDecodable, ExpressibleByArrayLiteral { + let flags: [MemcacheFlag] - init(_ flags: [String]) { + init(_ flags: [MemcacheFlag]) { self.flags = flags } - init(arrayLiteral elements: String...) { + init(arrayLiteral elements: MemcacheFlag...) { flags = elements } @@ -29,17 +29,140 @@ extension MemcacheBackendMessage { guard flagsSlice.readableBytes > 0 else { return [] } // The slice now only contains the flags separated by - guard let flagsString = flagsSlice.readString(length: flagsSlice.readableBytes) else { - preconditionFailure("We have readable bytes so we should be able to read a string") - } + let flagsString = flagsSlice.readString(length: flagsSlice.readableBytes)! - return Flags(flagsString.components(separatedBy: " ")) + return try Flags( + flagsString + .split(separator: " ") + .map { flag in + guard let codeCharacter = flag.first, + let code = MemcacheFlag.Code(rawValue: codeCharacter) + else { + throw MemcachePartialDecodingError.fieldNotDecodable(as: MemcacheFlag.Code.self, from: String(flag)) + } + return try .decode(from: flag.dropFirst(), for: code) + } + ) } } } extension MemcacheBackendMessage.Flags: CustomDebugStringConvertible { var debugDescription: String { - "[\(flags.map({ "\"\($0)\"" }).joined(separator: ", "))]" + "flags: [" + + flags + .map(String.init(describing:)) + .joined(separator: ", ") + + "]" + } +} + +// MARK: - Flag decoding + +extension MemcacheFlag { + static func decode(from substring: Substring, for code: Code) throws -> MemcacheFlag { + switch code { + case .b: + return .b + case .c: + return .c + case .k: + return .k + case .O: + return try .O(.decode(from: substring)) + case .q: + return .q + case .v: + return .v + case .t: + return .t + case .T: + return try .T(.decode(from: substring)) + case .C: + return try .C(.decode(from: substring)) + case .I: + return .I + case .N: + return try .N(.decode(from: substring)) + case .M: + return try .M(.decode(from: substring)) + case .f: + return .f + case .h: + return .h + case .l: + return .l + case .s: + return .s + case .u: + return .u + case .R: + return try .R(.decode(from: substring)) + case .W: + return .W + case .X: + return .X + case .Z: + return .Z + case .F: + return try .F(.decode(from: substring)) + case .J: + return try .J(.decode(from: substring)) + case .D: + return try .D(.decode(from: substring)) + case .P: + return try .P(.decode(from: substring)) + case .L: + return try .L(.decode(from: substring)) + } + } +} + +// MARK: - Token types + +protocol MemcacheMessageFlagTokenDecodable { + static func decode(from substring: Substring) throws -> Self +} + +extension MemcacheFlag.StringToken: MemcacheMessageFlagTokenDecodable { + static func decode(from substring: Substring) throws -> Self { + return Self(stringLiteral: String(substring)) + } +} + +extension MemcacheFlag.NumericToken: MemcacheMessageFlagTokenDecodable where Value: LosslessStringConvertible { + static func decode(from substring: Substring) throws -> Self { + guard let numericValue = Value(String(substring)) else { + throw MemcachePartialDecodingError.fieldNotDecodable(as: Value.self, from: String(substring)) + } + return Self(value: numericValue) + } +} + +extension MemcacheFlag.OpaqueToken: MemcacheMessageFlagTokenDecodable { + static func decode(from substring: Substring) throws -> Self { + guard substring.count <= 32 else { + throw MemcachePartialDecodingError.expectedAtMostNRemainingCharacters(32, actual: substring.count) + } + return Self(stringLiteral: String(substring)) + } +} + +extension MemcacheFlag.TTLToken: MemcacheMessageFlagTokenDecodable { + static func decode(from substring: Substring) throws -> Self { + let numeric = try MemcacheFlag.NumericToken.decode(from: substring) + return Self(integerLiteral: numeric.value) + } +} + +extension MemcacheFlag.ModeToken: MemcacheMessageFlagTokenDecodable { + static func decode(from substring: Substring) throws -> Self { + guard substring.count == 1 else { + throw MemcachePartialDecodingError.expectedExactlyNRemainingCharacters(1, actual: substring.count) + } + guard let token = Self(rawValue: substring.first!) else { + throw MemcachePartialDecodingError.fieldNotDecodable(as: Self.self, from: String(substring)) + } + return token } } diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift b/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift index c50a458..f82959b 100644 --- a/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift +++ b/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift @@ -1,7 +1,8 @@ +import ExtrasBase64 import NIOCore extension MemcacheBackendMessage { - struct Value: MemcacheMessagePayloadDecodable, Equatable { + struct Value: MemcacheMessagePayloadDecodable { var flags: Flags var data: ByteBuffer @@ -55,6 +56,6 @@ extension MemcacheBackendMessage { extension MemcacheBackendMessage.Value: CustomDebugStringConvertible { var debugDescription: String { - return "flags: \(flags), data: \(data.readableBytes) bytes" + return "\(flags), data: \(String(base64Encoding: data.readableBytesView))" } } diff --git a/Sources/Memcache/Flags/MemcacheFlag.swift b/Sources/Memcache/Flags/MemcacheFlag.swift new file mode 100644 index 0000000..06ad48f --- /dev/null +++ b/Sources/Memcache/Flags/MemcacheFlag.swift @@ -0,0 +1,300 @@ +/// Flags for Meta Get, Set, Delete, and Arithmetic commands. Meta Debug and Meta No-Op don't support flags. +enum MemcacheFlag { + // MARK: Common flags + + /// Interpret key as base64 encoded binary value. + /// + /// Available for the following commands: + /// - **Meta Get** + /// - **Meta Set** + /// - **Meta Delete** + /// - **Meta Arithmetic** + case b + + /// Return item CAS token. + /// + /// Available for the following commands: + /// - **Meta Get** + /// - **Meta Set**: Return item's CAS token if successfully stored. + /// - **Meta Arithmetic**: Return item's CAS token if successfully stored. + case c + + /// Return key as a token. + /// + /// Available for the following commands: + /// - **Meta Get** + /// - **Meta Set** + /// - **Meta Delete** + case k + + /// Opaque value, consumes a token and copies back with response. + /// + /// Available for the following commands: + /// - **Meta Get** + /// - **Meta Set** + /// - **Meta Delete** + case O(OpaqueToken) + + /// Use noreply semantics for return codes. + /// + /// Available for the following commands: + /// - **Meta Get** + /// - **Meta Set** + /// - **Meta Delete** + /// - **Meta Arithmetic** + case q + + /// Return item value in \. + /// + /// Available for the following commands: + /// - **Meta Get** + /// - **Meta Arithmetic**: Return new value. + case v + + /// Return item TTL remaining in seconds (-1 for unlimited). + /// + /// Available for the following commands: + /// - **Meta Get** + /// - **Meta Arithmetic** + case t + + /// Update remaining TTL. + /// + /// Available for the following commands: + /// - **Meta Get** + /// - **Meta Set** + /// - **Meta Delete**: Updates TTL, only when paired with the `.I` flag. + /// - **Meta Arithmetic**: Update TTL on success. + case T(TTLToken) + + /// Compare CAS value. + /// + /// Available for the following commands: + /// - **Meta Set**: Compare CAS value when storing item. + /// - **Meta Delete** + /// - **Meta Arithmetic** + case C(NumericToken) + + /// Invalidate: set to invalid if supplied CAS is older than item's CAS. + /// + /// Available for the following commands: + /// - **Meta Set** + /// - **Meta Delete** + case I + + /// Vivify on miss, takes TTL as a argument. + /// + /// Available for the following commands: + /// - **Meta Get** + /// - **Meta Arithmetic**: Auto-create item on miss with supplied TTL. + case N(TTLToken) + + /// Mode switch. + /// + /// Available for the following commands: + /// - **Meta Set**: Mode switch to change behavior to add, replace, append, prepend. + /// - **Meta Arithmetic**: Mode switch to change between incr and decr modes. + case M(ModeToken) + + // MARK: 'Get'-only flags + + /// Return client flags token. + /// + /// Available for the following commands: + /// - **Meta Get** + case f + + /// Return whether item has been hit before as a 0 or 1. + /// + /// Available for the following commands: + /// - **Meta Get** + case h + + /// Return time since item was last accessed in seconds. + /// + /// Available for the following commands: + /// - **Meta Get** + case l + + /// Return item size token. + /// + /// Available for the following commands: + /// - **Meta Get** + case s + + /// Don't bump the item in the LRU. + /// + /// Available for the following commands: + /// - **Meta Get** + case u + + /// If token is less than remaining TTL win for recache. + /// + /// Available for the following commands: + /// - **Meta Get** + case R(TTLToken) + + /// Client has "won" the recache flag. + /// + /// Available for the following commands: + /// - **Meta Get** + case W + + /// Item is stale. + /// + /// Available for the following commands: + /// - **Meta Get** + case X + + /// Item has already sent a winning flag. + /// + /// Available for the following commands: + /// - **Meta Get** + case Z + + // MARK: 'Set'-only flags + + /// Set client flags to token. + /// + /// Available for the following commands: + /// - **Meta Set** + case F(NumericToken) // TODO: Docs say '32 bit unsigned numeric' + + // MARK: 'Delete'-only flags + + // No 'Delete'-only flags + + // MARK: 'Arithmetic'-only flags + + /// Initial value to use if auto created after miss (default 0). + /// + /// Available for the following commands: + /// - **Meta Arithmetic** + case J(NumericToken) + + /// Delta to apply (default 1). + /// + /// Available for the following commands: + /// - **Meta Arithmetic** + case D(NumericToken) // TODO: Docs say 'unsigned decimal number' + + // MARK: Ignored tokens + + /// This flag is completely ignored by the memcached daemon. It can be used as a hint or path specification to a proxy or + /// router inbetween a client and the memcached daemon. + case P(StringToken) + + /// This flag is completely ignored by the memcached daemon. It can be used as a hint or path specification to a proxy or + /// router inbetween a client and the memcached daemon. + case L(StringToken) +} + +extension MemcacheFlag: CustomDebugStringConvertible { + var debugDescription: String { + switch self { + case .b: + return ".b" + case .c: + return ".c" + case .k: + return ".k" + case let .O(token): + return ".O(\(token))" + case .q: + return ".q" + case .v: + return ".v" + case .t: + return ".t" + case let .T(token): + return ".T(\(token))" + case let .C(token): + return ".C(\(token))" + case .I: + return ".I" + case let .N(token): + return ".N(\(token))" + case let .M(token): + return ".M(\(token))" + case .f: + return ".f" + case .h: + return ".h" + case .l: + return ".l" + case .s: + return ".s" + case .u: + return ".u" + case let .R(token): + return ".R(\(token))" + case .W: + return ".W" + case .X: + return ".X" + case .Z: + return ".Z" + case let .F(token): + return ".F(\(token))" + case let .J(token): + return ".J(\(token))" + case let .D(token): + return ".D(\(token))" + case let .P(token): + return ".P(\(token))" + case let .L(token): + return ".L(\(token))" + } + } +} + +// MARK: - + +extension MemcacheFlag { + enum Code: Character { + // MARK: Common flags + + case b = "b" + case c = "c" + case k = "k" + case O = "O" + case q = "q" + case v = "v" + case t = "t" + case T = "T" + case C = "C" + case I = "I" + case N = "N" + case M = "M" + + // MARK: 'Get'-only flags + + case f = "f" + case h = "h" + case l = "l" + case s = "s" + case u = "u" + case R = "R" + case W = "W" + case X = "X" + case Z = "Z" + + // MARK: 'Set'-only flags + + case F = "F" + + // MARK: 'Delete'-only flags + + // No 'Delete'-only flags + + // MARK: 'Arithmetic'-only flags + + case J = "J" + case D = "D" + + // MARK: Ignored flags + + case P = "P" + case L = "L" + } +} diff --git a/Sources/Memcache/Flags/MemcacheFlags+Tokens.swift b/Sources/Memcache/Flags/MemcacheFlags+Tokens.swift new file mode 100644 index 0000000..34a0883 --- /dev/null +++ b/Sources/Memcache/Flags/MemcacheFlags+Tokens.swift @@ -0,0 +1,126 @@ +extension MemcacheFlag { + /// A string token without any constraints. + struct StringToken: ExpressibleByStringLiteral, CustomDebugStringConvertible { + var value: String + + init(stringLiteral value: String) { + self.value = value + } + + var debugDescription: String { + "string: \(value)" + } + } + + /// A numeric token. + struct NumericToken: CustomDebugStringConvertible { + var value: Value + + var debugDescription: String { + "numeric: \(value)" + } + } + + /// Opaque tokens may be up to 32 bytes in length. + struct OpaqueToken: ExpressibleByStringLiteral, CustomDebugStringConvertible { + var value: String + + init(stringLiteral value: StringLiteralType) { + self.value = value + } + + var debugDescription: String { + "opaque: \(value)" + } + } + + /// A numeric token representing a TTL value. + struct TTLToken: ExpressibleByIntegerLiteral, CustomDebugStringConvertible { + let value: UInt32 + + init(integerLiteral value: UInt32) { + self.value = value + } + + var debugDescription: String { + "ttl: \(value)" + } + } + + /// Mode switch token used in Set and Arithmetic commands. + enum ModeToken: RawRepresentable, CustomDebugStringConvertible { + typealias RawValue = Character + + // MARK: 'Set'-modes + + case add + case append + case prepend + case replace + case set + + // MARK: 'Arithmetic'-modes + + case increment + case decrement + + init?(rawValue: Character) { + switch rawValue { + case "E": + self = .add + case "A": + self = .append + case "P": + self = .prepend + case "R": + self = .replace + case "S": + self = .set + case "I", "+": + self = .increment + case "D", "-": + self = .decrement + default: + return nil + } + } + + var rawValue: Character { + switch self { + case .add: + return "E" + case .append: + return "A" + case .prepend: + return "P" + case .replace: + return "R" + case .set: + return "S" + case .increment: + return "I" + case .decrement: + return "D" + } + } + + var debugDescription: String { + switch self { + case .add: + return ".add" + case .append: + return ".append" + case .prepend: + return ".prepend" + case .replace: + return ".replace" + case .set: + return ".set" + case .increment: + return ".increment" + case .decrement: + return ".decrement" + } + } + } +} diff --git a/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift b/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift index f854421..0822023 100644 --- a/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift +++ b/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift @@ -20,3 +20,62 @@ extension MemcacheBackendMessage: Equatable { } } } + +extension MemcacheBackendMessage.Value: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.flags == rhs.flags && lhs.data == rhs.data + } +} + +extension MemcacheBackendMessage.Flags: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.flags == rhs.flags + } +} + +extension MemcacheFlag: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.b, .b), (.c, .c), (.k, .k), (.q, .q), (.v, .v), (.t, .t), (.I, .I), (.f, .f), (.h, .h), (.l, .l), (.s, .s), (.u, .u), (.W, .W), (.X, .X), (.Z, .Z): + return true + case let (.P(lhs), .P(rhs)), let (.L(lhs), .L(rhs)): + return lhs == rhs + case let (.F(lhs), .F(rhs)): + return lhs == rhs + case let (.C(lhs), .C(rhs)), let (.J(lhs), .J(rhs)), let (.D(lhs), .D(rhs)): + return lhs == rhs + case let (.O(lhs), .O(rhs)): + return lhs == rhs + case let (.T(lhs), .T(rhs)), let (.N(lhs), .N(rhs)), let (.R(lhs), .R(rhs)): + return lhs == rhs + case let (.M(lhs), .M(rhs)): + return lhs == rhs + default: + return false + } + } +} + +extension MemcacheFlag.StringToken: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.value == rhs.value + } +} + +extension MemcacheFlag.NumericToken: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.value == rhs.value + } +} + +extension MemcacheFlag.OpaqueToken: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.value == rhs.value + } +} + +extension MemcacheFlag.TTLToken: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.value == rhs.value + } +} diff --git a/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift b/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift index 75fba92..726af0b 100644 --- a/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift +++ b/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift @@ -7,7 +7,7 @@ import XCTest final class MemcacheBackendMessageDecoderTests: XCTestCase { func testDecodeMessageWithAndWithoutFlags() { // TODO: Test decoding these messages also without flags. Test EN\r\n should fail when we receive it with flags. - let flags: MemcacheBackendMessage.Flags = ["B1", "a", "r"] + let flags: MemcacheBackendMessage.Flags = [.b, .R(0), .f] let expected: [MemcacheBackendMessage] = [ .header(flags), .notFound(flags), @@ -16,8 +16,9 @@ final class MemcacheBackendMessageDecoderTests: XCTestCase { .end ] + let flagsString = "b R0 f" let messageString = [MemcacheBackendMessage.Verb]([.header, .notFound, .notStored, .exists]) - .map { "\($0.rawValue) \(flags.flags.joined(separator: " "))\r\n" } + .map { "\($0.rawValue) \(flagsString)\r\n" } .joined() + "EN\r\n" @@ -31,11 +32,11 @@ final class MemcacheBackendMessageDecoderTests: XCTestCase { var buffer = ByteBuffer() buffer.writeString("foo") let expected: [MemcacheBackendMessage] = [ - .value(.init(flags: ["b", "a", "r"], data: buffer)) + .value(.init(flags: [.b, .R(0), .f], data: buffer)) ] XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( - stringInputOutputPairs: [("VA 3 b a r\r\nfoo\r\n", expected)], + stringInputOutputPairs: [("VA 3 b R0 f\r\nfoo\r\n", expected)], decoderFactory: { MemcacheBackendMessageDecoder() } )) } diff --git a/Tests/MemcacheTests/MemcacheBackendMessageTests.swift b/Tests/MemcacheTests/MemcacheBackendMessageTests.swift index 97048f5..9170759 100644 --- a/Tests/MemcacheTests/MemcacheBackendMessageTests.swift +++ b/Tests/MemcacheTests/MemcacheBackendMessageTests.swift @@ -26,11 +26,11 @@ final class MemcacheBackendMessageTests: XCTestCase { } func testDebugDescription() { - XCTAssertEqual("\(MemcacheBackendMessage.header(["T1", "v"]))", #".header(["T1", "v"])"#) - XCTAssertEqual("\(MemcacheBackendMessage.notFound(["T1", "v"]))", #".notFound(["T1", "v"])"#) - XCTAssertEqual("\(MemcacheBackendMessage.notStored(["T1", "v"]))", #".notStored(["T1", "v"])"#) - XCTAssertEqual("\(MemcacheBackendMessage.exists(["T1", "v"]))", #".exists(["T1", "v"])"#) - XCTAssertEqual("\(MemcacheBackendMessage.value(.init(flags: ["T1", "v"], data: ByteBuffer())))", #".value(flags: ["T1", "v"], data: 0 bytes)"#) + XCTAssertEqual("\(MemcacheBackendMessage.header([.T(1), .v]))", ".header(flags: [.T(ttl: 1), .v])") + XCTAssertEqual("\(MemcacheBackendMessage.notFound([.T(1), .v]))", ".notFound(flags: [.T(ttl: 1), .v])") + XCTAssertEqual("\(MemcacheBackendMessage.notStored([.T(1), .v]))", ".notStored(flags: [.T(ttl: 1), .v])") + XCTAssertEqual("\(MemcacheBackendMessage.exists([.T(1), .v]))", ".exists(flags: [.T(ttl: 1), .v])") + XCTAssertEqual("\(MemcacheBackendMessage.value(.init(flags: [.T(1), .v], data: ByteBuffer())))", ".value(flags: [.T(ttl: 1), .v], data: )") XCTAssertEqual("\(MemcacheBackendMessage.end)", ".end") } } From b7502d13e0ce5a53c412711af48a3949755a7ce9 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Sun, 25 Sep 2022 22:55:51 +0200 Subject: [PATCH 09/12] Decode errors --- .../MemcachePartialDecodingError.swift | 13 +++++++ .../Messages/Backend+ErrorMessage.swift | 22 +++++++++++ .../Messages/MemcacheBackendMessage.swift | 39 ++++++++++++++++++- 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift diff --git a/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift b/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift index f0a4d94..6088196 100644 --- a/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift +++ b/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift @@ -18,6 +18,19 @@ struct MemcachePartialDecodingError: Error { ) } + static func expectedAtLeastNRemainingCharacters( + _ expected: Int, + actual: Int, + file: String = #fileID, + line: UInt = #line + ) -> Self { + MemcachePartialDecodingError( + description: "Expected at least '\(expected)' but found '\(actual)' remaining characters", + file: file, + line: line + ) + } + static func expectedAtMostNRemainingCharacters( _ expected: Int, actual: Int, diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift b/Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift new file mode 100644 index 0000000..99530f4 --- /dev/null +++ b/Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift @@ -0,0 +1,22 @@ +import NIOCore + +extension MemcacheBackendMessage { + struct ErrorMessage: MemcacheMessagePayloadDecodable { + let message: String + + static func decode(from buffer: inout ByteBuffer) throws -> Self { + // An error message is always the last part of a text line, which is terminated by \r\n. + guard var messageSlice = buffer.readCarriageReturnNewlineTerminatedSlice() else { + preconditionFailure("Expected to only see messages that contain \r\n here.") + } + + guard messageSlice.readableBytes > 0 else { + throw MemcachePartialDecodingError.expectedAtLeastNRemainingCharacters(1, actual: messageSlice.readableBytes) + } + + let messageString = messageSlice.readString(length: messageSlice.readableBytes)! + + return ErrorMessage(message: messageString) + } + } +} diff --git a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift index ee76370..6723894 100644 --- a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift +++ b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift @@ -23,7 +23,14 @@ enum MemcacheBackendMessage { /// End (`EN\r\n`) case end - // TODO: error responses + /// Nonexistent command name (`ERROR\r\n`) + case nonExistentCommandError + + /// Client error (`CLIENT_ERROR \r\n`) + case clientError(ErrorMessage) + + /// Server error (`SERVER_ERROR \r\n`) + case serverError(ErrorMessage) } extension MemcacheBackendMessage { @@ -36,7 +43,10 @@ extension MemcacheBackendMessage { case exists case value case end - // TODO: error responses + + case nonExistentCommandError + case clientError + case serverError init?(rawValue: String) { switch rawValue { @@ -52,6 +62,12 @@ extension MemcacheBackendMessage { self = .value case "EN": self = .end + case "ERROR": + self = .nonExistentCommandError + case "CLIENT_ERROR": + self = .clientError + case "SERVER_ERROR": + self = .serverError default: return nil } @@ -71,6 +87,12 @@ extension MemcacheBackendMessage { return "VA" case .end: return "EN" + case .nonExistentCommandError: + return "ERROR" + case .clientError: + return "CLIENT_ERROR" + case .serverError: + return "SERVER_ERROR" } } } @@ -92,6 +114,13 @@ extension MemcacheBackendMessage { case .end: buffer.moveReaderIndex(forwardBy: 2) return .end + case .nonExistentCommandError: + buffer.moveReaderIndex(forwardBy: 2) + return .nonExistentCommandError + case .clientError: + return try .clientError(.decode(from: &buffer)) + case .serverError: + return try .serverError(.decode(from: &buffer)) } } } @@ -111,6 +140,12 @@ extension MemcacheBackendMessage: CustomDebugStringConvertible { return ".value(\(String(reflecting: value)))" case .end: return ".end" + case .nonExistentCommandError: + return ".nonExistentCommandError" + case let .clientError(string): + fatalError(string) + case let .serverError(string): + fatalError(string) } } } From edc61fa9dcda80b05013a08c2010276f8da9e332 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Mon, 26 Sep 2022 11:20:59 +0200 Subject: [PATCH 10/12] Copy buffer to handle indices less --- .../MemcacheBackendMessageDecoder.swift | 42 ++++++++++--------- .../Messages/Backend+Value.swift | 5 +-- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift b/Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift index 7d6c005..993387b 100644 --- a/Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift +++ b/Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift @@ -5,47 +5,49 @@ struct MemcacheBackendMessageDecoder: NIOSingleStepByteToMessageDecoder { typealias InboundOut = MemcacheBackendMessage func decode(buffer: inout ByteBuffer) throws -> MemcacheBackendMessage? { - // Keep track of the reader index in case we later notice that we need more data - let startReaderIndex = buffer.readerIndex + // Keep a copy in case we later notice that we need more data + var peekableBuffer = buffer // Peek at the message to read the verb. It is before the first \r\n and before the first if the message // contains one. - guard let messageSlice = buffer.getCarriageReturnNewlineTerminatedSlice(at: buffer.readerIndex) else { - // reader index wasn't moved, wait for more bytes - return nil + guard let textLine = peekableBuffer.getCarriageReturnNewlineTerminatedSlice(at: peekableBuffer.readerIndex) else { + return nil // wait for more bytes } - // The verb in messageSlice is either the entire slice or the part before the first - let endVerbIndex = messageSlice.readableBytesView.firstIndex(of: .space) ?? messageSlice.writerIndex + // The verb is either the entire text line or the part before the first + let verbLength = (textLine.readableBytesView.firstIndex(of: .space) ?? textLine.writerIndex) - textLine.readerIndex - guard let verbString = messageSlice.getString(at: messageSlice.readerIndex, length: endVerbIndex) else { - // If we can't read a string, the messageSlice must be empty (i.e. no characters before the first occurence of \r\n) - throw MemcacheDecodingError.emptyMessageReceived(bytes: buffer) + guard let verbString = textLine.getString(at: textLine.readerIndex, length: verbLength) else { + // If we can't read a string, the text line must be empty (i.e. no characters before the first occurence of \r\n) + throw MemcacheDecodingError.emptyMessageReceived(bytes: peekableBuffer) } guard let verb = MemcacheBackendMessage.Verb(rawValue: verbString) else { - throw MemcacheDecodingError.unknownVerbReceived(messageVerb: verbString, messageBytes: messageSlice) + throw MemcacheDecodingError.unknownVerbReceived(messageVerb: verbString, messageBytes: peekableBuffer) } - // Move the buffer's readerIndex to after the verb so we can continue reading flags and/or data. - buffer.moveReaderIndex(forwardBy: endVerbIndex) + // Move the peekable buffer's readerIndex to after the verb so we can continue reading flags and/or data. + peekableBuffer.moveReaderIndex(forwardBy: verbLength) - if buffer.readableBytesView.first == .space { + if peekableBuffer.readableBytesView.first == .space { // Move the reader index to after the that is following the verb - buffer.moveReaderIndex(forwardBy: 1) + peekableBuffer.moveReaderIndex(forwardBy: 1) } do { - // Pass the buffer instead of messageSlice because .value messages continue after the first \r\n - let result = try MemcacheBackendMessage.decode(from: &buffer, for: verb) + // Pass the entire buffer instead of the text line because .value messages continue after the first \r\n + let result = try MemcacheBackendMessage.decode(from: &peekableBuffer, for: verb) + + // Message was read successfully, write new reader index back + buffer = peekableBuffer + // TODO: Can we make sure the message was read entirely? Difficult because we don't know the length of VA messages here. return result } catch _ as MemcacheNeedMoreDataError { - // A message decoder told us that it expects more data. Move the reader index back to the start and try again. - buffer.moveReaderIndex(to: startReaderIndex) + // A message decoder expects more data. Try again. return nil } catch let error as MemcachePartialDecodingError { - throw MemcacheDecodingError.withPartialError(error, messageVerb: verb.rawValue, messageBytes: messageSlice) + throw MemcacheDecodingError.withPartialError(error, messageVerb: verb.rawValue, messageBytes: peekableBuffer) } catch { preconditionFailure("Expected to only see `MemcachePartialDecodingError` here.") } diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift b/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift index f82959b..50e1fa3 100644 --- a/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift +++ b/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift @@ -20,10 +20,7 @@ extension MemcacheBackendMessage { // The size value in valueMetaSlice is either the entire slice or the part before the first let endSizeIndex = valueMetaSlice.readableBytesView.firstIndex(of: .space) ?? valueMetaSlice.writerIndex - - guard let sizeString = valueMetaSlice.getString(at: valueMetaSlice.readerIndex, length: endSizeIndex) else { - preconditionFailure("We have readable bytes so we should be able to read a string") - } + let sizeString = valueMetaSlice.getString(at: valueMetaSlice.readerIndex, length: endSizeIndex)! guard let size = Int(sizeString) else { throw MemcachePartialDecodingError.fieldNotDecodable(as: Int.self, from: sizeString) From 80f277494f28c941855a783946498ca7749ec242 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Mon, 26 Sep 2022 19:20:51 +0200 Subject: [PATCH 11/12] Add tests for error responses --- .../Messages/Backend+ErrorMessage.swift | 14 ++++++++++++-- .../Messages/MemcacheBackendMessage.swift | 8 ++++---- .../MemcacheBackendMessage+Equatable.swift | 12 ++++++++++++ .../MemcacheBackendMessageDecoderTests.swift | 6 +++++- .../MemcacheBackendMessageTests.swift | 9 +++++++++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift b/Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift index 99530f4..ca8667e 100644 --- a/Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift +++ b/Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift @@ -1,9 +1,13 @@ import NIOCore extension MemcacheBackendMessage { - struct ErrorMessage: MemcacheMessagePayloadDecodable { + struct ErrorMessage: MemcacheMessagePayloadDecodable, ExpressibleByStringLiteral { let message: String + init(stringLiteral value: String) { + self.message = value + } + static func decode(from buffer: inout ByteBuffer) throws -> Self { // An error message is always the last part of a text line, which is terminated by \r\n. guard var messageSlice = buffer.readCarriageReturnNewlineTerminatedSlice() else { @@ -16,7 +20,13 @@ extension MemcacheBackendMessage { let messageString = messageSlice.readString(length: messageSlice.readableBytes)! - return ErrorMessage(message: messageString) + return ErrorMessage(stringLiteral: messageString) } } } + +extension MemcacheBackendMessage.ErrorMessage: CustomDebugStringConvertible { + var debugDescription: String { + "message: \"\(message)\"" + } +} diff --git a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift index 6723894..3975d7c 100644 --- a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift +++ b/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift @@ -142,10 +142,10 @@ extension MemcacheBackendMessage: CustomDebugStringConvertible { return ".end" case .nonExistentCommandError: return ".nonExistentCommandError" - case let .clientError(string): - fatalError(string) - case let .serverError(string): - fatalError(string) + case let .clientError(message): + return ".clientError(\(String(reflecting: message)))" + case let .serverError(message): + return ".serverError(\(String(reflecting: message)))" } } } diff --git a/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift b/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift index 0822023..d1223c8 100644 --- a/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift +++ b/Tests/MemcacheTests/Extensions/MemcacheBackendMessage+Equatable.swift @@ -15,6 +15,12 @@ extension MemcacheBackendMessage: Equatable { return lhs == rhs case (.end, .end): return true + case (.nonExistentCommandError, .nonExistentCommandError): + return true + case let (.clientError(lhs), .clientError(rhs)): + return lhs == rhs + case let (.serverError(lhs), .serverError(rhs)): + return lhs == rhs default: return false } @@ -79,3 +85,9 @@ extension MemcacheFlag.TTLToken: Equatable { lhs.value == rhs.value } } + +extension MemcacheBackendMessage.ErrorMessage: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.message == rhs.message + } +} diff --git a/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift b/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift index 726af0b..28b77b2 100644 --- a/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift +++ b/Tests/MemcacheTests/MemcacheBackendMessageDecoderTests.swift @@ -13,7 +13,10 @@ final class MemcacheBackendMessageDecoderTests: XCTestCase { .notFound(flags), .notStored(flags), .exists(flags), - .end + .end, + .nonExistentCommandError, + .clientError("Test Error"), + .serverError("Test Error") ] let flagsString = "b R0 f" @@ -21,6 +24,7 @@ final class MemcacheBackendMessageDecoderTests: XCTestCase { .map { "\($0.rawValue) \(flagsString)\r\n" } .joined() + "EN\r\n" + + "ERROR\r\nCLIENT_ERROR Test Error\r\nSERVER_ERROR Test Error\r\n" XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( stringInputOutputPairs: [(messageString, expected)], diff --git a/Tests/MemcacheTests/MemcacheBackendMessageTests.swift b/Tests/MemcacheTests/MemcacheBackendMessageTests.swift index 9170759..8618ce9 100644 --- a/Tests/MemcacheTests/MemcacheBackendMessageTests.swift +++ b/Tests/MemcacheTests/MemcacheBackendMessageTests.swift @@ -12,6 +12,9 @@ final class MemcacheBackendMessageTests: XCTestCase { XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "EX"), .exists) XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "VA"), .value) XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "EN"), .end) + XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "ERROR"), .nonExistentCommandError) + XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "CLIENT_ERROR"), .clientError) + XCTAssertEqual(MemcacheBackendMessage.Verb(rawValue: "SERVER_ERROR"), .serverError) XCTAssertNil(MemcacheBackendMessage.Verb(rawValue: "")) } @@ -23,6 +26,9 @@ final class MemcacheBackendMessageTests: XCTestCase { XCTAssertEqual(MemcacheBackendMessage.Verb.exists.rawValue, "EX") XCTAssertEqual(MemcacheBackendMessage.Verb.value.rawValue, "VA") XCTAssertEqual(MemcacheBackendMessage.Verb.end.rawValue, "EN") + XCTAssertEqual(MemcacheBackendMessage.Verb.nonExistentCommandError.rawValue, "ERROR") + XCTAssertEqual(MemcacheBackendMessage.Verb.clientError.rawValue, "CLIENT_ERROR") + XCTAssertEqual(MemcacheBackendMessage.Verb.serverError.rawValue, "SERVER_ERROR") } func testDebugDescription() { @@ -32,5 +38,8 @@ final class MemcacheBackendMessageTests: XCTestCase { XCTAssertEqual("\(MemcacheBackendMessage.exists([.T(1), .v]))", ".exists(flags: [.T(ttl: 1), .v])") XCTAssertEqual("\(MemcacheBackendMessage.value(.init(flags: [.T(1), .v], data: ByteBuffer())))", ".value(flags: [.T(ttl: 1), .v], data: )") XCTAssertEqual("\(MemcacheBackendMessage.end)", ".end") + XCTAssertEqual("\(MemcacheBackendMessage.nonExistentCommandError)", ".nonExistentCommandError") + XCTAssertEqual("\(MemcacheBackendMessage.clientError("Test Error"))", ".clientError(message: \"Test Error\")") + XCTAssertEqual("\(MemcacheBackendMessage.serverError("Test Error"))", ".serverError(message: \"Test Error\")") } } From 5cc95b00e05ae9c35cb6806a1615479357ba823c Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Mon, 26 Sep 2022 19:25:09 +0200 Subject: [PATCH 12/12] Move files out of ChannelHandler folder --- .../{ChannelHandler => }/MemcacheBackendMessageDecoder.swift | 0 Sources/Memcache/{ChannelHandler => }/MemcacheDecodingError.swift | 0 .../Memcache/{ChannelHandler => }/MemcacheNeedMoreDataError.swift | 0 .../{ChannelHandler => }/MemcachePartialDecodingError.swift | 0 .../{ChannelHandler => }/Messages/Backend+ErrorMessage.swift | 0 .../Memcache/{ChannelHandler => }/Messages/Backend+Flags.swift | 0 .../Memcache/{ChannelHandler => }/Messages/Backend+Value.swift | 0 .../{ChannelHandler => }/Messages/MemcacheBackendMessage.swift | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename Sources/Memcache/{ChannelHandler => }/MemcacheBackendMessageDecoder.swift (100%) rename Sources/Memcache/{ChannelHandler => }/MemcacheDecodingError.swift (100%) rename Sources/Memcache/{ChannelHandler => }/MemcacheNeedMoreDataError.swift (100%) rename Sources/Memcache/{ChannelHandler => }/MemcachePartialDecodingError.swift (100%) rename Sources/Memcache/{ChannelHandler => }/Messages/Backend+ErrorMessage.swift (100%) rename Sources/Memcache/{ChannelHandler => }/Messages/Backend+Flags.swift (100%) rename Sources/Memcache/{ChannelHandler => }/Messages/Backend+Value.swift (100%) rename Sources/Memcache/{ChannelHandler => }/Messages/MemcacheBackendMessage.swift (100%) diff --git a/Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift b/Sources/Memcache/MemcacheBackendMessageDecoder.swift similarity index 100% rename from Sources/Memcache/ChannelHandler/MemcacheBackendMessageDecoder.swift rename to Sources/Memcache/MemcacheBackendMessageDecoder.swift diff --git a/Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift b/Sources/Memcache/MemcacheDecodingError.swift similarity index 100% rename from Sources/Memcache/ChannelHandler/MemcacheDecodingError.swift rename to Sources/Memcache/MemcacheDecodingError.swift diff --git a/Sources/Memcache/ChannelHandler/MemcacheNeedMoreDataError.swift b/Sources/Memcache/MemcacheNeedMoreDataError.swift similarity index 100% rename from Sources/Memcache/ChannelHandler/MemcacheNeedMoreDataError.swift rename to Sources/Memcache/MemcacheNeedMoreDataError.swift diff --git a/Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift b/Sources/Memcache/MemcachePartialDecodingError.swift similarity index 100% rename from Sources/Memcache/ChannelHandler/MemcachePartialDecodingError.swift rename to Sources/Memcache/MemcachePartialDecodingError.swift diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift b/Sources/Memcache/Messages/Backend+ErrorMessage.swift similarity index 100% rename from Sources/Memcache/ChannelHandler/Messages/Backend+ErrorMessage.swift rename to Sources/Memcache/Messages/Backend+ErrorMessage.swift diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift b/Sources/Memcache/Messages/Backend+Flags.swift similarity index 100% rename from Sources/Memcache/ChannelHandler/Messages/Backend+Flags.swift rename to Sources/Memcache/Messages/Backend+Flags.swift diff --git a/Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift b/Sources/Memcache/Messages/Backend+Value.swift similarity index 100% rename from Sources/Memcache/ChannelHandler/Messages/Backend+Value.swift rename to Sources/Memcache/Messages/Backend+Value.swift diff --git a/Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift b/Sources/Memcache/Messages/MemcacheBackendMessage.swift similarity index 100% rename from Sources/Memcache/ChannelHandler/Messages/MemcacheBackendMessage.swift rename to Sources/Memcache/Messages/MemcacheBackendMessage.swift