mirror of
https://github.com/Yu-Vitaqua-fer-Chronos/NULID.git
synced 2025-07-27 15:41:47 +00:00
Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
|
59d884d18f | ||
|
62bb09c7f0 | ||
|
952412e5f5 | ||
|
a5d234e0c2 | ||
|
8c3165a000 | ||
|
5d35f54bac | ||
|
5e3beea010 | ||
|
d0470fca92 | ||
|
f998c0d751 | ||
|
8ce252be13 | ||
|
677855bbd8 | ||
|
138ea30fb7 | ||
|
a889cc57f8 | ||
|
3c52145834 | ||
|
5221a0b9b5 | ||
|
534ba1c2f5 | ||
|
57daeacda6 | ||
|
924c385154 | ||
|
0976774b98 | ||
|
0007fd249e |
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
|
@ -47,10 +47,10 @@ jobs:
|
||||||
uses: actions/configure-pages@v3
|
uses: actions/configure-pages@v3
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v1
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.deploy-dir }}
|
path: ${{ env.deploy-dir }}
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v1
|
uses: actions/deploy-pages@v4
|
||||||
|
|
42
.github/workflows/test.yml
vendored
Normal file
42
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
env:
|
||||||
|
nim-version: 'stable'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: jiro4989/setup-nim-action@v1
|
||||||
|
with:
|
||||||
|
nim-version: ${{ env.nim-version }}
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- run: nimble install -Y
|
||||||
|
|
||||||
|
- name: Run Tests C (Normal Usage)
|
||||||
|
run: nimble test
|
||||||
|
|
||||||
|
- name: Run Tests C (Locks Disabled)
|
||||||
|
run: nimble test -d:nulidNoLocks
|
||||||
|
|
||||||
|
- name: Run Tests C (Insecure Random)
|
||||||
|
run: nimble test -d:nulidInsecureRandom
|
||||||
|
|
||||||
|
- name: Run Tests C (Insecure Random & Locks Disabled)
|
||||||
|
run: nimble test -d:nulidInsecureRandom -d:nulidNoLocks
|
||||||
|
|
||||||
|
- name: Run Tests JS (Normal Usage; Locks Disabled by Default)
|
||||||
|
run: nimble test -b:js
|
||||||
|
|
||||||
|
- name: Run Tests JS (Insecure Random)
|
||||||
|
run: nimble test -b:js -d:nulidInsecureRandom
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
|
tests/test1.js
|
||||||
tests/test1
|
tests/test1
|
||||||
htmldocs/
|
htmldocs/
|
22
README.md
22
README.md
|
@ -1,11 +1,25 @@
|
||||||
# NULID
|
# NULID
|
||||||
This is an implementation of the [ULID](https://github.com/ulid/spec)
|
This is an implementation of the [ULID](https://github.com/ulid/spec)
|
||||||
spec in Nim!
|
spec in Nim! This also supports the JS backend for ULID generation!
|
||||||
|
|
||||||
|
This supports [`jsony`](https://github.com/treeform/jsony) and
|
||||||
|
[`debby`](https://github.com/treeform/debby) out of the box too!
|
||||||
|
|
||||||
|
Random fun fact: I coded the initial code for ULID generation on my phone
|
||||||
|
via Termux!
|
||||||
|
|
||||||
|
## Compile Flags
|
||||||
|
`-d:nulidInsecureRandom`: Uses `std/random` instead of `std/sysrand`.
|
||||||
|
|
||||||
|
`-d:nulidNoLocks`: Disables any usage of locks within the program.
|
||||||
|
|
||||||
|
The JS backend automatically defines `-d:nulidNoLocks`.
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
```nim
|
```nim
|
||||||
let gen = NULIDGenerator()
|
let gen = initUlidGenerator()
|
||||||
let nulid = gen.nulidSync()
|
let ulid = gen.ulid()
|
||||||
|
|
||||||
echo nulid
|
echo ulid
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.2.2"
|
version = "1.3.1"
|
||||||
author = "Yu Vitaqua fer Chronos"
|
author = "Yu Vitaqua fer Chronos"
|
||||||
description = "An implementation of ULID!"
|
description = "An implementation of ULID!"
|
||||||
license = "CC0"
|
license = "CC0"
|
||||||
|
@ -11,4 +11,4 @@ srcDir = "src"
|
||||||
|
|
||||||
requires "nim >= 2.0.0"
|
requires "nim >= 2.0.0"
|
||||||
requires "nint128 >= 0.3.2"
|
requires "nint128 >= 0.3.2"
|
||||||
requires "crockfordb32 >= 0.2.0"
|
requires "crockfordb32 >= 2.0.0"
|
||||||
|
|
409
src/nulid.nim
409
src/nulid.nim
|
@ -1,66 +1,140 @@
|
||||||
|
{.define: crockfordb32NintSupport.}
|
||||||
|
|
||||||
import std/[
|
import std/[
|
||||||
asyncdispatch,
|
times,
|
||||||
times
|
json
|
||||||
]
|
]
|
||||||
|
|
||||||
import pkg/[
|
import crockfordb32
|
||||||
crockfordb32,
|
|
||||||
nint128
|
|
||||||
]
|
|
||||||
|
|
||||||
const insecureRandom = defined(nulidInsecureRandom) or defined(js) # No sysrand on the JS backend
|
import ./nulid/private/constants
|
||||||
|
|
||||||
when insecureRandom:
|
when not defined(js):
|
||||||
|
import std/os
|
||||||
|
import nint128
|
||||||
|
|
||||||
|
import ./nulid/private/stew/endians2
|
||||||
|
|
||||||
|
else:
|
||||||
|
import std/jsbigints
|
||||||
|
|
||||||
|
when InsecureRandom:
|
||||||
import std/random
|
import std/random
|
||||||
|
|
||||||
else:
|
else:
|
||||||
import std/sysrand
|
import std/sysrand
|
||||||
|
|
||||||
# Relying on internal implementation details isn't great, but it's unlikely to change so...
|
when not NoLocks:
|
||||||
import pkg/nint128/vendor/stew/endians2
|
import std/rlocks
|
||||||
|
|
||||||
const HighUint80 = u128("1208925819614629174706176")
|
##[
|
||||||
|
Note: There are 2 defines that can be passed to the compiler to trigger different
|
||||||
|
functionality in this library at runtime, they are listed here:
|
||||||
|
- `--define:nulidInsecureRandom`: Uses `std/random` instead of `std/sysrand`.
|
||||||
|
- `--define:nulidNoLocks`: Disables the use of locks.
|
||||||
|
|
||||||
|
The JS backend used `-d:nulidNoLocks` by default.
|
||||||
|
]##
|
||||||
|
|
||||||
|
when not defined(js):
|
||||||
|
type UInt128DropIn = UInt128
|
||||||
|
|
||||||
|
else:
|
||||||
|
type UInt128DropIn = JsBigInt
|
||||||
|
|
||||||
type
|
type
|
||||||
NULID* = object
|
ULIDError* = object of CatchableError
|
||||||
|
ULIDDefect* = object of Defect
|
||||||
|
|
||||||
|
ULIDGenerationError* = object of ULIDError
|
||||||
|
ULIDGenerationDefect* = object of ULIDDefect
|
||||||
|
|
||||||
|
ULID* = object
|
||||||
## An object representing a ULID.
|
## An object representing a ULID.
|
||||||
timestamp*: int64 = 0
|
timestamp*: int64
|
||||||
randomness*: UInt128 = 0.u128
|
when not defined(js):
|
||||||
|
randomness*: UInt128
|
||||||
|
else:
|
||||||
|
randomness*: JsBigInt
|
||||||
|
|
||||||
NULIDGenerator* = ref object
|
ULIDGenerator* = ref object
|
||||||
## A NULID generator object, contains details needed to follow the spec.
|
## A `ULID` generator object, contains details needed to follow the spec.
|
||||||
## A generator was made to be compliant with the NULID spec and also to be
|
## A generator was made to be compliant with the ULID spec and also to be
|
||||||
## threadsafe not use globals that could change.
|
## threadsafe.
|
||||||
lastTime: int64 ## Timestamp of last ULID
|
when NoLocks:
|
||||||
random: UInt128 ## A random number
|
lastTime: int64 # Timestamp of last ULID, 48 bits
|
||||||
|
when not defined(js):
|
||||||
|
random: UInt128 # A random number, 80 bits
|
||||||
|
else:
|
||||||
|
random: JsBigInt
|
||||||
|
|
||||||
when insecureRandom:
|
when InsecureRandom:
|
||||||
rand: Rand ## Random generator when using insecure random
|
rand: Rand # Random generator when using insecure random
|
||||||
|
|
||||||
proc initNulidGenerator*(): NULIDGenerator =
|
else:
|
||||||
## Initialises a `NULIDGenerator` for use.
|
lock*: RLock
|
||||||
result = NULIDGenerator(lastTime: 0, random: 0.u128)
|
lastTime {.guard: lock.}: int64 # Timestamp of last ULID, 48 bits
|
||||||
|
random {.guard: lock.}: UInt128 # A random number, 80 bits
|
||||||
|
|
||||||
when insecureRandom:
|
when InsecureRandom:
|
||||||
result.rand = initRand()
|
rand {.guard: lock.}: Rand # Random generator when using insecure random
|
||||||
|
|
||||||
let globalGen = initNulidGenerator()
|
|
||||||
|
|
||||||
func swapBytes(x: Int128): Int128 =
|
template withLock(gen: ULIDGenerator, body: typed) =
|
||||||
result.lo = swapBytes(cast[uint64](x.hi))
|
when NoLocks:
|
||||||
result.hi = cast[int64](swapBytes(x.lo))
|
body
|
||||||
|
else:
|
||||||
|
{.cast(gcsafe).}:
|
||||||
|
acquire(gen.lock)
|
||||||
|
|
||||||
|
{.locks: [gen.lock].}:
|
||||||
|
try:
|
||||||
|
body
|
||||||
|
finally:
|
||||||
|
{.cast(gcsafe).}:
|
||||||
|
release(gen.lock)
|
||||||
|
|
||||||
|
|
||||||
|
proc initUlidGenerator*(): ULIDGenerator =
|
||||||
|
## Initialises a `ULIDGenerator` for use.
|
||||||
|
when NoLocks:
|
||||||
|
result = ULIDGenerator(lastTime: LowInt48, random: LowUint80)
|
||||||
|
else:
|
||||||
|
result = ULIDGenerator(lock: RLock(), lastTime: LowInt48, random: LowUint80)
|
||||||
|
initRLock(result.lock)
|
||||||
|
|
||||||
|
when InsecureRandom:
|
||||||
|
result.withLock:
|
||||||
|
result.rand = initRand()
|
||||||
|
|
||||||
|
|
||||||
|
let globalGen = initUlidGenerator()
|
||||||
|
|
||||||
|
|
||||||
|
when not defined(js):
|
||||||
|
func swapBytes(x: Int128): Int128 =
|
||||||
|
result.lo = swapBytes(cast[uint64](x.hi))
|
||||||
|
result.hi = cast[int64](swapBytes(x.lo))
|
||||||
|
|
||||||
|
|
||||||
func toArray[T](oa: openArray[T], size: static Slice[int]): array[size.len, T] =
|
func toArray[T](oa: openArray[T], size: static Slice[int]): array[size.len, T] =
|
||||||
result[0..<size.len] = oa[size]
|
result[0..<size.len] = oa[size]
|
||||||
|
|
||||||
proc randomBits(n: NULIDGenerator): UInt128 =
|
|
||||||
|
proc randomBits(n: ULIDGenerator): UInt128DropIn {.gcsafe.} =
|
||||||
var arr: array[16, byte]
|
var arr: array[16, byte]
|
||||||
|
|
||||||
when insecureRandom:
|
when InsecureRandom:
|
||||||
var rnd: array[10, byte]
|
var rnd: array[10, byte]
|
||||||
|
|
||||||
rnd[0..7] = cast[array[8, byte]](n.rand.next())
|
n.withLock:
|
||||||
rnd[8..9] = cast[array[2, byte]](n.rand.rand(high(int16)).int16)
|
when not defined(js):
|
||||||
|
rnd[0..7] = cast[array[8, byte]](n.rand.next())
|
||||||
|
rnd[8..9] = cast[array[2, byte]](n.rand.rand(high(uint16)).uint16)
|
||||||
|
|
||||||
|
else:
|
||||||
|
for i in 0..9:
|
||||||
|
rnd[i] = n.rand.rand(high(byte)).byte
|
||||||
|
|
||||||
arr[6..15] = rnd
|
arr[6..15] = rnd
|
||||||
|
|
||||||
|
@ -68,121 +142,220 @@ proc randomBits(n: NULIDGenerator): UInt128 =
|
||||||
var rnd: array[10, byte]
|
var rnd: array[10, byte]
|
||||||
|
|
||||||
if not urandom(rnd):
|
if not urandom(rnd):
|
||||||
raise newException(OSError, "Was unable to use a secure source of randomness! " &
|
raise newException(ULIDGenerationDefect, "Was unable to use a secure source of randomness! " &
|
||||||
"Please either compile with `--define:nulidInsecureRandom` or fix this!")
|
"Please either compile with `-d:nulidInsecureRandom` or fix this somehow!")
|
||||||
|
|
||||||
arr[6..15] = rnd
|
arr[6..15] = rnd
|
||||||
|
|
||||||
result = UInt128.fromBytesBE(arr)
|
when not defined(js):
|
||||||
|
result = UInt128.fromBytesBE(arr)
|
||||||
|
|
||||||
|
else:
|
||||||
|
for i in arr:
|
||||||
|
result = result shl 8'big
|
||||||
|
result += big(i)
|
||||||
|
|
||||||
|
|
||||||
template getTime: int64 = (epochTime() * 1000).int64
|
template getTime: int64 = (epochTime() * 1000).int64
|
||||||
|
|
||||||
proc wait(gen: NULIDGenerator): Future[int64] {.async.} =
|
|
||||||
result = getTime()
|
|
||||||
|
|
||||||
while result <= gen.lastTime:
|
proc wait(gen: ULIDGenerator): int64 {.gcsafe.} =
|
||||||
await sleepAsync(1)
|
when not defined(js):
|
||||||
result = getTime()
|
result = getTime()
|
||||||
|
|
||||||
proc nulid*(gen: NULIDGenerator, timestamp: int64 = 0): Future[NULID] {.async.} =
|
gen.withLock:
|
||||||
## Asynchronously generate a `NULID`.
|
while result <= gen.lastTime:
|
||||||
|
sleep(1)
|
||||||
|
result = getTime()
|
||||||
|
|
||||||
|
if result < gen.lastTime:
|
||||||
|
raise newException(ULIDGenerationError, "Time went backwards!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise newException(ULIDGenerationError, "Couldn't generate ULID! Try again in a millisecond.")
|
||||||
|
|
||||||
|
|
||||||
|
proc ulid*(gen: ULIDGenerator, timestamp = LowInt48, randomness = LowUint80): ULID {.gcsafe.} =
|
||||||
|
## Generate a `ULID`, if timestamp is equal to `0`, the `randomness` parameter
|
||||||
|
## will be ignored.
|
||||||
|
##
|
||||||
|
## See also:
|
||||||
|
## * `ulid(int64, UInt128) <#ulid_2>`_
|
||||||
|
runnableExamples:
|
||||||
|
let gen = initUlidGenerator()
|
||||||
|
|
||||||
|
echo gen.ulid()
|
||||||
|
|
||||||
if timestamp == 0:
|
if timestamp == 0:
|
||||||
var now = getTime()
|
var now = getTime()
|
||||||
|
|
||||||
if now < gen.lastTime:
|
gen.withLock:
|
||||||
raise newException(OSError, "Time went backwards!")
|
if gen.lastTime == now:
|
||||||
|
inc gen.random
|
||||||
|
|
||||||
if gen.lastTime == now:
|
if gen.random == HighUInt80:
|
||||||
inc gen.random
|
now = gen.wait()
|
||||||
|
gen.random = gen.randomBits()
|
||||||
|
|
||||||
if gen.random == HighUInt80:
|
else:
|
||||||
now = await gen.wait()
|
gen.random = gen.randomBits()
|
||||||
|
|
||||||
else:
|
result.randomness = gen.random
|
||||||
gen.random = gen.randomBits()
|
|
||||||
|
|
||||||
result.timestamp = now
|
result.timestamp = now
|
||||||
|
|
||||||
else:
|
else:
|
||||||
result.timestamp = timestamp
|
result.timestamp = clamp(timestamp, LowInt48, HighInt48)
|
||||||
result.randomness = gen.random
|
result.randomness = clamp(randomness, LowUint80, HighUint80)
|
||||||
|
|
||||||
proc nulidSync*(gen: NULIDGenerator, timestamp: int64 = 0): NULID =
|
|
||||||
## Synchronously generate a `NULID`.
|
|
||||||
result = waitFor gen.nulid(timestamp)
|
|
||||||
|
|
||||||
proc nulid*(timestamp: int64 = 0): Future[NULID] =
|
proc ulid*(timestamp = LowInt48, randomness = LowUint80): ULID =
|
||||||
## Asynchronously generate a `NULID` using the global generator.
|
## Generate a `ULID` using the global generator.
|
||||||
result = nulid(globalGen, timestamp)
|
##
|
||||||
|
## See also:
|
||||||
proc nulidSync*(timestamp: int64 = 0): NULID =
|
## * `ulid(ULIDGenerator, int64, UInt128) <#ulid,ULIDGenerator>`_
|
||||||
## Synchronously generate a `NULID` using the global generator.
|
|
||||||
runnableExamples:
|
runnableExamples:
|
||||||
echo nulidSync()
|
echo ulid()
|
||||||
|
|
||||||
result = waitFor nulid(timestamp)
|
result = ulid(globalGen, timestamp)
|
||||||
|
|
||||||
func toInt128*(ulid: NULID): Int128 =
|
|
||||||
## Allows for a ULID to be converted to an Int128.
|
when not defined(js):
|
||||||
|
func toInt128*(ulid: ULID): Int128 =
|
||||||
|
## Allows for a `ULID` to be converted to an `Int128`.
|
||||||
|
##
|
||||||
|
## **Note:** On the JS backend this returns a `JsBigInt` from `std/jsbigints`
|
||||||
|
runnableExamples:
|
||||||
|
echo ulid().toInt128()
|
||||||
|
|
||||||
|
result = i128(ulid.timestamp) shl 80
|
||||||
|
|
||||||
|
result.hi += cast[int64](ulid.randomness.hi)
|
||||||
|
result.lo += ulid.randomness.lo
|
||||||
|
|
||||||
|
|
||||||
|
func fromInt128*(_: typedesc[ULID], val: Int128): ULID =
|
||||||
|
## Parses an `Int128` to a `ULID`.
|
||||||
|
##
|
||||||
|
## **Note:** On the JS backend this accepts a `JsBigInt` from `std/jsbigints`
|
||||||
|
result.timestamp = (val shr 16).hi
|
||||||
|
result.randomness = UInt128(
|
||||||
|
hi: cast[uint64]((val.hi shl 48) shr 48),
|
||||||
|
lo: val.lo
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
func toInt128*(ulid: ULID): JsBigInt =
|
||||||
|
## Allows for a `ULID` to be converted to a `JsBigInt`.
|
||||||
|
##
|
||||||
|
## **Note:** On the native backends this returns an `Int128` from `nint128`.
|
||||||
|
runnableExamples:
|
||||||
|
echo ulid().toInt128()
|
||||||
|
|
||||||
|
result = big(ulid.timestamp) shl 80'big
|
||||||
|
result += ulid.randomness
|
||||||
|
|
||||||
|
|
||||||
|
proc fromInt128*(_: typedesc[ULID], val: JsBigInt): ULID =
|
||||||
|
## Parses an `JsBigInt` to a `ULID`.
|
||||||
|
##
|
||||||
|
## **Note:** On the native backends this accepts an `Int128` from `nint128`
|
||||||
|
assert val <= HighInt128
|
||||||
|
|
||||||
|
result.timestamp = ((val and TimestampBitmask) shr 80'big).toNumber().int64
|
||||||
|
result.randomness = val and RandomnessBitmask
|
||||||
|
|
||||||
|
|
||||||
|
when not defined(js):
|
||||||
|
func toBytes*(ulid: ULID): array[16, byte] =
|
||||||
|
## Allows for a `ULID` to be converted to a byte array for the binary format.
|
||||||
|
##
|
||||||
|
## **Note:** This isn't available for the JS backend.
|
||||||
|
runnableExamples:
|
||||||
|
let
|
||||||
|
ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
|
||||||
|
ulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
|
||||||
|
|
||||||
|
assert ulid == ULID.fromBytes(ulidBytes)
|
||||||
|
|
||||||
|
when cpuEndian == littleEndian:
|
||||||
|
return cast[array[16, byte]](ulid.toInt128().swapBytes())
|
||||||
|
|
||||||
|
else:
|
||||||
|
return cast[array[16, byte]](ulid.toInt128())
|
||||||
|
|
||||||
|
|
||||||
|
func fromBytes*(_: typedesc[ULID], ulidBytes: openArray[byte]): ULID =
|
||||||
|
## Parses a byte array to a `ULID.`.
|
||||||
|
##
|
||||||
|
## **Note:** This isn't available for the JS backend.
|
||||||
|
|
||||||
|
if ulidBytes.len != 16:
|
||||||
|
raise newException(RangeDefect, "Given byte array must be 16 bytes long!")
|
||||||
|
|
||||||
|
when cpuEndian == littleEndian:
|
||||||
|
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)).swapBytes())
|
||||||
|
|
||||||
|
else:
|
||||||
|
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)))
|
||||||
|
|
||||||
|
|
||||||
|
func parse*(_: typedesc[ULID], ulidStr: string): ULID =
|
||||||
|
## Parses a `ULID` from a string.
|
||||||
runnableExamples:
|
runnableExamples:
|
||||||
echo nulidSync().toInt128()
|
echo ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
|
||||||
|
|
||||||
result = i128(ulid.timestamp) shl 80
|
|
||||||
|
|
||||||
result.hi += cast[int64](ulid.randomness.hi)
|
|
||||||
result.lo += ulid.randomness.lo
|
|
||||||
|
|
||||||
func fromInt128*(_: typedesc[NULID], val: Int128): NULID =
|
|
||||||
## Parses an Int128 to a NULID.
|
|
||||||
result.timestamp = (val shr 16).hi
|
|
||||||
result.randomness = UInt128(
|
|
||||||
hi: cast[uint64]((val.hi shl 48) shr 48),
|
|
||||||
lo: val.lo
|
|
||||||
)
|
|
||||||
|
|
||||||
func toBytes*(ulid: NULID): array[16, byte] =
|
|
||||||
## Allows for a NULID to be converted to a byte array.
|
|
||||||
runnableExamples:
|
|
||||||
let
|
|
||||||
ulid = parseNulid("01H999MBGTEA8BDS0M5AWEBB1A")
|
|
||||||
ulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
|
|
||||||
|
|
||||||
echo ulid == NULID.fromBytes(ulidBytes)
|
|
||||||
|
|
||||||
when cpuEndian == littleEndian:
|
|
||||||
return cast[array[16, byte]](ulid.toInt128().swapBytes())
|
|
||||||
|
|
||||||
else:
|
|
||||||
return cast[array[16, byte]](ulid.toInt128())
|
|
||||||
|
|
||||||
func fromBytes*(_: typedesc[NULID], ulidBytes: openArray[byte]): NULID =
|
|
||||||
## Parses a byte array to a NULID.
|
|
||||||
if ulidBytes.len != 16:
|
|
||||||
raise newException(RangeDefect, "Given byte array must be 16 bytes long!")
|
|
||||||
|
|
||||||
when cpuEndian == littleEndian:
|
|
||||||
return NULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)).swapBytes())
|
|
||||||
|
|
||||||
else:
|
|
||||||
return NULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)))
|
|
||||||
|
|
||||||
func parseNulid*(ulidStr: string): NULID =
|
|
||||||
## Parses a string to a NULID.
|
|
||||||
runnableExamples:
|
|
||||||
echo parseNulid("01H999MBGTEA8BDS0M5AWEBB1A")
|
|
||||||
|
|
||||||
if ulidStr.len != 26:
|
if ulidStr.len != 26:
|
||||||
raise newException(RangeDefect, "Invalid ULID! Must be 26 characters long!")
|
raise newException(RangeDefect, "Invalid ULID! Must be 26 characters long!")
|
||||||
|
|
||||||
result.timestamp = int64.decode(ulidStr[0..9])
|
result.timestamp = int64.decode(ulidStr[0..9])
|
||||||
result.randomness = UInt128.decode(ulidStr[10..25])
|
when not defined(js):
|
||||||
|
result.randomness = UInt128.decode(ulidStr[10..25])
|
||||||
|
else:
|
||||||
|
result.randomness = JsBigInt.decode(ulidStr[10..25])
|
||||||
|
|
||||||
func `$`*(ulid: NULID): string =
|
|
||||||
|
proc `==`*(a, b: ULID): bool = a.toInt128() == b.toInt128()
|
||||||
|
|
||||||
|
|
||||||
|
func `$`*(ulid: ULID): string =
|
||||||
## Returns the string representation of a ULID.
|
## Returns the string representation of a ULID.
|
||||||
runnableExamples:
|
runnableExamples:
|
||||||
echo nulidSync()
|
echo $ulid()
|
||||||
|
|
||||||
result = Int128.encode(ulid.toInt128(), 26)
|
when not defined(js):
|
||||||
|
result = Int128.encode(ulid.toInt128(), 26)
|
||||||
|
else:
|
||||||
|
result = JsBigInt.encode(ulid.toInt128(), 26)
|
||||||
|
|
||||||
if result.len < 26: # Crappy fix
|
# std/json support
|
||||||
result.insert("0")
|
proc `%`*(u: ULID): JsonNode =
|
||||||
|
## Serializes a `ULID` to JSON.
|
||||||
|
newJString($u)
|
||||||
|
|
||||||
|
proc to*(j: JsonNode, _: typedesc[ULID]): ULID =
|
||||||
|
## Deserializes a `ULID` from JSON.
|
||||||
|
if j.kind != JString:
|
||||||
|
raise newException(JsonKindError, "Expected a string!")
|
||||||
|
|
||||||
|
result = ULID.parse(j.getStr())
|
||||||
|
|
||||||
|
when HasJsony:
|
||||||
|
import jsony
|
||||||
|
|
||||||
|
proc dumpHook*(s: var string, ulid: ULID) = s.dumpHook($ulid)
|
||||||
|
proc parseHook*(s: string, i: var int, ulid: var ULID) =
|
||||||
|
var res: string
|
||||||
|
parseHook(s, i, res)
|
||||||
|
ulid = ULID.parse(res)
|
||||||
|
|
||||||
|
when HasDebby:
|
||||||
|
import std/sequtils
|
||||||
|
import debby/common
|
||||||
|
|
||||||
|
func sqlDumpHook*(v: ULID): string = sqlDumpHook(cast[Bytes](v.toBytes().toSeq()))
|
||||||
|
func sqlParseHook*(data: string, v: var ULID) =
|
||||||
|
var res: Bytes
|
||||||
|
sqlParseHook(data, res)
|
||||||
|
v = ULID.fromBytes(cast[seq[byte]](res))
|
||||||
|
|
44
src/nulid/private/constants.nim
Normal file
44
src/nulid/private/constants.nim
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
const
|
||||||
|
LowInt48* = 0'i64
|
||||||
|
HighInt48* = 281474976710655'i64
|
||||||
|
|
||||||
|
const
|
||||||
|
HighUint80Str = "1208925819614629174706176"
|
||||||
|
TimestampBitmaskStr = "340282366920937254537554992802593505280"
|
||||||
|
RandomnessBitmaskStr = "1208925819614629174706175"
|
||||||
|
|
||||||
|
when not defined(js):
|
||||||
|
import nint128
|
||||||
|
|
||||||
|
const
|
||||||
|
LowUint80* = u128(0)
|
||||||
|
HighUint80* = u128(HighUint80Str)
|
||||||
|
|
||||||
|
#TimestampBitmask = big(TimestampBitmaskStr)
|
||||||
|
#RandomnessBitmask = big(RandomnessBitmaskStr)
|
||||||
|
|
||||||
|
else:
|
||||||
|
import std/[
|
||||||
|
jsbigints
|
||||||
|
]
|
||||||
|
|
||||||
|
let
|
||||||
|
LowUint80* = big(0)
|
||||||
|
HighUint80* = big(HighUint80Str)
|
||||||
|
|
||||||
|
HighInt128* = big("340282366920938463463374607431768211455")
|
||||||
|
|
||||||
|
TimestampBitmask* = big(TimestampBitmaskStr)
|
||||||
|
RandomnessBitmask* = big(RandomnessBitmaskStr)
|
||||||
|
|
||||||
|
|
||||||
|
# No sysrand on the JS backend nor VM.
|
||||||
|
const InsecureRandom* = defined(nulidInsecureRandom) or defined(nimscript)
|
||||||
|
const NoLocks* = defined(nulidNoLocks) or defined(js) or defined(nimscript)
|
||||||
|
|
||||||
|
# Support for other libraries
|
||||||
|
{.warning[UnusedImport]:off.}
|
||||||
|
const
|
||||||
|
HasJsony* = compiles do: import jsony
|
||||||
|
|
||||||
|
HasDebby* = compiles do: import debby/common
|
186
src/nulid/private/stew/endians2.nim
Normal file
186
src/nulid/private/stew/endians2.nim
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
# Copyright (c) 2018-2019 Status Research & Development GmbH
|
||||||
|
# Licensed and distributed under either of
|
||||||
|
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
|
||||||
|
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
|
||||||
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||||
|
|
||||||
|
# Endian conversion operations for unsigned integers, suitable for serializing
|
||||||
|
# and deserializing data. The operations are only defined for unsigned
|
||||||
|
# integers - if you wish to encode signed integers, convert / cast them to
|
||||||
|
# unsigned first!
|
||||||
|
#
|
||||||
|
# Although it would be possible to enforce correctness with endians in the type
|
||||||
|
# (`BigEndian[uin64]`) this seems like overkill. That said, some
|
||||||
|
# static analysis tools allow you to annotate fields with endianness - perhaps
|
||||||
|
# an idea for the future, akin to `TaintedString`?
|
||||||
|
#
|
||||||
|
# Keeping the above in mind, it's generally safer to use `array[N, byte]` to
|
||||||
|
# hold values of specific endianness and read them out with `fromBytes` when the
|
||||||
|
# integer interpretation of the bytes is needed.
|
||||||
|
|
||||||
|
{.push raises: [].}
|
||||||
|
|
||||||
|
type
|
||||||
|
SomeEndianInt* = uint8|uint16|uint32|uint64
|
||||||
|
## types that we support endian conversions for - uint8 is there for
|
||||||
|
## for syntactic / generic convenience. Other candidates:
|
||||||
|
## * int/uint - uncertain size, thus less suitable for binary interop
|
||||||
|
## * intX - over and underflow protection in nim might easily cause issues -
|
||||||
|
## need to consider before adding here
|
||||||
|
|
||||||
|
const
|
||||||
|
useBuiltins = not defined(noIntrinsicsEndians)
|
||||||
|
|
||||||
|
when (defined(gcc) or defined(llvm_gcc) or defined(clang)) and useBuiltins:
|
||||||
|
func swapBytesBuiltin(x: uint8): uint8 = x
|
||||||
|
func swapBytesBuiltin(x: uint16): uint16 {.
|
||||||
|
importc: "__builtin_bswap16", nodecl.}
|
||||||
|
|
||||||
|
func swapBytesBuiltin(x: uint32): uint32 {.
|
||||||
|
importc: "__builtin_bswap32", nodecl.}
|
||||||
|
|
||||||
|
func swapBytesBuiltin(x: uint64): uint64 {.
|
||||||
|
importc: "__builtin_bswap64", nodecl.}
|
||||||
|
|
||||||
|
elif defined(icc) and useBuiltins:
|
||||||
|
func swapBytesBuiltin(x: uint8): uint8 = x
|
||||||
|
func swapBytesBuiltin(a: uint16): uint16 {.importc: "_bswap16", nodecl.}
|
||||||
|
func swapBytesBuiltin(a: uint32): uint32 {.importc: "_bswap", nodec.}
|
||||||
|
func swapBytesBuiltin(a: uint64): uint64 {.importc: "_bswap64", nodecl.}
|
||||||
|
|
||||||
|
elif defined(vcc) and useBuiltins:
|
||||||
|
func swapBytesBuiltin(x: uint8): uint8 = x
|
||||||
|
func swapBytesBuiltin(a: uint16): uint16 {.
|
||||||
|
importc: "_byteswap_ushort", cdecl, header: "<intrin.h>".}
|
||||||
|
|
||||||
|
func swapBytesBuiltin(a: uint32): uint32 {.
|
||||||
|
importc: "_byteswap_ulong", cdecl, header: "<intrin.h>".}
|
||||||
|
|
||||||
|
func swapBytesBuiltin(a: uint64): uint64 {.
|
||||||
|
importc: "_byteswap_uint64", cdecl, header: "<intrin.h>".}
|
||||||
|
|
||||||
|
func swapBytesNim(x: uint8): uint8 = x
|
||||||
|
func swapBytesNim(x: uint16): uint16 = (x shl 8) or (x shr 8)
|
||||||
|
|
||||||
|
func swapBytesNim(x: uint32): uint32 =
|
||||||
|
let v = (x shl 16) or (x shr 16)
|
||||||
|
|
||||||
|
((v shl 8) and 0xff00ff00'u32) or ((v shr 8) and 0x00ff00ff'u32)
|
||||||
|
|
||||||
|
func swapBytesNim(x: uint64): uint64 =
|
||||||
|
var v = (x shl 32) or (x shr 32)
|
||||||
|
v =
|
||||||
|
((v and 0x0000ffff0000ffff'u64) shl 16) or
|
||||||
|
((v and 0xffff0000ffff0000'u64) shr 16)
|
||||||
|
|
||||||
|
((v and 0x00ff00ff00ff00ff'u64) shl 8) or
|
||||||
|
((v and 0xff00ff00ff00ff00'u64) shr 8)
|
||||||
|
|
||||||
|
func swapBytes*[T: SomeEndianInt](x: T): T {.inline.} =
|
||||||
|
## Reverse the bytes within an integer, such that the most significant byte
|
||||||
|
## changes place with the least significant one, etc
|
||||||
|
##
|
||||||
|
## Example:
|
||||||
|
## doAssert swapBytes(0x01234567'u32) == 0x67452301
|
||||||
|
when nimvm:
|
||||||
|
swapBytesNim(x)
|
||||||
|
else:
|
||||||
|
when declared(swapBytesBuiltin):
|
||||||
|
swapBytesBuiltin(x)
|
||||||
|
else:
|
||||||
|
swapBytesNim(x)
|
||||||
|
|
||||||
|
func toBytes*(x: SomeEndianInt, endian: Endianness = system.cpuEndian):
|
||||||
|
array[sizeof(x), byte] {.noinit, inline.} =
|
||||||
|
## Convert integer to its corresponding byte sequence using the chosen
|
||||||
|
## endianness. By default, native endianness is used which is not portable!
|
||||||
|
let v =
|
||||||
|
if endian == system.cpuEndian: x
|
||||||
|
else: swapBytes(x)
|
||||||
|
|
||||||
|
when nimvm: # No copyMem in vm
|
||||||
|
for i in 0..<sizeof(result):
|
||||||
|
result[i] = byte((v shr (i * 8)) and 0xff)
|
||||||
|
else:
|
||||||
|
copyMem(addr result, unsafeAddr v, sizeof(result))
|
||||||
|
|
||||||
|
func toBytesLE*(x: SomeEndianInt):
|
||||||
|
array[sizeof(x), byte] {.inline.} =
|
||||||
|
## Convert a native endian integer to a little endian byte sequence
|
||||||
|
toBytes(x, littleEndian)
|
||||||
|
|
||||||
|
func toBytesBE*(x: SomeEndianInt):
|
||||||
|
array[sizeof(x), byte] {.inline.} =
|
||||||
|
## Convert a native endian integer to a native endian byte sequence
|
||||||
|
toBytes(x, bigEndian)
|
||||||
|
|
||||||
|
func fromBytes*(
|
||||||
|
T: typedesc[SomeEndianInt],
|
||||||
|
x: openArray[byte],
|
||||||
|
endian: Endianness = system.cpuEndian): T {.inline.} =
|
||||||
|
## Read bytes and convert to an integer according to the given endianness.
|
||||||
|
##
|
||||||
|
## Note: The default value of `system.cpuEndian` is not portable across
|
||||||
|
## machines.
|
||||||
|
##
|
||||||
|
## Panics when `x.len < sizeof(T)` - for shorter buffers, copy the data to
|
||||||
|
## an `array` first using `arrayops.initCopyFrom`, taking care to zero-fill
|
||||||
|
## at the right end - usually the beginning for big endian and the end for
|
||||||
|
## little endian, but this depends on the serialization of the bytes.
|
||||||
|
|
||||||
|
# This check gets optimized away when the compiler can prove that the length
|
||||||
|
# is large enough - passing in an `array` or using a construct like
|
||||||
|
# ` toOpenArray(pos, pos + sizeof(T) - 1)` are two ways that this happens
|
||||||
|
doAssert x.len >= sizeof(T), "Not enough bytes for endian conversion"
|
||||||
|
|
||||||
|
when nimvm: # No copyMem in vm
|
||||||
|
for i in 0..<sizeof(result):
|
||||||
|
result = result or (T(x[i]) shl (i * 8))
|
||||||
|
else:
|
||||||
|
# `copyMem` helps compilers optimize the copy into a single instruction, when
|
||||||
|
# alignment etc permits
|
||||||
|
copyMem(addr result, unsafeAddr x[0], sizeof(result))
|
||||||
|
|
||||||
|
if endian != system.cpuEndian:
|
||||||
|
# The swap is turned into a CPU-specific instruction and/or combined with
|
||||||
|
# the copy above, again when conditions permit it - for example, on X86
|
||||||
|
# fromBytesBE gets compiled into a single `MOVBE` instruction
|
||||||
|
result = swapBytes(result)
|
||||||
|
|
||||||
|
func fromBytesBE*(
|
||||||
|
T: typedesc[SomeEndianInt],
|
||||||
|
x: openArray[byte]): T {.inline.} =
|
||||||
|
## Read big endian bytes and convert to an integer. At runtime, v must contain
|
||||||
|
## at least sizeof(T) bytes. By default, native endianness is used which is
|
||||||
|
## not portable!
|
||||||
|
fromBytes(T, x, bigEndian)
|
||||||
|
|
||||||
|
func toBE*[T: SomeEndianInt](x: T): T {.inline.} =
|
||||||
|
## Convert a native endian value to big endian. Consider toBytesBE instead
|
||||||
|
## which may prevent some confusion.
|
||||||
|
if cpuEndian == bigEndian: x
|
||||||
|
else: x.swapBytes
|
||||||
|
|
||||||
|
func fromBE*[T: SomeEndianInt](x: T): T {.inline.} =
|
||||||
|
## Read a big endian value and return the corresponding native endian
|
||||||
|
# there's no difference between this and toBE, except when reading the code
|
||||||
|
toBE(x)
|
||||||
|
|
||||||
|
func fromBytesLE*(
|
||||||
|
T: typedesc[SomeEndianInt],
|
||||||
|
x: openArray[byte]): T {.inline.} =
|
||||||
|
## Read little endian bytes and convert to an integer. At runtime, v must
|
||||||
|
## contain at least sizeof(T) bytes. By default, native endianness is used
|
||||||
|
## which is not portable!
|
||||||
|
fromBytes(T, x, littleEndian)
|
||||||
|
|
||||||
|
func toLE*[T: SomeEndianInt](x: T): T {.inline.} =
|
||||||
|
## Convert a native endian value to little endian. Consider toBytesLE instead
|
||||||
|
## which may prevent some confusion.
|
||||||
|
if cpuEndian == littleEndian: x
|
||||||
|
else: x.swapBytes
|
||||||
|
|
||||||
|
func fromLE*[T: SomeEndianInt](x: T): T {.inline.} =
|
||||||
|
## Read a little endian value and return the corresponding native endian
|
||||||
|
# there's no difference between this and toLE, except when reading the code
|
||||||
|
toLE(x)
|
|
@ -4,36 +4,54 @@
|
||||||
# the letter 't').
|
# the letter 't').
|
||||||
#
|
#
|
||||||
# To run these tests, simply execute `nimble test`.
|
# To run these tests, simply execute `nimble test`.
|
||||||
|
import std/[
|
||||||
|
unittest,
|
||||||
|
json
|
||||||
|
]
|
||||||
|
|
||||||
import unittest
|
const UlidRandStr = "541019288874337045949482"
|
||||||
|
|
||||||
import pkg/nint128
|
when not defined(js):
|
||||||
|
import nint128
|
||||||
|
|
||||||
|
const UlidRand = u128(UlidRandStr)
|
||||||
|
else:
|
||||||
|
import std/jsbigints
|
||||||
|
|
||||||
|
let UlidRand = big(UlidRandStr)
|
||||||
|
|
||||||
import nulid
|
import nulid
|
||||||
|
|
||||||
let gen = initNulidGenerator()
|
test "ULID Generation":
|
||||||
|
|
||||||
test "NULID Generation":
|
|
||||||
for _ in 0..5:
|
for _ in 0..5:
|
||||||
let nulid = gen.nulidSync()
|
let ulid = ulid()
|
||||||
echo nulid
|
echo ulid
|
||||||
|
|
||||||
test "NULID Parsing":
|
test "ULID Parsing":
|
||||||
let nulidStr = "01H999MBGTEA8BDS0M5AWEBB1A"
|
let ulidStr = "01H999MBGTEA8BDS0M5AWEBB1A"
|
||||||
let nulid = NULID(timestamp: 1693602950682,
|
let ulid = ULID(timestamp: 1693602950682,
|
||||||
randomness: u128("541019288874337045949482"))
|
randomness: UlidRand)
|
||||||
|
|
||||||
check parseNulid(nulidStr) == nulid
|
check ULID.parse(ulidStr) == ulid
|
||||||
|
|
||||||
test "NULID Int128 Conversion":
|
test "ULID Int128 Conversion":
|
||||||
let nulid = parseNulid("01H999MBGTEA8BDS0M5AWEBB1A")
|
let ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
|
||||||
|
|
||||||
check NULID.fromInt128(nulid.toInt128()) == nulid
|
check ULID.fromInt128(ulid.toInt128()) == ulid
|
||||||
|
|
||||||
test "NULID Binary Format":
|
when not defined(js):
|
||||||
let
|
# Not planned to be implemented yet for the JS backend
|
||||||
nulid = parseNulid("01H999MBGTEA8BDS0M5AWEBB1A")
|
|
||||||
nulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
|
|
||||||
|
|
||||||
check nulid == NULID.fromBytes(nulidBytes)
|
test "ULID Binary Format":
|
||||||
check nulid.toBytes == nulidBytes
|
let
|
||||||
|
ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
|
||||||
|
ulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
|
||||||
|
|
||||||
|
check ulid == ULID.fromBytes(ulidBytes)
|
||||||
|
check ulid.toBytes == ulidBytes
|
||||||
|
|
||||||
|
test "ULID std/json support":
|
||||||
|
let ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
|
||||||
|
|
||||||
|
check (%ulid).getStr() == "01H999MBGTEA8BDS0M5AWEBB1A"
|
||||||
|
check (%ulid).to(ULID) == ulid
|
Loading…
Reference in a new issue