diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 81efe5cc9..20d02a1c7 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -5,11 +5,14 @@ ### Features Added - Request logs to now include the `accept-range`, `content-range`, `range`, `WWW-Authenticate`, `x-ms-date`, `x-ms-error-code`, `x-ms-range`, and `x-ms-version` headers. +- Added default constructor, `Parse()`, and equality comparison operators to `Azure::Core::Uuid`. ### Breaking Changes ### Bugs Fixed +- `Azure::Core::Uuid::ToString()` is now `const`. + ### Other Changes ## 1.14.0-beta.1 (2024-08-01) diff --git a/sdk/core/azure-core/inc/azure/core/uuid.hpp b/sdk/core/azure-core/inc/azure/core/uuid.hpp index a6bc8190e..756aa60a2 100644 --- a/sdk/core/azure-core/inc/azure/core/uuid.hpp +++ b/sdk/core/azure-core/inc/azure/core/uuid.hpp @@ -8,12 +8,8 @@ #pragma once -#include "azure/core/platform.hpp" - #include -#include // defines std::uint8_t -#include -#include // deprecated, defines uint8_t in global namespace. TODO: Remove in the future when references to uint8_t and friends are removed. +#include #include namespace Azure { namespace Core { @@ -21,19 +17,31 @@ namespace Azure { namespace Core { * @brief Universally unique identifier. */ class Uuid final { + public: + /** + * @brief Represents a byte array where the UUID value can be stored. + * + */ + using ValueArray = std::array; private: - static constexpr size_t UuidSize = 16; - - std::array m_uuid{}; + ValueArray m_uuid{}; private: - Uuid(uint8_t const uuid[UuidSize]) { std::memcpy(m_uuid.data(), uuid, UuidSize); } + constexpr Uuid(ValueArray const& uuid) : m_uuid(uuid) {} public: + /** + * @brief Constructs a Nil UUID (`00000000-0000-0000-0000-000000000000`). + * + */ + // Nil UUID, per RFC9562, consists of all zeros: + // https://www.rfc-editor.org/rfc/rfc9562.html#name-nil-uuid + constexpr explicit Uuid() : m_uuid{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} {} + /** * @brief Gets Uuid as a string. - * @details A string is in canonical format (8-4-4-4-12 lowercase hex and dashes only). + * @details A string is in canonical format (`8-4-4-4-12` lowercase hex and dashes only). */ std::string ToString() const; @@ -42,7 +50,7 @@ namespace Azure { namespace Core { * representation of the Uuid. * @returns An array with the binary representation of the Uuid. */ - std::array const& AsArray() const { return m_uuid; } + constexpr ValueArray const& AsArray() const { return m_uuid; } /** * @brief Creates a new random UUID. @@ -54,6 +62,60 @@ namespace Azure { namespace Core { * @brief Construct a Uuid from an existing UUID represented as an array of bytes. * @details Creates a Uuid from a UUID created in an external scope. */ - static Uuid CreateFromArray(std::array const& uuid); + static constexpr Uuid CreateFromArray(ValueArray const& uuid) { return Uuid{uuid}; } + + /** + * @brief Construct a Uuid by parsing its representation. + * @param s a string in `8-4-4-4-12` hex characters format. + * @throw `std::invalid_argument` if \p s cannot be parsed. + */ + static Uuid Parse(std::string const& s); + + /** + * @brief Compares with another instance of Uuid for equality. + * @param other another instance of Uuid. + * @return `true` if values of two Uuids are equal, `false` otherwise. + * + */ + constexpr bool operator==(Uuid const& other) const + { + // std::array::operator==() is not a constexpr until C++20 + for (size_t i = 0; i < m_uuid.size(); ++i) + { + if (m_uuid[i] != other.m_uuid[i]) + { + return false; + } + } + + return true; + } + + /** + * @brief Compares with another instance of Uuid for inequality. + * @param other another instance of Uuid. + * @return `true` if values of two Uuids are not equal, `false` otherwise. + * + */ + constexpr bool operator!=(Uuid const& other) const { return !(*this == other); } + + /** + * @brief Checks if the value represents a Nil UUID (`00000000-0000-0000-0000-000000000000`). + * + */ + constexpr bool IsNil() const + { + // Nil UUID, per RFC9562, consists of all zeros: + // https://www.rfc-editor.org/rfc/rfc9562.html#name-nil-uuid + for (size_t i = 0; i < m_uuid.size(); ++i) + { + if (m_uuid[i] != 0) + { + return false; + } + } + + return true; + } }; }} // namespace Azure::Core diff --git a/sdk/core/azure-core/src/uuid.cpp b/sdk/core/azure-core/src/uuid.cpp index 9d4a443be..7f717a529 100644 --- a/sdk/core/azure-core/src/uuid.cpp +++ b/sdk/core/azure-core/src/uuid.cpp @@ -3,10 +3,15 @@ #include "azure/core/uuid.hpp" -#include "azure/core/azure_assert.hpp" +#include "azure/core/internal/strings.hpp" +#include "azure/core/platform.hpp" +#include #include +#include #include +#include +#include #if defined(AZ_PLATFORM_POSIX) #include @@ -18,84 +23,158 @@ static thread_local std::mt19937_64 randomGenerator(std::random_device{}()); } // namespace #endif +using Azure::Core::_internal::StringExtensions; + namespace { -static char ByteToHexChar(uint8_t byte) +/* +"00000000-0000-0000-0000-000000000000" + ^ ^ ^ ^ + 000000000011111111112222222222333333 + 012345678901234567890123456789012345 + \______________ = 36 ______________/ +*/ +constexpr size_t UuidStringLength = 36; +constexpr bool IsDashIndex(size_t i) { return i == 8 || i == 13 || i == 18 || i == 23; } + +constexpr std::uint8_t HexToNibble(char c) // does not check for errors { - if (byte <= 9) + if (c >= 'a') { - return '0' + byte; + return 10 + (c - 'a'); } - AZURE_ASSERT_MSG( - byte >= 10 && byte <= 15, - "It is expected, for a valid Uuid, to have byte values, where each of the two nibbles fit " - "into a hexadecimal character"); + if (c >= 'A') + { + return 10 + (c - 'A'); + } - return 'a' + (byte - 10); + return c - '0'; +} + +constexpr char NibbleToHex(std::uint8_t nibble) // does not check for errors +{ + if (nibble <= 9) + { + return '0' + nibble; + } + + return 'a' + (nibble - 10); } } // namespace namespace Azure { namespace Core { std::string Uuid::ToString() const { - std::string s(36, '-'); + std::string s(UuidStringLength, '-'); - for (size_t i = 0, j = 0; j < s.size() && i < UuidSize; i++) + for (size_t bi = 0, si = 0; bi < m_uuid.size(); ++bi) { - if (i == 4 || i == 6 || i == 8 || i == 10) + if (IsDashIndex(si)) { - j++; // Add hyphens at the appropriate places + ++si; } - uint8_t highNibble = (m_uuid[i] >> 4) & 0x0F; - uint8_t lowNibble = m_uuid[i] & 0x0F; - s[j++] = ByteToHexChar(highNibble); - s[j++] = ByteToHexChar(lowNibble); + assert((si < UuidStringLength) && (si + 1 < UuidStringLength)); + + const std::uint8_t b = m_uuid[bi]; + s[si] = NibbleToHex((b >> 4) & 0x0F); + s[si + 1] = NibbleToHex(b & 0x0F); + si += 2; } + return s; } + Uuid Uuid::Parse(std::string const& s) + { + bool parseError = false; + Uuid result; + if (s.size() != UuidStringLength) + { + parseError = true; + } + else + { + for (size_t si = 0, bi = 0; si < UuidStringLength; ++si) + { + const auto c = s[si]; + if (IsDashIndex(si)) + { + if (c != '-') + { + parseError = true; + break; + } + } + else + { + assert(si + 1 < UuidStringLength && bi < result.m_uuid.size()); + + const auto c2 = s[si + 1]; + if (!StringExtensions::IsHexDigit(c) || !StringExtensions::IsHexDigit(c2)) + { + parseError = true; + break; + } + + result.m_uuid[bi] = (HexToNibble(c) << 4) | HexToNibble(c2); + ++si; + ++bi; + } + } + } + + return parseError ? throw std::invalid_argument( + "Error parsing Uuid: '" + s + + "' is not in the '00112233-4455-6677-8899-aAbBcCdDeEfF' format.") + : result; + } + Uuid Uuid::CreateUuid() { - uint8_t uuid[UuidSize] = {}; + Uuid result{}; + + // Using RngResultType and RngResultSize to highlight the places where the same type is used. + using RngResultType = std::uint32_t; + static_assert(sizeof(RngResultType) == 4, "sizeof(RngResultType) must be 4."); + constexpr size_t RngResultSize = 4; #if defined(AZ_PLATFORM_WINDOWS) std::random_device rd; + + static_assert( + std::is_same::value, + "random_device::result_type must be of RngResultType."); #else - std::uniform_int_distribution distribution; + std::uniform_int_distribution distribution; #endif - for (size_t i = 0; i < UuidSize; i += 4) + for (size_t i = 0; i < result.m_uuid.size(); i += RngResultSize) { #if defined(AZ_PLATFORM_WINDOWS) - const uint32_t x = rd(); + const RngResultType x = rd(); #else - const uint32_t x = distribution(randomGenerator); + const RngResultType x = distribution(randomGenerator); #endif - std::memcpy(uuid + i, &x, 4); + std::memcpy(result.m_uuid.data() + i, &x, RngResultSize); } // The variant field consists of a variable number of the most significant bits of octet 8 of // the UUID. - // https://www.rfc-editor.org/rfc/rfc4122.html#section-4.1.1 - // For setting the variant to conform to RFC4122, the high bits need to be of the form 10xx, + // https://www.rfc-editor.org/rfc/rfc9562.html#name-variant-field + // For setting the variant to conform to RFC9562, the high bits need to be of the form 10xx, // which means the hex value of the first 4 bits can only be either 8, 9, A|a, B|b. The 0-7 // values are reserved for backward compatibility. The C|c, D|d values are reserved for // Microsoft, and the E|e, F|f values are reserved for future use. // Therefore, we have to zero out the two high bits, and then set the highest bit to 1. - uuid[8] = (uuid[8] & 0x3F) | 0x80; + result.m_uuid.data()[8] = (result.m_uuid.data()[8] & 0x3F) | 0x80; - constexpr uint8_t version = 4; // Version 4: Pseudo-random number + { + // https://www.rfc-editor.org/rfc/rfc9562.html#name-version-field + constexpr std::uint8_t Version = 4; // Version 4: Pseudo-random number + result.m_uuid.data()[6] = (result.m_uuid.data()[6] & 0xF) | (Version << 4); + } - uuid[6] = (uuid[6] & 0xF) | (version << 4); - - return Uuid(uuid); + return result; } - - Uuid Uuid::CreateFromArray(std::array const& uuid) - { - Uuid rv{uuid.data()}; - return rv; - } - }} // namespace Azure::Core diff --git a/sdk/core/azure-core/test/ut/uuid_test.cpp b/sdk/core/azure-core/test/ut/uuid_test.cpp index 9aaaf89c8..239725e21 100644 --- a/sdk/core/azure-core/test/ut/uuid_test.cpp +++ b/sdk/core/azure-core/test/ut/uuid_test.cpp @@ -127,3 +127,81 @@ TEST(Uuid, validChars) uuidKey, 4); } + +TEST(Uuid, nilAndDefault) +{ + Uuid uuid; + ASSERT_TRUE(uuid.IsNil()); + ASSERT_EQ(uuid.ToString(), "00000000-0000-0000-0000-000000000000"); + ASSERT_EQ(uuid, Uuid{}); + ASSERT_EQ(uuid.AsArray(), Uuid::ValueArray({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); +} + +TEST(Uuid, parse) +{ + Uuid uuid1 = Uuid::Parse("00112233-4455-6677-8899-aAbBcCdDeEfF"); + + ASSERT_FALSE(uuid1.IsNil()); + ASSERT_EQ(uuid1.ToString(), "00112233-4455-6677-8899-aabbccddeeff"); + ASSERT_NE(uuid1, Uuid{}); + ASSERT_EQ( + uuid1.AsArray(), + Uuid::ValueArray( + {0x00, + 0x11, + 0x22, + 0x33, + 0x44, + 0x55, + 0x66, + 0x77, + 0x88, + 0x99, + 0xAA, + 0xBB, + 0xCC, + 0xDD, + 0xEE, + 0xFF})); + + // Empty string + ASSERT_THROW(Uuid::Parse(""), std::invalid_argument); + + // Special characters - make sure we're not treating them as byte array + ASSERT_THROW(Uuid::Parse("\a\a\a\a\a\a\a\a\a\a\a\a\a\a\a\a"), std::invalid_argument); + + // Spaces before, after, and both. + ASSERT_THROW(Uuid::Parse("00000000-0000-0000-0000-000000000000 "), std::invalid_argument); + ASSERT_THROW(Uuid::Parse(" 00000000-0000-0000-0000-000000000000"), std::invalid_argument); + ASSERT_THROW(Uuid::Parse("00000000-0000-0000-0000-00000000000"), std::invalid_argument); + + // Valid characters, but in places where dashes should be + ASSERT_THROW(Uuid::Parse("00000000a0000-0000-0000-000000000000"), std::invalid_argument); + ASSERT_THROW(Uuid::Parse("00000000-0000a0000-0000-000000000000"), std::invalid_argument); + ASSERT_THROW(Uuid::Parse("00000000-0000-0000a0000-000000000000"), std::invalid_argument); + ASSERT_THROW(Uuid::Parse("00000000-0000-0000-0000a000000000000"), std::invalid_argument); + + // Another ToString() formats + // (https://learn.microsoft.com/dotnet/api/system.guid.tostring?view=net-8.0) + ASSERT_THROW(Uuid::Parse("00000000000000000000000000000000"), std::invalid_argument); + ASSERT_THROW(Uuid::Parse("{00000000-0000-0000-0000-000000000000}"), std::invalid_argument); + ASSERT_THROW(Uuid::Parse("(00000000-0000-0000-0000-000000000000)"), std::invalid_argument); + ASSERT_THROW( + Uuid::Parse("{0x00000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}}"), + std::invalid_argument); + + // Correct length, invalid characters + ASSERT_THROW(Uuid::Parse("o000000000-0000-0000-0000-000000000000"), std::invalid_argument); + ASSERT_THROW(Uuid::Parse("0000000000-0000-0000-0000-00000000000o"), std::invalid_argument); + + // Incorrect length, incorrect caracters + ASSERT_THROW(Uuid::Parse("00000000-0000-0000-0000-0000000000G"), std::invalid_argument); + + // Less dashes + ASSERT_THROW(Uuid::Parse("00000000-000000000000000000000000"), std::invalid_argument); + ASSERT_THROW(Uuid::Parse("00000000-0000-00000000000000000000"), std::invalid_argument); + ASSERT_THROW(Uuid::Parse("00000000-0000-0000-0000000000000000"), std::invalid_argument); + + // Just a string of text + ASSERT_THROW(Uuid::Parse("The quick brown fox jumps over the lazy dog."), std::invalid_argument); +}