cl-frugal-uuid

2024-10-12

Common Lisp UUID library with zero dependencies

Upstream URL

github.com/ak-coram/cl-frugal-uuid

Author

Ákos Kiss <ak@coram.pub>

License

MIT License
README

1cl-frugal-uuid

Build Status

Common Lisp UUID library with zero dependencies

1.1Rationale

  • Permissive license (MIT)
  • Small (e.g. doesn't rely on Ironclad by default)
  • Comprehensive (e.g. tries to cover new versions from RFC 9562)

1.2Limitations

Please note that by default the implementation dependent built-in CL random number generator is used, which might not be of sufficient quality for your purposes. The use of cryptographic-quality random numbers is strongly recommended in order to reduce the probability of repeated values. Please see the section on randomness in this README for setting up an alternative source of random numbers especially for generating UUID versions 4 and 7.

You can also use the frugal-uuid/non-frugal system to change the defaults and provide support for generating name-based UUIDs (versions 3 and 5). This should only be necessary if you intend to securely generate new UUIDs via this library: for representing existing UUIDs (parsing, comparing and converting values) relying on the frugal-uuid system should be sufficient.

Please also see this section of the README if you intend on saving your program as a Lisp image.

The following implementations and operating systems are tested via CI:

  • SBCL (Linux, Windows, macOS)
  • CCL (Linux)
  • ECL (Linux, macOS)

1.3Installation

cl-frugal-uuid can be installed via Quicklisp from the Ultralisp distribution:

  (ql-dist:install-dist "http://dist.ultralisp.org/" :prompt nil)
  (ql:quickload :frugal-uuid)

1.4Basic usage

  ;; Generate version 1 UUID
  (fuuid:make-v1) ; => #<FRUGAL-UUID:UUID 58c3e000-1875-100c-a524-89154ef00c1c>

  ;; Generate random UUID
  (fuuid:make-v4) ; => #<FRUGAL-UUID:UUID 3ffc05ba-9c35-4f21-8535-beba03a2495c>

  ;; Convert random UUID to canonical string representation
  (fuuid:to-string (fuuid:make-v4)) ; => "2172e412-06a6-4cfb-bbf1-3584aadaed15"

  ;; Parse UUID from string
  (fuuid:from-string "0909e4f4-8333-4712-8609-5ae02d735772")
  ;; => #<FRUGAL-UUID:UUID 0909e4f4-8333-4712-8609-5ae02d735772>

  ;; Convert UUID to octets
  (fuuid:to-octets (fuuid:make-v4))
  ;; => #(33 194 68 252 59 137 78 19 177 22 25 166 226 241 44 55)

  ;; Compare two random UUID values
  (fuuid:uuid= (fuuid:make-v4) (fuuid:make-v4)) ; => NIL

  ;; Loosely compare UUID with canonical string representation
  (fuuid:uuid-equal-p
   (fuuid:from-string "0909e4f4-8333-4712-8609-5ae02d735772")
   "0909e4f4-8333-4712-8609-5ae02d735772") ; => T

1.5Embedding UUIDs

You can efficiently embed UUID literals in your source using the ~ macro:

  (fuuid:~ 1d2f8148-c30c-4f9d-8aae-2ac5807000dc)
  ;; => #<FRUGAL-UUID:UUID 1d2f8148-c30c-4f9d-8aae-2ac5807000dc>

  (multiple-value-list (fuuid:~ 1d2f8148-c30c-4f9d-8aae-2ac5807000dc
                                fa23520b-972c-4f84-b5bf-91f0d7d4f4b4))
  ;; => (#<FRUGAL-UUID:UUID 1d2f8148-c30c-4f9d-8aae-2ac5807000dc>
  ;;     #<FRUGAL-UUID:UUID fa23520b-972c-4f84-b5bf-91f0d7d4f4b4>)

  ;; When called without arguments, the macro generates a random
  ;; value during expansion (via MAKE-V4):
  (fuuid:~)
  ;; => #<FRUGAL-UUID:UUID f890220f-0a9d-4380-80ce-f88d59619480>

For writing your own macros, you can rely on the COMPILE-LITERAL function.

1.6non-frugal setup

  (ql:quickload :frugal-uuid/non-frugal)

The above system is provided to conveniently setup the following:

frugal-uuid/non-frugal is composed of multiple systems which can also be loaded individually if you only require parts of the above. See frugal-uuid.asd for details.

1.7UUID Versions

1.7.1Version 1

Node ID and clock sequence are initialized randomly by default, but you can provide your own values (or even your own function for generating timestamp values) using MAKE-V1-GENERATOR. Currently there's no mechanism included in this library for determining the systems MAC address, but the PARSE-NODE-ID function is included for parsing it once obtained.

The clock sequence is reinitialized on every new clock tick with the highest bit set to zero and a random value for the remaining bits.

To avoid repeated values, it is recommended for multithreaded applications to use a separate generator for each thread. This is automatically done using bordeaux-threads if you use the frugal-uuid/non-frugal system.

Please also see the section on randomness for setting up alternative sources for random numbers.

  (bordeaux-threads-2:make-thread
   (lambda ()
     (format t "~A" (fuuid:make-v1)))
   :initial-bindings `((fuuid:*v1-generator* . ,(fuuid:make-v1-generator))))

1.7.2Version 2

Generating "DCE security" UUIDs (version 2) is not implemented.

1.7.3Version 3

See section for version 5 below.

1.7.4Version 4

  ;; Generate random UUID
  (fuuid:make-v4)

  ;; Provide 128-bit random number directly and set the bits for version 4
  (fuuid:make-v4-from-integer
   (secure-random:number #xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))

1.7.5Version 5

If you're using the frugal-uuid/non-frugal system, then you can also generate name-based (version 3 or version 5) UUIDs:

  (fuuid:make-v3 fuuid:*ns-url* "https://html5zombo.com/")
  ;; => #<FRUGAL-UUID:UUID a76f94c8-b970-33d8-bac6-84f18fbbc489>

  (let ((cheese-namespace (fuuid:make-v4)))
    (fuuid:make-v5 cheese-namespace "Orda"))
  ;; => #<FRUGAL-UUID:UUID dd4d48d9-d46b-58a0-977e-e9e5e20a6e9c>

1.7.6Version 6

The implementation first generates a version 1 UUID (see above) and reorders the timestamp fields to create a version 6 UUID value. Please note that the slot accessors for the UUID class (TIME-LOW, TIME-MID and TIME-HI-AND-VERSION) are based on RFC 4122 and do not match the timestamp part names used for version 6. Please rely on the following functions instead:

  • V6-TIME-LOW-AND-VERSION
  • V6-TIME-MID
  • V6-TIME-HIGH
  (fuuid:make-v6)
  ;; => #<FRUGAL-UUID:UUID 1ee2d53a-729c-6929-8c8c-e780ff7b0e6a>

1.7.7Version 7

When an accurate system clock is available (see frugal-uuid/non-frugal):

  • 48 bit unsigned unix timestamp (milliseconds)
  • 12 bits for storing sub-millisecond precision
  • 62 bit monotonic random counter

This approach is a combination of methods 2 and 3 from the specification for providing monotonicity.

When no accurate clock is available:

  • 48 bit unsigned unix timestamp (also stored as a number ofmilliseconds, but only updates every second)
  • 74 bit monotonic random counter

This corresponds to method 2 from the specification for providing monotonicity.

Regarding the monotonic random counter:

  • Is randomly reinitialized on every clock tick (or when the systemclock is changed backwards) while generating successive UUIDvalues. The highest 2 bits are always set to zero to minimizerollover.
  • Is incremented when successive UUID values are generated for thesame timestamp (which includes the sub-millisecond bits for accurateclocks). The increment is a 16-bit random number to make values lessguessable.
  (fuuid:make-v7)
  ;; => #<UUID 0189a085-b115-7e5d-8664-3c5fbe542e4c>

1.7.8Version 8

The MAKE-V8 function is provided to create version 8 UUID values from specifying the three custom components:

  • custom_a (48 bits)
  • custom_b (12 bits)
  • custom_c (62 bits)

1.7.8.1MiNaRa UUID (custom UUID relying on version 8)

The frugal-uuid/non-frugal system provides a custom UUID generation scheme which encodes a nanosecond precision timestamp in a version 8 UUID and randomizes the remaining variable bits. It consists of three components which also make up the name:

  • Mi-lliseconds elapsed since the unix epoch (48-bit unsigned value,identical to timestamp in UUID version 7)
  • Additional Na-noseconds (20-bit unsigned value, not greaterthan 999999)
  • Ra-ndom data (remaining 54 bits)

The random component may also be used for encoding custom data.

  (fuuid:make-minara)
  ;; => #<FRUGAL-UUID:UUID 01899a63-6540-8d89-b9db-f0fa388bf86e>

  (fuuid:minara-components *)
  ;; => 1690512352576 (41 bits, #x1899A636540)
  ;;    887271 (20 bits, #xD89E7)
  ;;    7864781852375150 (53 bits, #x1BF0FA388BF86E)

  (fuuid:minara-components (fuuid:make-minara 42))
  ;; => 1690514402692 (41 bits, #x1899A82AD84)
  ;;    507614 (19 bits, #x7BEDE)
  ;;    42 (6 bits, #x2A, #o52, #b101010)

1.8Timestamps

The Common Lisp standard only provides a function to retrieve the current wall-clock time as a number of whole seconds elapsed since the Common Lisp epoch. In order to make use of the subsecond bits of the timestamp (in UUID versions 1, 6 and 7), the default implementation uses them as a counter which is incremented every time the clock sequence values are exhausted within the same clock tick. If the total number of unique values is exhausted, the counter wraps around and starts at zero again.

Within the frugal-uuid/non-frugal system a more accurate clock is available and the above doesn't apply.

1.9Randomness

If you have an alternative source of random numbers, you can use it instead of the built-in random number generator. Please consult the documentation of your chosen implementation or library for details on thread-safety if you intend to use this in a multi-threaded program.

1.9.1Ironclad

A setup using Ironclad PRNG:

  (ql:quickload :ironclad/prngs)

  ;; Use the default Ironclad PRNG:
  (fuuid:initialize-random #'crypto:strong-random)

  ;; Setup with custom PRNG:
  (fuuid:initialize-random #'crypto:strong-random
                           (lambda () (ironclad:make-prng :os)))

  ;; Dynamically bind the generator:
  (fuuid:with-random-number-generator (ironclad:make-prng :os)
    (fuuid:make-v4))

1.9.2secure-random

Below you'll find and example using the secure-random library which relies on OpenSSL:

  ;; Load library for generating secure random numbers
  (ql:quickload :secure-random)

  ;; Dynamically bind both random number generator & random function:
  (fuuid:with-random (#'secure-random:number secure-random:*generator*)
    (fuuid:make-v4))

1.9.3Saving a Lisp image

If you generate UUID values while building your Lisp image, it can include global random state which already has been initialized. This would mean that executing the image multiple times could lead to generating repeated UUID values.

To avoid this, you can clear the global state before saving your image or on image startup (it will be reinitialized on first use):

    (setf fuuid:*random-number-generator* nil
          fuuid:*v1-generator* nil
          fuuid:*v7-generator* nil)

If you only load the systems in this project this should not be an issue as the global random state is initialized on first use (when generating UUID values of either version 1 or version 4).

Here's an example session illustrating the issue:

  $ sbcl

  * (ql:quickload :frugal-uuid)
  To load "frugal-uuid":
    Load 1 ASDF system:
      frugal-uuid
  ; Loading "frugal-uuid"
  (:FRUGAL-UUID)

  * (fuuid:make-v4)
  #<FRUGAL-UUID:UUID 88d17bef-3541-4660-b7fe-ecc588778311>

  * (ql:quickload :trivial-dump-core)
  To load "trivial-dump-core":
    Load 1 ASDF system:
      trivial-dump-core
  ; Loading "trivial-dump-core"

  (:TRIVIAL-DUMP-CORE)

  * (trivial-dump-core:save-executable
     "echo-random-uuid"
     (lambda () (format t "~a~%" (fuuid:to-string (fuuid:make-v4)))))
  [undoing binding stack and other enclosing state... done]
  [performing final GC... done]
  [defragmenting immobile space... (inst,fdefn,code,sym)=959+18456+19452+26866... done]
  [saving current Lisp image into echo-random-uuid:
  writing 3376 bytes from the static space at 0x50000000
  writing 21266432 bytes from the dynamic space at 0x1000000000
  writing 7443312 bytes from the read-only space at 0xfff8e0000
  writing 2015232 bytes from the fixedobj space at 0x50100000
  writing 11993088 bytes from the text space at 0x52a00000
  done]

  $ ./echo-random-uuid
  cb09eb20-64c6-4ed0-b5be-c89388a673fe
  $ ./echo-random-uuid
  cb09eb20-64c6-4ed0-b5be-c89388a673fe

1.10Running tests

  • Load the tests via Quicklisp:
  (ql:quickload :frugal-uuid/test)
  ;; Using ASDF:
  (asdf:test-system :frugal-uuid)
  ;; Using FiveAM directly:
  (fiveam:run! :frugal-uuid)

1.11Legal

Dependencies (6)

  • babel
  • bordeaux-threads
  • cl-trivial-clock
  • fiveam
  • ironclad
  • trivial-benchmark
  • GitHub
  • Quicklisp