mirror of
https://github.com/Yu-Vitaqua-fer-Chronos/NULID.git
synced 2024-11-21 22:12:46 +00:00
Merge pull request #1 from Yu-Vitaqua-fer-Chronos/devel
Merge Devel into Main
This commit is contained in:
commit
8ce252be13
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
|
@ -20,4 +20,20 @@ jobs:
|
|||
|
||||
- run: nimble install -Y
|
||||
|
||||
- run: nimble test
|
||||
- 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
|
||||
htmldocs/
|
|
@ -1,6 +1,6 @@
|
|||
# NULID
|
||||
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!
|
||||
|
@ -13,6 +13,9 @@ via Termux!
|
|||
|
||||
`-d:nulidNoLocks`: Disables any usage of locks within the program.
|
||||
|
||||
The JS backend automatically defines `-d:nulidNoLocks`.
|
||||
|
||||
|
||||
## Usage
|
||||
```nim
|
||||
let gen = initUlidGenerator()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Package
|
||||
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
author = "Yu Vitaqua fer Chronos"
|
||||
description = "An implementation of ULID!"
|
||||
license = "CC0"
|
||||
|
@ -11,4 +11,4 @@ srcDir = "src"
|
|||
|
||||
requires "nim >= 2.0.0"
|
||||
requires "nint128 >= 0.3.2"
|
||||
requires "crockfordb32 >= 1.0.0"
|
||||
requires "crockfordb32 >= 1.1.0"
|
||||
|
|
237
src/nulid.nim
237
src/nulid.nim
|
@ -1,13 +1,19 @@
|
|||
import std/[
|
||||
times,
|
||||
os
|
||||
times
|
||||
]
|
||||
|
||||
import crockfordb32
|
||||
import nint128
|
||||
|
||||
import ./nulid/private/constants
|
||||
import ./nulid/private/stew/endians2
|
||||
|
||||
when not defined(js):
|
||||
import std/os
|
||||
import nint128
|
||||
|
||||
import ./nulid/private/stew/endians2
|
||||
|
||||
else:
|
||||
import std/jsbigints
|
||||
|
||||
when InsecureRandom:
|
||||
import std/random
|
||||
|
@ -24,15 +30,30 @@ functionality in this library at runtime, they are listed here:
|
|||
- `--define:nulidInsecureRandom`: Uses `std/random` instead of `std/sysrand`.
|
||||
- `--define:nulidNoLocks`
|
||||
|
||||
The JS backend and Nimscript use both of these flags by default (whether either work
|
||||
with NULID is untested).
|
||||
The JS backend used `-d:nulidNoLocks` by default and Nimscript uses both.
|
||||
these flags by default (whether either work with NULID is untested).
|
||||
]##
|
||||
|
||||
when not defined(js):
|
||||
type UInt128DropIn = UInt128
|
||||
|
||||
else:
|
||||
type UInt128DropIn = JsBigInt
|
||||
|
||||
type
|
||||
ULIDError* = object of CatchableError
|
||||
ULIDDefect* = object of Defect
|
||||
|
||||
ULIDGenerationError* = object of ULIDError
|
||||
ULIDGenerationDefect* = object of ULIDDefect
|
||||
|
||||
ULID* = object
|
||||
## An object representing a ULID.
|
||||
timestamp*: int64
|
||||
randomness*: UInt128
|
||||
when not defined(js):
|
||||
randomness*: UInt128
|
||||
else:
|
||||
randomness*: JsBigInt
|
||||
|
||||
ULIDGenerator* = ref object
|
||||
## A `ULID` generator object, contains details needed to follow the spec.
|
||||
|
@ -40,7 +61,10 @@ type
|
|||
## threadsafe not use globals that could change.
|
||||
when NoLocks:
|
||||
lastTime: int64 # Timestamp of last ULID, 48 bits
|
||||
random: UInt128 # A random number, 80 bits
|
||||
when not defined(js):
|
||||
random: UInt128 # A random number, 80 bits
|
||||
else:
|
||||
random: JsBigInt
|
||||
|
||||
when InsecureRandom:
|
||||
rand: Rand # Random generator when using insecure random
|
||||
|
@ -53,44 +77,62 @@ type
|
|||
when InsecureRandom:
|
||||
rand {.guard: lock.}: Rand # Random generator when using insecure random
|
||||
|
||||
|
||||
template withLock(gen: ULIDGenerator, body: typed) =
|
||||
when NoLocks:
|
||||
body
|
||||
else:
|
||||
{.cast(gcsafe).}:
|
||||
gen.lock.withRLock:
|
||||
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: 0, random: 0.u128)
|
||||
result = ULIDGenerator(lastTime: LowInt48, random: LowUint80)
|
||||
else:
|
||||
result = ULIDGenerator(lock: RLock(), lastTime: 0, random: 0.u128)
|
||||
result = ULIDGenerator(lock: RLock(), lastTime: LowInt48, random: LowUint80)
|
||||
initRLock(result.lock)
|
||||
|
||||
when InsecureRandom:
|
||||
result.withLock:
|
||||
result.rand = initRand()
|
||||
|
||||
|
||||
let globalGen = initUlidGenerator()
|
||||
|
||||
func swapBytes(x: Int128): Int128 =
|
||||
result.lo = swapBytes(cast[uint64](x.hi))
|
||||
result.hi = cast[int64](swapBytes(x.lo))
|
||||
|
||||
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] =
|
||||
result[0..<size.len] = oa[size]
|
||||
|
||||
proc randomBits(n: ULIDGenerator): UInt128 {.gcsafe.} =
|
||||
|
||||
proc randomBits(n: ULIDGenerator): UInt128DropIn {.gcsafe.} =
|
||||
var arr: array[16, byte]
|
||||
|
||||
when InsecureRandom:
|
||||
var rnd: array[10, byte]
|
||||
|
||||
n.withLock:
|
||||
rnd[0..7] = cast[array[8, byte]](n.rand.next())
|
||||
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
|
||||
|
||||
|
@ -98,27 +140,40 @@ proc randomBits(n: ULIDGenerator): UInt128 {.gcsafe.} =
|
|||
var rnd: array[10, byte]
|
||||
|
||||
if not urandom(rnd):
|
||||
raise newException(OSError, "Was unable to use a secure source of randomness! " &
|
||||
"Please either compile with `-d:nulidInsecureRandom` or fix this!")
|
||||
raise newException(ULIDGenerationDefect, "Was unable to use a secure source of randomness! " &
|
||||
"Please either compile with `-d:nulidInsecureRandom` or fix this somehow!")
|
||||
|
||||
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
|
||||
|
||||
|
||||
proc wait(gen: ULIDGenerator): int64 {.gcsafe.} =
|
||||
result = getTime()
|
||||
when not defined(js):
|
||||
result = getTime()
|
||||
|
||||
gen.withLock:
|
||||
while result <= gen.lastTime:
|
||||
sleep(1)
|
||||
result = getTime()
|
||||
gen.withLock:
|
||||
while result <= gen.lastTime:
|
||||
sleep(1)
|
||||
result = getTime()
|
||||
|
||||
if result < gen.lastTime:
|
||||
raise newException(OSError, "Time went backwards!")
|
||||
if result < gen.lastTime:
|
||||
raise newException(ULIDGenerationError, "Time went backwards!")
|
||||
|
||||
proc ulid*(gen: ULIDGenerator, timestamp = 0'i64, randomness = u128(0)): ULID =
|
||||
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.
|
||||
runnableExamples:
|
||||
|
@ -145,10 +200,11 @@ proc ulid*(gen: ULIDGenerator, timestamp = 0'i64, randomness = u128(0)): ULID =
|
|||
result.timestamp = now
|
||||
|
||||
else:
|
||||
result.timestamp = clamp(timestamp, 0, HighInt48)
|
||||
result.randomness = clamp(randomness, low(UInt128), HighUint80)
|
||||
result.timestamp = clamp(timestamp, LowInt48, HighInt48)
|
||||
result.randomness = clamp(randomness, LowUint80, HighUint80)
|
||||
|
||||
proc ulid*(timestamp = 0'i64, randomness = u128(0)): ULID =
|
||||
|
||||
proc ulid*(timestamp = LowInt48, randomness = LowUint80): ULID =
|
||||
## Generate a `ULID` using the global generator.
|
||||
##
|
||||
## See also:
|
||||
|
@ -158,49 +214,87 @@ proc ulid*(timestamp = 0'i64, randomness = u128(0)): ULID =
|
|||
|
||||
result = ulid(globalGen, timestamp)
|
||||
|
||||
func toInt128*(ulid: ULID): Int128 =
|
||||
## Allows for a `ULID` to be converted to an Int128.
|
||||
runnableExamples:
|
||||
echo ulid().toInt128()
|
||||
|
||||
result = i128(ulid.timestamp) shl 80
|
||||
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.hi += cast[int64](ulid.randomness.hi)
|
||||
result.lo += ulid.randomness.lo
|
||||
result = i128(ulid.timestamp) shl 80
|
||||
|
||||
func fromInt128*(_: typedesc[ULID], val: Int128): ULID =
|
||||
## Parses an Int128 to a ULID.
|
||||
result.timestamp = (val shr 16).hi
|
||||
result.randomness = UInt128(
|
||||
hi: cast[uint64]((val.hi shl 48) shr 48),
|
||||
lo: val.lo
|
||||
)
|
||||
result.hi += cast[int64](ulid.randomness.hi)
|
||||
result.lo += ulid.randomness.lo
|
||||
|
||||
func toBytes*(ulid: ULID): array[16, byte] =
|
||||
## Allows for a `ULID` to be converted to a byte array for the binary format.
|
||||
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)
|
||||
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
|
||||
)
|
||||
|
||||
when cpuEndian == littleEndian:
|
||||
return cast[array[16, byte]](ulid.toInt128().swapBytes())
|
||||
|
||||
else:
|
||||
return cast[array[16, byte]](ulid.toInt128())
|
||||
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()
|
||||
|
||||
func fromBytes*(_: typedesc[ULID], ulidBytes: openArray[byte]): ULID =
|
||||
## Parses a byte array to a `ULID.`.
|
||||
if ulidBytes.len != 16:
|
||||
raise newException(RangeDefect, "Given byte array must be 16 bytes long!")
|
||||
result = big(ulid.timestamp) shl 80'big
|
||||
result += ulid.randomness
|
||||
|
||||
when cpuEndian == littleEndian:
|
||||
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)).swapBytes())
|
||||
|
||||
else:
|
||||
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)))
|
||||
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.
|
||||
|
@ -211,16 +305,25 @@ func parse*(_: typedesc[ULID], ulidStr: string): ULID =
|
|||
raise newException(RangeDefect, "Invalid ULID! Must be 26 characters long!")
|
||||
|
||||
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])
|
||||
|
||||
|
||||
proc `==`*(a, b: ULID): bool = a.toInt128() == b.toInt128()
|
||||
|
||||
|
||||
func `$`*(ulid: ULID): string =
|
||||
## Returns the string representation of a ULID.
|
||||
runnableExamples:
|
||||
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)
|
||||
|
||||
|
||||
when HasJsony:
|
||||
import jsony
|
||||
|
@ -239,4 +342,4 @@ when HasDebby:
|
|||
func sqlParseHook*(data: string, v: var ULID) =
|
||||
var res: Bytes
|
||||
sqlParseHook(data, res)
|
||||
v = ULID.fromBytes(cast[seq[byte]](res))
|
||||
v = ULID.fromBytes(cast[seq[byte]](res))
|
||||
|
|
|
@ -1,16 +1,44 @@
|
|||
import nint128
|
||||
const
|
||||
LowInt48* = 0'i64
|
||||
HighInt48* = 281474976710655'i64
|
||||
|
||||
const
|
||||
HighInt48* = 281474976710655'i64
|
||||
HighUint80* = u128("1208925819614629174706176")
|
||||
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(js) or defined(nimvm)
|
||||
const NoLocks* = defined(nulidNoLocks) or defined(js) or defined(nimvm)
|
||||
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
|
||||
HasDebby* = compiles do: import debby/common
|
||||
|
|
|
@ -7,7 +7,16 @@
|
|||
|
||||
import unittest
|
||||
|
||||
import pkg/nint128
|
||||
const UlidRandStr = "541019288874337045949482"
|
||||
|
||||
when not defined(js):
|
||||
import nint128
|
||||
|
||||
const UlidRand = u128(UlidRandStr)
|
||||
else:
|
||||
import std/jsbigints
|
||||
|
||||
let UlidRand = big(UlidRandStr)
|
||||
|
||||
import nulid
|
||||
|
||||
|
@ -19,7 +28,7 @@ test "ULID Generation":
|
|||
test "ULID Parsing":
|
||||
let ulidStr = "01H999MBGTEA8BDS0M5AWEBB1A"
|
||||
let ulid = ULID(timestamp: 1693602950682,
|
||||
randomness: u128("541019288874337045949482"))
|
||||
randomness: UlidRand)
|
||||
|
||||
check ULID.parse(ulidStr) == ulid
|
||||
|
||||
|
@ -28,10 +37,13 @@ test "ULID Int128 Conversion":
|
|||
|
||||
check ULID.fromInt128(ulid.toInt128()) == ulid
|
||||
|
||||
test "ULID Binary Format":
|
||||
let
|
||||
ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
|
||||
ulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
|
||||
when not defined(js):
|
||||
# Not planned to be implemented yet for the JS backend
|
||||
|
||||
check ulid == ULID.fromBytes(ulidBytes)
|
||||
check ulid.toBytes == ulidBytes
|
||||
test "ULID Binary Format":
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue