| // Copyright 2018 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "src/developer/debug/zxdb/expr/cast.h" | |
| #include <limits> | |
| #include <gtest/gtest.h> | |
| #include "src/developer/debug/zxdb/common/err.h" | |
| #include "src/developer/debug/zxdb/common/test_with_loop.h" | |
| #include "src/developer/debug/zxdb/expr/eval_test_support.h" | |
| #include "src/developer/debug/zxdb/expr/expr_value.h" | |
| #include "src/developer/debug/zxdb/expr/mock_eval_context.h" | |
| #include "src/developer/debug/zxdb/expr/virtual_inheritance_test_setup.h" | |
| #include "src/developer/debug/zxdb/symbols/base_type.h" | |
| #include "src/developer/debug/zxdb/symbols/collection.h" | |
| #include "src/developer/debug/zxdb/symbols/enumeration.h" | |
| #include "src/developer/debug/zxdb/symbols/inherited_from.h" | |
| #include "src/developer/debug/zxdb/symbols/modified_type.h" | |
| #include "src/developer/debug/zxdb/symbols/type_test_support.h" | |
| namespace zxdb { | |
| namespace { | |
| class Cast : public TestWithLoop { | |
| public: | |
| ErrOrValue SyncCastExprValue(const fxl::RefPtr<EvalContext>& eval_context, CastType cast_type, | |
| const ExprValue& source, const fxl::RefPtr<Type>& dest_type, | |
| const ExprValueSource& dest_source = ExprValueSource()) { | |
| ErrOrValue result(Err("Uncalled")); | |
| bool called = false; | |
| CastExprValue(eval_context, cast_type, source, dest_type, dest_source, | |
| [&result, &called](ErrOrValue v) { | |
| result = std::move(v); | |
| called = true; | |
| }); | |
| loop().RunUntilNoTasks(); | |
| EXPECT_TRUE(called); | |
| return result; | |
| } | |
| // When a cast is supported by CastNumericExprValue, an implicit cast should give the same | |
| // result. This runs both, expecting success, and that the answers match. | |
| // | |
| // To test a case where CastNumericExprValue() fails, call it directly since the more general case | |
| // might return a different answer. | |
| ExprValue SyncCastNumericExprValue(const fxl::RefPtr<EvalContext>& eval_context, | |
| const ExprValue& source, const fxl::RefPtr<Type>& dest_type, | |
| const ExprValueSource& dest_source = ExprValueSource()) { | |
| ErrOrValue full_result = | |
| SyncCastExprValue(eval_context, CastType::kImplicit, source, dest_type, dest_source); | |
| EXPECT_TRUE(full_result.ok()); | |
| if (full_result.has_error()) | |
| return ExprValue(); | |
| ErrOrValue numeric_result = CastNumericExprValue(eval_context, source, dest_type, dest_source); | |
| EXPECT_TRUE(numeric_result.ok()); | |
| if (numeric_result.has_error()) | |
| return ExprValue(); | |
| EXPECT_EQ(numeric_result.value(), full_result.value()); | |
| return numeric_result.value(); | |
| } | |
| }; | |
| } // namespace | |
| // Tests both the implicit numeric and implicit regular cast functions. | |
| TEST_F(Cast, Implicit) { | |
| auto eval_context = fxl::MakeRefCounted<MockEvalContext>(); | |
| auto int32_type = MakeInt32Type(); | |
| auto uint32_type = MakeInt32Type(); | |
| auto int64_type = MakeInt64Type(); | |
| auto uint64_type = MakeInt64Type(); | |
| auto char_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeSignedChar, 1, "char"); | |
| auto bool_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeBoolean, 1, "bool"); | |
| auto float_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeFloat, 4, "float"); | |
| auto double_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeFloat, 8, "double"); | |
| ExprValue int32_one(int32_type, {1, 0, 0, 0}); | |
| ExprValue int32_minus_one(int32_type, {0xff, 0xff, 0xff, 0xff}); | |
| // Simple identity conversion. | |
| ExprValue out = SyncCastNumericExprValue(eval_context, int32_one, int32_type); | |
| EXPECT_EQ(int32_one, out); | |
| // Signed/unsigned conversion, should just copy the bits (end up with 0xffffffff), | |
| out = SyncCastNumericExprValue(eval_context, int32_minus_one, uint32_type); | |
| EXPECT_EQ(uint32_type.get(), out.type()); | |
| EXPECT_EQ(int32_minus_one.data(), out.data()); | |
| // Signed integer promotion. | |
| out = SyncCastNumericExprValue(eval_context, int32_minus_one, uint64_type); | |
| EXPECT_EQ(uint64_type.get(), out.type()); | |
| EXPECT_EQ(std::numeric_limits<uint64_t>::max(), out.GetAs<uint64_t>()); | |
| // Integer truncation | |
| out = SyncCastNumericExprValue(eval_context, int32_minus_one, char_type); | |
| EXPECT_EQ(char_type.get(), out.type()); | |
| EXPECT_EQ(-1, out.GetAs<int8_t>()); | |
| // Zero integer to boolean. | |
| ExprValue int32_zero(int32_type, {0, 0, 0, 0}); | |
| out = SyncCastNumericExprValue(eval_context, int32_zero, bool_type); | |
| EXPECT_EQ(bool_type.get(), out.type()); | |
| EXPECT_EQ(0, out.GetAs<int8_t>()); | |
| // Nonzero integer to boolean. | |
| out = SyncCastNumericExprValue(eval_context, int32_minus_one, bool_type); | |
| EXPECT_EQ(bool_type.get(), out.type()); | |
| EXPECT_EQ(1, out.GetAs<int8_t>()); | |
| // Zero floating point to boolean. | |
| ExprValue float_minus_zero(float_type, {0, 0, 0, 0x80}); | |
| out = SyncCastNumericExprValue(eval_context, float_minus_zero, bool_type); | |
| EXPECT_EQ(0, out.GetAs<int8_t>()); | |
| // Nonzero floating point to boolean. | |
| ExprValue float_one_third(float_type, {0xab, 0xaa, 0xaa, 0x3e}); | |
| out = SyncCastNumericExprValue(eval_context, float_one_third, bool_type); | |
| EXPECT_EQ(1, out.GetAs<int8_t>()); | |
| // Float to signed. | |
| ExprValue float_minus_two(float_type, {0x00, 0x00, 0x00, 0xc0}); | |
| out = SyncCastNumericExprValue(eval_context, float_minus_two, int32_type); | |
| EXPECT_EQ(-2, out.GetAs<int32_t>()); | |
| // Float to unsigned. | |
| out = SyncCastNumericExprValue(eval_context, float_minus_two, uint64_type); | |
| EXPECT_EQ(static_cast<uint64_t>(-2), out.GetAs<uint64_t>()); | |
| // Floating point promotion. | |
| out = SyncCastNumericExprValue(eval_context, float_minus_two, double_type); | |
| EXPECT_EQ(-2.0, out.GetAs<double>()); | |
| ExprValue double_minus_two = out; | |
| // Floating point truncation. | |
| out = SyncCastNumericExprValue(eval_context, double_minus_two, float_type); | |
| EXPECT_EQ(-2.0f, out.GetAs<float>()); | |
| // Pointer to integer with truncation. | |
| auto ptr_to_double_type = fxl::MakeRefCounted<ModifiedType>(DwarfTag::kPointerType, double_type); | |
| ExprValue ptr_value(ptr_to_double_type, {0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12}); | |
| out = SyncCastNumericExprValue(eval_context, ptr_value, uint32_type); | |
| EXPECT_EQ(0x9abcdef0, out.GetAs<uint32_t>()); | |
| ExprValue big_int_value = out; | |
| // Integer to pointer with expansion. | |
| out = SyncCastNumericExprValue(eval_context, int32_minus_one, ptr_to_double_type); | |
| EXPECT_EQ(-1, out.GetAs<int64_t>()); | |
| // Test explicit boolean casts for the above values. | |
| EXPECT_TRUE(CastNumericExprValueToBool(eval_context, int32_one).value()); | |
| EXPECT_TRUE(CastNumericExprValueToBool(eval_context, int32_minus_one).value()); | |
| EXPECT_FALSE(CastNumericExprValueToBool(eval_context, int32_zero).value()); | |
| EXPECT_FALSE(CastNumericExprValueToBool(eval_context, float_minus_zero).value()); | |
| EXPECT_TRUE(CastNumericExprValueToBool(eval_context, float_one_third).value()); | |
| EXPECT_TRUE(CastNumericExprValueToBool(eval_context, float_minus_two).value()); | |
| EXPECT_TRUE(CastNumericExprValueToBool(eval_context, double_minus_two).value()); | |
| EXPECT_TRUE(CastNumericExprValueToBool(eval_context, ptr_value).value()); | |
| } | |
| // Enums can be casted to and fro. | |
| TEST_F(Cast, Enum) { | |
| auto eval_context = fxl::MakeRefCounted<MockEvalContext>(); | |
| // Enumeration values used in both enums below. | |
| Enumeration::Map values; | |
| values[0] = "kZero"; | |
| values[1] = "kOne"; | |
| values[static_cast<uint64_t>(-1)] = "kMinus"; | |
| // An untyped enum (old-style C with no qualification). This one is unsigned. | |
| auto untyped = fxl::MakeRefCounted<Enumeration>("Untyped", LazySymbol(), 4, false, values); | |
| // An explicitly-typed 8-bit signed enum | |
| auto char_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeSignedChar, 1, "char"); | |
| auto typed = fxl::MakeRefCounted<Enumeration>("Untyped", char_type, 1, true, values); | |
| ExprValue untyped_value(untyped, {1, 0, 0, 0}); | |
| ExprValue typed_value(typed, {1}); | |
| // Untyped to int32. | |
| auto int32_type = MakeInt32Type(); | |
| ErrOrValue out = SyncCastExprValue(eval_context, CastType::kImplicit, untyped_value, int32_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(1, out.value().GetAs<int32_t>()); | |
| // Typed to int32. | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, typed_value, int32_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(1, out.value().GetAs<int32_t>()); | |
| // Untyped to char. | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, untyped_value, char_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(1, out.value().GetAs<int8_t>()); | |
| // Typed to char. | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, typed_value, char_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(1, out.value().GetAs<int8_t>()); | |
| // Signed char to untyped (should be sign-extended). | |
| ExprValue char_minus_one(char_type, {0xff}); | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, char_minus_one, untyped); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(-1, out.value().GetAs<int32_t>()); | |
| // Signed char to typed. | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, char_minus_one, typed); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(-1, out.value().GetAs<int8_t>()); | |
| } | |
| // Tests implicit casting when there are derived classes. | |
| TEST_F(Cast, ImplicitDerived) { | |
| auto eval_context = fxl::MakeRefCounted<MockEvalContext>(); | |
| DerivedClassTestSetup d; | |
| // Should be able to implicit cast Derived to Base1 object. | |
| ErrOrValue out = | |
| SyncCastExprValue(eval_context, CastType::kImplicit, d.derived_value, d.base1_type); | |
| EXPECT_FALSE(out.has_error()); | |
| EXPECT_EQ(d.base1_type.get(), out.value().type()); | |
| EXPECT_EQ(d.base1_value, out.value()); | |
| // Check the source explicitly since == doesn't check sources. | |
| EXPECT_EQ(d.base1_value.source(), out.value().source()); | |
| // Same for base 2. | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, d.derived_value, d.base2_type); | |
| EXPECT_FALSE(out.has_error()); | |
| EXPECT_EQ(d.base2_type.get(), out.value().type()); | |
| EXPECT_EQ(d.base2_value, out.value()); | |
| // Check the source explicitly since == doesn't check sources. | |
| EXPECT_EQ(d.base2_value.source(), out.value().source()); | |
| // Should not be able to implicit cast from Base2 to Derived. | |
| out.value() = ExprValue(); | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, d.base2_value, d.derived_type); | |
| EXPECT_TRUE(out.has_error()); | |
| // Pointer casting: should be able to implicit cast derived ptr to base ptr type. This data | |
| // matches kDerivedAddr in 64-bit little endian. | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, d.derived_ptr_value, d.base2_ptr_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| // That should have adjusted the pointer value appropriately. | |
| EXPECT_EQ(d.base2_ptr_value, out.value()); | |
| // Should not allow implicit casts from Base to Derived. | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, d.base2_ptr_value, d.derived_ptr_type); | |
| EXPECT_TRUE(out.has_error()); | |
| EXPECT_EQ("Can't convert 'Base2*' to unrelated type 'Derived*'.", out.err().msg()); | |
| // Cast a derived to void* is allowed, just a numeric copy. | |
| auto void_ptr_type = fxl::MakeRefCounted<ModifiedType>(DwarfTag::kPointerType, LazySymbol()); | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, d.derived_ptr_value, void_ptr_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| ExprValue void_ptr_value(void_ptr_type, d.derived_ptr_value.data()); | |
| EXPECT_EQ(void_ptr_value, out.value()); | |
| // Cast void* to any pointer type (in C this would require an explicit cast). | |
| out = SyncCastExprValue(eval_context, CastType::kImplicit, void_ptr_value, d.derived_ptr_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(d.derived_ptr_value, out.value()); | |
| } | |
| TEST_F(Cast, Reinterpret) { | |
| auto eval_context = fxl::MakeRefCounted<MockEvalContext>(); | |
| auto int32_type = MakeInt32Type(); | |
| auto uint32_type = MakeInt32Type(); | |
| auto int64_type = MakeInt64Type(); | |
| auto uint64_type = MakeInt64Type(); | |
| auto char_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeSignedChar, 1, "char"); | |
| auto bool_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeBoolean, 1, "bool"); | |
| auto double_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeFloat, 8, "double"); | |
| auto ptr_to_int32_type = fxl::MakeRefCounted<ModifiedType>(DwarfTag::kPointerType, int32_type); | |
| auto ptr_to_void_type = fxl::MakeRefCounted<ModifiedType>(DwarfTag::kPointerType, LazySymbol()); | |
| ExprValue int32_minus_one(int32_type, {0xff, 0xff, 0xff, 0xff}); | |
| ExprValue int64_big_num(int32_type, {8, 7, 6, 5, 4, 3, 2, 1}); | |
| ExprValue ptr_to_void(ptr_to_void_type, {8, 7, 6, 5, 4, 3, 2, 1}); | |
| ExprValue double_pi(double_type, {0x18, 0x2d, 0x44, 0x54, 0xfb, 0x21, 0x09, 0x40}); | |
| // Two pointer types: reinterpret_cast<int32_t*>(ptr_to_void); | |
| ErrOrValue out = | |
| SyncCastExprValue(eval_context, CastType::kReinterpret, ptr_to_void, ptr_to_int32_type); | |
| EXPECT_FALSE(out.has_error()); | |
| EXPECT_EQ(0x0102030405060708u, out.value().GetAs<uint64_t>()); | |
| EXPECT_EQ(ptr_to_int32_type.get(), out.value().type()); | |
| // Conversion from int to void*. C++ would prohibit this case because the integer is 32 bits and | |
| // the pointer is 64, but the debugger allows it. This should not be sign extended. | |
| out = SyncCastExprValue(eval_context, CastType::kReinterpret, int32_minus_one, ptr_to_void_type); | |
| EXPECT_FALSE(out.has_error()); | |
| EXPECT_EQ(0xffffffffu, out.value().GetAs<uint64_t>()); | |
| // Truncation of a number. This is also disallowed in C++. | |
| out = SyncCastExprValue(eval_context, CastType::kReinterpret, int64_big_num, int32_type); | |
| EXPECT_FALSE(out.has_error()); | |
| EXPECT_EQ(4u, out.value().data().size()); | |
| EXPECT_EQ(0x05060708u, out.value().GetAs<uint32_t>()); | |
| // Prohibit conversions between a double and a pointer: reinterpret_cast<void*>(3.14159265258979); | |
| out = SyncCastExprValue(eval_context, CastType::kReinterpret, double_pi, ptr_to_void_type); | |
| EXPECT_TRUE(out.has_error()); | |
| EXPECT_EQ("Can't cast from a 'double'.", out.err().msg()); | |
| } | |
| // Static cast is mostly implement as implicit cast. This only tests the additional behavior. | |
| TEST_F(Cast, Static) { | |
| auto eval_context = fxl::MakeRefCounted<MockEvalContext>(); | |
| DerivedClassTestSetup d; | |
| // Should NOT be able to static cast from Base2 to Derived. This is "static_cast<Derived>(base);" | |
| ErrOrValue out = | |
| SyncCastExprValue(eval_context, CastType::kStatic, d.base2_value, d.derived_type); | |
| EXPECT_TRUE(out.has_error()); | |
| // Cast a derived class reference to a base class reference. This is | |
| // "static_cast<Base&>(derived_reference); | |
| out = SyncCastExprValue(eval_context, CastType::kStatic, d.derived_ref_value, d.base2_ref_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(d.base2_ref_value, out.value()); | |
| // Should be able to go from base->derived with pointers. This is "static_cast<Derived*>(&base);" | |
| out = SyncCastExprValue(eval_context, CastType::kStatic, d.base2_ptr_value, d.derived_ptr_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(d.derived_ptr_value, out.value()); | |
| // Base->derived conversion for references. | |
| out = SyncCastExprValue(eval_context, CastType::kStatic, d.base2_ref_value, d.derived_ref_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(d.derived_ref_value, out.value()); | |
| // Allow conversion of rvalue references and regular references. | |
| auto derived_rvalue_type = | |
| fxl::MakeRefCounted<ModifiedType>(DwarfTag::kRvalueReferenceType, d.derived_type); | |
| ExprValue derived_rvalue_value(derived_rvalue_type, d.derived_ref_value.data()); | |
| out = SyncCastExprValue(eval_context, CastType::kStatic, derived_rvalue_value, d.base2_ref_type); | |
| EXPECT_FALSE(out.has_error()); | |
| EXPECT_EQ(d.base2_ref_value, out.value()); | |
| // Don't allow reference->pointer or pointer->reference casts. | |
| out = SyncCastExprValue(eval_context, CastType::kStatic, d.derived_ref_value, d.derived_ptr_type); | |
| EXPECT_TRUE(out.has_error()); | |
| out = SyncCastExprValue(eval_context, CastType::kStatic, d.derived_ptr_value, d.derived_ref_type); | |
| EXPECT_TRUE(out.has_error()); | |
| } | |
| TEST_F(Cast, C) { | |
| auto eval_context = fxl::MakeRefCounted<MockEvalContext>(); | |
| DerivedClassTestSetup d; | |
| // A C-style cast should be like a static cast when casting between related types. | |
| ErrOrValue out = | |
| SyncCastExprValue(eval_context, CastType::kC, d.derived_ref_value, d.base2_ref_type); | |
| EXPECT_FALSE(out.has_error()) << out.err().msg(); | |
| EXPECT_EQ(d.base2_ref_value, out.value()); | |
| // When there are unrelated pointers, it should fall back to reinterpret. | |
| auto double_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeFloat, 8, "double"); | |
| auto ptr_to_double_type = fxl::MakeRefCounted<ModifiedType>(DwarfTag::kPointerType, double_type); | |
| ExprValue ptr_value(ptr_to_double_type, {0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12}); | |
| out = SyncCastExprValue(eval_context, CastType::kC, ptr_value, d.base2_ptr_type); | |
| EXPECT_FALSE(out.has_error()); | |
| // For the reinterpret cast, the type should be the new one, with the data being the original. | |
| EXPECT_EQ(d.base2_ptr_type.get(), out.value().type()); | |
| EXPECT_EQ(ptr_value.data(), out.value().data()); | |
| // Can't cast from a ptr to a ref. | |
| out = SyncCastExprValue(eval_context, CastType::kC, ptr_value, d.base2_ref_type); | |
| EXPECT_TRUE(out.has_error()); | |
| } | |
| // Casting a pointer when there is virtual inheritance means evaluating a DWARF expression and | |
| // fetching a bunch of memory to adjust the pointers. | |
| // | |
| // Note that virtual inheritance isn't inheritance with virtual methods, but rather: | |
| // | |
| // class Derived : public virtual Base {}; | |
| // | |
| // And this means that Base is accessed indirectly from a pointer in Derived, usually so that | |
| // diamond inheritance can be resolved. See resolve_collection_unittest.cc for more virtual | |
| // inheritance tests. | |
| TEST_F(Cast, StaticCastVirtualInheritance) { | |
| auto eval_context = fxl::MakeRefCounted<MockEvalContext>(); | |
| VirtualInheritanceTestSetup setup; | |
| setup.SaveMockData(eval_context->data_provider()); | |
| // Casing from "derived" to "base" covers two regular and one virtual inheritance step. This is | |
| // the cast for literal objects. | |
| ErrOrValue result = | |
| SyncCastExprValue(eval_context, CastType::kStatic, setup.derived_value, setup.base); | |
| ASSERT_TRUE(result.ok()) << result.err().msg(); | |
| EXPECT_EQ(setup.base.get(), result.value().type()); | |
| EXPECT_EQ(setup.kBaseAddress, result.value().source().address()); | |
| // The data of the base is the last 4 bytes of the full derived data. | |
| std::vector<uint8_t> expected_base_data(setup.derived_value.data().end() - 4, | |
| setup.derived_value.data().end()); | |
| EXPECT_EQ(expected_base_data, result.value().data()); | |
| // Now do a cast of pointers with the same thing. | |
| auto base_ptr_type = fxl::MakeRefCounted<ModifiedType>(DwarfTag::kPointerType, setup.base); | |
| auto derived_ptr_type = fxl::MakeRefCounted<ModifiedType>(DwarfTag::kPointerType, setup.derived); | |
| ExprValue derived_ptr(setup.kDerivedAddress, derived_ptr_type); | |
| result = SyncCastExprValue(eval_context, CastType::kStatic, derived_ptr, base_ptr_type); | |
| ASSERT_TRUE(result.ok()) << result.err().msg(); | |
| EXPECT_EQ(base_ptr_type.get(), result.value().type()); | |
| EXPECT_EQ(setup.kBaseAddress, result.value().GetAs<uint64_t>()); | |
| } | |
| } // namespace zxdb |