2
0

GRPCStatusMessageMarshaller.swift 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. /*
  2. * Copyright 2019, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. // swiftformat:disable:next enumNamespaces
  17. public struct GRPCStatusMessageMarshaller {
  18. /// Adds percent encoding to the given message.
  19. ///
  20. /// - Parameter message: Message to percent encode.
  21. /// - Returns: Percent encoded string, or `nil` if it could not be encoded.
  22. public static func marshall(_ message: String) -> String? {
  23. return percentEncode(message)
  24. }
  25. /// Removes percent encoding from the given message.
  26. ///
  27. /// - Parameter message: Message to remove encoding from.
  28. /// - Returns: The string with percent encoding removed, or the input string if the encoding
  29. /// could not be removed.
  30. public static func unmarshall(_ message: String) -> String {
  31. return removePercentEncoding(message)
  32. }
  33. }
  34. extension GRPCStatusMessageMarshaller {
  35. /// Adds percent encoding to the given message.
  36. ///
  37. /// gRPC uses percent encoding as defined in RFC 3986 § 2.1 but with a different set of restricted
  38. /// characters. The allowed characters are all visible printing characters except for (`%`,
  39. /// `0x25`). That is: `0x20`-`0x24`, `0x26`-`0x7E`.
  40. ///
  41. /// - Parameter message: The message to encode.
  42. /// - Returns: Percent encoded string, or `nil` if it could not be encoded.
  43. private static func percentEncode(_ message: String) -> String? {
  44. let utf8 = message.utf8
  45. let encodedLength = self.percentEncodedLength(for: utf8)
  46. // Fast-path: all characters are valid, nothing to encode.
  47. if encodedLength == utf8.count {
  48. return message
  49. }
  50. var bytes: [UInt8] = []
  51. bytes.reserveCapacity(encodedLength)
  52. for char in message.utf8 {
  53. switch char {
  54. // See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses
  55. case 0x20 ... 0x24,
  56. 0x26 ... 0x7E:
  57. bytes.append(char)
  58. default:
  59. bytes.append(UInt8(ascii: "%"))
  60. bytes.append(self.toHex(char >> 4))
  61. bytes.append(self.toHex(char & 0xF))
  62. }
  63. }
  64. return String(bytes: bytes, encoding: .utf8)
  65. }
  66. /// Returns the percent encoded length of the given `UTF8View`.
  67. private static func percentEncodedLength(for view: String.UTF8View) -> Int {
  68. var count = view.count
  69. for byte in view {
  70. switch byte {
  71. case 0x20 ... 0x24,
  72. 0x26 ... 0x7E:
  73. ()
  74. default:
  75. count += 2
  76. }
  77. }
  78. return count
  79. }
  80. /// Encode the given byte as hexadecimal.
  81. ///
  82. /// - Precondition: Only the four least significant bits may be set.
  83. /// - Parameter nibble: The nibble to convert to hexadecimal.
  84. private static func toHex(_ nibble: UInt8) -> UInt8 {
  85. assert(nibble & 0xF == nibble)
  86. switch nibble {
  87. case 0 ... 9:
  88. return nibble &+ UInt8(ascii: "0")
  89. default:
  90. return nibble &+ (UInt8(ascii: "A") &- 10)
  91. }
  92. }
  93. /// Remove gRPC percent encoding from `message`. If any portion of the string could not be decoded
  94. /// then the encoded message will be returned.
  95. ///
  96. /// - Parameter message: The message to remove percent encoding from.
  97. /// - Returns: The decoded message.
  98. private static func removePercentEncoding(_ message: String) -> String {
  99. let utf8 = message.utf8
  100. let decodedLength = self.percentDecodedLength(for: utf8)
  101. // Fast-path: no decoding to do! Note that we may also have detected that the encoding is
  102. // invalid, in which case we will return the encoded message: this is fine.
  103. if decodedLength == utf8.count {
  104. return message
  105. }
  106. var chars: [UInt8] = []
  107. // We can't decode more characters than are already encoded.
  108. chars.reserveCapacity(decodedLength)
  109. var currentIndex = utf8.startIndex
  110. let endIndex = utf8.endIndex
  111. while currentIndex < endIndex {
  112. let byte = utf8[currentIndex]
  113. switch byte {
  114. case UInt8(ascii: "%"):
  115. guard let (nextIndex, nextNextIndex) = utf8.nextTwoIndices(after: currentIndex),
  116. let nextHex = fromHex(utf8[nextIndex]),
  117. let nextNextHex = fromHex(utf8[nextNextIndex])
  118. else {
  119. // If we can't decode the message, aborting and returning the encoded message is fine
  120. // according to the spec.
  121. return message
  122. }
  123. chars.append((nextHex << 4) | nextNextHex)
  124. currentIndex = nextNextIndex
  125. default:
  126. chars.append(byte)
  127. }
  128. currentIndex = utf8.index(after: currentIndex)
  129. }
  130. return String(decoding: chars, as: Unicode.UTF8.self)
  131. }
  132. /// Returns the expected length of the decoded `UTF8View`.
  133. private static func percentDecodedLength(for view: String.UTF8View) -> Int {
  134. var encoded = 0
  135. for byte in view {
  136. switch byte {
  137. case UInt8(ascii: "%"):
  138. // This can't overflow since it can't be larger than view.count.
  139. encoded &+= 1
  140. default:
  141. ()
  142. }
  143. }
  144. let notEncoded = view.count - (encoded * 3)
  145. guard notEncoded >= 0 else {
  146. // We've received gibberish: more '%' than expected. gRPC allows for the status message to
  147. // be left encoded should it be incorrectly encoded. We'll do exactly that by returning
  148. // the number of bytes in the view which will causes us to take the fast-path exit.
  149. return view.count
  150. }
  151. return notEncoded + encoded
  152. }
  153. private static func fromHex(_ byte: UInt8) -> UInt8? {
  154. switch byte {
  155. case UInt8(ascii: "0") ... UInt8(ascii: "9"):
  156. return byte &- UInt8(ascii: "0")
  157. case UInt8(ascii: "A") ... UInt8(ascii: "Z"):
  158. return byte &- (UInt8(ascii: "A") &- 10)
  159. case UInt8(ascii: "a") ... UInt8(ascii: "z"):
  160. return byte &- (UInt8(ascii: "a") &- 10)
  161. default:
  162. return nil
  163. }
  164. }
  165. }
  166. extension String.UTF8View {
  167. /// Return the next two valid indices after the given index. The indices are considered valid if
  168. /// they less than `endIndex`.
  169. fileprivate func nextTwoIndices(after index: Index) -> (Index, Index)? {
  170. let secondIndex = self.index(index, offsetBy: 2)
  171. guard secondIndex < self.endIndex else {
  172. return nil
  173. }
  174. return (self.index(after: index), secondIndex)
  175. }
  176. }