1 //
2 // Copyright 2022 The Abseil Authors.
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 // https://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 // Tests for stripping of literal strings.
17 // ---------------------------------------
18 //
19 // When a `LOG` statement can be trivially proved at compile time to never fire,
20 // e.g. due to `ABSL_MIN_LOG_LEVEL`, `NDEBUG`, or some explicit condition, data
21 // streamed in can be dropped from the compiled program completely if they are
22 // not used elsewhere. This most commonly affects string literals, which users
23 // often want to strip to reduce binary size and/or redact information about
24 // their program's internals (e.g. in a release build).
25 //
26 // These tests log strings and then validate whether they appear in the compiled
27 // binary. This is done by opening the file corresponding to the running test
28 // and running a simple string search on its contents. The strings to be logged
29 // and searched for must be unique, and we must take care not to emit them into
30 // the binary in any other place, e.g. when searching for them. The latter is
31 // accomplished by computing them using base64; the source string appears in the
32 // binary but the target string is computed at runtime.
33
34 #include <stdio.h>
35
36 #if defined(__MACH__)
37 #include <mach-o/dyld.h>
38 #elif defined(_WIN32)
39 #include <Windows.h>
40 #include <tchar.h>
41 #endif
42
43 #include <algorithm>
44 #include <functional>
45 #include <memory>
46 #include <ostream>
47 #include <string>
48
49 #include "gmock/gmock.h"
50 #include "gtest/gtest.h"
51 #include "absl/base/internal/strerror.h"
52 #include "absl/base/log_severity.h"
53 #include "absl/flags/internal/program_name.h"
54 #include "absl/log/check.h"
55 #include "absl/log/internal/test_helpers.h"
56 #include "absl/log/log.h"
57 #include "absl/status/status.h"
58 #include "absl/strings/escaping.h"
59 #include "absl/strings/str_format.h"
60 #include "absl/strings/string_view.h"
61
62 // Set a flag that controls whether we actually execute fatal statements, but
63 // prevent the compiler from optimizing it out.
64 static volatile bool kReallyDie = false;
65
66 namespace {
67 using ::testing::_;
68 using ::testing::Eq;
69 using ::testing::NotNull;
70
71 using absl::log_internal::kAbslMinLogLevel;
72
Base64UnescapeOrDie(absl::string_view data)73 std::string Base64UnescapeOrDie(absl::string_view data) {
74 std::string decoded;
75 CHECK(absl::Base64Unescape(data, &decoded));
76 return decoded;
77 }
78
79 // -----------------------------------------------------------------------------
80 // A Googletest matcher which searches the running binary for a given string
81 // -----------------------------------------------------------------------------
82
83 // This matcher is used to validate that literal strings streamed into
84 // `LOG` statements that ought to be compiled out (e.g. `LOG_IF(INFO, false)`)
85 // do not appear in the binary.
86 //
87 // Note that passing the string to be sought directly to `FileHasSubstr()` all
88 // but forces its inclusion in the binary regardless of the logging library's
89 // behavior. For example:
90 //
91 // LOG_IF(INFO, false) << "you're the man now dog";
92 // // This will always pass:
93 // // EXPECT_THAT(fp, FileHasSubstr("you're the man now dog"));
94 // // So use this instead:
95 // EXPECT_THAT(fp, FileHasSubstr(
96 // Base64UnescapeOrDie("eW91J3JlIHRoZSBtYW4gbm93IGRvZw==")));
97
98 class FileHasSubstrMatcher final : public ::testing::MatcherInterface<FILE*> {
99 public:
FileHasSubstrMatcher(absl::string_view needle)100 explicit FileHasSubstrMatcher(absl::string_view needle) : needle_(needle) {}
101
MatchAndExplain(FILE * fp,::testing::MatchResultListener * listener) const102 bool MatchAndExplain(
103 FILE* fp, ::testing::MatchResultListener* listener) const override {
104 std::string buf(
105 std::max<std::string::size_type>(needle_.size() * 2, 163840000), '\0');
106 size_t buf_start_offset = 0; // The file offset of the byte at `buf[0]`.
107 size_t buf_data_size = 0; // The number of bytes of `buf` which contain
108 // data.
109
110 ::fseek(fp, 0, SEEK_SET);
111 while (true) {
112 // Fill the buffer to capacity or EOF:
113 while (buf_data_size < buf.size()) {
114 const size_t ret = fread(&buf[buf_data_size], sizeof(char),
115 buf.size() - buf_data_size, fp);
116 if (ret == 0) break;
117 buf_data_size += ret;
118 }
119 if (ferror(fp)) {
120 *listener << "error reading file";
121 return false;
122 }
123 const absl::string_view haystack(&buf[0], buf_data_size);
124 const auto off = haystack.find(needle_);
125 if (off != haystack.npos) {
126 *listener << "string found at offset " << buf_start_offset + off;
127 return true;
128 }
129 if (feof(fp)) {
130 *listener << "string not found";
131 return false;
132 }
133 // Copy the end of `buf` to the beginning so we catch matches that span
134 // buffer boundaries. `buf` and `buf_data_size` are always large enough
135 // that these ranges don't overlap.
136 memcpy(&buf[0], &buf[buf_data_size - needle_.size()], needle_.size());
137 buf_start_offset += buf_data_size - needle_.size();
138 buf_data_size = needle_.size();
139 }
140 }
DescribeTo(std::ostream * os) const141 void DescribeTo(std::ostream* os) const override {
142 *os << "contains the string \"" << needle_ << "\" (base64(\""
143 << Base64UnescapeOrDie(needle_) << "\"))";
144 }
145
DescribeNegationTo(std::ostream * os) const146 void DescribeNegationTo(std::ostream* os) const override {
147 *os << "does not ";
148 DescribeTo(os);
149 }
150
151 private:
152 std::string needle_;
153 };
154
155 class StrippingTest : public ::testing::Test {
156 protected:
SetUp()157 void SetUp() override {
158 #ifndef NDEBUG
159 // Non-optimized builds don't necessarily eliminate dead code at all, so we
160 // don't attempt to validate stripping against such builds.
161 GTEST_SKIP() << "StrippingTests skipped since this build is not optimized";
162 #elif defined(__EMSCRIPTEN__)
163 // These tests require a way to examine the running binary and look for
164 // strings; there's no portable way to do that.
165 GTEST_SKIP()
166 << "StrippingTests skipped since this platform is not optimized";
167 #endif
168 }
169
170 // Opens this program's executable file. Returns `nullptr` and writes to
171 // `stderr` on failure.
OpenTestExecutable()172 std::unique_ptr<FILE, std::function<void(FILE*)>> OpenTestExecutable() {
173 #if defined(__linux__)
174 std::unique_ptr<FILE, std::function<void(FILE*)>> fp(
175 fopen("/proc/self/exe", "rb"), [](FILE* fp) { fclose(fp); });
176 if (!fp) {
177 const std::string err = absl::base_internal::StrError(errno);
178 absl::FPrintF(stderr, "Failed to open /proc/self/exe: %s\n", err);
179 }
180 return fp;
181 #elif defined(__Fuchsia__)
182 // TODO(b/242579714): We need to restore the test coverage on this platform.
183 std::unique_ptr<FILE, std::function<void(FILE*)>> fp(
184 fopen(absl::StrCat("/pkg/bin/",
185 absl::flags_internal::ShortProgramInvocationName())
186 .c_str(),
187 "rb"),
188 [](FILE* fp) { fclose(fp); });
189 if (!fp) {
190 const std::string err = absl::base_internal::StrError(errno);
191 absl::FPrintF(stderr, "Failed to open /pkg/bin/<binary name>: %s\n", err);
192 }
193 return fp;
194 #elif defined(__MACH__)
195 uint32_t size = 0;
196 int ret = _NSGetExecutablePath(nullptr, &size);
197 if (ret != -1) {
198 absl::FPrintF(stderr,
199 "Failed to get executable path: "
200 "_NSGetExecutablePath(nullptr) returned %d\n",
201 ret);
202 return nullptr;
203 }
204 std::string path(size, '\0');
205 ret = _NSGetExecutablePath(&path[0], &size);
206 if (ret != 0) {
207 absl::FPrintF(
208 stderr,
209 "Failed to get executable path: _NSGetExecutablePath(buffer) "
210 "returned %d\n",
211 ret);
212 return nullptr;
213 }
214 std::unique_ptr<FILE, std::function<void(FILE*)>> fp(
215 fopen(path.c_str(), "rb"), [](FILE* fp) { fclose(fp); });
216 if (!fp) {
217 const std::string err = absl::base_internal::StrError(errno);
218 absl::FPrintF(stderr, "Failed to open executable at %s: %s\n", path, err);
219 }
220 return fp;
221 #elif defined(_WIN32)
222 std::basic_string<TCHAR> path(4096, _T('\0'));
223 while (true) {
224 const uint32_t ret = ::GetModuleFileName(nullptr, &path[0],
225 static_cast<DWORD>(path.size()));
226 if (ret == 0) {
227 absl::FPrintF(
228 stderr,
229 "Failed to get executable path: GetModuleFileName(buffer) "
230 "returned 0\n");
231 return nullptr;
232 }
233 if (ret < path.size()) break;
234 path.resize(path.size() * 2, _T('\0'));
235 }
236 std::unique_ptr<FILE, std::function<void(FILE*)>> fp(
237 _tfopen(path.c_str(), _T("rb")), [](FILE* fp) { fclose(fp); });
238 if (!fp) absl::FPrintF(stderr, "Failed to open executable\n");
239 return fp;
240 #else
241 absl::FPrintF(stderr,
242 "OpenTestExecutable() unimplemented on this platform\n");
243 return nullptr;
244 #endif
245 }
246
FileHasSubstr(absl::string_view needle)247 ::testing::Matcher<FILE*> FileHasSubstr(absl::string_view needle) {
248 return MakeMatcher(new FileHasSubstrMatcher(needle));
249 }
250 };
251
252 // This tests whether out methodology for testing stripping works on this
253 // platform by looking for one string that definitely ought to be there and one
254 // that definitely ought not to. If this fails, none of the `StrippingTest`s
255 // are going to produce meaningful results.
TEST_F(StrippingTest,Control)256 TEST_F(StrippingTest, Control) {
257 constexpr char kEncodedPositiveControl[] =
258 "U3RyaXBwaW5nVGVzdC5Qb3NpdGl2ZUNvbnRyb2w=";
259 const std::string encoded_negative_control =
260 absl::Base64Escape("StrippingTest.NegativeControl");
261
262 // Verify this mainly so we can encode other strings and know definitely they
263 // won't encode to `kEncodedPositiveControl`.
264 EXPECT_THAT(Base64UnescapeOrDie("U3RyaXBwaW5nVGVzdC5Qb3NpdGl2ZUNvbnRyb2w="),
265 Eq("StrippingTest.PositiveControl"));
266
267 auto exe = OpenTestExecutable();
268 ASSERT_THAT(exe, NotNull());
269 EXPECT_THAT(exe.get(), FileHasSubstr(kEncodedPositiveControl));
270 EXPECT_THAT(exe.get(), Not(FileHasSubstr(encoded_negative_control)));
271 }
272
TEST_F(StrippingTest,Literal)273 TEST_F(StrippingTest, Literal) {
274 // We need to load a copy of the needle string into memory (so we can search
275 // for it) without leaving it lying around in plaintext in the executable file
276 // as would happen if we used a literal. We might (or might not) leave it
277 // lying around later; that's what the tests are for!
278 const std::string needle = absl::Base64Escape("StrippingTest.Literal");
279 LOG(INFO) << "U3RyaXBwaW5nVGVzdC5MaXRlcmFs";
280 auto exe = OpenTestExecutable();
281 ASSERT_THAT(exe, NotNull());
282 if (absl::LogSeverity::kInfo >= kAbslMinLogLevel) {
283 EXPECT_THAT(exe.get(), FileHasSubstr(needle));
284 } else {
285 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle)));
286 }
287 }
288
TEST_F(StrippingTest,LiteralInExpression)289 TEST_F(StrippingTest, LiteralInExpression) {
290 // We need to load a copy of the needle string into memory (so we can search
291 // for it) without leaving it lying around in plaintext in the executable file
292 // as would happen if we used a literal. We might (or might not) leave it
293 // lying around later; that's what the tests are for!
294 const std::string needle =
295 absl::Base64Escape("StrippingTest.LiteralInExpression");
296 LOG(INFO) << absl::StrCat("secret: ",
297 "U3RyaXBwaW5nVGVzdC5MaXRlcmFsSW5FeHByZXNzaW9u");
298 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
299 ASSERT_THAT(exe, NotNull());
300 if (absl::LogSeverity::kInfo >= kAbslMinLogLevel) {
301 EXPECT_THAT(exe.get(), FileHasSubstr(needle));
302 } else {
303 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle)));
304 }
305 }
306
TEST_F(StrippingTest,Fatal)307 TEST_F(StrippingTest, Fatal) {
308 // We need to load a copy of the needle string into memory (so we can search
309 // for it) without leaving it lying around in plaintext in the executable file
310 // as would happen if we used a literal. We might (or might not) leave it
311 // lying around later; that's what the tests are for!
312 const std::string needle = absl::Base64Escape("StrippingTest.Fatal");
313 // We don't care if the LOG statement is actually executed, we're just
314 // checking that it's stripped.
315 if (kReallyDie) LOG(FATAL) << "U3RyaXBwaW5nVGVzdC5GYXRhbA==";
316
317 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
318 ASSERT_THAT(exe, NotNull());
319 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) {
320 EXPECT_THAT(exe.get(), FileHasSubstr(needle));
321 } else {
322 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle)));
323 }
324 }
325
TEST_F(StrippingTest,DFatal)326 TEST_F(StrippingTest, DFatal) {
327 // We need to load a copy of the needle string into memory (so we can search
328 // for it) without leaving it lying around in plaintext in the executable file
329 // as would happen if we used a literal. We might (or might not) leave it
330 // lying around later; that's what the tests are for!
331 const std::string needle = absl::Base64Escape("StrippingTest.DFatal");
332 // We don't care if the LOG statement is actually executed, we're just
333 // checking that it's stripped.
334 if (kReallyDie) LOG(DFATAL) << "U3RyaXBwaW5nVGVzdC5ERmF0YWw=";
335
336 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
337 ASSERT_THAT(exe, NotNull());
338 // `DFATAL` can be `ERROR` or `FATAL`, and a compile-time optimizer doesn't
339 // know which, because `absl::kLogDebugFatal` is declared `extern` and defined
340 // in another TU. Link-time optimization might do better. We have six cases:
341 // | `AMLL` is-> | `<=ERROR` | `FATAL` | `>FATAL` |
342 // | ------------------- | --------- | ------- | -------- |
343 // | `DFATAL` is `ERROR` | present | ? | stripped |
344 // | `DFATAL` is `FATAL` | present | present | stripped |
345
346 // These constexpr variables are used to suppress unreachable code warnings
347 // in the if-else statements below.
348
349 // "present" in the table above: `DFATAL` exceeds `ABSL_MIN_LOG_LEVEL`, so
350 // `DFATAL` statements should not be stripped (and they should be logged
351 // when executed, but that's a different testsuite).
352 constexpr bool kExpectPresent = absl::kLogDebugFatal >= kAbslMinLogLevel;
353
354 // "stripped" in the table above: even though the compiler may not know
355 // which value `DFATAL` has, it should be able to strip it since both
356 // possible values ought to be stripped.
357 constexpr bool kExpectStripped = kAbslMinLogLevel > absl::LogSeverity::kFatal;
358
359 if (kExpectPresent) {
360 EXPECT_THAT(exe.get(), FileHasSubstr(needle));
361 } else if (kExpectStripped) {
362 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle)));
363 } else {
364 // "?" in the table above; may or may not be stripped depending on whether
365 // any link-time optimization is done. Either outcome is ok.
366 }
367 }
368
TEST_F(StrippingTest,Level)369 TEST_F(StrippingTest, Level) {
370 const std::string needle = absl::Base64Escape("StrippingTest.Level");
371 volatile auto severity = absl::LogSeverity::kWarning;
372 // Ensure that `severity` is not a compile-time constant to prove that
373 // stripping works regardless:
374 LOG(LEVEL(severity)) << "U3RyaXBwaW5nVGVzdC5MZXZlbA==";
375 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
376 ASSERT_THAT(exe, NotNull());
377 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) {
378 // This can't be stripped at compile-time because it might evaluate to a
379 // level that shouldn't be stripped.
380 EXPECT_THAT(exe.get(), FileHasSubstr(needle));
381 } else {
382 #if (defined(_MSC_VER) && !defined(__clang__)) || defined(__APPLE__)
383 // Dead code elimination misses this case.
384 #else
385 // All levels should be stripped, so it doesn't matter what the severity
386 // winds up being.
387 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle)));
388 #endif
389 }
390 }
391
TEST_F(StrippingTest,Check)392 TEST_F(StrippingTest, Check) {
393 // Here we also need a variable name with enough entropy that it's unlikely to
394 // appear in the binary by chance. `volatile` keeps the tautological
395 // comparison (and the rest of the `CHECK`) from being optimized away.
396 const std::string var_needle = absl::Base64Escape("StrippingTestCheckVar");
397 const std::string msg_needle = absl::Base64Escape("StrippingTest.Check");
398 volatile int U3RyaXBwaW5nVGVzdENoZWNrVmFy = 0xCAFE;
399 // We don't care if the CHECK is actually executed, just that stripping works.
400 // Hiding it behind `kReallyDie` works around some overly aggressive
401 // optimizations in older versions of MSVC.
402 if (kReallyDie) {
403 CHECK(U3RyaXBwaW5nVGVzdENoZWNrVmFy != U3RyaXBwaW5nVGVzdENoZWNrVmFy)
404 << "U3RyaXBwaW5nVGVzdC5DaGVjaw==";
405 }
406
407 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
408 ASSERT_THAT(exe, NotNull());
409 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) {
410 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle));
411 EXPECT_THAT(exe.get(), FileHasSubstr(msg_needle));
412 } else {
413 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle)));
414 EXPECT_THAT(exe.get(), Not(FileHasSubstr(msg_needle)));
415 }
416 }
417
TEST_F(StrippingTest,CheckOp)418 TEST_F(StrippingTest, CheckOp) {
419 // See `StrippingTest.Check` for some hairy implementation notes.
420 const std::string var_needle1 =
421 absl::Base64Escape("StrippingTestCheckOpVar1");
422 const std::string var_needle2 =
423 absl::Base64Escape("StrippingTestCheckOpVar2");
424 const std::string msg_needle = absl::Base64Escape("StrippingTest.CheckOp");
425 volatile int U3RyaXBwaW5nVGVzdENoZWNrT3BWYXIx = 0xFEED;
426 volatile int U3RyaXBwaW5nVGVzdENoZWNrT3BWYXIy = 0xCAFE;
427 if (kReallyDie) {
428 CHECK_EQ(U3RyaXBwaW5nVGVzdENoZWNrT3BWYXIx, U3RyaXBwaW5nVGVzdENoZWNrT3BWYXIy)
429 << "U3RyaXBwaW5nVGVzdC5DaGVja09w";
430 }
431
432 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
433 ASSERT_THAT(exe, NotNull());
434
435 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) {
436 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle1));
437 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle2));
438 EXPECT_THAT(exe.get(), FileHasSubstr(msg_needle));
439 } else {
440 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle1)));
441 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle2)));
442 EXPECT_THAT(exe.get(), Not(FileHasSubstr(msg_needle)));
443 }
444 }
445
TEST_F(StrippingTest,CheckStrOp)446 TEST_F(StrippingTest, CheckStrOp) {
447 // See `StrippingTest.Check` for some hairy implementation notes.
448 const std::string var_needle1 =
449 absl::Base64Escape("StrippingTestCheckStrOpVar1");
450 const std::string var_needle2 =
451 absl::Base64Escape("StrippingTestCheckStrOpVar2");
452 const std::string msg_needle = absl::Base64Escape("StrippingTest.CheckStrOp");
453 const char *volatile U3RyaXBwaW5nVGVzdENoZWNrU3RyT3BWYXIx = "FEED";
454 const char *volatile U3RyaXBwaW5nVGVzdENoZWNrU3RyT3BWYXIy = "CAFE";
455 if (kReallyDie) {
456 CHECK_STREQ(U3RyaXBwaW5nVGVzdENoZWNrU3RyT3BWYXIx,
457 U3RyaXBwaW5nVGVzdENoZWNrU3RyT3BWYXIy)
458 << "U3RyaXBwaW5nVGVzdC5DaGVja1N0ck9w";
459 }
460
461 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
462 ASSERT_THAT(exe, NotNull());
463
464 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) {
465 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle1));
466 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle2));
467 EXPECT_THAT(exe.get(), FileHasSubstr(msg_needle));
468 } else {
469 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle1)));
470 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle2)));
471 EXPECT_THAT(exe.get(), Not(FileHasSubstr(msg_needle)));
472 }
473 }
474
TEST_F(StrippingTest,CheckOk)475 TEST_F(StrippingTest, CheckOk) {
476 // See `StrippingTest.Check` for some hairy implementation notes.
477 const std::string var_needle = absl::Base64Escape("StrippingTestCheckOkVar1");
478 const std::string msg_needle = absl::Base64Escape("StrippingTest.CheckOk");
479 volatile bool x = false;
480 auto U3RyaXBwaW5nVGVzdENoZWNrT2tWYXIx = absl::OkStatus();
481 if (x) {
482 U3RyaXBwaW5nVGVzdENoZWNrT2tWYXIx =
483 absl::InvalidArgumentError("Stripping this is not my job!");
484 }
485 if (kReallyDie) {
486 CHECK_OK(U3RyaXBwaW5nVGVzdENoZWNrT2tWYXIx)
487 << "U3RyaXBwaW5nVGVzdC5DaGVja09r";
488 }
489
490 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
491 ASSERT_THAT(exe, NotNull());
492
493 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) {
494 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle));
495 EXPECT_THAT(exe.get(), FileHasSubstr(msg_needle));
496 } else {
497 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle)));
498 EXPECT_THAT(exe.get(), Not(FileHasSubstr(msg_needle)));
499 }
500 }
501
502 } // namespace
503