GRPCStatusMessageMarshaller.swift 6.5 KB

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