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