Uuid: add default ctor and Parse() (#5935)

* Uuid: add default ctor and Parse()

* Fix merge, integrate better

* nano-optimization for IsNil()

* Changelog

* Pedantism

* GCC fix

* GCC fix (include <stdexcept>)

* PR feedback

* Update sdk/core/azure-core/test/ut/uuid_test.cpp

Co-authored-by: Rick Winter <rick.winter@microsoft.com>

* Comments for bad tests

* Add doxygen comments

* One more constexpr, and clang-format

---------

Co-authored-by: Anton Kolesnyk <antkmsft@users.noreply.github.com>
Co-authored-by: Rick Winter <rick.winter@microsoft.com>
This commit is contained in:
Anton Kolesnyk 2024-08-23 23:15:24 -07:00 коммит произвёл GitHub
Родитель 227ae0ea0b
Коммит ee0c7565df
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
4 изменённых файлов: 271 добавлений и 49 удалений

Просмотреть файл

@ -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)

Просмотреть файл

@ -8,12 +8,8 @@
#pragma once
#include "azure/core/platform.hpp"
#include <array>
#include <cstdint> // defines std::uint8_t
#include <cstring>
#include <stdint.h> // deprecated, defines uint8_t in global namespace. TODO: Remove in the future when references to uint8_t and friends are removed.
#include <cstdint>
#include <string>
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<std::uint8_t, 16>;
private:
static constexpr size_t UuidSize = 16;
std::array<uint8_t, UuidSize> 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<uint8_t, UuidSize> 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<uint8_t, UuidSize> 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

Просмотреть файл

@ -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 <cassert>
#include <cstdio>
#include <cstring>
#include <random>
#include <stdexcept>
#include <type_traits>
#if defined(AZ_PLATFORM_POSIX)
#include <thread>
@ -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<RngResultType, decltype(rd())>::value,
"random_device::result_type must be of RngResultType.");
#else
std::uniform_int_distribution<uint32_t> distribution;
std::uniform_int_distribution<RngResultType> 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<uint8_t, UuidSize> const& uuid)
{
Uuid rv{uuid.data()};
return rv;
}
}} // namespace Azure::Core

Просмотреть файл

@ -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);
}