Skip to content

Commit

Permalink
feat: Support v6 UUIDs as specified by RFC 9562
Browse files Browse the repository at this point in the history
  • Loading branch information
BjoernPetersen committed Jun 24, 2024
1 parent ca03a8d commit b8b704e
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 26 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

A properly designed, efficient UUID library for Dart.

- Supports v1, v4, and v5 generation
- Immutable `Uuid` type with equality, comparison and accessors for properties defined by RFC4122
- Supports v1, v4, v5 and v6 generation
- Immutable `Uuid` type with equality, comparison and accessors for properties defined by RFC 4122
- The internal representation of the UUID is a byte array, not a String
- Support for all syntactically correct UUIDs (regardless of RFC4122 semantics)

Expand Down
53 changes: 43 additions & 10 deletions lib/src/generation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import 'package:sane_uuid/src/uuid_base.dart';

const _variant = 2 << 6;

final class Uuid1Generator {
static const _version = 1 << 12;
sealed class GregorianUuidGenerator {
static final _random = Late(() => Random.secure());
static DateTime? _lastTime;
static int? _clockSequence;
static int? _node;
DateTime? _lastTime;
int? _clockSequence;
int? _node;

int _generateClockSequence() {
final random = _random.value;
Expand Down Expand Up @@ -51,12 +50,16 @@ final class Uuid1Generator {
return _node ??= _generateNode();
}

@nonVirtual
@visibleForTesting
void setClockSequenceToZero() {
_clockSequence = 0;
void setClockSequence([int value = 0]) {
_clockSequence = value;
_lastTime = DateTime.fromMillisecondsSinceEpoch(0);
}

void _setTimestampAndVersion(ByteData builder, int timestamp);

@nonVirtual
Uint8List generate({DateTime? time, int? nodeId}) {
final utcTime = (time ?? DateTime.now()).toUtc();
final clockSequence = _updateClockSequence(utcTime);
Expand All @@ -72,9 +75,7 @@ final class Uuid1Generator {
final node = nodeId ?? _getNode();

final builder = ByteData(16);
builder.setUint32(0, timestamp & 0xFFFFFFFF);
builder.setUint16(4, (timestamp >> 32) & 0xFFFF);
builder.setUint16(6, (timestamp >> 48) + _version);
_setTimestampAndVersion(builder, timestamp);

builder.setUint8(8, (clockSequence >> 8) + _variant);
builder.setUint8(9, clockSequence & 0xFF);
Expand All @@ -85,6 +86,38 @@ final class Uuid1Generator {
}
}

final class Uuid1Generator extends GregorianUuidGenerator {
static const _version = 1 << 12;
static final _instance = Late(() => Uuid1Generator._());

Uuid1Generator._();

factory Uuid1Generator() => _instance.value;

@override
void _setTimestampAndVersion(ByteData builder, int timestamp) {
builder.setUint32(0, timestamp & 0xFFFFFFFF);
builder.setUint16(4, (timestamp >> 32) & 0xFFFF);
builder.setUint16(6, (timestamp >> 48) + _version);
}
}

final class Uuid6Generator extends GregorianUuidGenerator {
static const _version = 6 << 12;
static final _instance = Late(() => Uuid6Generator._());

Uuid6Generator._();

factory Uuid6Generator() => _instance.value;

@override
void _setTimestampAndVersion(ByteData builder, int timestamp) {
builder.setUint32(0, timestamp >> 28);
builder.setUint16(4, (timestamp >> 12) & 0xFFFF);
builder.setUint16(6, (timestamp & 0xFFF) + _version);
}
}

/// Builds UUID bytes by reading from [getByte] and multiplexing the variant
/// and [version] in the appropriate places.
Uint8List _buildBytes({
Expand Down
62 changes: 49 additions & 13 deletions lib/src/uuid_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ enum UuidVariant {
/// Reserved, NCS backward compatibility.
ncsReserved,

/// The variant specified by RFC 4122.
/// The variant specified by RFC 9562. The constant is still named after the
/// obsoleted older RFC 4122 that originally specified UUIDs.
rfc4122,

/// Reserved, Microsoft Corporation backward compatibility.
Expand Down Expand Up @@ -54,6 +55,8 @@ final class Uuid implements Comparable<Uuid> {
/// This field will be random for v4 UUIDs.
///
/// Note that the full timestamp can be retrieved using [time].
///
/// **Warning:** This accessor is not accurate for v6 UUIDs.
int get timeLow => _byteData.getUint32(0);

/// The mid field of the timestamp, i.e. octets 4-5.
Expand All @@ -67,17 +70,21 @@ final class Uuid implements Comparable<Uuid> {
/// version bits will be 4 (`0b0100`).
///
/// Note that the full timestamp can be retrieved using [time].
///
/// **Warning:** This accessor is not accurate for v6 UUIDs.
int get timeHighAndVersion => _byteData.getUint16(6);

/// The high field of the timestamp without the version number that was
/// originally multiplexed into [timeHighAndVersion].
///
/// Note that the full timestamp can be retrieved using [time].
///
/// **Warning:** This accessor is not accurate for v6 UUIDs.
int get timeHigh => timeHighAndVersion & 0x0FFF;

/// The full "timestamp" of the UUID.
/// Note that [parsedTime] provides a parsed version of this timestamp,
/// albeit only for v1 UUIDs.
/// albeit only for v1 and v6 UUIDs.
///
/// ## Definition from RFC 4122
///
Expand All @@ -99,20 +106,27 @@ final class Uuid implements Comparable<Uuid> {
/// For UUID version 4, the timestamp is a randomly or pseudo-randomly
/// generated 60-bit value, as described in
/// [Section 4.4](https://tools.ietf.org/html/rfc4122#section-4.4).
int get time => (timeHigh << 48) + (timeMid << 32) + timeLow;
int get time {
if (version != 6) {
return (timeHigh << 48) + (timeMid << 32) + timeLow;
} else {
// Cheating a bit by using the wrongly named accessors here.
return (timeLow << 28) + (timeMid << 12) + timeHigh;
}
}

/// The parsed [time] as a usable [DateTime] object.
///
/// This method is only useful for v1 UUIDs, because the [time] field has
/// different semantics for other version.
/// This method is only useful for v1 and v6 UUIDs, because the [time] field
/// has different semantics for other version.
///
/// Throws a [StateError] if [variant] is not [UuidVariant.rfc4122] or
/// [version] is not 1.
/// [version] is not 1 or 6.
DateTime get parsedTime {
if (variant != UuidVariant.rfc4122) {
throw StateError('Only available for RFC 4122 UUIDs');
} else if (version != 1) {
throw StateError('Only available for v1 UUIDs');
} else if (version != 1 && version != 6) {
throw StateError('Only available for v1 and v6 UUIDs');
}
// time is the count of 100-nanosecond intervals
// since 00:00:00.00, 15 October 1582.
Expand All @@ -125,14 +139,18 @@ final class Uuid implements Comparable<Uuid> {

/// The version that was originally multiplexed in [timeHighAndVersion].
///
/// If this UUID conforms to the structure laid out in RFC 4122, this will
/// be a number between 1 and 5 with the following descriptions:
/// If this UUID conforms to the structure laid out in RFC 9562 (obsoletes
/// RFC 4122), this will be a number between 1 and 8 with the following
/// descriptions:
///
/// - 1: Time-based version
/// - 2: DCE Security version, with embedded POSIX UIDs
/// - 1: Gregorian time-based version
/// - 2: DCE Security version, with embedded POSIX UUIDs
/// - 3: Name-based version with MD5 hashing
/// - 4: Random version
/// - 4: Randomly generated version
/// - 5: Name-based version with SHA-1 hashing
/// - 6: Reordered Gregorian time-based version
/// - 7: Unix Epoch time-based version
/// - 8: Custom formats specified by RFC 9562
int get version => timeHighAndVersion >> 12;

/// The high field of the clock sequence multiplexed with the variant.
Expand Down Expand Up @@ -236,6 +254,24 @@ final class Uuid implements Comparable<Uuid> {
return Uuid._fromValidBytes(bytes);
}

/// Generates a v6 (time-based) UUID as defined by RFC 9562.
///
/// The implementation behaves exactly like the [v1] implementation, just with
/// the timestamp parts reordered so UUIDs are automatically sorted by time.
///
/// The default implementation doesn't use a real MAC address as a node ID.
/// Instead it generates a random node ID and sets the "multi-cast bit"
/// as recommended by RFC 4122. A generated node ID will be kept in-memory
/// and reused during the lifetime of a process, but won't be persisted.
///
/// Instead of using a generated node ID, you may specify one using [nodeId].
/// If the given node ID is larger than 48-bit, an [ArgumentError] is thrown.
factory Uuid.v6({int? nodeId}) {
final bytes = Uuid6Generator().generate(nodeId: nodeId);
// We trust our own generator not to modify the bytes anymore.
return Uuid._fromValidBytes(bytes);
}

/// Parses a UUID from the given String.
///
/// The following forms are accepted:
Expand Down
2 changes: 1 addition & 1 deletion test/generation/uuid1_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ void main() {
test('matches reference', () async {
final time = DateTime.utc(2024, 06, 24, 1, 2, 3, 4, 5);
final generator = Uuid1Generator();
generator.setClockSequenceToZero();
generator.setClockSequence();

final uuid = Uuid.fromBytes(generator.generate(
time: time,
Expand Down
76 changes: 76 additions & 0 deletions test/generation/uuid6_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:sane_uuid/src/generation.dart';
import 'package:sane_uuid/uuid.dart';
import 'package:test/test.dart';

void main() {
group('UUIDv6 generation', () {
test('random node ID is stable', () {
final uuid = Uuid.v6();
final uuid2 = Uuid.v6();
expect(uuid, isNot(uuid2));
expect(uuid.node, uuid2.node);
});

group('using specific node ID', () {
test('that is too long', () {
// 49 bit long
final nodeId = 0x1000000000000;
expect(() => Uuid.v6(nodeId: nodeId), throwsArgumentError);
});

test('with leading zero', () {
// 47 bit long
final nodeId = 0x010000000000;
final uuid = Uuid.v6(nodeId: nodeId);
expect(uuid.node, nodeId);
expect(uuid.toString(), endsWith('-010000000000'));
});

test('with exactly 48 bit', () {
// 48 bit long
final nodeId = 0xF123456789AB;
final uuid = Uuid.v6(nodeId: nodeId);
expect(uuid.node, nodeId);
expect(uuid.toString(), endsWith('-f123456789ab'));
});
});

group('generator', () {
test('changes clock sequence if clock drifts backward', () async {
final time = DateTime.now();
await Future.delayed(const Duration(seconds: 1));
final laterTime = DateTime.now();
final uuid = Uuid.fromBytes(Uuid6Generator().generate(
time: laterTime,
nodeId: 0,
));
final uuid2 = Uuid.fromBytes(Uuid6Generator().generate(
time: time,
nodeId: 0,
));

expect(uuid, isNot(uuid2));
expect(uuid.node, uuid2.node);
expect(uuid.clockSequence, isNot(uuid2.clockSequence));
});

test('matches reference', () async {
final time = DateTime.utc(2022, 2, 22, 19, 22, 22, 0, 0);
final generator = Uuid6Generator();
generator.setClockSequence(0x33C8);

final uuid = Uuid.fromBytes(generator.generate(
time: time,
nodeId: 0x9F6BDECED846,
));

// Test vector from RFC 9562
final reference = Uuid.fromString(
'1EC9414C-232A-6B00-B3C8-9F6BDECED846',
);
expect(uuid, reference);
expect(uuid.parsedTime, time);
});
});
});
}

0 comments on commit b8b704e

Please sign in to comment.