Skip to content

Commit

Permalink
QUuid: add support for creating UUID v7
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
Ahmad Samir committed Dec 7, 2024
1 parent 84a5f50 commit d89cef4
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 8 deletions.
2 changes: 1 addition & 1 deletion src/corelib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 25 additions & 3 deletions src/corelib/plugin/quuid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
*/

/*!
Expand All @@ -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()
*/

/*!
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*/

/*!
Expand Down
25 changes: 23 additions & 2 deletions src/corelib/plugin/quuid.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
61 changes: 61 additions & 0 deletions src/corelib/plugin/quuid_p.h
Original file line number Diff line number Diff line change
@@ -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 <QtCore/qrandom.h>

#include <chrono>

QT_BEGIN_NAMESPACE

#ifndef QT_BOOTSTRAPPED
static inline QUuid createUuidV7_internal(
std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> tp)
{
QUuid result;

using namespace std::chrono;
const nanoseconds nsecSinceEpoch = tp.time_since_epoch();
const auto msecSinceEpoch = floor<milliseconds>(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
56 changes: 54 additions & 2 deletions tests/auto/corelib/plugin/quuid/tst_quuid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@

#include <qcoreapplication.h>
#include <quuid.h>
#include <QtCore/private/quuid_p.h>

#ifdef Q_OS_ANDROID
#include <QStandardPaths>
#endif

using namespace std::chrono_literals;
using namespace Qt::StringLiterals;

class tst_QUuid : public QObject
{
Q_OBJECT
Expand All @@ -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();
Expand Down Expand Up @@ -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<QUuid> 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<QDateTime>("dt");
QTest::addColumn<QUuid>("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<system_clock, milliseconds>(dt.toMSecsSinceEpoch() * 1ms));
QCOMPARE_EQ(extractTimestamp(result), extractTimestamp(expected));
}

void tst_QUuid::check_QDataStream()
{
QUuid tmp;
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit d89cef4

Please sign in to comment.