| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- /*
- * Copyright 2024, gRPC Authors All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- enum GRPCStatusMessageMarshaller {
- /// Adds percent encoding to the given message.
- ///
- /// - Parameter message: Message to percent encode.
- /// - Returns: Percent encoded string, or `nil` if it could not be encoded.
- static func marshall(_ message: String) -> String? {
- return percentEncode(message)
- }
- /// Removes percent encoding from the given message.
- ///
- /// - Parameter message: Message to remove encoding from.
- /// - Returns: The string with percent encoding removed, or the input string if the encoding
- /// could not be removed.
- static func unmarshall(_ message: String) -> String {
- return removePercentEncoding(message)
- }
- }
- extension GRPCStatusMessageMarshaller {
- /// Adds percent encoding to the given message.
- ///
- /// gRPC uses percent encoding as defined in RFC 3986 § 2.1 but with a different set of restricted
- /// characters. The allowed characters are all visible printing characters except for (`%`,
- /// `0x25`). That is: `0x20`-`0x24`, `0x26`-`0x7E`.
- ///
- /// - Parameter message: The message to encode.
- /// - Returns: Percent encoded string, or `nil` if it could not be encoded.
- private static func percentEncode(_ message: String) -> String? {
- let utf8 = message.utf8
- let encodedLength = self.percentEncodedLength(for: utf8)
- // Fast-path: all characters are valid, nothing to encode.
- if encodedLength == utf8.count {
- return message
- }
- var bytes: [UInt8] = []
- bytes.reserveCapacity(encodedLength)
- for char in message.utf8 {
- switch char {
- // See: https://github.com/grpc/grpc/blob/7f664c69b2a636386fbf95c16bc78c559734ce0f/doc/PROTOCOL-HTTP2.md#responses
- case 0x20 ... 0x24,
- 0x26 ... 0x7E:
- bytes.append(char)
- default:
- bytes.append(UInt8(ascii: "%"))
- bytes.append(self.toHex(char >> 4))
- bytes.append(self.toHex(char & 0xF))
- }
- }
- return String(decoding: bytes, as: UTF8.self)
- }
- /// Returns the percent encoded length of the given `UTF8View`.
- private static func percentEncodedLength(for view: String.UTF8View) -> Int {
- var count = view.count
- for byte in view {
- switch byte {
- case 0x20 ... 0x24,
- 0x26 ... 0x7E:
- ()
- default:
- count += 2
- }
- }
- return count
- }
- /// Encode the given byte as hexadecimal.
- ///
- /// - Precondition: Only the four least significant bits may be set.
- /// - Parameter nibble: The nibble to convert to hexadecimal.
- private static func toHex(_ nibble: UInt8) -> UInt8 {
- assert(nibble & 0xF == nibble)
- switch nibble {
- case 0 ... 9:
- return nibble &+ UInt8(ascii: "0")
- default:
- return nibble &+ (UInt8(ascii: "A") &- 10)
- }
- }
- /// Remove gRPC percent encoding from `message`. If any portion of the string could not be decoded
- /// then the encoded message will be returned.
- ///
- /// - Parameter message: The message to remove percent encoding from.
- /// - Returns: The decoded message.
- private static func removePercentEncoding(_ message: String) -> String {
- let utf8 = message.utf8
- let decodedLength = self.percentDecodedLength(for: utf8)
- // Fast-path: no decoding to do! Note that we may also have detected that the encoding is
- // invalid, in which case we will return the encoded message: this is fine.
- if decodedLength == utf8.count {
- return message
- }
- var chars: [UInt8] = []
- // We can't decode more characters than are already encoded.
- chars.reserveCapacity(decodedLength)
- var currentIndex = utf8.startIndex
- let endIndex = utf8.endIndex
- while currentIndex < endIndex {
- let byte = utf8[currentIndex]
- switch byte {
- case UInt8(ascii: "%"):
- guard let (nextIndex, nextNextIndex) = utf8.nextTwoIndices(after: currentIndex),
- let nextHex = fromHex(utf8[nextIndex]),
- let nextNextHex = fromHex(utf8[nextNextIndex])
- else {
- // If we can't decode the message, aborting and returning the encoded message is fine
- // according to the spec.
- return message
- }
- chars.append((nextHex << 4) | nextNextHex)
- currentIndex = nextNextIndex
- default:
- chars.append(byte)
- }
- currentIndex = utf8.index(after: currentIndex)
- }
- return String(decoding: chars, as: Unicode.UTF8.self)
- }
- /// Returns the expected length of the decoded `UTF8View`.
- private static func percentDecodedLength(for view: String.UTF8View) -> Int {
- var encoded = 0
- for byte in view {
- switch byte {
- case UInt8(ascii: "%"):
- // This can't overflow since it can't be larger than view.count.
- encoded &+= 1
- default:
- ()
- }
- }
- let notEncoded = view.count - (encoded * 3)
- guard notEncoded >= 0 else {
- // We've received gibberish: more '%' than expected. gRPC allows for the status message to
- // be left encoded should it be incorrectly encoded. We'll do exactly that by returning
- // the number of bytes in the view which will causes us to take the fast-path exit.
- return view.count
- }
- return notEncoded + encoded
- }
- private static func fromHex(_ byte: UInt8) -> UInt8? {
- switch byte {
- case UInt8(ascii: "0") ... UInt8(ascii: "9"):
- return byte &- UInt8(ascii: "0")
- case UInt8(ascii: "A") ... UInt8(ascii: "Z"):
- return byte &- (UInt8(ascii: "A") &- 10)
- case UInt8(ascii: "a") ... UInt8(ascii: "z"):
- return byte &- (UInt8(ascii: "a") &- 10)
- default:
- return nil
- }
- }
- }
- extension String.UTF8View {
- /// Return the next two valid indices after the given index. The indices are considered valid if
- /// they less than `endIndex`.
- fileprivate func nextTwoIndices(after index: Index) -> (Index, Index)? {
- let secondIndex = self.index(index, offsetBy: 2)
- guard secondIndex < self.endIndex else {
- return nil
- }
- return (self.index(after: index), secondIndex)
- }
- }
|