/src/hothd/payload_update.cpp
Line | Count | Source |
1 | | // Copyright 2024 Google LLC |
2 | | // |
3 | | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | #include "payload_update.hpp" |
16 | | |
17 | | #include "google3/host_commands.h" |
18 | | |
19 | | #include "message_util.hpp" |
20 | | #include "sys.hpp" |
21 | | |
22 | | #include <fcntl.h> |
23 | | #include <unistd.h> |
24 | | |
25 | | #include <boost/endian/conversion.hpp> |
26 | | #include <stdplus/print.hpp> |
27 | | #include <stdplus/raw.hpp> |
28 | | #include <xyz/openbmc_project/Control/Hoth/error.hpp> |
29 | | |
30 | | #include <array> |
31 | | #include <cstdint> |
32 | | #include <format> |
33 | | #include <memory> |
34 | | #include <span> |
35 | | #include <string> |
36 | | #include <vector> |
37 | | |
38 | | namespace google |
39 | | { |
40 | | namespace hoth |
41 | | { |
42 | | namespace internal |
43 | | { |
44 | | |
45 | | enum hoth_status : uint16_t |
46 | | { |
47 | | /* |
48 | | There is no update payload pending confirmation |
49 | | */ |
50 | | HOTH_PAYLOAD_UPDATE_CONFIRM_NO_PENDING_PAYLOAD = 0xD004, |
51 | | }; |
52 | | |
53 | | using sdbusplus::error::xyz::openbmc_project::control::hoth::ResponseFailure; |
54 | | |
55 | | void PayloadUpdateImpl::initiate() const |
56 | 2.38k | { |
57 | 2.38k | sendCommand(PAYLOAD_UPDATE_INITIATE); |
58 | 2.38k | } |
59 | | |
60 | | void PayloadUpdateImpl::erase(const uint32_t offset, const uint32_t size) const |
61 | 734 | { |
62 | 734 | sendCommand(PAYLOAD_UPDATE_ERASE, offset, size); |
63 | 734 | } |
64 | | |
65 | | bool PayloadUpdateImpl::findDescriptor(const std::string& path, |
66 | | uint32_t* desc_offset) const |
67 | 2.92k | { |
68 | 2.92k | Fd fd(sys->open(path.c_str(), O_RDONLY), sys); |
69 | | |
70 | 2.92k | if (*fd < 0) |
71 | 0 | { |
72 | 0 | (void)fd.release(); |
73 | 0 | throw errnoException(std::format("Failed to open file {}", path)); |
74 | 0 | } |
75 | | |
76 | 2.92k | auto seekRet = sys->lseek(*fd, 0, SEEK_END); |
77 | 2.92k | size_t fsize = seekRet; |
78 | | |
79 | 2.92k | if (seekRet < 0) |
80 | 0 | { |
81 | 0 | throw errnoException(std::format("Error seeking on file", path)); |
82 | 0 | } |
83 | | |
84 | 2.92k | uint64_t read_data = 0; |
85 | 2.92k | bool desc_found = false; |
86 | | |
87 | | // Find the nearest aligned address to the end of the file. We can't |
88 | | // assume that the file-size is aligned. |
89 | 2.92k | ssize_t offset = static_cast<ssize_t>( |
90 | 2.92k | (fsize / kImageDescriptorAlignment) * kImageDescriptorAlignment); |
91 | 2.92k | if (offset + sizeof(kDescriptorMagic) > fsize) |
92 | 461 | { |
93 | 461 | offset -= kImageDescriptorAlignment; |
94 | 461 | } |
95 | | |
96 | 4.09k | for (; offset >= 0; offset -= kImageDescriptorAlignment) |
97 | 3.17k | { |
98 | 3.17k | auto seek = sys->lseek(*fd, offset, SEEK_SET); |
99 | 3.17k | if (seek < 0) |
100 | 0 | { |
101 | 0 | throw errnoException(std::format("Error seeking on file", path)); |
102 | 0 | } |
103 | | |
104 | 3.17k | auto actualReadSize = sys->read(*fd, &read_data, sizeof(read_data)); |
105 | | |
106 | 3.17k | if (actualReadSize < 0) |
107 | 0 | { |
108 | 0 | throw errnoException( |
109 | 0 | std::format("Failed to read from file {}", path)); |
110 | 0 | } |
111 | | |
112 | 3.17k | if (read_data == kDescriptorMagic) |
113 | 2.01k | { |
114 | 2.01k | if (desc_offset) |
115 | 2.01k | { |
116 | 2.01k | *desc_offset = offset; |
117 | 2.01k | desc_found = true; |
118 | 2.01k | break; |
119 | 2.01k | } |
120 | 2.01k | } |
121 | 3.17k | } |
122 | | |
123 | 2.92k | return desc_found; |
124 | 2.92k | } |
125 | | |
126 | | void PayloadUpdateImpl::eraseAndSendStaticWPRegions(const std::string& path, |
127 | | uint32_t desc_offset) const |
128 | 2.01k | { |
129 | 2.01k | Fd fd(sys->open(path.c_str(), O_RDONLY), sys); |
130 | | |
131 | 2.01k | if (*fd < 0) |
132 | 0 | { |
133 | 0 | (void)fd.release(); |
134 | 0 | throw errnoException(std::format("Failed to open file {}", path)); |
135 | 0 | } |
136 | | |
137 | 2.01k | struct image_descriptor descriptor; |
138 | | |
139 | 2.01k | if (sys->lseek(*fd, desc_offset, SEEK_SET) < 0) |
140 | 0 | { |
141 | 0 | throw errnoException(std::format("Error seeking on file", path)); |
142 | 0 | } |
143 | 2.01k | auto actualReadSize = sys->read(*fd, &descriptor, sizeof(descriptor)); |
144 | | |
145 | 2.01k | if (actualReadSize < 0) |
146 | 0 | { |
147 | 0 | throw errnoException(std::format("Failed to read from file {}", path)); |
148 | 0 | } |
149 | | |
150 | 2.01k | if (descriptor.descriptor_magic != DESCRIPTOR_MAGIC || |
151 | 2.01k | descriptor.descriptor_offset != desc_offset || |
152 | 1.86k | descriptor.region_count == 0) |
153 | 266 | { |
154 | 266 | throw errnoException( |
155 | 266 | std::format("Invalid descriptor at offset {}", path)); |
156 | 266 | } |
157 | | |
158 | 1.74k | std::vector<struct image_region> image_regions(descriptor.region_count); |
159 | | |
160 | 1.74k | actualReadSize = |
161 | 1.74k | sys->read(*fd, image_regions.data(), |
162 | 1.74k | sizeof(struct image_region) * descriptor.region_count); |
163 | 1.74k | if (actualReadSize < 0) |
164 | 0 | { |
165 | 0 | throw errnoException(std::format("Failed to read from file {}", path)); |
166 | 0 | } |
167 | | |
168 | | // Erase all the static and Write Protected regions of the payload |
169 | 134k | for (uint32_t i = 0; i < descriptor.region_count; i++) |
170 | 132k | { |
171 | 132k | if ((image_regions[i].region_attributes & IMAGE_REGION_STATIC) || |
172 | 130k | (image_regions[i].region_attributes & IMAGE_REGION_WRITE_PROTECTED)) |
173 | 3.10k | { |
174 | 3.10k | if (image_regions[i].region_size == 0 || |
175 | 3.05k | (image_regions[i].region_size % kSectorSizeBytes) != 0) |
176 | 501 | { |
177 | 501 | throw errnoException(std::format("invalid staging area size")); |
178 | 501 | } |
179 | 2.59k | uint32_t regionOffset = image_regions[i].region_offset; |
180 | 2.59k | uint32_t toEraseSize = image_regions[i].region_size; |
181 | 25.3M | while (toEraseSize >= kEraseChunkSizeBytes) |
182 | 25.3M | { |
183 | 25.3M | sendCommand(PAYLOAD_UPDATE_ERASE, regionOffset, |
184 | 25.3M | kEraseChunkSizeBytes); |
185 | 25.3M | regionOffset += kEraseChunkSizeBytes; |
186 | 25.3M | toEraseSize -= kEraseChunkSizeBytes; |
187 | 25.3M | } |
188 | 2.59k | if (toEraseSize > 0) |
189 | 1.84k | { |
190 | 1.84k | sendCommand(PAYLOAD_UPDATE_ERASE, regionOffset, toEraseSize); |
191 | 1.84k | } |
192 | 2.59k | } |
193 | 132k | } |
194 | | |
195 | | // Read and send static and WP sections of the payload |
196 | 28.0k | for (uint32_t i = 0; i < descriptor.region_count; i++) |
197 | 27.8k | { |
198 | 27.8k | if ((image_regions[i].region_attributes & IMAGE_REGION_STATIC) || |
199 | 26.4k | (image_regions[i].region_attributes & IMAGE_REGION_WRITE_PROTECTED)) |
200 | 1.75k | { |
201 | 1.75k | uint32_t offset = image_regions[i].region_offset; |
202 | 1.75k | auto regionSize = image_regions[i].region_size; |
203 | 1.75k | size_t readSize = 0; |
204 | 1.75k | std::vector<uint8_t> readBuffer(max_packet_size); |
205 | | |
206 | 1.75k | if (sys->lseek(*fd, offset, SEEK_SET) != offset) |
207 | 536 | { |
208 | 536 | throw errnoException( |
209 | 536 | std::format("Failed to read file size {}", path)); |
210 | 536 | } |
211 | 1.21k | do |
212 | 11.2k | { |
213 | 11.2k | if (regionSize <= max_packet_size) |
214 | 869 | { |
215 | 869 | readSize = regionSize; |
216 | 869 | } |
217 | 10.3k | else |
218 | 10.3k | { |
219 | 10.3k | readSize = max_packet_size; |
220 | 10.3k | } |
221 | | |
222 | 11.2k | if (!sys->read(*fd, readBuffer.data(), readSize)) |
223 | 422 | { |
224 | 422 | throw errnoException( |
225 | 422 | std::format("Failed to read from file {}", path)); |
226 | 422 | } |
227 | | |
228 | | // Decided not to use initializer list to create the span here |
229 | | // as actualReadSize is ssize_t and we'd need to static_cast |
230 | 10.8k | trimAndSend(std::span<uint8_t>(readBuffer.data(), readSize), |
231 | 10.8k | offset); |
232 | 10.8k | offset += readSize; |
233 | 10.8k | regionSize -= readSize; |
234 | | |
235 | 10.8k | } while (regionSize); // actualReadSize will be 0 when EOF is |
236 | | // reached, exit loop |
237 | 1.21k | } |
238 | 27.8k | } |
239 | 1.24k | } |
240 | | |
241 | | void PayloadUpdateImpl::eraseAndSendStaticWP(const std::string& path) const |
242 | 2.92k | { |
243 | 2.92k | uint32_t desc_offset = 0; |
244 | | |
245 | 2.92k | if (!findDescriptor(path, &desc_offset)) |
246 | 912 | { |
247 | 912 | throw errnoException(std::format("No valid descriptor found")); |
248 | 912 | } |
249 | | |
250 | 2.01k | eraseAndSendStaticWPRegions(path, desc_offset); |
251 | 2.01k | } |
252 | | |
253 | | void PayloadUpdateImpl::read(uint32_t offset, std::span<uint8_t> data) const |
254 | 1.80k | { |
255 | 1.80k | std::vector<uint8_t> buf; |
256 | 1.80k | auto rsp = sendCommand(buf, PAYLOAD_UPDATE_READ, offset, data.size()); |
257 | 1.80k | if (rsp.size() != data.size()) |
258 | 1.52k | { |
259 | 1.52k | stdplus::print( |
260 | 1.52k | stderr, |
261 | 1.52k | "Payload command received a short response from Hoth `{} < {}`\n", |
262 | 1.52k | rsp.size(), data.size()); |
263 | 1.52k | throw ResponseFailure(); |
264 | 1.52k | } |
265 | 287 | memcpy(data.data(), rsp.data(), data.size()); |
266 | 287 | } |
267 | | |
268 | | void PayloadUpdateImpl::verify() const |
269 | 444 | { |
270 | 444 | payload_update_packet request = {}; |
271 | 444 | request.type = PAYLOAD_UPDATE_VERIFY_DESCRIPTOR; |
272 | 444 | auto buf = hostCmd->sendCommand( |
273 | 444 | EC_CMD_BOARD_SPECIFIC_BASE + EC_PRV_CMD_HOTH_PAYLOAD_UPDATE, 0, |
274 | 444 | &request, sizeof(request)); |
275 | 444 | std::span<uint8_t> rsp = buf; |
276 | 444 | auto& hdr = stdplus::raw::extractRef<RspHeader>(rsp); |
277 | | |
278 | | // In case PAYLOAD_UPDATE_VERIFY is not supported (EC_RES_ERROR), |
279 | | // we fall back to PAYLOAD_UPDATE_VERIFY instead. |
280 | 444 | if (hdr.result == EC_RES_INVALID_PARAM || hdr.result == EC_RES_ERROR) |
281 | 0 | { |
282 | 0 | sendCommand(PAYLOAD_UPDATE_VERIFY); |
283 | 0 | return; |
284 | 0 | } |
285 | 444 | if (hdr.result != EC_RES_SUCCESS) |
286 | 0 | { |
287 | 0 | stdplus::print(stderr, "VERIFY_DESCRIPTOR error: {:#x}\n", |
288 | 0 | static_cast<uint16_t>(hdr.result)); |
289 | 0 | throw ResponseFailure(); |
290 | 0 | } |
291 | 444 | } |
292 | | |
293 | | payload_update_status PayloadUpdateImpl::getStatus() const |
294 | 572 | { |
295 | 572 | std::vector<uint8_t> buf; |
296 | 572 | std::span<const uint8_t> output = |
297 | 572 | sendCommand(buf, PAYLOAD_UPDATE_GET_STATUS); |
298 | | |
299 | 572 | return stdplus::raw::copyFrom<payload_update_status>(output); |
300 | 572 | } |
301 | | |
302 | | void PayloadUpdateImpl::activate(Side side, Persistence persistence) const |
303 | 540 | { |
304 | 540 | ActivateRequest request; |
305 | 540 | request.header.offset = 0; |
306 | 540 | request.header.len = sizeof(request.activate); |
307 | 540 | request.header.type = PAYLOAD_UPDATE_ACTIVATE; |
308 | 540 | request.activate.half = static_cast<uint8_t>(side); |
309 | 540 | request.activate.make_persistent = static_cast<uint8_t>(persistence); |
310 | | |
311 | 540 | sendCommand( |
312 | 540 | {reinterpret_cast<const uint8_t*>(&request), sizeof(ActivateRequest)}); |
313 | 540 | } |
314 | | |
315 | | std::optional<payload_update_confirm_response> PayloadUpdateImpl::confirm( |
316 | | enum payload_update_confirm_option option, uint32_t timeout, |
317 | | uint64_t confirmation_cookie) const |
318 | 687 | { |
319 | 687 | ConfirmRequest request; |
320 | 687 | request.header.offset = 0; |
321 | 687 | request.header.len = sizeof(request.confirm); |
322 | 687 | request.header.type = PAYLOAD_UPDATE_CONFIRM; |
323 | 687 | request.confirm.option = option; |
324 | 687 | request.confirm.timeout_value = boost::endian::native_to_little(timeout); |
325 | 687 | request.confirm.confirmation_cookie = |
326 | 687 | boost::endian::native_to_little(confirmation_cookie); |
327 | | |
328 | 687 | std::vector<uint8_t> buf = hostCmd->sendCommand( |
329 | 687 | EC_CMD_BOARD_SPECIFIC_BASE + EC_PRV_CMD_HOTH_PAYLOAD_UPDATE, 0, |
330 | 687 | &request, sizeof(request)); |
331 | | // Extract the response header out when returning the span |
332 | 687 | std::span<const uint8_t> output = buf; |
333 | 687 | auto rsp = stdplus::raw::extract<RspHeader>(output); |
334 | 687 | if (rsp.result == HOTH_PAYLOAD_UPDATE_CONFIRM_NO_PENDING_PAYLOAD) |
335 | 0 | { |
336 | 0 | return std::nullopt; |
337 | 0 | } |
338 | | |
339 | 687 | if (rsp.result != EC_RES_SUCCESS) |
340 | 0 | { |
341 | 0 | stdplus::print( |
342 | 0 | stderr, |
343 | 0 | "Payload confirm command received a bad response from Hoth {:#x}\n", |
344 | 0 | static_cast<uint16_t>(rsp.result)); |
345 | 0 | throw ResponseFailure(); |
346 | 0 | } |
347 | | |
348 | 687 | return stdplus::raw::copyFrom<payload_update_confirm_response>(output); |
349 | 687 | } |
350 | | |
351 | | namespace |
352 | | { |
353 | | // Helper functions for PayloadUpdateImpl::send |
354 | | size_t SizeToSkip(std::span<const uint8_t> data) |
355 | 17.6k | { |
356 | 17.6k | size_t i; |
357 | 285k | for (i = 0; i < data.size(); i++) |
358 | 285k | { |
359 | 285k | if (data[i] != 0xff) |
360 | 17.0k | { |
361 | 17.0k | break; |
362 | 17.0k | } |
363 | 285k | } |
364 | 17.6k | return i; |
365 | 17.6k | } |
366 | | |
367 | | size_t SizeToSend(std::span<const uint8_t> data) |
368 | 17.0k | { |
369 | 17.0k | size_t i; |
370 | 110k | for (i = data.size(); i > 0; i--) |
371 | 110k | { |
372 | 110k | if (data[i - 1] != 0xff) |
373 | 17.0k | { |
374 | 17.0k | break; |
375 | 17.0k | } |
376 | 110k | } |
377 | 17.0k | return i; |
378 | 17.0k | } |
379 | | } // namespace |
380 | | |
381 | | void PayloadUpdateImpl::send(const std::string& path) const |
382 | 900 | { |
383 | 900 | Fd fd(sys->open(path.c_str(), O_RDONLY), sys); |
384 | | |
385 | 900 | if (*fd < 0) |
386 | 0 | { |
387 | 0 | (void)fd.release(); |
388 | 0 | throw errnoException(std::format("Failed to open file {}", path)); |
389 | 0 | } |
390 | | |
391 | 900 | std::array<uint8_t, max_packet_size> readBuffer; |
392 | 900 | uint32_t offset = 0; |
393 | 900 | ssize_t actualReadSize = 0; |
394 | | // Read and send payload until we reach EOF of the given file |
395 | 900 | do |
396 | 7.72k | { |
397 | 7.72k | actualReadSize = sys->read(*fd, readBuffer.data(), max_packet_size); |
398 | | |
399 | 7.72k | if (actualReadSize < 0) |
400 | 0 | { |
401 | 0 | throw errnoException( |
402 | 0 | std::format("Failed to read from file {}", path)); |
403 | 0 | } |
404 | 7.72k | if (actualReadSize > 0) |
405 | 6.82k | { |
406 | | // Decided not to use initializer list to create the span here as |
407 | | // actualReadSize is ssize_t and we'd need to static_cast |
408 | 6.82k | trimAndSend(std::span<uint8_t>(readBuffer.data(), actualReadSize), |
409 | 6.82k | offset); |
410 | 6.82k | offset += actualReadSize; |
411 | 6.82k | } |
412 | | // actualReadSize will be 0 when EOF is reached, exit loop |
413 | 7.72k | } while (actualReadSize); |
414 | 900 | } |
415 | | |
416 | | /* Parse and send the given data after trimming 0xff from the front and back |
417 | | * - This works because PayloadUpdateImpl::Initiate erased the |
418 | | * EEPROM to 0xff. |
419 | | * - This is also more efficient than sending everything, as our |
420 | | * image binaries tend to have large sections of 0xff and we can |
421 | | * avoid sending those sections. |
422 | | */ |
423 | | void PayloadUpdateImpl::trimAndSend(std::span<const uint8_t> data, |
424 | | uint32_t globalOffset) const |
425 | 17.6k | { |
426 | | // Allocate the payload buffer at "buffer". |
427 | | // "request" will point to the payload_update_packet section of the buffer |
428 | | // "request_payload" will point to the beginning of the payload |
429 | | // (after the payload_update_packet header). |
430 | 17.6k | std::array<uint8_t, sizeof(payload_update_packet) + max_packet_size> buffer; |
431 | 17.6k | payload_update_packet* request = |
432 | 17.6k | reinterpret_cast<payload_update_packet*>(buffer.data()); |
433 | 17.6k | uint8_t* request_payload = &buffer[sizeof(payload_update_packet)]; |
434 | | |
435 | 17.6k | const size_t dataSize = data.size(); |
436 | 17.6k | if (dataSize > max_packet_size) |
437 | 0 | { |
438 | 0 | throw errnoException( |
439 | 0 | "data to send cannot be bigger than max_packet_size"); |
440 | 0 | } |
441 | 17.6k | size_t localOffset = SizeToSkip(data); |
442 | 17.6k | if (localOffset >= dataSize) |
443 | 532 | { |
444 | | // No need to send any payload |
445 | 532 | return; |
446 | 532 | } |
447 | 17.0k | size_t max_size_to_send = std::min(dataSize - localOffset, max_packet_size); |
448 | 17.0k | size_t size_to_send = |
449 | 17.0k | SizeToSend(data.subspan(localOffset, max_size_to_send)); |
450 | | |
451 | | // Construct and send payload |
452 | 17.0k | request->offset = globalOffset + localOffset; |
453 | 17.0k | request->len = size_to_send; |
454 | 17.0k | request->type = PAYLOAD_UPDATE_CONTINUE; |
455 | 17.0k | memcpy(request_payload, &data[localOffset], size_to_send); |
456 | 17.0k | sendCommand({buffer.data(), sizeof(*request) + size_to_send}); |
457 | 17.0k | } |
458 | | |
459 | | void PayloadUpdateImpl::sendCommand(uint8_t command, uint32_t offset, |
460 | | uint32_t len) const |
461 | 25.3M | { |
462 | 25.3M | std::vector<uint8_t> buf; |
463 | 25.3M | (void)sendCommand(buf, command, offset, len); |
464 | 25.3M | } |
465 | | |
466 | | void PayloadUpdateImpl::sendCommand(std::span<const uint8_t> request) const |
467 | 17.6k | { |
468 | 17.6k | std::vector<uint8_t> buf; |
469 | 17.6k | (void)sendCommand(buf, request); |
470 | 17.6k | } |
471 | | |
472 | | [[nodiscard]] std::span<const uint8_t> PayloadUpdateImpl::sendCommand( |
473 | | std::vector<uint8_t>& buf, uint8_t command, uint32_t offset, |
474 | | uint32_t len) const |
475 | 25.3M | { |
476 | | // For most basic commands, request offset and length are zero |
477 | 25.3M | payload_update_packet request; |
478 | 25.3M | request.offset = offset; |
479 | 25.3M | request.len = len; |
480 | 25.3M | request.type = command; |
481 | 25.3M | return sendCommand(buf, {reinterpret_cast<const uint8_t*>(&request), |
482 | 25.3M | sizeof(payload_update_packet)}); |
483 | 25.3M | } |
484 | | |
485 | | [[nodiscard]] std::span<const uint8_t> PayloadUpdateImpl::sendCommand( |
486 | | std::vector<uint8_t>& buf, std::span<const uint8_t> request) const |
487 | 25.3M | { |
488 | 25.3M | buf = hostCmd->sendCommand( |
489 | 25.3M | EC_CMD_BOARD_SPECIFIC_BASE + EC_PRV_CMD_HOTH_PAYLOAD_UPDATE, 0, |
490 | 25.3M | request.data(), request.size()); |
491 | | // Extract the response header out when returning the span |
492 | 25.3M | std::span<const uint8_t> output = buf; |
493 | 25.3M | auto rsp = stdplus::raw::extract<RspHeader>(output); |
494 | 25.3M | if (rsp.result != EC_RES_SUCCESS) |
495 | 0 | { |
496 | 0 | stdplus::print( |
497 | 0 | stderr, "Payload command received a bad response from Hoth {:#x}\n", |
498 | 0 | static_cast<uint16_t>(rsp.result)); |
499 | 0 | throw ResponseFailure(); |
500 | 0 | } |
501 | | |
502 | 25.3M | return output; |
503 | 25.3M | } |
504 | | |
505 | | } // namespace internal |
506 | | |
507 | | } // namespace hoth |
508 | | |
509 | | } // namespace google |