| --- jit-uuid |
| -- Fast and dependency-free UUID library for LuaJIT/ngx_lua. |
| -- @module jit-uuid |
| -- @author Thibault Charbonnier |
| -- @license MIT |
| -- @release 0.0.7 |
| |
| |
| local bit = require 'bit' |
| |
| |
| local tohex = bit.tohex |
| local band = bit.band |
| local bor = bit.bor |
| |
| |
| local _M = { |
| _VERSION = '0.0.7' |
| } |
| |
| |
| ---------- |
| -- seeding |
| ---------- |
| |
| |
| --- Seed the random number generator. |
| -- Under the hood, this function calls `math.randomseed`. |
| -- It makes sure to use the most appropriate seeding technique for |
| -- the current environment, guaranteeing a unique seed. |
| -- |
| -- To guarantee unique UUIDs, you must have correctly seeded |
| -- the Lua pseudo-random generator (with `math.randomseed`). |
| -- You are free to seed it any way you want, but this function |
| -- can do it for you if you'd like, with some added guarantees. |
| -- |
| -- @param[type=number] seed (Optional) A seed to use. If none given, will |
| -- generate one trying to use the most appropriate technique. |
| -- @treturn number `seed`: the seed given to `math.randomseed`. |
| -- @usage |
| -- local uuid = require 'resty.jit-uuid' |
| -- uuid.seed() |
| -- |
| -- -- in ngx_lua, seed in the init_worker context: |
| -- init_worker_by_lua { |
| -- local uuid = require 'resty.jit-uuid' |
| -- uuid.seed() |
| -- } |
| function _M.seed(seed) |
| if not seed then |
| if ngx then |
| seed = ngx.time() + ngx.worker.pid() |
| |
| elseif package.loaded['socket'] and package.loaded['socket'].gettime then |
| seed = package.loaded['socket'].gettime()*10000 |
| |
| else |
| seed = os.time() |
| end |
| end |
| |
| math.randomseed(seed) |
| |
| return seed |
| end |
| |
| |
| ------------- |
| -- validation |
| ------------- |
| |
| |
| do |
| if ngx and string.find(ngx.config.nginx_configure(),'--with-pcre-jit',nil,true) then |
| local type = type |
| local re_find = ngx.re.find |
| local regex = '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' |
| |
| |
| --- Validate a string as a UUID. |
| -- To be considered valid, a UUID must be given in its canonical |
| -- form (hexadecimal digits including the hyphen characters). |
| -- This function validates UUIDs disregarding their generation algorithm, |
| -- and in a case-insensitive manner, but checks the variant field. |
| -- |
| -- Use JIT PCRE if available in OpenResty or fallbacks on Lua patterns. |
| -- |
| -- @param[type=string] str String to verify. |
| -- @treturn boolean `valid`: true if valid UUID, false otherwise. |
| -- @usage |
| -- local uuid = require 'resty.jit-uuid' |
| -- |
| -- uuid.is_valid 'cbb297c0-a956-486d-ad1d-f9bZZZZZZZZZ' --> false |
| -- uuid.is_valid 'cbb297c0-a956-486d-dd1d-f9b42df9465a' --> false (invalid variant) |
| -- uuid.is_valid 'cbb297c0a956486dad1df9b42df9465a' --> false (no dashes) |
| -- uuid.is_valid 'cbb297c0-a956-486d-ad1d-f9b42df9465a' --> true |
| function _M.is_valid(str) |
| -- it has proven itself efficient to first check the length with an |
| -- evenly distributed set of valid and invalid uuid lengths. |
| if type(str) ~= 'string' or #str ~= 36 then |
| return false |
| end |
| |
| return re_find(str, regex, 'ioj') ~= nil |
| end |
| |
| else |
| local match = string.match |
| local d = '[0-9a-fA-F]' |
| local p = '^' .. table.concat({ |
| d:rep(8), |
| d:rep(4), |
| d:rep(4), |
| '[89ab]' .. d:rep(3), |
| d:rep(12) |
| }, '%-') .. '$' |
| |
| |
| function _M.is_valid(str) |
| if type(str) ~= 'string' or #str ~= 36 then |
| return false |
| end |
| |
| return match(str, p) ~= nil |
| end |
| end |
| end |
| |
| |
| ---------------- |
| -- v4 generation |
| ---------------- |
| |
| |
| do |
| local fmt = string.format |
| local random = math.random |
| |
| |
| --- Generate a v4 UUID. |
| -- v4 UUIDs are created from randomly generated numbers. |
| -- |
| -- @treturn string `uuid`: a v4 (randomly generated) UUID. |
| -- @usage |
| -- local uuid = require 'resty.jit-uuid' |
| -- |
| -- local u1 = uuid() ---> __call metamethod |
| -- local u2 = uuid.generate_v4() |
| function _M.generate_v4() |
| return (fmt('%s%s%s%s-%s%s-%s%s-%s%s-%s%s%s%s%s%s', |
| tohex(random(0, 255), 2), |
| tohex(random(0, 255), 2), |
| tohex(random(0, 255), 2), |
| tohex(random(0, 255), 2), |
| |
| tohex(random(0, 255), 2), |
| tohex(random(0, 255), 2), |
| |
| tohex(bor(band(random(0, 255), 0x0F), 0x40), 2), |
| tohex(random(0, 255), 2), |
| |
| tohex(bor(band(random(0, 255), 0x3F), 0x80), 2), |
| tohex(random(0, 255), 2), |
| |
| tohex(random(0, 255), 2), |
| tohex(random(0, 255), 2), |
| tohex(random(0, 255), 2), |
| tohex(random(0, 255), 2), |
| tohex(random(0, 255), 2), |
| tohex(random(0, 255), 2))) |
| end |
| end |
| |
| |
| ---------------- |
| -- v3/v5 generation |
| ---------------- |
| |
| |
| do |
| if ngx then |
| local ffi = require 'ffi' |
| |
| |
| local tonumber = tonumber |
| local assert = assert |
| local error = error |
| local concat = table.concat |
| local type = type |
| local char = string.char |
| local fmt = string.format |
| local sub = string.sub |
| local gmatch = ngx.re.gmatch |
| local sha1_bin = ngx.sha1_bin |
| local md5 = ngx.md5 |
| local C = ffi.C |
| local ffi_new = ffi.new |
| local ffi_str = ffi.string |
| local ffi_cast = ffi.cast |
| local new_tab |
| do |
| local ok |
| ok, new_tab = pcall(require, 'table.new') |
| if not ok then |
| new_tab = function(narr, nrec) return {} end |
| end |
| end |
| |
| |
| ffi.cdef [[ |
| typedef unsigned char u_char; |
| typedef intptr_t ngx_int_t; |
| |
| u_char * ngx_hex_dump(u_char *dst, const u_char *src, size_t len); |
| ngx_int_t ngx_hextoi(u_char *line, size_t n); |
| ]] |
| |
| |
| local str_type = ffi.typeof('uint8_t[?]') |
| local u_char_type = ffi.typeof('u_char *') |
| |
| |
| local function bin_tohex(s) |
| local slen = #s |
| local blen = slen * 2 |
| local buf = ffi_new(str_type, blen) |
| |
| C.ngx_hex_dump(buf, s, slen) |
| |
| return ffi_str(buf, blen) |
| end |
| |
| |
| local function hex_to_i(s) |
| local buf = ffi_cast(u_char_type, s) |
| |
| local n = tonumber(C.ngx_hextoi(buf, #s)) |
| if n == -1 then |
| error("could not convert hex to number") |
| end |
| |
| return n |
| end |
| |
| |
| local buf = new_tab(16, 0) |
| |
| |
| local function factory(namespace, hash_fn) |
| if not _M.is_valid(namespace) then |
| return nil, 'namespace must be a valid UUID' |
| end |
| |
| local i = 0 |
| local iter, err = gmatch(namespace, [[([\da-f][\da-f])]]) |
| if not iter then |
| return nil, 'could not create iter: ' .. err |
| end |
| |
| while true do |
| local m, err = iter() |
| if err then |
| return nil, err |
| end |
| |
| if not m then |
| break |
| end |
| |
| i = i + 1 |
| buf[i] = char(tonumber(m[0], 16)) |
| end |
| |
| assert(i == 16, "invalid binary namespace buffer length") |
| local ns = concat(buf) |
| |
| return function(name) |
| if type(name) ~= 'string' then |
| return nil, 'name must be a string' |
| end |
| |
| local hash, ver, var = hash_fn(ns, name) |
| |
| return (fmt('%s-%s-%s%s-%s%s-%s', sub(hash, 1, 8), |
| sub(hash, 9, 12), |
| ver, |
| sub(hash, 15, 16), |
| var, |
| sub(hash, 19, 20), |
| sub(hash, 21, 32))) |
| end |
| end |
| |
| |
| local function v3_hash(binary, name) |
| local hash = md5(binary .. name) |
| |
| return hash, |
| tohex(bor(band(hex_to_i(sub(hash, 13, 14)), 0x0F), 0x30), 2), |
| tohex(bor(band(hex_to_i(sub(hash, 17, 18)), 0x3F), 0x80), 2) |
| end |
| |
| |
| local function v5_hash(binary, name) |
| local hash = bin_tohex(sha1_bin(binary .. name)) |
| |
| return hash, |
| tohex(bor(band(hex_to_i(sub(hash, 13, 14)), 0x0F), 0x50), 2), |
| tohex(bor(band(hex_to_i(sub(hash, 17, 18)), 0x3F), 0x80), 2) |
| end |
| |
| |
| --- Instanciate a v3 UUID factory. |
| -- @function factory_v3 |
| -- Creates a closure generating namespaced v3 UUIDs. |
| -- @param[type=string] namespace (must be a valid UUID according to `is_valid`) |
| -- @treturn function `factory`: a v3 UUID generator. |
| -- @treturn string `err`: a string describing an error |
| -- @usage |
| -- local uuid = require 'resty.jit-uuid' |
| -- |
| -- local fact = assert(uuid.factory_v3('e6ebd542-06ae-11e6-8e82-bba81706b27d')) |
| -- |
| -- local u1 = fact('hello') |
| -- ---> 3db7a435-8c56-359d-a563-1b69e6802c78 |
| -- |
| -- local u2 = fact('foobar') |
| -- ---> e8d3eeba-7723-3b72-bbc5-8f598afa6773 |
| function _M.factory_v3(namespace) |
| return factory(namespace, v3_hash) |
| end |
| |
| |
| --- Instanciate a v5 UUID factory. |
| -- @function factory_v5 |
| -- Creates a closure generating namespaced v5 UUIDs. |
| -- @param[type=string] namespace (must be a valid UUID according to `is_valid`) |
| -- @treturn function `factory`: a v5 UUID generator. |
| -- @treturn string `err`: a string describing an error |
| -- @usage |
| -- local uuid = require 'resty.jit-uuid' |
| -- |
| -- local fact = assert(uuid.factory_v5('e6ebd542-06ae-11e6-8e82-bba81706b27d')) |
| -- |
| -- local u1 = fact('hello') |
| -- ---> 4850816f-1658-5890-8bfd-1ed14251f1f0 |
| -- |
| -- local u2 = fact('foobar') |
| -- ---> c9be99fc-326b-5066-bdba-dcd31a6d01ab |
| function _M.factory_v5(namespace) |
| return factory(namespace, v5_hash) |
| end |
| |
| |
| --- Generate a v3 UUID. |
| -- v3 UUIDs are created from a namespace and a name (a UUID and a string). |
| -- The same name and namespace result in the same UUID. The same name and |
| -- different namespaces result in different UUIDs, and vice-versa. |
| -- The resulting UUID is derived using MD5 hashing. |
| -- |
| -- This is a sugar function which instanciates a short-lived v3 UUID factory. |
| -- It is an expensive operation, and intensive generation using the same |
| -- namespaces should prefer allocating their own long-lived factory with |
| -- `factory_v3`. |
| -- |
| -- @param[type=string] namespace (must be a valid UUID according to `is_valid`) |
| -- @param[type=string] name |
| -- @treturn string `uuid`: a v3 (namespaced) UUID. |
| -- @treturn string `err`: a string describing an error |
| -- @usage |
| -- local uuid = require 'resty.jit-uuid' |
| -- |
| -- local u = uuid.generate_v3('e6ebd542-06ae-11e6-8e82-bba81706b27d', 'hello') |
| -- ---> 3db7a435-8c56-359d-a563-1b69e6802c78 |
| function _M.generate_v3(namespace, name) |
| local fact, err = _M.factory_v3(namespace) |
| if not fact then |
| return nil, err |
| end |
| |
| return fact(name) |
| end |
| |
| |
| --- Generate a v5 UUID. |
| -- v5 UUIDs are created from a namespace and a name (a UUID and a string). |
| -- The same name and namespace result in the same UUID. The same name and |
| -- different namespaces result in different UUIDs, and vice-versa. |
| -- The resulting UUID is derived using SHA-1 hashing. |
| -- |
| -- This is a sugar function which instanciates a short-lived v5 UUID factory. |
| -- It is an expensive operation, and intensive generation using the same |
| -- namespaces should prefer allocating their own long-lived factory with |
| -- `factory_v5`. |
| -- |
| -- @param[type=string] namespace (must be a valid UUID according to `is_valid`) |
| -- @param[type=string] name |
| -- @treturn string `uuid`: a v5 (namespaced) UUID. |
| -- @treturn string `err`: a string describing an error |
| -- @usage |
| -- local uuid = require 'resty.jit-uuid' |
| -- |
| -- local u = uuid.generate_v5('e6ebd542-06ae-11e6-8e82-bba81706b27d', 'hello') |
| -- ---> 4850816f-1658-5890-8bfd-1ed14251f1f0 |
| function _M.generate_v5(namespace, name) |
| local fact, err = _M.factory_v5(namespace) |
| if not fact then |
| return nil, err |
| end |
| |
| return fact(name) |
| end |
| |
| else |
| function _M.factory_v3() error('v3 UUID generation only supported in ngx_lua', 2) end |
| function _M.generate_v3() error('v3 UUID generation only supported in ngx_lua', 2) end |
| function _M.factory_v5() error('v5 UUID generation only supported in ngx_lua', 2) end |
| function _M.generate_v5() error('v5 UUID generation only supported in ngx_lua', 2) end |
| end |
| end |
| |
| |
| return setmetatable(_M, { |
| __call = _M.generate_v4 |
| }) |