master bd064471e6a 1/2: New package uuid.el
Eli Zaretskii <[email protected]>
| Newsgroups | gmane.emacs.diffs |
|---|---|
| Message-ID | <[email protected]> |
branch: master commit bd064471e6aaf6348f52e66c3d34d7050695f21b Author: Andrew Hyatt <[email protected]> Commit: Eli Zaretskii <[email protected]> New package uuid.el * lisp/emacs-lisp/uuid.el: New file. * test/lisp/emacs-lisp/uuid-tests.el: New tests. * doc/lispref/processes.texi (UUIDs): New section. --- doc/lispref/processes.texi | 152 +++++++++++++++++++++++ lisp/emacs-lisp/uuid.el | 245 +++++++++++++++++++++++++++++++++++++ test/lisp/emacs-lisp/uuid-tests.el | 175 ++++++++++++++++++++++++++ 3 files changed, 572 insertions(+) diff --git a/doc/lispref/processes.texi b/doc/lispref/processes.texi index b340067592f..bc4fcdbe27a 100644 --- a/doc/lispref/processes.texi +++ b/doc/lispref/processes.texi @@ -68,6 +68,7 @@ Processes}. * Misc Network:: Additional relevant functions for net connections. * Serial Ports:: Communicating with serial ports. * Byte Packing:: Using bindat to pack and unpack binary data. +* UUIDs:: Generating and converting UUIDs. @end menu @node Subprocess Creation @@ -3883,3 +3884,154 @@ arguments @var{args}. Its behavior follows that of @code{defmacro}, which the important difference that the new forms can only be used within Bindat type expressions. @end defmac + +@node UUIDs +@section UUIDs +@cindex uuids + + Emacs Lisp can generate and parse the most popular UUID variants to +and from strings and binary. It can generate UUIDv4, UUIDv5 and UUIDv7 +according to RFC 9562. UUIDv4 is a randomly generated identifier, +UUIDv5 is an identifier generated from a standard namespace and a name, +and UUIDv7 is a combination of a timestamp and random identifier, with +the advantage of having good index performance in databases. + +All UUIDs are 16 byte identifiers with a common string representation, +consisting of hex digits and dashes of a predetermined length, such as +@samp{919108f7-52d1-4320-9bac-f847db4148a8}. + +@menu +* UUID Generation:: +* UUID Conversion:: +* Special UUID values:: +@end menu + +@node UUID Generation +@subsection UUID Generation +@cindex uuid generation + + To generate UUIDs, call @code{uuid-v4}, @code{uuid-v5} or +@code{uuid-v7}. UUID v4 and v7 will by default use the normal elisp +@dfn{random} for random numbers, but a different random number generator +can be passed in, as long as it can be called with an arg of the end of +the range of acceptable integers to generate (exclusive), and returns an +integer. + +@defun uuid-v4 &key rng +Return a @code{uuid-v4} Lisp object representing a new UUIDv4. +@var{rng}, if provided, is a function that will, when called with a +numeric argument, return a random number between 0 and that number +(exclusive). +@end defun + +@defun uuid-v7 &key rng +Similar to @code{uuid-v4} but returns a @code{uuid-v7}, which is +generated with a timestamp in addition to random numbers (generated from +@var{rng} in a similar manner as @code{uuid-v4}). +@end defun + +These use random numbers, which should be cryptographically secure. +@xref{Random Numbers}, but generally these random numbers are not +sufficient to generate sufficiently random UUIDs. For private uses, the +default behavior should be sufficient, but for professional uses, +@var{rng} should be used, and provide cryptographically secure random +numbers. + +@noindent +UUIDv5s must be created with a namespace and a name. The namespaces are +defined in @code{uuid-namespace-alist} and are by default @code{dns}, +@code{url}, @code{oid}, and @code{x500}. + +@defun uuid-v5 namespace name +This function returns a new @code{uuid-v5} Lisp object based on +@var{namespace}, which is a symbol in the key of the +@code{uuid-namespace-alist}: @code{dns}, @code{url}, @code{oid}, +@code{x500}, or any symbol added. This is combined with @var{name} to +construct a new UUIDv5. +@end defun + +An example is: + +@cindex uuidv5 +@example +(uuid-v5 'dns "www.example.com") +@end example + +@defvar uuid-namespace-alist +A list of namespace symbols and their corresponding UUIDs. Each alist +entry is a cons of the symbol and a UUID Lisp object representing the +namespace. This is used in @code{uuid-v5}, see above for more info on +how this is used. +@end defvar + +@node UUID Conversion +@subsection UUID Conversion +@cindex uuid conversion + +To use a UUID, typically it is converted to a string, and sometimes to a +binary representation. This is done using @code{uuid-to-string} function. + +@defun uuid-to-string uuid +Return the standard string value, with hex values and dashes, of Lisp +object @code{uuid}. +@end defun + +@example +(uuid-to-string (uuid-v4)) ; @r{"230156d4-488a-45d1-93c9-507ec8be37ca"} +@end example + +@noindent +Binary can also be generated, which generates a unibyte string, using +@code{uuid-to-bytes}, and a numeric representation of a UUID can be +generated with @code{uuid-to-number}. + +@defun uuid-to-bytes uuid +Return a unibyte string representing the binary value of @code{uuid}. +@end defun + +@defun uuid-to-number uuid +Return a numeric representation of the value of @code{uuid}. +@end defun + +@noindent +String UUIDs can also be converted to a UUID Lisp object, with +@code{uuid-from-string}, and bytes can be converted with +@code{uuid-from-bytes}. These work with all UUIDs, not just the ones +this module can generate. + +@defun uuid-from-string uuid-str +Return a UUID Lisp object that represents @var{uuid-str}. +@end defun + +@defun uuid-from-bytes uuid-bytes +Return a UUID Lisp object that represents @var{uuid-bytes}, which is +expected to be a unibyte string. +@end defun + +Valid UUIDs have types, which can be used with @code{cl-typep}, +@code{cl-check-type} and @code{cl-typecase}. The type is specified with +the type specifier @code{(uuid-v <version>)}. @var{version} can be any +version of UUID, but we make sure the UUID variant is correctly +@code{2}; any other value is not a valid version. + +@example +(when (cl-typep my-uuid '(uuid-v 4)) + (process-v4-uuid my-uuid)) + +(defun process-v4-uuid (uuid) + ;; Error if not the correct type + (cl-check-type uuid (uuid-v 4)) + ;; Proceed to process the UUIDv4. + (more-processing uuid)) +@end example + +@node Special UUID values +@subsection Special UUID values +@cindex special uuid values +@cindex uuid nil and uuid max +@cindex nil uuid +@cindex max uuid + +@code{uuid-nil} and @code{uuid-max} are constant values representing the +UUID equivalents of @code{nil} and a max sentinel value, as defined in +RFC 9562. diff --git a/lisp/emacs-lisp/uuid.el b/lisp/emacs-lisp/uuid.el new file mode 100644 index 00000000000..592751a427e --- /dev/null +++ b/lisp/emacs-lisp/uuid.el @@ -0,0 +1,245 @@ +;;; uuid.el --- UUID creation and handling -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Free Software Foundation, Inc. + +;; Author: Andrew Hyatt <[email protected]> +;; Keywords: tools + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. + + +;;; Commentary: +;; This library provides useful code for handling UUIDs. It provides +;; methods for generating UUIDs, parsing them, and outputting them as +;; a string, a unibyte binary string, or as a number. +;; +;;; Code: + +(require 'cl-lib) +(require 'bindat) +(require 'seq) + +(define-error + 'uuid-invalid-string + "Invalid UUID string not conforming to expected UUID shape.") + +(define-error + 'uuid-invalid-bytes + "Invalid UUID bytes not conforming to expected 16-byte length.") + +(define-error + 'uuid-invalid-namespace + "Namespace not found in `uuid-namespace-alist'.") + +(defconst uuid--bindat-type + (bindat-type (:high uint 48) + ;; We can't separate out ver and mid due to bindat + ;; limitations on byte alignment. + (:ver-mid uint 16) + ;; Same issue with var and low. + (:var-low uint 64)) + "The bindat type for UUIDs, as defined in RFC 9562.") + +(cl-defstruct (uuid + (:constructor nil) + (:conc-name uuid--) + (:constructor + uuid--from-parts + (high mid low ver var))) + "The base type for all UUIDs. + +This will represent versions where we don't have a more specific version +defined, so all versions except for v4, v5, and v7." + high mid low ver var) + +(defun uuid--upper-bits (num n bitsize) + "Return the upper N bits of NUM of BITSIZE. + +BITSIZE is the total size of the num." + (ash num (- n bitsize))) + +(defun uuid--lower-bits (num n) + "Return the lower N bits of NUM." + (logand num (- (ash 1 n) 1))) + +(defun uuid--concat-bits (&rest num-and-sizes) + "Return number doing bitwise concatenating NUM-AND-SIZES. + +NUM-AND-SIZES is a list of alternating numbers and their sizes in bits." + (cl-loop for num-and-size in (reverse + (seq-partition num-and-sizes 2)) + with shift = 0 + sum (ash (car num-and-size) shift) + do (incf shift (cadr num-and-size)))) + +(defun uuid-to-string (id) + "Convert the uuid object ID to a string." + (let ((low (uuid--low id)) + (high (uuid--high id))) + (format "%08x-%04x-%04x-%04x-%012x" + (uuid--upper-bits high 32 48) + (uuid--lower-bits high 16) + (uuid--concat-bits + (uuid--ver id) 4 + (uuid--mid id) 12) + (uuid--concat-bits + (uuid--var id) 2 + (uuid--upper-bits low 14 62) 14) + (uuid--lower-bits low 48)))) + +(defun uuid-to-bytes (id) + "Convert the uuid object ID to a 16-byte unibyte string." + (bindat-pack uuid--bindat-type + `((:high . ,(uuid--high id)) + (:ver-mid . ,(uuid--concat-bits + (uuid--ver id) 4 + (uuid--mid id) 12)) + (:var-low . ,(uuid--concat-bits + (uuid--var id) 2 + (uuid--low id) 62))))) + +(defun uuid-to-number (id) + "Convert ID, a `uuid' lisp object, to a numerical representation." + (uuid--concat-bits + (uuid--high id) 48 + (uuid--ver id) 4 + (uuid--mid id) 12 + (uuid--var id) 2 + (uuid--low id) 62)) + +(defconst uuid-nil + (uuid--from-parts 0 0 0 0 0) + "A UUID representing `nil', as defined in RFC 9562.") + +(defconst uuid-max + (uuid--from-parts (1- (ash 1 48)) (1- (ash 1 12)) (1- (ash 1 62)) 15 3) + "A UUID representing the maximum UUID, per RFC 9562.") + +(cl-deftype uuid-v (var) + `(and uuid (satisfies + ,(lambda (id) (and (= (uuid--ver id) var) + (= (uuid--var id) 2)))))) + +(defun uuid--random-bits (n &optional rng) + "Return a random number with N bits." + (funcall (or rng #'random) + (expt 2 n))) + +(defun uuid-from-string (uuid-str) + "Parse UUID-STR and return the appropriate UUID object." + (let* ((parts (split-string uuid-str "-")) + (_ (unless (and (= (length parts) 5) + (= (length (nth 0 parts)) 8) + (= (length (nth 1 parts)) 4) + (= (length (nth 2 parts)) 4) + (= (length (nth 3 parts)) 4) + (= (length (nth 4 parts)) 12) + (string-match "^[0-9a-fA-F-]+$" uuid-str)) + (signal 'uuid-invalid-string (list uuid-str)))) + (hex-parts (mapcar (lambda (part) (string-to-number part 16)) parts)) + (version (uuid--upper-bits (nth 2 hex-parts) 4 16)) + (variant (uuid--upper-bits (nth 3 hex-parts) 2 16)) + (high (uuid--concat-bits + (nth 0 hex-parts) 32 + (nth 1 hex-parts) 16)) + (mid (uuid--lower-bits (nth 2 hex-parts) 12)) + (low + (uuid--concat-bits + (uuid--lower-bits (nth 3 hex-parts) 14) 14 + (nth 4 hex-parts) 48))) + (uuid--from-parts high mid low version variant))) + +(defun uuid-from-bytes (uuid-bytes) + "Parse unibyte string UUID-BYTES and return a UUID object. + +If UUID-BYTES are not unibyte, or not 16 bytes, a `uuid-invalid-bytes' +error is signaled." + (unless (and + (not (multibyte-string-p uuid-bytes)) + (= 16 (string-bytes uuid-bytes))) + (signal 'uuid-invalid-bytes (list uuid-bytes))) + (let* ((parts (bindat-unpack uuid--bindat-type uuid-bytes)) + (version (uuid--upper-bits (assoc-default :ver-mid parts) 4 16)) + (variant (uuid--upper-bits (assoc-default :var-low parts) 2 64)) + (high (assoc-default :high parts)) + (mid (uuid--lower-bits (assoc-default :ver-mid parts) 12)) + (low (uuid--lower-bits (assoc-default :var-low parts) 62))) + (uuid--from-parts high mid low version variant))) + +(defconst uuid-namespace-alist + (mapcar (lambda (x) (cons (car x) (uuid-from-string (cdr x)))) + '((dns . "6ba7b810-9dad-11d1-80b4-00c04fd430c8") + (url . "6ba7b811-9dad-11d1-80b4-00c04fd430c8") + (oid . "6ba7b812-9dad-11d1-80b4-00c04fd430c8") + (x500 . "6ba7b814-9dad-11d1-80b4-00c04fd430c8"))) + "An alist of namespaces and their canonical UUIDs. + This is defined at https://www.rfc-editor.org/info/rfc9562/#namespaces.") + +(cl-defun uuid-v4 (&key rng) + "Return a new UUIDv4 ID. + +RNG is an alternate random number function which should take a single +argument, the limit (exclusive) for the random number, and return an +integer between 0 and that number. To be valid according to RFC 9562, +the random numbers should be cryptographically secure, which the default +random number generator typically is not, so if these UUIDs are +important, it's advised to use a better random number function, which +typically requires getting random numbers from outside of Emacs." + (uuid--from-parts (uuid--random-bits 48 rng) + (uuid--random-bits 12 rng) + (uuid--random-bits 62 rng) + 4 2)) + +(defun uuid-v5 (namespace name) + "Return a new UUIDv5 ID from the given NAMESPACE and NAME. + +NAMESPACE should be a symbol corresponding to a namespace in +`uuid-namespace-alist'. If this is not recognized it will signal an +`uuid-invalid-namespace' signal. + +NAME is the name from which to generate the UUID, and should be a +string." + (let* ((namespace-uuid (or + (alist-get namespace uuid-namespace-alist) + (signal 'uuid-invalid-namespace (list namespace)))) + (hash-bytes (sha1 + (concat + (uuid-to-bytes namespace-uuid) + (encode-coding-string name 'utf-8)) + nil nil t)) + (hash-vals (bindat-unpack uuid--bindat-type hash-bytes))) + (uuid--from-parts (assoc-default :high hash-vals) + (uuid--lower-bits (assoc-default :ver-mid hash-vals) 12) + (uuid--lower-bits (assoc-default :var-low hash-vals) 62) + 5 2))) + +(cl-defun uuid-v7 (&key rng) + "Return a new UUIDv7 ID. + +The UUIDv7 uses a timestamp instead of being purely random, which makes +it more suitable for use cases such as database keys. + +RNG is an alternate random number function which should take a single +argument, the limit (exclusive) for the random number, and return an +integer between 0 and that number. See `uuid-v4' for more details on +the expected behavior of this function." + (uuid--from-parts (floor (* (float-time) 1000)) + (uuid--random-bits 12 rng) + (uuid--random-bits 62 rng) + 7 2)) + +(provide 'uuid) +;;; uuid.el ends here diff --git a/test/lisp/emacs-lisp/uuid-tests.el b/test/lisp/emacs-lisp/uuid-tests.el new file mode 100644 index 00000000000..e2631e49079 --- /dev/null +++ b/test/lisp/emacs-lisp/uuid-tests.el @@ -0,0 +1,175 @@ +;; uuid-tests.el --- unit tests for uuid.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Free Software Foundation, Inc. + +;; Author: Andrew Hyatt <[email protected]> + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: +;; Unit tests for uuid.el. + +;;; Code: + +(require 'uuid) + +(ert-deftest uuid-v4-input () + "Verifies that a UUID-V4 can be parsed and detected correctly." + ;; Example comes from RFC 9562, A.3. + (let ((id (uuid-from-string "919108f7-52d1-4320-9bac-f847db4148a8"))) + (should (cl-typep id '(uuid-v 4))) + (should (equal (uuid-to-string id) "919108f7-52d1-4320-9bac-f847db4148a8")) + (should (equal (uuid--high id) #x919108f752d1)) + (should (equal (uuid--mid id) #x320)) + (should (equal (uuid--low id) #x1bacf847db4148a8)) + (should (equal (uuid--var id) 2)) + (should (equal (uuid--ver id) 4)))) + +(ert-deftest uuid-v4-generator () + "Verifies that a generated UUID-V4 is valid." + (let ((id (uuid-v4))) + (should (cl-typep id '(uuid-v 4))) + (should (equal (uuid--var id) 2)) + (should (equal (uuid--ver id) 4)))) + +(ert-deftest uuid-v4-generator-custom-rnd () + "Verifies that a UUID-V4 uses the RND function provided to it." + (let ((id (uuid-v4 :rng (lambda (n) 23)))) + (should (cl-typep id '(uuid-v 4))) + (should (equal (uuid--var id) 2)) + (should (equal (uuid--ver id) 4)) + (should (equal (uuid--high id) 23)) + (should (equal (uuid--mid id) 23)) + (should (equal (uuid--low id) 23)))) + +(ert-deftest uuid-v5-input () + "Verifies that a UUIDv5 can be parsed and detected correctly." + ;; Example comes from RFC 9562, A.4. + (let ((id (uuid-from-string "2ed6657d-e927-568b-95e1-2665a8aea6a2"))) + (should (cl-typep id '(uuid-v 5))) + (should (equal (uuid-to-string id) "2ed6657d-e927-568b-95e1-2665a8aea6a2")) + (should (equal (uuid--high id) #x2ed6657de927)) + (should (equal (uuid--mid id) #x68b)) + (should (equal (uuid--low id) #x015e12665a8aea6a2)) + (should (equal (uuid--var id) 2)) + (should (equal (uuid--ver id) 5)))) + +(ert-deftest uuid-v5-generator () + "Verifies that a UUIDv5 can be generated correctly." + ;; Example comes from RFC 9562, A.4. + (let ((id (uuid-v5 'dns "www.example.com"))) + (should (cl-typep id '(uuid-v 5))) + (should (equal (uuid-to-string id) "2ed6657d-e927-568b-95e1-2665a8aea6a2")) + (should (equal (uuid--high id) #x2ed6657de927)) + (should (equal (uuid--mid id) #x68b)) + (should (equal (uuid--low id) #x015e12665a8aea6a2)))) + +(ert-deftest uuid-v5-invalid-namespace () + "Verifies an invalid namespace errors out." + (should-error (uuid-v5 'invalid "www.example.com") + :type 'uuid-invalid-namespace)) + +(ert-deftest uuid-v7-input () + "Verifies that a UUIDv7 can be parsed and detected correctly." + ;; Example comes from RFC 9562, A.5. + (let ((id (uuid-from-string "017f22e2-79b0-7cc3-98c4-dc0c0c07398f"))) + (should (cl-typep id '(uuid-v 7))) + (should (equal (uuid-to-string id) "017f22e2-79b0-7cc3-98c4-dc0c0c07398f")) + (should (equal (uuid--high id) #x017f22e279b0)) + (should (equal (uuid--mid id) #xcc3)) + (should (equal (uuid--low id) #x18c4dc0c0c07398f)) + (should (equal (uuid--var id) 2)) + (should (equal (uuid--ver id) 7)))) + +(ert-deftest uuid-v7-generator () + "Verifies that a UUIDv7 can be generated correctly." + (let ((id (uuid-v7))) + (should (cl-typep id '(uuid-v 7))) + (should (equal (uuid--var id) 2)) + (should (equal (uuid--ver id) 7)))) + +(ert-deftest uuid-v7-generator-known-rnd-and-ts () + "Verifies that a UUIDv7 uses random, timestamps correctly." + (cl-letf (((symbol-function 'float-time) + (lambda () 0.123))) + (let ((id (uuid-v7 :rng (lambda (n) 789)))) + (should (cl-typep id '(uuid-v 7))) + (should (equal (uuid--var id) 2)) + (should (equal (uuid--ver id) 7)) + ;; 123 = 0x7b + ;; 789 = 0x315 + (should (equal (uuid-to-string id) "00000000-007b-7315-8000-000000000315"))))) + +(ert-deftest uuid-bytes () + "Verifies that a UUID can be converted to bytes and back." + (let* ((id (uuid-v4)) + (bytes (uuid-to-bytes id)) + (id2 (uuid-from-bytes bytes))) + (should (equal id id2)) + (should (= (length bytes) 16)) + (should-not (multibyte-string-p bytes)))) + +(ert-deftest uuid-to-number () + "Test UUIDs can convert into the correct number." + (should (equal 193491124287564075115561252409011423400 + (uuid-to-number (uuid-from-string "919108f7-52d1-4320-9bac-f847db4148a8"))))) + +(ert-deftest uuid-invalid-string () + "Verifies that an invalid UUID string is rejected." + (should-error (uuid-from-string "invaluuid-string") + :type 'uuid-invalid-string) + ;; Too short + (should-error (uuid-from-string "017f22e2-79b0-7cc3-98c4-dc0c0c07398") + :type 'uuid-invalid-string) + ;; Too long + (should-error (uuid-from-string "017f22e2-79b0-7cc3-98c4-dc0c0c07398ff")) + ;; Not valid hex + (should-error (uuid-from-string "017f22e2-79b0-7cc3-98c4-dc0c0c07398zz"))) + +(ert-deftest uuid-invalid-bytes () + "Verifies that an invalid UUID byte string is rejected." + (should-error (uuid-from-bytes "short-bytes") + :type 'uuid-invalid-bytes) + ;; Too long + (should-error (uuid-from-bytes (make-string 17 ?\0))) + ;; Multibyte + (should-error (uuid-from-bytes (encode-coding-string (make-string 17 ?\0) 'utf-8)))) + +(ert-deftest uuid-unsupported-version () + "Verifies that non v4,5, or 7 versions are a usable UUID." + (dolist (uuid-str + '("017f22e2-79b0-8cc3-98c4-dc0c0c07398f" ;; UUIDv8 + "d5103018-6530-328e-b76b-9132edeba856" ;; UUIDv3 + "358f9528-5e27-11f1-b58b-def57c109056" ;; UUIDv1 + )) + (let* ((id (uuid-from-string uuid-str))) + (should (equal id (uuid-from-bytes (uuid-to-bytes id)))) + (should (uuid-p id)) + (dolist (v '(4 5 7)) + (should-not (cl-typep id `(uuid-v ,v)))) + (should (equal (uuid-to-string id) uuid-str))))) + +(ert-deftest uuid-nil () + "Tests the nil UUID handling." + (should (equal (uuid-to-string uuid-nil) "00000000-0000-0000-0000-000000000000")) + (should (equal 0 (uuid-to-number uuid-nil)))) + +(ert-deftest uuid-max () + "Tests the max UUID handling." + (should (equal (uuid-to-string uuid-max) "ffffffff-ffff-ffff-ffff-ffffffffffff")) + (should (= 340282366920938463463374607431768211455 (uuid-to-number uuid-max)))) + +;;; uuid-tests.el ends here