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 <thiago.macieira@intel.com>
This commit is contained in:
Ahmad Samir 2024-11-29 18:29:16 +02:00
parent 84a5f50c77
commit d89cef439f
5 changed files with 164 additions and 8 deletions

View File

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

View File

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

View File

@ -48,7 +48,8 @@ public:
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 @@ public:
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 @@ public:
// 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;
}

View File

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

View File

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