blob: 32de06095f8675ae38a84bd29d52126e28c0aa0b [file] [log] [blame]
/// Generates UUID v1, v4, v5 following RFC4122 standard.
library uuid;
import 'dart:typed_data';
import 'uuid_util.dart';
import 'package:crypto/crypto.dart' as crypto;
/// uuid for Dart
/// Author: Yulian Kuncheff
/// Released under MIT License.
class Uuid {
// RFC4122 provided namespaces for v3 and v5 namespace based UUIDs
static const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
static const NAMESPACE_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
static const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8';
static const NAMESPACE_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8';
static const NAMESPACE_NIL = '00000000-0000-0000-0000-000000000000';
// Easy number <-> hex conversion
static final List<String> _byteToHex = List<String>.generate(256, (i) {
return i.toRadixString(16).padLeft(2, '0');
});
final Map<String, dynamic>? options;
static final _stateExpando = Expando<Map<String, dynamic>>();
Map<String, dynamic> get _state => _stateExpando[this] ??= {
'seedBytes': null,
'node': null,
'clockSeq': null,
'mSecs': 0,
'nSecs': 0,
'hasInitV1': false,
'hasInitV4': false
};
const Uuid({this.options});
/// Validates the provided [uuid] to make sure it has all the necessary
/// components and formatting and returns a [bool]
/// You can choose to validate from a string or from a byte list based on
/// which parameter is passed.
static bool isValidUUID(
{String fromString = '',
Uint8List? fromByteList,
ValidationMode validationMode = ValidationMode.strictRFC4122}) {
if (fromByteList != null) {
fromString = unparse(fromByteList);
}
// UUID of all 0s is ok.
if (fromString == NAMESPACE_NIL) {
return true;
}
// If its not 36 characters in length, don't bother (including dashes).
if (fromString.length != 36) {
return false;
}
// Make sure if it passes the above, that it's a valid UUID or GUID.
switch (validationMode) {
case ValidationMode.strictRFC4122:
{
const pattern =
r'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
final regex = RegExp(pattern, caseSensitive: false, multiLine: true);
final match = regex.hasMatch(fromString);
return match;
}
case ValidationMode.nonStrict:
{
const pattern =
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$';
final regex = RegExp(pattern, caseSensitive: false, multiLine: true);
final match = regex.hasMatch(fromString);
return match;
}
default:
{
throw Exception('`$validationMode` is an invalid ValidationMode.');
}
}
}
static void isValidOrThrow(
{String fromString = '',
Uint8List? fromByteList,
ValidationMode validationMode = ValidationMode.strictRFC4122}) {
final isValid = isValidUUID(
fromString: fromString,
fromByteList: fromByteList,
validationMode: validationMode);
if (!isValid) {
// let's check if it is a non RFC4122 uuid and help the developer
if (validationMode != ValidationMode.nonStrict) {
final isValidNonStrict = isValidUUID(
fromString: fromString,
fromByteList: fromByteList,
validationMode: ValidationMode.nonStrict);
if (isValidNonStrict) {
throw FormatException(
'The provided UUID is not RFC4122 compliant. It seems you might be using a Microsoft GUID. Try setting `validationMode = ValidationMode.nonStrict`',
fromString);
}
}
throw FormatException('The provided UUID is invalid.', fromString);
}
}
/// Parses the provided [uuid] into a list of byte values as a List<int>.
///
/// Can optionally be provided a [buffer] to write into and
/// a positional [offset] for where to start inputting into the buffer.
///
/// Returns the buffer containing the bytes. If no buffer was provided,
/// a new buffer is created and returned. If a _buffer_ was provided, it
/// is returned (even if the uuid bytes are not placed at the beginning of
/// that buffer).
///
/// Throws FormatException if the UUID is invalid. Optionally you can set
/// [validate] to false to disable validation of the UUID before parsing.
///
/// Throws _RangeError_ if a _buffer_ is provided and it is too small.
/// It is also thrown if a non-zero _offset_ is provided without providing
/// a _buffer_.
static List<int> parse(
String uuid, {
List<int>? buffer,
int offset = 0,
bool validate = true,
ValidationMode validationMode = ValidationMode.strictRFC4122,
}) {
if (validate) {
isValidOrThrow(fromString: uuid, validationMode: validationMode);
}
var i = offset, ii = 0;
// Get buffer to store the result
if (buffer == null) {
// Buffer not provided: create a 16 item buffer
if (offset != 0) {
throw RangeError('non-zero offset without providing a buffer');
}
buffer = Uint8List(16);
} else {
// Buffer provided: check it is large enough
if (buffer.length - offset < 16) {
throw RangeError('buffer too small: need 16: length=${buffer.length}'
'${offset != 0 ? ', offset=$offset' : ''}');
}
}
// Convert to lowercase and replace all hex with bytes then
// string.replaceAll() does a lot of work that I don't need, and a manual
// regex gives me more control.
final regex = RegExp('[0-9a-f]{2}');
for (Match match in regex.allMatches(uuid.toLowerCase())) {
if (ii < 16) {
var hex = uuid.toLowerCase().substring(match.start, match.end);
buffer[i + ii++] = int.parse(hex, radix: 16);
}
}
// Zero out any left over bytes if the string was too short.
while (ii < 16) {
buffer[i + ii++] = 0;
}
return buffer;
}
///Parses the provided [uuid] into a list of byte values as a Uint8List.
/// Can optionally be provided a [buffer] to write into and
/// a positional [offset] for where to start inputting into the buffer.
/// Throws FormatException if the UUID is invalid. Optionally you can set
/// [validate] to false to disable validation of the UUID before parsing.
static Uint8List parseAsByteList(String uuid,
{List<int>? buffer,
int offset = 0,
bool validate = true,
ValidationMode validationMode = ValidationMode.strictRFC4122}) {
return Uint8List.fromList(parse(uuid,
buffer: buffer,
offset: offset,
validate: validate,
validationMode: validationMode));
}
/// Unparses a [buffer] of bytes and outputs a proper UUID string.
/// An optional [offset] is allowed if you want to start at a different point
/// in the buffer.
///
/// Throws a [RangeError] exception if the _buffer_ is not large enough to
/// hold the bytes. That is, if the length of the _buffer_ after the _offset_
/// is less than 16.
static String unparse(List<int> buffer, {int offset = 0}) {
if (buffer.length - offset < 16) {
throw RangeError('buffer too small: need 16: length=${buffer.length}'
'${offset != 0 ? ', offset=$offset' : ''}');
}
var i = offset;
return '${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}'
'${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}-'
'${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}-'
'${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}-'
'${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}-'
'${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}'
'${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}'
'${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}';
}
void _initV1() {
final options = this.options ?? const {};
if (!(_state['hasInitV1']! as bool)) {
var v1PositionalArgs = (options['v1rngPositionalArgs'] != null)
? options['v1rngPositionalArgs']
: [];
var v1NamedArgs = (options['v1rngNamedArgs'] != null)
? options['v1rngNamedArgs'] as Map<Symbol, dynamic>
: const <Symbol, dynamic>{};
Uint8List seedBytes = (options['v1rng'] != null)
? Function.apply(options['v1rng'], v1PositionalArgs, v1NamedArgs)
: UuidUtil.mathRNG();
(_state['seedBytes'] != null)
? _state['seedBytes']
: _state['seedBytes'] = seedBytes;
// Per 4.5, create a 48-bit node id (47 random bits + multicast bit = 1)
var nodeId = [
seedBytes[0] | 0x01,
seedBytes[1],
seedBytes[2],
seedBytes[3],
seedBytes[4],
seedBytes[5]
];
(_state['node'] != null) ? _state['node'] : _state['node'] = nodeId;
// Per 4.2.2, randomize (14 bit) clockseq
var clockSeq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3ffff;
_state['clockSeq'] ??= clockSeq;
_state['mSecs'] = 0;
_state['nSecs'] = 0;
_state['hasInitV1'] = true;
}
}
void _initV4() {
final options = this.options ?? const {};
if (!(_state['hasInitV4']! as bool)) {
// Set the globalRNG function to mathRNG with the option to set an alternative globally
var gPositionalArgs = (options['gPositionalArgs'] != null)
? options['gPositionalArgs']
: const [];
var gNamedArgs = (options['gNamedArgs'] != null)
? options['gNamedArgs'] as Map<Symbol, dynamic>
: const <Symbol, dynamic>{};
final grng = options['grng'];
_state['globalRNG'] = (grng != null)
? () => Function.apply(grng, gPositionalArgs, gNamedArgs)
: UuidUtil.mathRNG;
_state['hasInitV4'] = true;
}
}
/// v1() Generates a time-based version 1 UUID
///
/// By default it will generate a string based off current time, and will
/// return a string.
///
/// The first argument is an options map that takes various configuration
/// options detailed in the readme.
///
/// http://tools.ietf.org/html/rfc4122.html#section-4.2.2
String v1({Map<String, dynamic>? options}) {
var i = 0;
var buf = Uint8List(16);
options ??= {};
_initV1();
var clockSeq = options['clockSeq'] != null
? options['clockSeq'] as int
: _state['clockSeq'] as int;
// UUID timestamps are 100 nano-second units since the Gregorian epoch,
// (1582-10-15 00:00). Time is handled internally as 'msecs' (integer
// milliseconds) and 'nsecs' (100-nanoseconds offset from msecs) since unix
// epoch, 1970-01-01 00:00.
var mSecs = (options['mSecs'] != null)
? (options['mSecs'] as int)
: DateTime.now().millisecondsSinceEpoch;
// Per 4.2.1.2, use count of uuid's generated during the current clock
// cycle to simulate higher resolution clock
var nSecs = options['nSecs'] != null
? (options['nSecs'] as int)
: (_state['nSecs']! as int) + 1;
// Time since last uuid creation (in msecs)
var dt = (mSecs - _state['mSecs']) + (nSecs - _state['nSecs']) / 10000;
// Per 4.2.1.2, Bump clockseq on clock regression
if (dt < 0 && options['clockSeq'] == null) {
clockSeq = clockSeq + 1 & 0x3fff;
}
// Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
// time interval
if ((dt < 0 || mSecs > _state['mSecs']) && options['nSecs'] == null) {
nSecs = 0;
}
// Per 4.2.1.2 Throw error if too many uuids are requested
if (nSecs >= 10000) {
throw Exception('uuid.v1(): Can\'t create more than 10M uuids/sec');
}
_state['mSecs'] = mSecs;
_state['nSecs'] = nSecs;
_state['clockSeq'] = clockSeq;
// Per 4.1.4 - Convert from unix epoch to Gregorian epoch
mSecs += 12219292800000;
// time Low
var tl = ((mSecs & 0xfffffff) * 10000 + nSecs) % 0x100000000;
buf[i++] = tl >> 24 & 0xff;
buf[i++] = tl >> 16 & 0xff;
buf[i++] = tl >> 8 & 0xff;
buf[i++] = tl & 0xff;
// time mid
var tmh = (mSecs / 0x100000000 * 10000).floor() & 0xfffffff;
buf[i++] = tmh >> 8 & 0xff;
buf[i++] = tmh & 0xff;
// time high and version
buf[i++] = tmh >> 24 & 0xf | 0x10; // include version
buf[i++] = tmh >> 16 & 0xff;
// clockSeq high and reserved (Per 4.2.2 - include variant)
buf[i++] = (clockSeq & 0x3F00) >> 8 | 0x80;
// clockSeq low
buf[i++] = clockSeq & 0xff;
// node
var node = options['node'] != null
? options['node'] as List
: _state['node'] as List;
for (var n = 0; n < 6; n++) {
buf[i + n] = node[n];
}
return unparse(buf);
}
/// v1buffer() Generates a time-based version 1 UUID
///
/// By default it will generate a string based off current time, and will
/// place the result into the provided [buffer]. The [buffer] will also be returned..
///
/// Optionally an [offset] can be provided with a start position in the buffer.
///
/// The first argument is an options map that takes various configuration
/// options detailed in the readme.
///
/// http://tools.ietf.org/html/rfc4122.html#section-4.2.2
List<int> v1buffer(
List<int> buffer, {
Map<String, dynamic>? options,
int offset = 0,
}) {
return parse(v1(options: options), buffer: buffer, offset: offset);
}
/// v1obj() Generates a time-based version 1 UUID
///
/// By default it will generate a string based off current time, and will
/// return it as a [UuidValue] object.
///
/// The first argument is an options map that takes various configuration
/// options detailed in the readme.
///
/// http://tools.ietf.org/html/rfc4122.html#section-4.2.2
UuidValue v1obj({Map<String, dynamic>? options}) {
var uuid = v1(options: options);
return UuidValue(uuid);
}
/// v4() Generates a RNG version 4 UUID
///
/// By default it will generate a string based mathRNG, and will return
/// a string. If you wish to use crypto-strong RNG, pass in UuidUtil.cryptoRNG
///
/// The first argument is an options map that takes various configuration
/// options detailed in the readme.
///
/// http://tools.ietf.org/html/rfc4122.html#section-4.4
String v4({Map<String, dynamic>? options}) {
options ??= {};
_initV4();
// Use the built-in RNG or a custom provided RNG
var positionalArgs =
(options['positionalArgs'] != null) ? options['positionalArgs'] : [];
var namedArgs = (options['namedArgs'] != null)
? options['namedArgs'] as Map<Symbol, dynamic>
: const <Symbol, dynamic>{};
// We cast to 'dynamic Function()' below instead of 'List<int> Function()'
// as existing code may not return a closure of the correct type.
var rng = (options['rng'] != null)
? Function.apply(options['rng'], positionalArgs, namedArgs) as List<int>
: (_state['globalRNG']! as dynamic Function())() as List<int>;
// Use provided values over RNG
var rnds = options['random'] != null ? options['random'] as List<int> : rng;
// per 4.4, set bits for version and clockSeq high and reserved
rnds[6] = (rnds[6] & 0x0f) | 0x40;
rnds[8] = (rnds[8] & 0x3f) | 0x80;
return unparse(rnds);
}
/// v4buffer() Generates a RNG version 4 UUID
///
/// By default it will generate a string based off mathRNG, and will
/// place the result into the provided [buffer]. The [buffer] will also be returned.
/// If you wish to have crypto-strong RNG, pass in UuidUtil.cryptoRNG.
///
/// Optionally an [offset] can be provided with a start position in the buffer.
///
/// The first argument is an options map that takes various configuration
/// options detailed in the readme.
///
/// http://tools.ietf.org/html/rfc4122.html#section-4.4
List<int> v4buffer(
List<int> buffer, {
Map<String, dynamic>? options,
int offset = 0,
}) {
return parse(v4(options: options), buffer: buffer, offset: offset);
}
/// v4obj() Generates a RNG version 4 UUID
///
/// By default it will generate a string based mathRNG, and will return
/// a [UuidValue] object. If you wish to use crypto-strong RNG, pass in UuidUtil.cryptoRNG
///
/// The first argument is an options map that takes various configuration
/// options detailed in the readme.
///
/// http://tools.ietf.org/html/rfc4122.html#section-4.4
UuidValue v4obj({Map<String, dynamic>? options}) {
var uuid = v4(options: options);
return UuidValue(uuid);
}
/// v5() Generates a namespace & name-based version 5 UUID
///
/// By default it will generate a string based on a provided uuid namespace and
/// name, and will return a string.
///
/// The first argument is an options map that takes various configuration
/// options detailed in the readme.
///
/// http://tools.ietf.org/html/rfc4122.html#section-4.4
String v5(String? namespace, String? name, {Map<String, dynamic>? options}) {
options ??= {};
// Check if user wants a random namespace generated by v4() or a NIL namespace.
var useRandom = (options['randomNamespace'] != null)
? options['randomNamespace']
: true;
// If useRandom is true, generate UUIDv4, else use NIL
var blankNS = useRandom ? v4() : NAMESPACE_NIL;
// Use provided namespace, or use whatever is decided by options.
namespace = (namespace != null) ? namespace : blankNS;
// Use provided name,
name = (name != null) ? name : '';
// Convert namespace UUID to Byte List
var bytes = parse(namespace);
// Convert name to a list of bytes
var nameBytes = <int>[];
for (var singleChar in name.codeUnits) {
nameBytes.add(singleChar);
}
// Generate SHA1 using namespace concatenated with name
var hashBytes = crypto.sha1.convert([...bytes, ...nameBytes]).bytes;
// per 4.4, set bits for version and clockSeq high and reserved
hashBytes[6] = (hashBytes[6] & 0x0f) | 0x50;
hashBytes[8] = (hashBytes[8] & 0x3f) | 0x80;
return unparse(hashBytes.sublist(0, 16));
}
/// v5buffer() Generates a RNG version 4 UUID
///
/// By default it will generate a string based off current time, and will
/// place the result into the provided [buffer]. The [buffer] will also be returned..
///
/// Optionally an [offset] can be provided with a start position in the buffer.
///
/// The first argument is an options map that takes various configuration
/// options detailed in the readme.
///
/// http://tools.ietf.org/html/rfc4122.html#section-4.4
List<int> v5buffer(
String? namespace,
String? name,
List<int>? buffer, {
Map<String, dynamic>? options,
int offset = 0,
}) {
return parse(v5(namespace, name, options: options),
buffer: buffer, offset: offset);
}
/// v5obj() Generates a namspace & name-based version 5 UUID
///
/// By default it will generate a string based on a provided uuid namespace and
/// name, and will return a [UuidValue] object.
///
/// The first argument is an options map that takes various configuration
/// options detailed in the readme.
///
/// http://tools.ietf.org/html/rfc4122.html#section-4.4
UuidValue v5obj(String? namespace, String? name,
{Map<String, dynamic>? options}) {
var uuid = v5(namespace, name, options: options);
return UuidValue(uuid);
}
}
enum ValidationMode { nonStrict, strictRFC4122 }
class UuidValue {
final String uuid;
/// UuidValue() Constructor for creating a uuid value.
///
/// Takes in a string representation of a [uuid] to wrap.
///
/// Optionally , you can disable the validation check in the constructor
/// by setting [validate] to `false`.
factory UuidValue(String uuid,
[bool validate = true,
ValidationMode validationMode = ValidationMode.strictRFC4122]) {
if (validate) {
Uuid.isValidOrThrow(fromString: uuid, validationMode: validationMode);
}
return UuidValue._(uuid.toLowerCase());
}
factory UuidValue.fromByteList(Uint8List byteList, {int? offset}) {
return UuidValue(Uuid.unparse(byteList, offset: offset ?? 0));
}
factory UuidValue.fromList(List<int> byteList, {int? offset}) {
return UuidValue(Uuid.unparse(byteList, offset: offset ?? 0));
}
UuidValue._(this.uuid);
// toBytes() converts the internal string representation to a list of bytes.
Uint8List toBytes() {
return Uuid.parseAsByteList(uuid);
}
// toString() returns the String representation of the UUID
@override
String toString() {
return uuid;
}
// equals() compares to UuidValue objects' uuids.
bool equals(UuidValue other) {
return uuid == other.uuid;
}
@override
bool operator ==(Object other) => other is UuidValue && uuid == other.uuid;
@override
int get hashCode => uuid.hashCode;
}