From d89cef439f5c1a58aeff879a12d9a33292764b7f Mon Sep 17 00:00:00 2001 From: Ahmad Samir Date: Fri, 29 Nov 2024 18:29:16 +0200 Subject: [PATCH] QUuid: add support for creating UUID v7 Thanks to Thiago for the more efficient way of using 12 bits of the sub-milliseconds part of the timestamp. Previously I used the method described in the RFC (value of type double multiplied by 4096). Add a private helper to check the version since now there is a gap in the Version enum values (UUID v6 isn't supported). Drive-by, document Version::Sha1 enumerator. [ChangeLog][QtCore][QUuid] Added support for creating UUID v7 as described in https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7 Fixes: QTBUG-130672 Change-Id: Idfea9fbb12a7f28e25b27b56a3b402799afb4864 Reviewed-by: Thiago Macieira --- src/corelib/CMakeLists.txt | 2 +- src/corelib/plugin/quuid.cpp | 28 ++++++++- src/corelib/plugin/quuid.h | 25 +++++++- src/corelib/plugin/quuid_p.h | 61 +++++++++++++++++++ tests/auto/corelib/plugin/quuid/tst_quuid.cpp | 56 ++++++++++++++++- 5 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 src/corelib/plugin/quuid_p.h diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 063bb859b48..7adbc7014fa 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -194,7 +194,7 @@ qt_internal_add_module(Core plugin/qfactoryloader.cpp plugin/qfactoryloader_p.h plugin/qplugin.h plugin/qplugin_p.h plugin/qpluginloader.cpp plugin/qpluginloader.h - plugin/quuid.cpp plugin/quuid.h + plugin/quuid.cpp plugin/quuid.h plugin/quuid_p.h serialization/qcborarray.h serialization/qcborcommon.cpp serialization/qcborcommon.h serialization/qcborcommon_p.h serialization/qcbordiagnostic.cpp diff --git a/src/corelib/plugin/quuid.cpp b/src/corelib/plugin/quuid.cpp index 0b1615c6499..e0d621dec80 100644 --- a/src/corelib/plugin/quuid.cpp +++ b/src/corelib/plugin/quuid.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "quuid.h" +#include "quuid_p.h" #include "qcryptographichash.h" #include "qdatastream.h" @@ -543,7 +544,7 @@ QUuid QUuid::fromString(QAnyStringView text) noexcept \note In Qt versions prior to 6.8, this function took QByteArray, not QByteArrayView. - \sa variant(), version(), createUuidV5() + \sa variant(), version(), createUuidV5(), createUuidV7() */ /*! @@ -553,7 +554,7 @@ QUuid QUuid::fromString(QAnyStringView text) noexcept This function returns a new UUID with variant QUuid::DCE and version QUuid::Md5. \a ns is the namespace and \a baseData is the basic data as described by RFC 4122. - \sa variant(), version(), createUuidV5() + \sa variant(), version(), createUuidV5(), createUuidV7() */ /*! @@ -590,6 +591,25 @@ QUuid QUuid::createUuidV5(QUuid ns, QByteArrayView baseData) noexcept return createFromName(ns, baseData, QCryptographicHash::Sha1, 5); } +/*! + \since 6.9 + + This function returns a new UUID with variant QUuid::DCE and version + QUuid::UnixEpoch. + + It uses a time-ordered value field derived from the number of milliseconds + since the UNIX Epoch as described by + \l {https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7}{RFC9562}. + + \sa variant(), version(), createUuidV3(), createUuidV5() +*/ +#ifndef QT_BOOTSTRAPPED +QUuid QUuid::createUuidV7() +{ + return createUuidV7_internal(std::chrono::system_clock::now()); +} +#endif // !defined(QT_BOOTSTRAPPED) + /*! Creates a QUuid object from the binary representation of the UUID, as specified by RFC 4122 section 4.1.2. See toRfc4122() for a further @@ -861,7 +881,9 @@ QDataStream &operator>>(QDataStream &s, QUuid &id) \value Name Name-based, by using values from a name for all sections \value Md5 Alias for Name \value Random Random-based, by using random numbers for all sections - \value Sha1 + \value Sha1 Name-based version that uses SHA-1 hashing + \value UnixEpoch Time-based UUID using the number of milliseconds since + the UNIX epoch */ /*! diff --git a/src/corelib/plugin/quuid.h b/src/corelib/plugin/quuid.h index 34a14cbc641..e19a4b7c52e 100644 --- a/src/corelib/plugin/quuid.h +++ b/src/corelib/plugin/quuid.h @@ -48,7 +48,8 @@ class Q_CORE_EXPORT QUuid Md5 = 3, // 0 0 1 1 Name = Md5, Random = 4, // 0 1 0 0 - Sha1 = 5 // 0 1 0 1 + Sha1 = 5, // 0 1 0 1 + UnixEpoch = 7, // 0 1 1 1 }; enum StringFormat { @@ -277,6 +278,26 @@ QT_WARNING_POP return QUuid::createUuidV5(ns, qToByteArrayViewIgnoringNull(baseData.toUtf8())); } + static QUuid createUuidV7(); + +private: + static constexpr bool isKnownVersion(Version v) noexcept + { + switch (v) { + case VerUnknown: + return false; + case Time: + case EmbeddedPOSIX: + case Md5: + case Random: + case Sha1: + case UnixEpoch: + return true; + } + return false; + } + +public: #if QT_CORE_REMOVED_SINCE(6, 9) QUuid::Variant variant() const noexcept; QUuid::Version version() const noexcept; @@ -296,7 +317,7 @@ QT_WARNING_POP // Check the 4 MSB of data3 const Version ver = Version(data3 >> 12); // Check that variant() == DCE and version is in a valid range - if (ver >= Time && ver <= Sha1 && (data4[0] & 0xC0) == 0x80) + if (isKnownVersion(ver) && (data4[0] & 0xC0) == 0x80) return ver; return VerUnknown; } diff --git a/src/corelib/plugin/quuid_p.h b/src/corelib/plugin/quuid_p.h new file mode 100644 index 00000000000..871fc57195d --- /dev/null +++ b/src/corelib/plugin/quuid_p.h @@ -0,0 +1,61 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// Copyright (C) 2021 Intel Corporation. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QUUID_P_H +#define QUUID_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the QLibrary class. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include "quuid.h" + +#include + +#include + +QT_BEGIN_NAMESPACE + +#ifndef QT_BOOTSTRAPPED +static inline QUuid createUuidV7_internal( + std::chrono::time_point tp) +{ + QUuid result; + + using namespace std::chrono; + const nanoseconds nsecSinceEpoch = tp.time_since_epoch(); + const auto msecSinceEpoch = floor(nsecSinceEpoch); + const quint64 frac = (nsecSinceEpoch - msecSinceEpoch).count(); + // Lower 48 bits of the timestamp + const quint64 msecs = quint64(msecSinceEpoch.count()) & 0xffffffffffff; + result.data1 = uint(msecs >> 16); + result.data2 = ushort(msecs); + // rand_a: use a 12-bit sub-millisecond timestamp for additional monotonicity + // https://datatracker.ietf.org/doc/html/rfc9562#monotonicity_counters (Method 3) + + // "frac" is a number between 0 and 999,999, so the lowest 20 bits + // should be roughly random. Use the high 12 of those for additional + // monotonicity. + result.data3 = frac >> 8; + result.data3 &= 0x0FFF; + result.data3 |= ushort(7) << 12; + + // rand_b: 62 bits of random data (64 - 2 bits for the variant) + const quint64 random = QRandomGenerator::system()->generate64(); + memcpy(result.data4, &random, sizeof(quint64)); + result.data4[0] = (result.data4[0] & 0x3F) | 0x80; // UV_DCE + return result; +} +#endif + +QT_END_NAMESPACE + +#endif // QUUID_P_H diff --git a/tests/auto/corelib/plugin/quuid/tst_quuid.cpp b/tests/auto/corelib/plugin/quuid/tst_quuid.cpp index 655c0965785..531200ed041 100644 --- a/tests/auto/corelib/plugin/quuid/tst_quuid.cpp +++ b/tests/auto/corelib/plugin/quuid/tst_quuid.cpp @@ -11,11 +11,15 @@ #include #include +#include #ifdef Q_OS_ANDROID #include #endif +using namespace std::chrono_literals; +using namespace Qt::StringLiterals; + class tst_QUuid : public QObject { Q_OBJECT @@ -35,6 +39,9 @@ private slots: void id128(); void uint128(); void createUuidV3OrV5(); + void createUuidV7_unique(); + void createUuidV7_data(); + void createUuidV7(); void check_QDataStream(); void isNull(); void equal(); @@ -323,6 +330,51 @@ void tst_QUuid::createUuidV3OrV5() QT_TEST_EQUALITY_OPS(uuidD, QUuid::createUuidV5(uuidNS, QString("www.widgets.com")), true); } +void tst_QUuid::createUuidV7_unique() +{ + const int count = 1000; + std::vector vec; + vec.reserve(count); + for (int i = 0; i < count; ++i) { + auto id = QUuid::createUuidV7(); + QCOMPARE(id.version(), QUuid::UnixEpoch); + QCOMPARE(id.variant(), QUuid::DCE); + vec.push_back(id); + } + + QVERIFY(std::unique(vec.begin(), vec.end()) == vec.end()); +} + +void tst_QUuid::createUuidV7_data() +{ + QTest::addColumn("dt"); + QTest::addColumn("expected"); + + // February 22, 2022 2:22:22.00 PM GMT-05:00, example from: + // https://datatracker.ietf.org/doc/html/rfc9562#name-example-of-a-uuidv7-value + QTest::newRow("feb2022") + << QDateTime::fromString("2022-02-22T14:22:22.00-05:00"_L1, Qt::ISODateWithMs) + << QUuid::fromString("017F22E2-79B0-7CC3-98C4-DC0C0C07398F"_L1); + + QTest::newRow("jan2000") + << QDateTime::fromString("2000-01-02T14:22:22.00-05:00"_L1, Qt::ISODateWithMs) + << QUuid("00dc741e-35b0-7643-947d-0380e108ce80"_L1); +} + +void tst_QUuid::createUuidV7() +{ + QFETCH(QDateTime, dt); + QFETCH(QUuid, expected); + + QVERIFY(dt.isValid()); + + using namespace std::chrono; + auto extractTimestamp = [](const QUuid &id) { return (quint64(id.data1) << 16) | id.data2; }; + const auto result = + createUuidV7_internal(time_point(dt.toMSecsSinceEpoch() * 1ms)); + QCOMPARE_EQ(extractTimestamp(result), extractTimestamp(expected)); +} + void tst_QUuid::check_QDataStream() { QUuid tmp; @@ -618,8 +670,8 @@ void tst_QUuid::versions_data() QTest::newRow("DCE-inv-less-than-Time->unknown") << QUuid(0, 0, 0b0000'1101'0101'1011, 0b1000'0000, 0, 0, 0, 0, 0, 0, 0) << QUuid::VerUnknown; - QTest::newRow("DCE-inv-greater-than-Sha1->unknown") - << QUuid(0, 0, 0b0111'1101'0101'1011, 0b1000'0000, 0, 0, 0, 0, 0, 0, 0) + QTest::newRow("DCE-inv-greater-than-UnixEpoch->unknown") + << QUuid(0, 0, 0b1000'1101'0101'1011, 0b1000'0000, 0, 0, 0, 0, 0, 0, 0) << QUuid::VerUnknown; QTest::newRow("NCS-Time->unknown") << QUuid(0, 0, 0b0001'0000'0000'0000, 0b0100'0000, 0, 0, 0, 0, 0, 0, 0)