1 module nulib.uuid;
2 import nulib.random;
3 import nulib.string;
4 import nulib.memory.endian;
5 import nulib.text.ascii;
6 import nulib.conv;
7 import numem;
8 
9 /**
10     UUID Variants
11 */
12 enum UUIDVariant {
13     ncs,
14     rfc4122,
15     microsoft,
16     reserved
17 }
18 
19 /**
20     Creates a UUID object at compile time from a UUID string.
21 
22     Params:
23         uuid = The UUID string to generate a UUID object from.
24         forceMS = Whether to force the microsoft format.
25 */
26 template CTUUID(string uuid, bool forceMS = false) {
27     UUID genUUID(string slice) {
28         UUID uuid;
29 
30         if (__ctfe) {
31             if (!UUID.validate(slice))
32                 return uuid;
33 
34             // Get from string
35             uuid.time_low = parseHex!uint(slice[0..8]);
36             uuid.time_mid = parseHex!ushort(slice[9..13]);
37             uuid.time_hi_and_version = parseHex!ushort(slice[14..18]);
38             uuid.clk_seq = parseHex!ushort(slice[19..23]);
39 
40             // Get bytes
41             foreach(i; 0..UUID.node.length) {
42                 uuid.node[i] = parseHex!ubyte(slice[24+(i*2)..24+(i*2)+2]);
43             }
44 
45             // Flip clk_seq if need be.
46             uuid.handleMsFormat(forceMS);
47         }
48         return uuid;
49     }
50 
51     enum CTUUID = genUUID(uuid);
52 }
53 
54 /**
55     RFC4122 compliant UUIDs
56 */
57 struct UUID {
58 @nogc nothrow:
59 private:
60     enum VERSION_BITMASK = 0b11110000_00000000;
61 
62     // NCS Variant Bits
63     enum V_NCS_BITMASK = 0b11000000_00000000;
64     enum V_NCS_BITS = 0b10000000_00000000;
65 
66     // RFC Variant Bits
67     enum V_RFC_BITMASK = 0b11100000_00000000;
68     enum V_RFC_BITS = 0b11000000_00000000;
69 
70     // Microsoft Variant Bits
71     enum V_MSF_BITMASK = 0b11100000_00000000;
72     enum V_MSF_BITS = 0b11100000_00000000;
73 
74     union {
75         struct {
76             uint time_low;
77             ushort time_mid;
78             ushort time_hi_and_version;
79             ushort clk_seq;
80             ubyte[6] node;
81         }
82         ubyte[16] bdata;
83         ulong[2] ldata;
84     }
85     
86     this(ulong[2] data) inout {
87         this.ldata = data;
88     }
89 
90     // Helper that swaps bytes in MS format.
91     void handleMsFormat(bool force = false) {
92         if ((clk_seq & V_MSF_BITMASK) == V_MSF_BITS || force) {
93             ushort old_seq = clk_seq;
94             this.clk_seq = 
95                 (old_seq >> 8) | 
96                 ((old_seq & 0xFF) << 8);
97         }
98     }
99 
100 public:
101 
102     /**
103         Length of a UUID string
104     */
105     enum uuidStringLength = 36;
106 
107     /**
108         Special "nil" UUID
109     */
110     enum UUID nil = UUID([0LU, 0LU]);
111     alias min = nil;
112 
113     /**
114         Special "max" UUID
115     */
116     enum UUID max = UUID([ulong.max, ulong.max]);
117 
118     /**
119         The underlying data of the UUID.
120     */
121     @property ubyte[] data() return { return this.bdata[0..$]; }
122 
123     /**
124         The version of the UUID structure
125     */
126     @property int uuidVersion() {
127         return cast(int)(time_hi_and_version >> 12);
128     }
129 
130     deprecated("use .uuidVersion instead!")
131     alias getVersion = uuidVersion;
132 
133     /**
134         The variant of the UUID structure
135     */
136     @property UUIDVariant uuidVariant() {
137         if ((clk_seq & V_MSF_BITMASK) == V_MSF_BITS) 
138             return UUIDVariant.microsoft;
139         if ((clk_seq & V_RFC_BITMASK) == V_RFC_BITS) 
140             return UUIDVariant.rfc4122;
141         if ((clk_seq & V_NCS_BITMASK) == V_NCS_BITS) 
142             return UUIDVariant.ncs;
143         
144         return UUIDVariant.reserved;
145     }
146     
147     deprecated("use .uuidVariant instead!")
148     alias getVariant = uuidVariant;
149 
150     /**
151         Constructs a UUID from the specified byte slice
152     */
153     this(ubyte[16] bytes) {
154         this.bdata = bytes;
155         this.time_low = nu_ntoh(this.time_low);
156         this.time_mid = nu_ntoh(this.time_mid);
157         this.time_hi_and_version = nu_ntoh(this.time_hi_and_version);
158     }
159 
160     /**
161         Provides compatibility with .NET's Guid struct.
162     */
163     this(uint time_low, ushort time_mid, ushort time_hi_and_version, ubyte clk0, ubyte clk1, ubyte d0, ubyte d1, ubyte d2, ubyte d3, ubyte d4, ubyte d5) {
164         this.time_low = time_low;
165         this.time_mid = time_mid;
166         this.time_hi_and_version = time_hi_and_version;
167         this.clk_seq = clk0 << 8 | clk1;
168         this.node[0] = d0;
169         this.node[1] = d1;
170         this.node[2] = d2;
171         this.node[3] = d3;
172         this.node[4] = d4;
173         this.node[5] = d5;
174 
175         this.handleMsFormat();
176     }   
177 
178     /**
179         Constructs a UUID from the specified string.
180     */
181     this(string text) {
182         this = UUID.parse(text);
183     }
184 
185     /**
186         Validates the correctness of a UUID string.
187 
188         Params:
189             slice = The string slice to validate
190         
191         Returns:
192             $(D true) if the slice is a valid UUID string,
193             $(D false) otherwise.
194     */
195     static bool validate(string slice) {
196 
197         // Incorrect length
198         if (slice.length != UUID.uuidStringLength)
199             return false;
200 
201         foreach(i; 0..UUID.uuidStringLength) {
202             
203             // Dash positions
204             if (i == 8 || i == 13 || i == 18 || i == 23) {
205                 if (slice[i] != '-') 
206                     return false;
207                 else
208                     continue;
209             }
210 
211             // Hex positions
212             if (!isHex(slice[i]))
213                 return false;
214         }
215 
216         return true;
217     }
218 
219     /**
220         Creates a new UUIDv3 with a specified random number generator.
221 
222         Params:
223             random = The random number generator to use.
224         
225         Returns:
226             A new psuedorandomly generated UUID.
227     */
228     static UUID createRandom(RandomBase random) {
229         UUID uuid;
230         uuid.time_hi_and_version &= 0x0FFF;
231         uuid.time_hi_and_version |= (3 << 12);
232         uuid.clk_seq &= 0x3F;
233         uuid.clk_seq |= 0x80;
234 
235         ubyte[] dst = uuid.node[0..$];
236         random.next(dst);
237         return uuid;
238     }
239 
240     /**
241         Tries to parse a UUID from a string.
242         
243         Params:
244             slice = The string slice to parse.
245 
246         Returns:
247             A new UUID parsed from the string, or a nil UUID on failure.
248     */
249     static UUID parse(string slice) {
250         if (!UUID.validate(slice))
251             return UUID.nil;
252 
253         UUID uuid;
254 
255         // Get from string
256         uuid.time_low = slice[0..8].toInt!uint(16);
257         uuid.time_mid = slice[9..13].toInt!ushort(16);
258         uuid.time_hi_and_version = slice[14..18].toInt!ushort(16);
259         uuid.clk_seq = slice[19..23].toInt!ushort(16);
260 
261         // Get bytes
262         foreach(i; 0..6) {
263             size_t start = 24+(i*2);
264             uuid.node[i] = slice[start..start+2].toInt!ubyte(16);
265         }
266         
267         uuid.handleMsFormat();
268         return uuid;
269     }
270 
271     /**
272         Returns byte stream from UUID with data in network order.
273     */
274     ubyte[16] toBytes() {
275         UUID datacopy;
276         datacopy.bdata[0..$] = bdata[0..$];
277 
278         datacopy.time_low = nu_ntoh(this.time_low);
279         datacopy.time_mid = nu_ntoh(this.time_mid);
280         datacopy.time_hi_and_version = nu_ntoh(this.time_hi_and_version);
281         return datacopy.bdata;
282     }
283 
284     /**
285         Returns UUID string
286     */
287     nstring toString() {
288         import nulib.c.stdio : snprintf;
289 
290         char[uuidStringLength+1] buffer;
291         snprintf(
292             cast(char*)buffer.ptr,
293             buffer.length,
294             "%0.8x-%0.4hx-%0.4hx-%0.4hx-%.2hhx%.2hhx%.2hhx%.2hhx%.2hhx%.2hhx", 
295             time_low, 
296             time_mid, 
297             time_hi_and_version,
298             clk_seq,
299             node[0],
300             node[1],
301             node[2],
302             node[3],
303             node[4],
304             node[5],
305         );
306         
307         nstring ret = nstring(cast(string)buffer[0..uuidStringLength]);
308         return ret;
309     }
310 
311     /**
312         Checks equality between 2 UUIDs.
313     */
314     bool opEquals(const UUID other) const {
315         return this.ldata[0] == other.ldata[0] && this.ldata[1] == other.ldata[1];
316     }
317 
318     /**
319         Compares 2 UUIDs lexically.
320 
321         Lexical order is NOT temporal!
322     */
323     int opCmp(const UUID other) const {
324 
325         // First check things which are endian dependent.
326         if (this.time_low != other.time_low) 
327             return this.time_low < other.time_low;
328 
329         if (this.time_mid != other.time_mid) 
330             return this.time_mid < other.time_mid;
331 
332         if (this.time_hi_and_version != other.time_hi_and_version) 
333             return this.time_hi_and_version < other.time_hi_and_version;
334         
335         // Then check all the nodes
336         static foreach(i; 0..node.length) {
337             if (this.node[i] < other.node[i]) return -1;
338             if (this.node[i] > other.node[i]) return 1;
339         }
340 
341         // They're the same.
342         return 0;
343     }
344 }
345 
346 @("uuid: parsing")
347 unittest {
348     const string[4] tests = [
349         "ce1a553c-762d-11ef-b864-0242ac120002",
350         "56ba8f28-d9aa-4452-80c1-4ce66d064f6c",
351         "01920815-46af-71fa-9025-35d7592a401b",
352         "7f204bc7-53fe-4ffb-acaf-3f0cd0cf69cb"
353     ];
354 
355     const UUIDVariant[4] variants = [
356         UUIDVariant.ncs,
357         UUIDVariant.ncs,
358         UUIDVariant.ncs,
359         UUIDVariant.ncs,
360     ];
361 
362     const int[4] versions = [
363         1,
364         4,
365         7,
366         4
367     ];
368 
369     foreach(i; 0..4) {
370         import std.format : format;
371         import std.conv : text;
372         UUID uuid = UUID.parse(tests[i]);
373 
374         assert(
375             uuid.uuidVariant == variants[i],
376             "Expected %s, got %s".format(variants[i], uuid.uuidVariant)
377         );
378 
379         assert(
380             uuid.uuidVersion == versions[i], 
381             "uuid=%s (test %s), version=%s, expected=%s!".format(tests[i], i+1, uuid.uuidVersion, versions[i])
382         );
383     }
384 }
385 
386 @("uuid: comparison")
387 unittest {
388     UUID uuid1 = UUID.parse("ce1a553c-762d-11ef-b864-0242ac120002");
389     UUID uuid2 = UUID.parse("56ba8f28-d9aa-4452-80c1-4ce66d064f6c");
390     UUID uuid3 = UUID.parse("ce1a553c-762d-11ef-b864-0242ac120002");
391 
392     assert(uuid1 == uuid3);
393     assert(uuid1 != uuid2);
394 }
395 
396 @("uuid: toString")
397 unittest {
398     import std.format : format;
399 
400     UUID uuid1 = UUID.parse("ce1a553c-762d-11ef-b864-0242ac120002");
401     nstring str = uuid1.toString();
402     assert(str == "ce1a553c-762d-11ef-b864-0242ac120002", "Expected %s, got %s".format("ce1a553c-762d-11ef-b864-0242ac120002", str[]));
403 }
404 
405 @("uuid: generation")
406 unittest {
407     import numem.core;
408 
409     Random random = nogc_new!Random(42);
410     scope(exit) nogc_delete(random);
411 
412     UUID uuid = UUID.createRandom(random);
413     scope(exit) nogc_delete(uuid);
414     assert(uuid != UUID.nil);
415 }
416 
417 @("uuid: UUID.validate")
418 unittest {
419     assert( UUID.validate("6ba7b810-9dad-11d1-80b4-00c04fd430c8"));
420     assert( UUID.validate("449ed9d5-6810-44c6-9506-504ceeb27e0d"));
421     assert(!UUID.validate(""));
422     assert(!UUID.validate("6ba7b81i-9dad-11d1-80b4-00c04fd430c8"));
423     assert(!UUID.validate("\0\0\0\0\0\0\0\0\0\0\0\0\0\0"));
424 }