diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 281213249..4d4ee277b 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -2,6 +2,10 @@ ## 1.0.0-beta.5 (Unreleased) +### New Features + +- Added support for HTTP validators `ETag`. + ### Breaking Changes - Make `ToLower` and `LocaleInvariantCaseInsensitiveEqual` internal by moving them from `Azure::Core::Strings` to `Azure::Core::Internal::Strings`. diff --git a/sdk/core/azure-core/CMakeLists.txt b/sdk/core/azure-core/CMakeLists.txt index fb79ae4ff..998977633 100644 --- a/sdk/core/azure-core/CMakeLists.txt +++ b/sdk/core/azure-core/CMakeLists.txt @@ -60,6 +60,7 @@ set( inc/azure/core/credentials.hpp inc/azure/core/datetime.hpp inc/azure/core/dll_import_export.hpp + inc/azure/core/etag.hpp inc/azure/core/exception.hpp inc/azure/core/nullable.hpp inc/azure/core/operation.hpp diff --git a/sdk/core/azure-core/inc/azure/core.hpp b/sdk/core/azure-core/inc/azure/core.hpp index 5c5550226..1e0c7f771 100644 --- a/sdk/core/azure-core/inc/azure/core.hpp +++ b/sdk/core/azure-core/inc/azure/core.hpp @@ -15,6 +15,7 @@ #include "azure/core/credentials.hpp" #include "azure/core/datetime.hpp" #include "azure/core/dll_import_export.hpp" +#include "azure/core/etag.hpp" #include "azure/core/nullable.hpp" #include "azure/core/response.hpp" #include "azure/core/uuid.hpp" diff --git a/sdk/core/azure-core/inc/azure/core/etag.hpp b/sdk/core/azure-core/inc/azure/core/etag.hpp new file mode 100644 index 000000000..0a0b15f29 --- /dev/null +++ b/sdk/core/azure-core/inc/azure/core/etag.hpp @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * @brief Define ETag. + */ + +#pragma once + +#include "azure/core/nullable.hpp" + +#include + +namespace Azure { namespace Core { + + /** + * @brief Represents an HTTP validator. + */ + class ETag { + // ETag is a validator based on https://tools.ietf.org/html/rfc7232#section-2.3.2 + private: + Nullable m_value; + + public: + /** + * @brief The comparison type. + */ + enum class ETagComparison + { + Strong, + Weak + }; + + /* + 2.3.2. Comparison + + There are two entity-tag comparison functions, depending on whether + or not the comparison context allows the use of weak validators: + + o Strong comparison: two entity-tags are equivalent if both are not + weak and their opaque-tags match character-by-character. + + o Weak comparison: two entity-tags are equivalent if their + opaque-tags match character-by-character, regardless of either or + both being tagged as "weak". + + +--------+--------+-------------------+-----------------+ + | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | + +--------+--------+-------------------+-----------------+ + | W/"1" | W/"1" | no match | match | + | W/"1" | W/"2" | no match | no match | + | W/"1" | "1" | no match | match | + | "1" | "1" | match | match | + +--------+--------+-------------------+-----------------+ + + // etag: //This is possible and means no etag is present + // etag:"" + // etag:"*" //This means the etag is value '*' + // etag:"some value" //This means the etag is value 'some value' + // etag:/W"" //Weak eTag + // etag:* //This is special, means any etag + // If-Match header can do this + // If-Match:"value1","value2","value3" // Do this if any of these match + + */ + + /* + * @brief Indicates whether two #ETag values are equal. + * @param left #ETag to compare. + * @param right #ETag to compare. + * @param comparisonKind Determines what #ETagComparison to perform, default is + * #ETagComparison Strong. + * @return `true` if #ETag matches, `false` otherwise. + */ + static bool Equals( + const ETag& left, + const ETag& right, + const ETagComparison comparisonKind = ETagComparison::Strong) + { + // ETags are != if one of the values is null + if (!left.m_value || !right.m_value) + { + // Caveat, If both values are null then we consider the ETag equal + return !left.m_value && !right.m_value; + } + + switch (comparisonKind) + { + case ETagComparison::Strong: + // Strong comparison + // If either is weak then there is no match + // else tags must match character for character + return !left.IsWeak() && !right.IsWeak() + && (left.m_value.GetValue().compare(right.m_value.GetValue()) == 0); + break; + + case ETagComparison::Weak: + + auto leftStart = left.IsWeak() ? 2 : 0; + auto rightStart = right.IsWeak() ? 2 : 0; + + auto leftVal = left.m_value.GetValue(); + auto rightVal = right.m_value.GetValue(); + + // Compare if lengths are equal + // Compare the strings character by character + return ((leftVal.length() - leftStart) == (rightVal.length() - rightStart)) + && (leftVal.compare(leftStart, leftVal.length() - leftStart, &rightVal[rightStart]) + == 0); + break; + } + // Unknown comparison + abort(); + } + + /** + * @brief Construct an empty (null) #ETag. + */ + ETag() = default; + + /** + * @brief Construct a #ETag. + * @param etag The string value representation. + */ + explicit ETag(std::string etag) : m_value(std::move(etag)) {} + + /** + * @brief Whether #ETag is present. + * @return `true` if #ETag has a value, `false` otherwise. + */ + bool HasValue() const { return m_value.HasValue(); } + + /* + * @brief Returns the resource metadata represented as a string. + * @return #std::string + */ + const std::string& ToString() const + { + if (!m_value.HasValue()) + { + abort(); + } + return m_value.GetValue(); + } + + /** + * @brief Compare with \p other #ETag for equality. + * @param other Other #ETag to compare with. + * @return `true` if #ETag instances are equal according to strong validation, `false` + * otherwise. + */ + bool operator==(const ETag& other) const + { + return Equals(*this, other, ETagComparison::Strong); + } + + /** + * @brief Compare with \p other #ETag for inequality. + * @param other Other #ETag to compare with. + * @return `true` if #ETag instances are not equal according to strong validation, `false` + * otherwise. + */ + bool operator!=(const ETag& other) const { return !(*this == other); } + + /** + * @brief Specifies whether the #ETag is strong or weak. + * @return `true` if #ETag is a weak validator, `false` otherwise. + */ + bool IsWeak() const + { + // Null ETag is considered Strong + // Shortest valid weak etag has length of 4 + // W/"" + // Valid weak format must start with W/" + // Must end with a /" + const bool weak = m_value && (m_value.GetValue().length() >= 4) + && ((m_value.GetValue()[0] == 'W') && (m_value.GetValue()[1] == '/') + && (m_value.GetValue()[2] == '"') + && (m_value.GetValue()[m_value.GetValue().size() - 1] == '"')); + + return weak; + } + + /** + * @brief #ETag representing everything. + * @notes The any #ETag is *, (unquoted). It is NOT the same as "*". + */ + static const ETag& Any() + { + static ETag any = ETag("*"); + return any; + } + }; +}} // namespace Azure::Core diff --git a/sdk/core/azure-core/test/ut/CMakeLists.txt b/sdk/core/azure-core/test/ut/CMakeLists.txt index c85aaec44..86b8ecdfb 100644 --- a/sdk/core/azure-core/test/ut/CMakeLists.txt +++ b/sdk/core/azure-core/test/ut/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable ( ${CURL_OPTIONS_TESTS} ${CURL_SESSION_TESTS} datetime.cpp + etag.cpp http.cpp json.cpp logging.cpp diff --git a/sdk/core/azure-core/test/ut/etag.cpp b/sdk/core/azure-core/test/ut/etag.cpp new file mode 100644 index 000000000..501bedea9 --- /dev/null +++ b/sdk/core/azure-core/test/ut/etag.cpp @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include + +#include + +using namespace Azure::Core; + +TEST(ETag, ToString) +{ + auto et1 = ETag("tag"); + EXPECT_EQ(et1.ToString(), "tag"); + + auto et2 = ETag("\"tag\""); + EXPECT_EQ(et2.ToString(), "\"tag\""); + + auto et3 = ETag("W/\"weakETag\""); + EXPECT_EQ(et3.ToString(), "W/\"weakETag\""); + + auto strongETag + = ETag("\"#$%&'()*+,-./" + "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\""); + + EXPECT_EQ( + strongETag.ToString(), + "\"#$%&'()*+,-./" + "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\""); +} + +TEST(ETag, IsWeak) +{ + auto et1 = ETag("tag"); + EXPECT_FALSE(et1.IsWeak()); + + auto et2 = ETag("\"tag\""); + EXPECT_FALSE(et2.IsWeak()); + + auto et3 = ETag("W/\"weakETag\""); + EXPECT_TRUE(et3.IsWeak()); + + auto et4 = ETag("W/\"\""); + EXPECT_TRUE(et4.IsWeak()); + + auto any = ETag::Any(); + EXPECT_FALSE(any.IsWeak()); +} + +TEST(ETag, Equals) +{ + ETag empty; + ETag empty2; + + auto weakTag = ETag("W/\"\""); + auto weakTag1 = ETag("W/\"1\""); + auto weakTag2 = ETag("W/\"Two\""); + auto strongTag1 = ETag("\"1\""); + auto strongTag2 = ETag("\"Two\""); + auto strongTagValidChars + = ETag("\"#$%&'()*+,-./" + "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\""); + auto weakTagValidChars + = ETag("W/\"#$%&'()*+,-./" + "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\""); + + EXPECT_TRUE(empty == empty); + EXPECT_TRUE(empty2 == empty2); + EXPECT_TRUE(empty == empty2); + + EXPECT_FALSE(weakTag == weakTag); + EXPECT_FALSE(weakTag1 == weakTag1); + EXPECT_FALSE(weakTag2 == weakTag2); + EXPECT_FALSE(weakTagValidChars == weakTagValidChars); + EXPECT_TRUE(strongTag1 == strongTag1); + EXPECT_TRUE(strongTag2 == strongTag2); + EXPECT_TRUE(strongTagValidChars == strongTagValidChars); + + EXPECT_TRUE(weakTag != weakTag); + EXPECT_TRUE(weakTag1 != weakTag1); + EXPECT_TRUE(weakTag2 != weakTag2); + EXPECT_TRUE(weakTagValidChars != weakTagValidChars); + EXPECT_FALSE(strongTag1 != strongTag1); + EXPECT_FALSE(strongTag2 != strongTag2); + EXPECT_FALSE(strongTagValidChars != strongTagValidChars); + + EXPECT_FALSE(weakTag == weakTag1); + EXPECT_FALSE(weakTag1 == weakTag); + EXPECT_FALSE(weakTagValidChars == strongTagValidChars); + + EXPECT_TRUE(weakTag != weakTag1); + EXPECT_TRUE(weakTag1 != weakTag); + EXPECT_TRUE(weakTagValidChars != strongTagValidChars); + + EXPECT_FALSE(weakTag1 == weakTag2); + EXPECT_FALSE(weakTag1 == strongTag1); + EXPECT_FALSE(strongTag1 == weakTag1); + + EXPECT_TRUE(weakTag1 != weakTag2); + EXPECT_TRUE(weakTag1 != strongTag1); + EXPECT_TRUE(strongTag1 != weakTag1); + + EXPECT_FALSE(weakTag2 == strongTag2); + EXPECT_FALSE(strongTag2 == weakTag2); + + EXPECT_TRUE(weakTag2 != strongTag2); + EXPECT_TRUE(strongTag2 != weakTag2); +} + +TEST(ETag, Any) +{ + auto anyETag = ETag::Any(); + auto star = ETag("*"); + auto weakStar = ETag("W\"*\""); + auto quotedStar = ETag("\"*\""); + + auto strongETag + = ETag("\"#$%&'()*+,-./" + "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\""); + + EXPECT_TRUE(anyETag == anyETag); + EXPECT_TRUE(anyETag == ETag::Any()); + EXPECT_FALSE(anyETag == strongETag); + + EXPECT_TRUE(star == star); + EXPECT_TRUE(star == ETag::Any()); + EXPECT_TRUE(star == anyETag); + + EXPECT_FALSE(star == weakStar); + EXPECT_FALSE(weakStar == anyETag); + EXPECT_FALSE(quotedStar == weakStar); + + EXPECT_FALSE(star == quotedStar); + EXPECT_TRUE(anyETag == star); +} + +TEST(ETag, EqualsStrong) +{ + // W/"" + auto weakTag = ETag("W/\"\""); + // W/"1" + auto weakTag1 = ETag("W/\"1\""); + // W/"Two" + auto weakTagTwo = ETag("W/\"Two\""); + // W/"two" + auto weakTagtwo = ETag("W/\"two\""); + // "1" + auto strongTag1 = ETag("\"1\""); + // "Two" + auto strongTagTwo = ETag("\"Two\""); + // "two" + auto strongTagtwo = ETag("\"two\""); + + EXPECT_FALSE(ETag::Equals(weakTag, weakTag, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(weakTag1, weakTag1, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(weakTagTwo, weakTagTwo, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(weakTagtwo, weakTagtwo, ETag::ETagComparison::Strong)); + + EXPECT_TRUE(ETag::Equals(strongTag1, strongTag1, ETag::ETagComparison::Strong)); + EXPECT_TRUE(ETag::Equals(strongTagTwo, strongTagTwo, ETag::ETagComparison::Strong)); + EXPECT_TRUE(ETag::Equals(strongTagtwo, strongTagtwo, ETag::ETagComparison::Strong)); + + EXPECT_FALSE(ETag::Equals(weakTag, weakTag1, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(weakTag1, weakTag, ETag::ETagComparison::Strong)); + + EXPECT_FALSE(ETag::Equals(weakTag1, weakTagTwo, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(weakTagTwo, weakTag1, ETag::ETagComparison::Strong)); + + EXPECT_FALSE(ETag::Equals(weakTag1, strongTag1, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(strongTag1, weakTag1, ETag::ETagComparison::Strong)); + + EXPECT_FALSE(ETag::Equals(weakTagTwo, strongTagTwo, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(strongTagTwo, weakTagTwo, ETag::ETagComparison::Strong)); + + EXPECT_FALSE(ETag::Equals(strongTagTwo, weakTag1, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(weakTag1, strongTagTwo, ETag::ETagComparison::Strong)); + + EXPECT_FALSE(ETag::Equals(strongTagTwo, strongTagtwo, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(strongTagtwo, strongTagTwo, ETag::ETagComparison::Strong)); + + EXPECT_FALSE(ETag::Equals(weakTagTwo, weakTagtwo, ETag::ETagComparison::Strong)); + EXPECT_FALSE(ETag::Equals(weakTagtwo, weakTagTwo, ETag::ETagComparison::Strong)); +} + +TEST(ETag, EqualsWeak) +{ + // W/"" + auto weakTag = ETag("W/\"\""); + // W/"1" + auto weakTag1 = ETag("W/\"1\""); + // W/"Two" + auto weakTagTwo = ETag("W/\"Two\""); + // W/"two" + auto weakTagtwo = ETag("W/\"two\""); + // "1" + auto strongTag1 = ETag("\"1\""); + // "Two" + auto strongTagTwo = ETag("\"Two\""); + // "two" + auto strongTagtwo = ETag("\"two\""); + + EXPECT_TRUE(ETag::Equals(weakTag, weakTag, ETag::ETagComparison::Weak)); + EXPECT_TRUE(ETag::Equals(weakTag1, weakTag1, ETag::ETagComparison::Weak)); + EXPECT_TRUE(ETag::Equals(weakTagTwo, weakTagTwo, ETag::ETagComparison::Weak)); + EXPECT_TRUE(ETag::Equals(weakTagtwo, weakTagtwo, ETag::ETagComparison::Weak)); + + EXPECT_TRUE(ETag::Equals(strongTag1, strongTag1, ETag::ETagComparison::Weak)); + EXPECT_TRUE(ETag::Equals(strongTagTwo, strongTagTwo, ETag::ETagComparison::Weak)); + + EXPECT_FALSE(ETag::Equals(weakTag, weakTag1, ETag::ETagComparison::Weak)); + EXPECT_FALSE(ETag::Equals(weakTag1, weakTag, ETag::ETagComparison::Weak)); + + EXPECT_FALSE(ETag::Equals(weakTag1, weakTagTwo, ETag::ETagComparison::Weak)); + EXPECT_FALSE(ETag::Equals(weakTagTwo, weakTag1, ETag::ETagComparison::Weak)); + + EXPECT_TRUE(ETag::Equals(weakTag1, strongTag1, ETag::ETagComparison::Weak)); + EXPECT_TRUE(ETag::Equals(strongTag1, weakTag1, ETag::ETagComparison::Weak)); + + EXPECT_TRUE(ETag::Equals(weakTagTwo, strongTagTwo, ETag::ETagComparison::Weak)); + EXPECT_TRUE(ETag::Equals(strongTagTwo, weakTagTwo, ETag::ETagComparison::Weak)); + + EXPECT_FALSE(ETag::Equals(strongTagTwo, weakTag1, ETag::ETagComparison::Weak)); + EXPECT_FALSE(ETag::Equals(weakTag1, strongTagTwo, ETag::ETagComparison::Weak)); + + EXPECT_FALSE(ETag::Equals(strongTagTwo, weakTagtwo, ETag::ETagComparison::Weak)); + EXPECT_FALSE(ETag::Equals(weakTagtwo, strongTagTwo, ETag::ETagComparison::Weak)); + + EXPECT_FALSE(ETag::Equals(strongTagTwo, strongTagtwo, ETag::ETagComparison::Weak)); + EXPECT_FALSE(ETag::Equals(strongTagtwo, strongTagTwo, ETag::ETagComparison::Weak)); + + EXPECT_FALSE(ETag::Equals(weakTagTwo, weakTagtwo, ETag::ETagComparison::Weak)); + EXPECT_FALSE(ETag::Equals(weakTagtwo, weakTagTwo, ETag::ETagComparison::Weak)); +} diff --git a/sdk/core/azure-core/test/ut/simplified_header.cpp b/sdk/core/azure-core/test/ut/simplified_header.cpp index 751100065..3dfac0586 100644 --- a/sdk/core/azure-core/test/ut/simplified_header.cpp +++ b/sdk/core/azure-core/test/ut/simplified_header.cpp @@ -17,13 +17,14 @@ class DllExportTest { AZ_CORE_DLLEXPORT static const bool DllExportHIncluded; }; -TEST(Logging, simplifiedHeader) +TEST(SimplifiedHeader, core) { EXPECT_NO_THROW(Azure::Core::Context c); EXPECT_NO_THROW(Azure::Core::DateTime(2020, 11, 03, 15, 30, 44)); - EXPECT_NO_THROW(Azure::Core::Nullable n); + EXPECT_NO_THROW(Azure::Core::ETag e); EXPECT_NO_THROW(Azure::Core::Http::RawResponse r( 1, 1, Azure::Core::Http::HttpStatusCode::Accepted, "phrase")); + EXPECT_NO_THROW(Azure::Core::Nullable n); EXPECT_NO_THROW(Azure::Core::Uuid::CreateUuid()); EXPECT_NO_THROW(Azure::Core::Details::Version::VersionString());