From b424b9dc4b4cbedd2c39217e981d3e26f39e0de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 25 Jul 2020 22:08:31 +0200 Subject: [PATCH] Trade: allow storing strings in MaterialData. Because We Can. No, actually, this will be used for upcoming material layers, it's not a bloat. However, this means you can do things like Trade::MaterialData weDoCssHere{{}, { {"color", "navy"}, {"highlight", "rgba(35, 255, 78, 0.75)"}, {"dropShadow", "var(--shadow-color)"} }}; --- src/Magnum/Trade/MaterialData.cpp | 50 ++++++++- src/Magnum/Trade/MaterialData.h | 110 +++++++++++++++++-- src/Magnum/Trade/Test/MaterialDataTest.cpp | 121 ++++++++++++++++++++- 3 files changed, 270 insertions(+), 11 deletions(-) diff --git a/src/Magnum/Trade/MaterialData.cpp b/src/Magnum/Trade/MaterialData.cpp index 947e8613e..b0e1ea061 100644 --- a/src/Magnum/Trade/MaterialData.cpp +++ b/src/Magnum/Trade/MaterialData.cpp @@ -104,12 +104,29 @@ std::size_t materialAttributeTypeSize(const MaterialAttributeType type) { case MaterialAttributeType::Pointer: case MaterialAttributeType::MutablePointer: return sizeof(void*); + + case MaterialAttributeType::String: + CORRADE_ASSERT_UNREACHABLE("Trade::materialAttributeTypeSize(): string size is unknown", {}); } CORRADE_ASSERT_UNREACHABLE("Trade::materialAttributeTypeSize(): invalid type" << type, {}); } MaterialAttributeData::MaterialAttributeData(const Containers::StringView name, const MaterialAttributeType type, const std::size_t size, const void* const value) noexcept: _data{} /* zero-initialized */ { + /* Special handling for strings */ + if(type == MaterialAttributeType::String) { + const auto& stringValue = *static_cast(value); + /* The 4 extra bytes are for a null byte after both the name and value, + a type and a string size */ + CORRADE_ASSERT(name.size() + stringValue.size() + 4 <= Implementation::MaterialAttributeDataSize, + "Trade::MaterialAttributeData: name" << name << "and value" << stringValue << "too long, expected at most" << Implementation::MaterialAttributeDataSize - 4 << "bytes in total but got" << name.size() + stringValue.size(), ); + _data.type = MaterialAttributeType::String; + std::memcpy(_data.data + 1, name.data(), name.size()); + std::memcpy(_data.data + Implementation::MaterialAttributeDataSize - stringValue.size() - 2, stringValue.data(), stringValue.size()); + _data.data[Implementation::MaterialAttributeDataSize - 1] = stringValue.size(); + return; + } + /* The 2 extra bytes are for a null byte after the name and a type */ CORRADE_ASSERT(name.size() + size + 2 <= Implementation::MaterialAttributeDataSize, "Trade::MaterialAttributeData: name" << name << "too long, expected at most" << Implementation::MaterialAttributeDataSize - size - 2 << "bytes for" << type << "but got" << name.size(), ); @@ -118,22 +135,38 @@ MaterialAttributeData::MaterialAttributeData(const Containers::StringView name, std::memcpy(_data.data + Implementation::MaterialAttributeDataSize - size, value, size); } -MaterialAttributeData::MaterialAttributeData(const Containers::StringView name, const MaterialAttributeType type, const void* const value) noexcept: MaterialAttributeData{name, type, materialAttributeTypeSize(type), value} {} +MaterialAttributeData::MaterialAttributeData(const Containers::StringView name, const MaterialAttributeType type, const void* const value) noexcept: MaterialAttributeData{name, type, type == MaterialAttributeType::String ? ~std::size_t{} : materialAttributeTypeSize(type), value} {} MaterialAttributeData::MaterialAttributeData(const MaterialAttribute name, const MaterialAttributeType type, const void* const value) noexcept { CORRADE_ASSERT(UnsignedInt(name) - 1 < Containers::arraySize(AttributeMap), "Trade::MaterialAttributeData: invalid name" << name, ); CORRADE_ASSERT(AttributeMap[UnsignedInt(name) - 1].type == type, "Trade::MaterialAttributeData: expected" << AttributeMap[UnsignedInt(name) - 1].type << "for" << name << "but got" << type, ); + + /* No builtin string attributes yet */ + CORRADE_INTERNAL_ASSERT(type != MaterialAttributeType::String); + _data.type = type; std::memcpy(_data.data + 1, AttributeMap[UnsignedInt(name) - 1].name.data(), AttributeMap[UnsignedInt(name) - 1].name.size()); std::memcpy(_data.data + Implementation::MaterialAttributeDataSize - AttributeMap[UnsignedInt(name) - 1].size, value, AttributeMap[UnsignedInt(name) - 1].size); } const void* MaterialAttributeData::value() const { + if(_data.type == MaterialAttributeType::String) + return _data.s.nameValue + Implementation::MaterialAttributeDataSize - _data.s.size - 3; return _data.data + Implementation::MaterialAttributeDataSize - materialAttributeTypeSize(_data.type); } +#ifndef DOXYGEN_GENERATING_OUTPUT +/* On Windows (MSVC, clang-cl and MinGw) it needs an explicit export otherwise + the symbol doesn't get exported. */ +template<> MAGNUM_TRADE_EXPORT Containers::StringView MaterialAttributeData::value() const { + CORRADE_ASSERT(_data.type == MaterialAttributeType::String, + "Trade::MaterialAttributeData::value():" << (_data.data + 1) << "of" << _data.type << "can't be retrieved as a string", {}); + return {_data.s.nameValue + Implementation::MaterialAttributeDataSize - _data.s.size - 3, _data.s.size, Containers::StringViewFlag::NullTerminated}; +} +#endif + MaterialData::MaterialData(const MaterialTypes types, Containers::Array&& data, const void* const importerState) noexcept: _data{std::move(data)}, _types{types}, _importerState{importerState} { #ifndef CORRADE_NO_ASSERT /* Not checking what's already done in MaterialAttributeData constructor. @@ -270,6 +303,20 @@ const void* MaterialData::attribute(const MaterialAttribute name) const { return attribute(string); } +#ifndef DOXYGEN_GENERATING_OUTPUT +/* On Windows (MSVC, clang-cl and MinGw) it needs an explicit export otherwise + the symbol doesn't get exported. */ +template<> MAGNUM_TRADE_EXPORT Containers::StringView MaterialData::attribute(const UnsignedInt id) const { + /* Can't delegate to attribute() returning const void* because that doesn't + include the size */ + CORRADE_ASSERT(id < _data.size(), + "Trade::MaterialData::attribute(): index" << id << "out of range for" << _data.size() << "attributes", {}); + CORRADE_ASSERT(_data[id]._data.type == MaterialAttributeType::String, + "Trade::MaterialData::attribute():" << (_data[id]._data.data + 1) << "of" << _data[id]._data.type << "can't be retrieved as a string", {}); + return {_data[id]._data.s.nameValue + Implementation::MaterialAttributeDataSize - _data[id]._data.s.size - 3, _data[id]._data.s.size, Containers::StringViewFlag::NullTerminated}; +} +#endif + const void* MaterialData::tryAttribute(const Containers::StringView name) const { const UnsignedInt id = attributeFor(name); if(id == ~UnsignedInt{}) return nullptr; @@ -355,6 +402,7 @@ Debug& operator<<(Debug& debug, const MaterialAttributeType value) { _c(Matrix4x3) _c(Pointer) _c(MutablePointer) + _c(String) #undef _c /* LCOV_EXCL_STOP */ } diff --git a/src/Magnum/Trade/MaterialData.h b/src/Magnum/Trade/MaterialData.h index 3d172ee80..83ab9c932 100644 --- a/src/Magnum/Trade/MaterialData.h +++ b/src/Magnum/Trade/MaterialData.h @@ -332,11 +332,21 @@ enum class MaterialAttributeType: UnsignedByte { * `T` but the user has to ensure the type is correct. */ MutablePointer, + + /** + * Null-terminated string. Can be stored using any type convertible to + * @ref Corrade::Containers::StringView, retrieval has to be done using + * @ref Corrade::Containers::StringView. + */ + String }; /** @brief Byte size of a material attribute type @m_since_latest + +Can't be used with @ref MaterialAttributeType::String, as the size varies +depending on the value. */ MAGNUM_TRADE_EXPORT std::size_t materialAttributeTypeSize(MaterialAttributeType type); @@ -382,11 +392,33 @@ class MAGNUM_TRADE_EXPORT MaterialAttributeData { * don't need @cpp constexpr @ce, as it additionally checks that given * attribute has the expected type. */ - template constexpr /*implicit*/ MaterialAttributeData(Containers::StringView name, const T& value) noexcept; + template::value>::type + #endif + > constexpr /*implicit*/ MaterialAttributeData(Containers::StringView name, const T& value) noexcept; + + /** + * @brief Construct with a string name and value + * @param name Attribute name + * @param value Attribute value + * + * The combined length of @p name and @p value is expected to fit into + * 61 bytes. Type is set to @ref MaterialAttributeType::String. + * + * This function is useful in @cpp constexpr @ce contexts and for + * creating custom material attributes. For known attributes prefer to + * use @ref MaterialAttributeData(MaterialAttribute, const T&) if you + * don't need @cpp constexpr @ce, as it additionally checks that given + * attribute has the expected type. + */ + constexpr /*implicit*/ MaterialAttributeData(Containers::StringView name, Containers::StringView value) noexcept; + #ifndef DOXYGEN_GENERATING_OUTPUT - /* "Sure can't be constexpr" overload to avoid going through the + /* "Sure can't be constexpr" overloads to avoid going through the *insane* overload puzzle when not needed */ - template /*implicit*/ MaterialAttributeData(const char* name, const T& value) noexcept: MaterialAttributeData{name, Implementation::MaterialAttributeTypeFor::type(), sizeof(T), &value} {} + template::value>::type> /*implicit*/ MaterialAttributeData(const char* name, const T& value) noexcept: MaterialAttributeData{name, Implementation::MaterialAttributeTypeFor::type(), sizeof(T), &value} {} + /*implicit*/ MaterialAttributeData(const char* name, Containers::StringView value) noexcept: MaterialAttributeData{name, MaterialAttributeType::String, 0, &value} {} #endif /** @@ -402,7 +434,11 @@ class MAGNUM_TRADE_EXPORT MaterialAttributeData { * * @snippet MagnumTrade.cpp MaterialAttributeData-name */ - template /*implicit*/ MaterialAttributeData(MaterialAttribute name, const T& value) noexcept: MaterialAttributeData{name, Implementation::MaterialAttributeTypeFor::type(), &value} {} + template::value>::type + #endif + > /*implicit*/ MaterialAttributeData(MaterialAttribute name, const T& value) noexcept: MaterialAttributeData{name, Implementation::MaterialAttributeTypeFor::type(), &value} {} /** * @brief Construct from a type-erased value @@ -410,9 +446,15 @@ class MAGNUM_TRADE_EXPORT MaterialAttributeData { * @param type Attribute type * @param value Type-erased value * - * Copies a number of bytes according to @ref materialAttributeTypeSize() - * from @p value. The @p name together with @p value is expected to fit - * into 62 bytes. + * In case @p type is not @ref MaterialAttributeType::String, copies a + * number of bytes according to @ref materialAttributeTypeSize() from + * @p value. The @p name together with @p value is expected to fit into + * 62 bytes. + * + * In case @p type is @ref MaterialAttributeType::String, @p value is + * expected to point to a @ref Containers::StringView. The combined + * length of @p name and @p value strings is expected to fit into 61 + * bytes. */ /*implicit*/ MaterialAttributeData(Containers::StringView name, MaterialAttributeType type, const void* value) noexcept; @@ -447,6 +489,12 @@ class MAGNUM_TRADE_EXPORT MaterialAttributeData { * in case of a @ref MaterialAttributeType::Pointer or a * @ref MaterialAttributeType::MutablePointer, returns a * *pointer to a pointer*, not the pointer value itself. + * + * In case of a @ref MaterialAttributeType::String, returns a + * null-terminated @cpp const char* @ce (not a pointer to + * @ref Containers::StringView). This doesn't preserve the actual + * string size in case the string data contain zero bytes, thus prefer + * to use typed access in that case. */ const void* value() const; @@ -484,6 +532,14 @@ class MAGNUM_TRADE_EXPORT MaterialAttributeData { lookup fast and efficient, and data being at the end (instead of right after the null-terminated string) makes them accessible in O(1) as well. */ + struct StringData { + template constexpr explicit StringData(MaterialAttributeType type, Containers::StringView name, Containers::StringView value, Math::Implementation::Sequence): type{type}, nameValue{(sequence < name.size() ? name[sequence] : (sequence - (Implementation::MaterialAttributeDataSize - value.size() - 3) < value.size() ? value[sequence - (Implementation::MaterialAttributeDataSize - value.size() - 3)] : '\0'))...}, size{UnsignedByte(value.size())} {} + constexpr explicit StringData(MaterialAttributeType type, Containers::StringView name, Containers::StringView value): StringData{type, name, value, typename Math::Implementation::GenerateSequence::Type{}} {} + + MaterialAttributeType type; + char nameValue[Implementation::MaterialAttributeDataSize - 2]; + UnsignedByte size; + }; union ErasedScalar { constexpr explicit ErasedScalar(Float value): f{value} {} constexpr explicit ErasedScalar(Deg value): f{Float(value)} {} @@ -529,6 +585,8 @@ class MAGNUM_TRADE_EXPORT MaterialAttributeData { union CORRADE_ALIGNAS(8) Storage { constexpr explicit Storage() noexcept: data{} {} + constexpr explicit Storage(Containers::StringView name, Containers::StringView value) noexcept: s{MaterialAttributeType::String, name, value} {} + template constexpr explicit Storage(typename std::enable_if::type type, Containers::StringView name, const T& value) noexcept: _1{type, name, value} {} template constexpr explicit Storage(typename std::enable_if::value, MaterialAttributeType>::type type, Containers::StringView name, const T& value) noexcept: _4{type, name, value} {} template constexpr explicit Storage(typename std::enable_if::value && !std::is_pointer::value, MaterialAttributeType>::type type, Containers::StringView name, const T& value) noexcept: _8{type, name, value} {} @@ -544,6 +602,7 @@ class MAGNUM_TRADE_EXPORT MaterialAttributeData { MaterialAttributeType type; char data[Implementation::MaterialAttributeDataSize]; + StringData s; Data _1; Data p; Data _4; @@ -810,6 +869,12 @@ class MAGNUM_TRADE_EXPORT MaterialData { * in case of a @ref MaterialAttributeType::Pointer or a * @ref MaterialAttributeType::MutablePointer, returns a * *pointer to a pointer*, not the pointer value itself. + * + * In case of a @ref MaterialAttributeType::String returns a + * null-terminated @cpp const char* @ce (not a pointer to + * @ref Containers::StringView). This doesn't preserve the actual + * string size in case the string data contain zero bytes, thus prefer + * to use typed access in that case. */ const void* attribute(UnsignedInt id) const; @@ -821,6 +886,12 @@ class MAGNUM_TRADE_EXPORT MaterialData { * in case of a @ref MaterialAttributeType::Pointer or a * @ref MaterialAttributeType::MutablePointer, returns a * *pointer to a pointer*, not the pointer value itself. + * + * In case of a @ref MaterialAttributeType::String returns a + * null-terminated @cpp const char* @ce (not a pointer to + * @ref Containers::StringView). This doesn't preserve the actual + * string size in case the string data contain zero bytes, thus prefer + * to use typed access in that case. * @see @ref hasAttribute(), @ref tryAttribute(), @ref attributeOr() */ const void* attribute(Containers::StringView name) const; @@ -831,7 +902,8 @@ class MAGNUM_TRADE_EXPORT MaterialData { * * The @p id is expected to be smaller than @ref attributeCount() const. * Expects that @p T corresponds to @ref attributeType(UnsignedInt) const - * for given @p id. + * for given @p id. In case of a string, the returned view always has + * @ref Corrade::Containers::StringViewFlag::NullTerminated set. */ template T attribute(UnsignedInt id) const; @@ -840,6 +912,8 @@ class MAGNUM_TRADE_EXPORT MaterialData { * * The @p name is expected to exist. Expects that @p T corresponds to * @ref attributeType(Containers::StringView) const for given @p name. + * In case of a string, the returned view always has + * @ref Corrade::Containers::StringViewFlag::NullTerminated set. * @see @ref hasAttribute() */ template T attribute(Containers::StringView name) const; @@ -996,6 +1070,8 @@ namespace Implementation { return MaterialAttributeType::MutablePointer; } }; + /* No specialization for StringView as this type trait should not be used + in that case */ #ifndef DOXYGEN_GENERATING_OUTPUT #define _c(type_) template<> struct MaterialAttributeTypeFor { \ constexpr static MaterialAttributeType type() { \ @@ -1035,7 +1111,15 @@ namespace Implementation { } /* The 2 extra bytes are for a null byte after the name and a type */ -template constexpr MaterialAttributeData::MaterialAttributeData(const Containers::StringView name, const T& value) noexcept: _data{Implementation::MaterialAttributeTypeFor::type(), (CORRADE_CONSTEXPR_ASSERT(name.size() + sizeof(T) + 2 <= Implementation::MaterialAttributeDataSize, "Trade::MaterialAttributeData: name" << name << "too long, expected at most" << Implementation::MaterialAttributeDataSize - sizeof(T) - 2 << "bytes for" << Implementation::MaterialAttributeTypeFor::type() << "but got" << name.size()), name), value} {} +template constexpr MaterialAttributeData::MaterialAttributeData(const Containers::StringView name, const T& value) noexcept: _data{Implementation::MaterialAttributeTypeFor::type(), (CORRADE_CONSTEXPR_ASSERT(name.size() + sizeof(T) + 2 <= Implementation::MaterialAttributeDataSize, "Trade::MaterialAttributeData: name" << name << "too long, expected at most" << Implementation::MaterialAttributeDataSize - sizeof(T) - 2 << "bytes for" << Implementation::MaterialAttributeTypeFor::type() << "but got" << name.size()), name), value} {} + +/* The 4 extra bytes are for a null byte after both the name and value, a type + and a string size */ +constexpr MaterialAttributeData::MaterialAttributeData(const Containers::StringView name, const Containers::StringView value) noexcept: _data{(CORRADE_CONSTEXPR_ASSERT(name.size() + value.size() + 4 <= Implementation::MaterialAttributeDataSize, "Trade::MaterialAttributeData: name" << name << "and value" << value << "too long, expected at most" << Implementation::MaterialAttributeDataSize - 4 << "bytes in total but got" << name.size() + value.size()), name), value} {} template T MaterialAttributeData::value() const { CORRADE_ASSERT(Implementation::MaterialAttributeTypeFor::type() == _data.type, @@ -1043,6 +1127,10 @@ template T MaterialAttributeData::value() const { return *reinterpret_cast(value()); } +#ifndef DOXYGEN_GENERATING_OUTPUT +template<> Containers::StringView MaterialAttributeData::value() const; +#endif + template T MaterialData::attribute(UnsignedInt id) const { const void* const value = attribute(id); #ifdef CORRADE_GRACEFUL_ASSERT @@ -1053,6 +1141,10 @@ template T MaterialData::attribute(UnsignedInt id) const { return *reinterpret_cast(value); } +#ifndef DOXYGEN_GENERATING_OUTPUT +template<> Containers::StringView MaterialData::attribute(UnsignedInt) const; +#endif + template T MaterialData::attribute(Containers::StringView name) const { const UnsignedInt id = attributeFor(name); CORRADE_ASSERT(id != ~UnsignedInt{}, diff --git a/src/Magnum/Trade/Test/MaterialDataTest.cpp b/src/Magnum/Trade/Test/MaterialDataTest.cpp index 8cfd7c9bf..ddc690703 100644 --- a/src/Magnum/Trade/Test/MaterialDataTest.cpp +++ b/src/Magnum/Trade/Test/MaterialDataTest.cpp @@ -57,13 +57,16 @@ class MaterialDataTest: public TestSuite::Tester { void constructAttributePointer(); void constructAttributeMutablePointer(); + void constructAttributeStringValue(); void constructAttributeInvalidName(); void constructAttributeWrongTypeForName(); void constructAttributeInvalidType(); void constructAttributeTooLarge(); + void constructAttributeTooLargeString(); void constructAttributeWrongAccessType(); void constructAttributeWrongAccessPointerType(); + void constructAttributeWrongAccessTypeString(); void construct(); void constructEmptyAttribute(); @@ -80,12 +83,14 @@ class MaterialDataTest: public TestSuite::Tester { void access(); void accessPointer(); + void accessString(); void accessOptional(); void accessOutOfBounds(); void accessInvalidAttributeName(); void accessNotFound(); void accessWrongType(); void accessWrongPointerType(); + void accessWrongTypeString(); void release(); @@ -162,13 +167,16 @@ MaterialDataTest::MaterialDataTest() { &MaterialDataTest::constructAttributePointer, &MaterialDataTest::constructAttributeMutablePointer, + &MaterialDataTest::constructAttributeStringValue, &MaterialDataTest::constructAttributeInvalidName, &MaterialDataTest::constructAttributeWrongTypeForName, &MaterialDataTest::constructAttributeInvalidType, &MaterialDataTest::constructAttributeTooLarge, + &MaterialDataTest::constructAttributeTooLargeString, &MaterialDataTest::constructAttributeWrongAccessType, &MaterialDataTest::constructAttributeWrongAccessPointerType, + &MaterialDataTest::constructAttributeWrongAccessTypeString, &MaterialDataTest::construct, &MaterialDataTest::constructEmptyAttribute}); @@ -188,12 +196,14 @@ MaterialDataTest::MaterialDataTest() { &MaterialDataTest::access, &MaterialDataTest::accessPointer, + &MaterialDataTest::accessString, &MaterialDataTest::accessOptional, &MaterialDataTest::accessOutOfBounds, &MaterialDataTest::accessInvalidAttributeName, &MaterialDataTest::accessNotFound, &MaterialDataTest::accessWrongType, &MaterialDataTest::accessWrongPointerType, + &MaterialDataTest::accessWrongTypeString, &MaterialDataTest::release, @@ -256,9 +266,11 @@ void MaterialDataTest::attributeTypeSizeInvalid() { Error redirectError{&out}; materialAttributeTypeSize(MaterialAttributeType(0x0)); materialAttributeTypeSize(MaterialAttributeType(0xfe)); + materialAttributeTypeSize(MaterialAttributeType::String); CORRADE_COMPARE(out.str(), "Trade::materialAttributeTypeSize(): invalid type Trade::MaterialAttributeType(0x0)\n" - "Trade::materialAttributeTypeSize(): invalid type Trade::MaterialAttributeType(0xfe)\n"); + "Trade::materialAttributeTypeSize(): invalid type Trade::MaterialAttributeType(0xfe)\n" + "Trade::materialAttributeTypeSize(): string size is unknown\n"); } void MaterialDataTest::attributeMap() { @@ -439,6 +451,41 @@ void MaterialDataTest::constructAttributeMutablePointer() { CORRADE_COMPARE(typeErased.value(), &data); } +void MaterialDataTest::constructAttributeStringValue() { + /* Explicitly using a non-null-terminated view on input to check the null + byte isn't read by accident*/ + MaterialAttributeData attribute{"name that's long", "and a value\0that's also long but still fits!!"_s.except(1)}; + CORRADE_COMPARE(attribute.name(), "name that's long"); + CORRADE_COMPARE(attribute.name().flags(), Containers::StringViewFlag::NullTerminated); + CORRADE_COMPARE(attribute.name()[attribute.name().size()], '\0'); + CORRADE_COMPARE(attribute.type(), MaterialAttributeType::String); + /* Pointer access will stop at the first null byte, but typed access won't */ + CORRADE_COMPARE(static_cast(attribute.value()), "and a value"_s); + CORRADE_COMPARE(attribute.value(), "and a value\0that's also long but still fits!"_s); + CORRADE_COMPARE(attribute.value().flags(), Containers::StringViewFlag::NullTerminated); + CORRADE_COMPARE(attribute.value()[attribute.value().size()], '\0'); + + constexpr MaterialAttributeData cattribute{"name that's long"_s, "and a value\0that's also long but still fits!!"_s.except(1)}; + CORRADE_COMPARE(cattribute.name(), "name that's long"); + CORRADE_COMPARE(cattribute.name().flags(), Containers::StringViewFlag::NullTerminated); + CORRADE_COMPARE(cattribute.name()[cattribute.name().size()], '\0'); + CORRADE_COMPARE(cattribute.type(), MaterialAttributeType::String); + CORRADE_COMPARE(cattribute.value(), "and a value\0that's also long but still fits!"_s); + CORRADE_COMPARE(cattribute.value().flags(), Containers::StringViewFlag::NullTerminated); + CORRADE_COMPARE(cattribute.value()[cattribute.value().size()], '\0'); + + /* Type-erased variant */ + const Containers::StringView value = "and a value\0that's also long but still fits!!"_s.except(1); + MaterialAttributeData typeErased{"name that's long", MaterialAttributeType::String, &value}; + CORRADE_COMPARE(typeErased.name(), "name that's long"); + CORRADE_COMPARE(typeErased.name().flags(), Containers::StringViewFlag::NullTerminated); + CORRADE_COMPARE(typeErased.name()[typeErased.name().size()], '\0'); + CORRADE_COMPARE(typeErased.type(), MaterialAttributeType::String); + CORRADE_COMPARE(typeErased.value(), "and a value\0that's also long but still fits!"_s); + CORRADE_COMPARE(typeErased.value().flags(), Containers::StringViewFlag::NullTerminated); + CORRADE_COMPARE(typeErased.value()[typeErased.value().size()], '\0'); +} + void MaterialDataTest::constructAttributeInvalidName() { #ifdef CORRADE_NO_ASSERT CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); @@ -495,6 +542,22 @@ void MaterialDataTest::constructAttributeTooLarge() { "Trade::MaterialAttributeData: name attributeIsLong too long, expected at most 14 bytes for Trade::MaterialAttributeType::Matrix3x4 but got 15\n"); } +void MaterialDataTest::constructAttributeTooLargeString() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + std::ostringstream out; + Error redirectError{&out}; + MaterialAttributeData{"attribute is long", "This is a problem, got a long piece of text!"}; + /* Constexpr variant has the same assert, but in the header. It should have + the same output. */ + /*constexpr*/ MaterialAttributeData{"attribute is long"_s, "This is a problem, got a long piece of text!"_s}; + CORRADE_COMPARE(out.str(), + "Trade::MaterialAttributeData: name attribute is long and value This is a problem, got a long piece of text! too long, expected at most 60 bytes in total but got 61\n" + "Trade::MaterialAttributeData: name attribute is long and value This is a problem, got a long piece of text! too long, expected at most 60 bytes in total but got 61\n"); +} + void MaterialDataTest::constructAttributeWrongAccessType() { #ifdef CORRADE_NO_ASSERT CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); @@ -523,6 +586,17 @@ void MaterialDataTest::constructAttributeWrongAccessPointerType() { "Trade::MaterialAttributeData::value(): improper type requested for boom of Trade::MaterialAttributeType::Pointer\n"); } +void MaterialDataTest::constructAttributeWrongAccessTypeString() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + std::ostringstream out; + Error redirectError{&out}; + MaterialAttributeData{"thing3", Matrix4x3{}}.value(); + CORRADE_COMPARE(out.str(), "Trade::MaterialAttributeData::value(): thing3 of Trade::MaterialAttributeType::Matrix4x3 can't be retrieved as a string\n"); +} + void MaterialDataTest::construct() { int state; MaterialData data{MaterialType::Phong, { @@ -818,6 +892,19 @@ void MaterialDataTest::accessPointer() { CORRADE_COMPARE(data.attribute("mutable"), &b); } +void MaterialDataTest::accessString() { + MaterialData data{{}, { + {"name?", "THIS IS\0WHO I AM!"_s} + }}; + CORRADE_COMPARE(data.attributeType("name?"), MaterialAttributeType::String); + + /* Pointer access will stop at the first null byte, but typed access won't */ + CORRADE_COMPARE(static_cast(data.attribute(0)), "THIS IS"_s); + CORRADE_COMPARE(data.attribute(0), "THIS IS\0WHO I AM!"_s); + CORRADE_COMPARE(data.attribute(0).flags(), Containers::StringViewFlag::NullTerminated); + CORRADE_COMPARE(data.attribute(0)[data.attribute(0).size()], '\0'); +} + void MaterialDataTest::accessOptional() { MaterialData data{{}, { {MaterialAttribute::AlphaMask, 0.5f}, @@ -859,10 +946,12 @@ void MaterialDataTest::accessOutOfBounds() { data.attributeType(2); data.attribute(2); data.attribute(2); + data.attribute(2); CORRADE_COMPARE(out.str(), "Trade::MaterialData::attributeName(): index 2 out of range for 2 attributes\n" "Trade::MaterialData::attributeType(): index 2 out of range for 2 attributes\n" "Trade::MaterialData::attribute(): index 2 out of range for 2 attributes\n" + "Trade::MaterialData::attribute(): index 2 out of range for 2 attributes\n" "Trade::MaterialData::attribute(): index 2 out of range for 2 attributes\n"); } @@ -990,6 +1079,36 @@ void MaterialDataTest::accessWrongPointerType() { "Trade::MaterialData::attribute(): improper type requested for pointer of Trade::MaterialAttributeType::Pointer\n"); } +void MaterialDataTest::accessWrongTypeString() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + MaterialData data{{}, { + {"Shininess", 0.0f} + }}; + + std::ostringstream out; + Error redirectError{&out}; + data.attribute(0); + data.attribute(MaterialAttribute::Shininess); + data.attribute("Shininess"); + data.tryAttribute(MaterialAttribute::Shininess); + data.tryAttribute("Shininess"); + data.attributeOr(MaterialAttribute::Shininess, Containers::StringView{}); + data.attributeOr("Shininess", Containers::StringView{}); + CORRADE_COMPARE(out.str(), + "Trade::MaterialData::attribute(): Shininess of Trade::MaterialAttributeType::Float can't be retrieved as a string\n" + "Trade::MaterialData::attribute(): Shininess of Trade::MaterialAttributeType::Float can't be retrieved as a string\n" + "Trade::MaterialData::attribute(): Shininess of Trade::MaterialAttributeType::Float can't be retrieved as a string\n" + /* tryAttribute() and attributeOr() delegate to attribute() so the + assert is the same */ + "Trade::MaterialData::attribute(): Shininess of Trade::MaterialAttributeType::Float can't be retrieved as a string\n" + "Trade::MaterialData::attribute(): Shininess of Trade::MaterialAttributeType::Float can't be retrieved as a string\n" + "Trade::MaterialData::attribute(): Shininess of Trade::MaterialAttributeType::Float can't be retrieved as a string\n" + "Trade::MaterialData::attribute(): Shininess of Trade::MaterialAttributeType::Float can't be retrieved as a string\n"); +} + void MaterialDataTest::release() { MaterialData data{{}, { {"DiffuseColor", 0xff3366aa_rgbaf},