hyperuuid 0.3.0-aarch64-linux → 0.5.0-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +274 -105
- data/lib/hyperuuid/3.4/hyperuuid_native.so +0 -0
- data/lib/hyperuuid/4.0/hyperuuid_native.so +0 -0
- data/lib/hyperuuid/errors.rb +12 -0
- data/lib/hyperuuid/native/linux-arm64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/linux-musl-arm64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/linux-musl-x64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/linux-x64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/osx-arm64/libhyperuuid.dylib +0 -0
- data/lib/hyperuuid/native/osx-x64/libhyperuuid.dylib +0 -0
- data/lib/hyperuuid/native/wasm32-wasip1/hyperuuid.wasm +0 -0
- data/lib/hyperuuid/native/win-arm64/hyperuuid.dll +0 -0
- data/lib/hyperuuid/native/win-x64/hyperuuid.dll +0 -0
- data/lib/hyperuuid/native_platform.rb +13 -7
- data/lib/hyperuuid/runtime.rb +64 -24
- data/lib/hyperuuid/uuid.rb +32 -13
- data/lib/hyperuuid/wasm_runtime.rb +33 -15
- data/lib/hyperuuid.rb +136 -35
- metadata +16 -22
- data/lib/hyperuuid/native/README.md +0 -9
data/lib/hyperuuid/runtime.rb
CHANGED
|
@@ -12,8 +12,18 @@ module HyperUuid
|
|
|
12
12
|
# files are already plain files on disk once installed — native/{rid}/{lib} can be
|
|
13
13
|
# dlopen'd directly, no extraction step needed.
|
|
14
14
|
module Runtime
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# The package's two exceptions live on HyperUuid itself (errors.rb). These are the names
|
|
16
|
+
# they had through 0.3.0, kept as aliases of the very same classes so a
|
|
17
|
+
# `rescue HyperUuid::Runtime::TimestampOutOfRangeError` written against an earlier
|
|
18
|
+
# release still catches.
|
|
19
|
+
RandomSourceError = HyperUuid::RandomSourceError
|
|
20
|
+
TimestampOutOfRangeError = HyperUuid::TimestampOutOfRangeError
|
|
21
|
+
|
|
22
|
+
# One copy of each out-of-range message for the Fiddle and wasm backends and for the
|
|
23
|
+
# doors' own range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs)
|
|
24
|
+
# carries the same text, and spec/native_backend_spec.rb pins that the two agree.
|
|
25
|
+
V6_TIMESTAMP_OUT_OF_RANGE = "unix_millis does not fit the 60-bit v6 timestamp field"
|
|
26
|
+
V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must fit within the RFC 9562 48-bit field"
|
|
17
27
|
|
|
18
28
|
NATIVE_DIR = File.join(__dir__, "native")
|
|
19
29
|
|
|
@@ -24,17 +34,18 @@ module HyperUuid
|
|
|
24
34
|
def new_v4
|
|
25
35
|
out = scratch
|
|
26
36
|
rc = functions[:new_v4].call(out)
|
|
27
|
-
raise
|
|
37
|
+
raise random_source_failure("uuid_new_v4") unless rc.zero?
|
|
28
38
|
out[0, 16]
|
|
29
39
|
end
|
|
30
40
|
|
|
31
41
|
def new_v5(namespace_bytes, name_bytes)
|
|
32
42
|
out = scratch
|
|
33
|
-
# Fiddle passes a String's bytes for void*
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
# Fiddle passes a String's own bytes for void* (read-only, no copy of them) — the
|
|
44
|
+
# same crossing every other input here uses. An empty name crosses as it is, a
|
|
45
|
+
# valid pointer with length 0, never as nil: NULL is not part of this export's
|
|
46
|
+
# contract.
|
|
47
|
+
rc = functions[:new_v5].call(namespace_bytes, name_bytes, name_bytes.bytesize, out)
|
|
48
|
+
raise random_source_failure("uuid_new_v5") unless rc.zero?
|
|
38
49
|
out[0, 16]
|
|
39
50
|
end
|
|
40
51
|
|
|
@@ -43,8 +54,8 @@ module HyperUuid
|
|
|
43
54
|
rc = functions[:new_v6].call(unix_millis, out)
|
|
44
55
|
case rc
|
|
45
56
|
when 0 then out[0, 16]
|
|
46
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
47
|
-
else raise
|
|
57
|
+
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
58
|
+
else raise random_source_failure("uuid_new_v6")
|
|
48
59
|
end
|
|
49
60
|
end
|
|
50
61
|
|
|
@@ -58,8 +69,8 @@ module HyperUuid
|
|
|
58
69
|
rc = functions[:new_v6_batch].call(unix_millis, count, out)
|
|
59
70
|
case rc
|
|
60
71
|
when 0 then out[0, count * 16]
|
|
61
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
62
|
-
else raise
|
|
72
|
+
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
73
|
+
else raise random_source_failure("uuid_new_v6_batch")
|
|
63
74
|
end
|
|
64
75
|
end
|
|
65
76
|
|
|
@@ -68,8 +79,8 @@ module HyperUuid
|
|
|
68
79
|
rc = functions[:new_v7].call(unix_millis, out)
|
|
69
80
|
case rc
|
|
70
81
|
when 0 then out[0, 16]
|
|
71
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
72
|
-
else raise
|
|
82
|
+
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
83
|
+
else raise random_source_failure("uuid_new_v7")
|
|
73
84
|
end
|
|
74
85
|
end
|
|
75
86
|
|
|
@@ -83,8 +94,8 @@ module HyperUuid
|
|
|
83
94
|
rc = functions[:new_v7_batch].call(unix_millis, count, out)
|
|
84
95
|
case rc
|
|
85
96
|
when 0 then out[0, count * 16]
|
|
86
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
87
|
-
else raise
|
|
97
|
+
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
98
|
+
else raise random_source_failure("uuid_new_v7_batch")
|
|
88
99
|
end
|
|
89
100
|
end
|
|
90
101
|
|
|
@@ -104,18 +115,44 @@ module HyperUuid
|
|
|
104
115
|
rewrite(:v6_to_rfc_order, bytes)
|
|
105
116
|
end
|
|
106
117
|
|
|
118
|
+
# The loaded core's version word, major << 16 | minor << 8 | patch, straight from the
|
|
119
|
+
# library's zero-argument hyperuuid_version export. HyperUuid.native_version unpacks
|
|
120
|
+
# it; like every method here, each backend replaces this one in place.
|
|
121
|
+
def packed_version
|
|
122
|
+
functions[:version].call
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The one failure every generating export shares, with the one message every backend
|
|
126
|
+
# raises for it: the export's name, and that the system's random source failed.
|
|
127
|
+
def random_source_failure(export)
|
|
128
|
+
RandomSourceError.new("#{export}: the system random source failed")
|
|
129
|
+
end
|
|
130
|
+
|
|
107
131
|
# Whether this platform has a shared library for Fiddle to dlopen at all: a known RID
|
|
108
|
-
# and the file actually present in this install
|
|
109
|
-
#
|
|
132
|
+
# and the file actually present — in this install, or in the in-repo cargo build the
|
|
133
|
+
# dev loop falls back to. Backend selection (hyperuuid.rb) asks this before falling
|
|
134
|
+
# back to the WebAssembly backend, which needs neither.
|
|
110
135
|
def fiddle_library_available?
|
|
111
|
-
|
|
112
|
-
File.exist?(File.join(NATIVE_DIR, rid, lib_name))
|
|
136
|
+
!library_path.nil?
|
|
113
137
|
rescue NativePlatform::UnsupportedPlatformError
|
|
114
138
|
false
|
|
115
139
|
end
|
|
116
140
|
|
|
117
141
|
private
|
|
118
142
|
|
|
143
|
+
# The shared library to dlopen: this install's native/{rid}/{lib}, or — the
|
|
144
|
+
# development loop — the in-repo cargo build, exactly what the other bindings' local
|
|
145
|
+
# staging does (HyperCast's runtime has had this since its first release). Nil when
|
|
146
|
+
# neither exists.
|
|
147
|
+
def library_path
|
|
148
|
+
rid, lib_name = NativePlatform.rid_and_library_name
|
|
149
|
+
path = File.join(NATIVE_DIR, rid, lib_name)
|
|
150
|
+
return path if File.exist?(path)
|
|
151
|
+
|
|
152
|
+
repo_build = File.expand_path(File.join(__dir__, "../../../rust/target/release", lib_name))
|
|
153
|
+
File.exist?(repo_build) ? repo_build : nil
|
|
154
|
+
end
|
|
155
|
+
|
|
119
156
|
# One 16-byte scratch allocation per thread, reused by every single-item call —
|
|
120
157
|
# Fiddle::Pointer.malloc(..., RUBY_FREE) registers a GC finalizer per call, measured
|
|
121
158
|
# (in HyperCast, same mechanism) as the dominant per-call cost by an order of
|
|
@@ -142,11 +179,12 @@ module HyperUuid
|
|
|
142
179
|
end
|
|
143
180
|
|
|
144
181
|
def load_functions
|
|
145
|
-
|
|
146
|
-
path
|
|
147
|
-
|
|
182
|
+
path = library_path
|
|
183
|
+
if path.nil?
|
|
184
|
+
rid, lib_name = NativePlatform.rid_and_library_name
|
|
148
185
|
raise LoadError,
|
|
149
|
-
"hyperuuid: #{
|
|
186
|
+
"hyperuuid: #{File.join(NATIVE_DIR, rid, lib_name)} not found (unsupported " \
|
|
187
|
+
"platform, or this gem was built without a native library for it)"
|
|
150
188
|
end
|
|
151
189
|
|
|
152
190
|
handle = Fiddle.dlopen(path)
|
|
@@ -207,6 +245,8 @@ module HyperUuid
|
|
|
207
245
|
[Fiddle::TYPE_VOIDP],
|
|
208
246
|
Fiddle::TYPE_VOID
|
|
209
247
|
),
|
|
248
|
+
# The one export that mints nothing: the zero-argument version probe.
|
|
249
|
+
version: Fiddle::Function.new(handle["hyperuuid_version"], [], Fiddle::TYPE_UINT32_T),
|
|
210
250
|
}
|
|
211
251
|
end
|
|
212
252
|
end
|
data/lib/hyperuuid/uuid.rb
CHANGED
|
@@ -9,12 +9,22 @@ module HyperUuid
|
|
|
9
9
|
# The UUID's 16 raw bytes in RFC 9562 (big-endian) order.
|
|
10
10
|
attr_reader :bytes
|
|
11
11
|
|
|
12
|
-
# Wraps a raw 16-byte RFC 9562 (big-endian) UUID value.
|
|
12
|
+
# Wraps a raw 16-byte RFC 9562 (big-endian) UUID value. The bytes are copied, so the
|
|
13
|
+
# String passed in stays the caller's.
|
|
14
|
+
#
|
|
15
|
+
# +owned+ is for this gem's own use: it says +bytes+ is a String the native core has
|
|
16
|
+
# just produced — sixteen bytes, already ASCII-8BIT, held by nothing else — so there is
|
|
17
|
+
# nothing to check, copy or re-tag, and that copy is most of what constructing one costs.
|
|
13
18
|
#
|
|
14
19
|
# @raise [ArgumentError] if +bytes+ isn't exactly 16 bytes.
|
|
15
|
-
def initialize(bytes)
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
def initialize(bytes, owned = false)
|
|
21
|
+
if owned
|
|
22
|
+
@bytes = bytes.freeze
|
|
23
|
+
else
|
|
24
|
+
raise ArgumentError, "bytes must be exactly 16 bytes" unless bytes.bytesize == 16
|
|
25
|
+
|
|
26
|
+
@bytes = bytes.dup.force_encoding(Encoding::BINARY).freeze
|
|
27
|
+
end
|
|
18
28
|
end
|
|
19
29
|
|
|
20
30
|
# The RFC 9562 §5.9 Nil UUID — all 128 bits zero.
|
|
@@ -23,13 +33,22 @@ module HyperUuid
|
|
|
23
33
|
# The RFC 9562 §5.10 Max UUID — all 128 bits one.
|
|
24
34
|
MAX = new(("\xFF" * 16).b).freeze
|
|
25
35
|
|
|
26
|
-
#
|
|
36
|
+
# The one text shape .parse accepts: 8-4-4-4-12 hex digits, either case, the four hyphens
|
|
37
|
+
# exactly where #to_s puts them.
|
|
38
|
+
HYPHENATED = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
39
|
+
private_constant :HYPHENATED
|
|
40
|
+
|
|
41
|
+
# Parses an 8-4-4-4-12 hyphenated hex UUID string — exactly the shape #to_s produces, in
|
|
42
|
+
# either case. Nothing else parses: not the bare 32 hex digits, not hyphens anywhere but
|
|
43
|
+
# those four positions, not braces or a `urn:uuid:` prefix.
|
|
27
44
|
#
|
|
28
|
-
# @raise [ArgumentError] if +string+ isn't a
|
|
45
|
+
# @raise [ArgumentError] if +string+ isn't a String in that shape.
|
|
29
46
|
def self.parse(string)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
unless string.is_a?(String) && string.match?(HYPHENATED)
|
|
48
|
+
raise ArgumentError, "invalid UUID string: #{string.inspect}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
new([string.delete("-")].pack("H*"))
|
|
33
52
|
end
|
|
34
53
|
|
|
35
54
|
# The RFC 9562 version nibble (bits 48-51, the high nibble of octet 6).
|
|
@@ -100,8 +119,8 @@ module HyperUuid
|
|
|
100
119
|
# Meaningful only for a genuine version 6 or 7 UUID.
|
|
101
120
|
def to_sql_order
|
|
102
121
|
case version
|
|
103
|
-
when 7 then self.class.new(Runtime.v7_to_sql_order(bytes))
|
|
104
|
-
when 6 then self.class.new(Runtime.v6_to_sql_order(bytes))
|
|
122
|
+
when 7 then self.class.new(Runtime.v7_to_sql_order(bytes), true)
|
|
123
|
+
when 6 then self.class.new(Runtime.v6_to_sql_order(bytes), true)
|
|
105
124
|
else raise ArgumentError, "to_sql_order is only defined for version 6 or 7 UUIDs, got version #{version}"
|
|
106
125
|
end
|
|
107
126
|
end
|
|
@@ -127,9 +146,9 @@ module HyperUuid
|
|
|
127
146
|
octet8_version = (bytes.getbyte(8) >> 4) & 0x0F
|
|
128
147
|
octet7_version = (bytes.getbyte(7) >> 4) & 0x0F
|
|
129
148
|
if octet8_version == 6
|
|
130
|
-
self.class.new(Runtime.v6_to_rfc_order(bytes))
|
|
149
|
+
self.class.new(Runtime.v6_to_rfc_order(bytes), true)
|
|
131
150
|
elsif octet7_version == 7
|
|
132
|
-
self.class.new(Runtime.v7_to_rfc_order(bytes))
|
|
151
|
+
self.class.new(Runtime.v7_to_rfc_order(bytes), true)
|
|
133
152
|
else
|
|
134
153
|
raise ArgumentError, "from_sql_order: not a recognized version 6 or 7 SQL-ordered UUID"
|
|
135
154
|
end
|
|
@@ -10,9 +10,10 @@ module HyperUuid
|
|
|
10
10
|
# Same integration shape as the Magnus extension: on require (after runtime.rb has defined
|
|
11
11
|
# the Fiddle backend) this redefines the `HyperUuid::Runtime` singleton methods **in place**
|
|
12
12
|
# — no delegation layer, no second surface. Everything above Runtime (the `Uuid` class, the
|
|
13
|
-
# module doors, batch slicing) stays shared byte-for-byte across
|
|
14
|
-
# same `
|
|
15
|
-
# same messages
|
|
13
|
+
# module doors and their argument checks, batch slicing) stays shared byte-for-byte across
|
|
14
|
+
# all three backends, and the same `HyperUuid::RandomSourceError` /
|
|
15
|
+
# `HyperUuid::TimestampOutOfRangeError` classes carry the same messages — literally the
|
|
16
|
+
# same constants and helper runtime.rb defines for the Fiddle backend.
|
|
16
17
|
#
|
|
17
18
|
# Two things differ from the native backends, both forced by the sandbox:
|
|
18
19
|
#
|
|
@@ -28,6 +29,11 @@ module HyperUuid
|
|
|
28
29
|
# library does natively.
|
|
29
30
|
module Runtime
|
|
30
31
|
WASM_MODULE_PATH = File.join(NATIVE_DIR, "wasm32-wasip1", "hyperuuid.wasm")
|
|
32
|
+
# Development loop: the in-repo cargo build, the same fallback runtime.rb takes for the
|
|
33
|
+
# native library, so `HYPERUUID_WASM=1 bundle exec rspec` needs nothing staged by hand.
|
|
34
|
+
WASM_REPO_BUILD_PATH = File.expand_path(
|
|
35
|
+
File.join(__dir__, "../../../rust/target/wasm32-wasip1/release/hyperuuid.wasm")
|
|
36
|
+
)
|
|
31
37
|
|
|
32
38
|
# One instantiated module: the exported functions, the exported memory, and the two
|
|
33
39
|
# 16-byte scratch buffers (one in, one out) the single-item doors reuse for the life of
|
|
@@ -42,9 +48,11 @@ module HyperUuid
|
|
|
42
48
|
uuid_new_v4 uuid_new_v5 uuid_new_v6 uuid_v6_unix_millis uuid_new_v6_batch
|
|
43
49
|
uuid_new_v7 uuid_v7_unix_millis uuid_new_v7_batch
|
|
44
50
|
uuid_v7_to_sql_order uuid_v7_to_rfc_order uuid_v6_to_sql_order uuid_v6_to_rfc_order
|
|
51
|
+
hyperuuid_version
|
|
45
52
|
].freeze
|
|
46
53
|
|
|
47
54
|
def initialize(path)
|
|
55
|
+
path = WASM_REPO_BUILD_PATH if !File.exist?(path) && File.exist?(WASM_REPO_BUILD_PATH)
|
|
48
56
|
unless File.exist?(path)
|
|
49
57
|
raise LoadError,
|
|
50
58
|
"hyperuuid: #{path} not found (this gem was built without its WebAssembly module)"
|
|
@@ -62,7 +70,11 @@ module HyperUuid
|
|
|
62
70
|
instance = linker.instantiate(store, mod)
|
|
63
71
|
|
|
64
72
|
@memory = instance.export("memory").to_memory
|
|
65
|
-
@fn = EXPORTS.to_h
|
|
73
|
+
@fn = EXPORTS.to_h do |name|
|
|
74
|
+
export = instance.export(name.to_s) or
|
|
75
|
+
raise LoadError, "hyperuuid: #{path} does not export #{name} (a module older than this binding)"
|
|
76
|
+
[name, export.to_func]
|
|
77
|
+
end
|
|
66
78
|
@in = malloc(16)
|
|
67
79
|
@out = malloc(16)
|
|
68
80
|
end
|
|
@@ -99,7 +111,7 @@ module HyperUuid
|
|
|
99
111
|
def new_v4
|
|
100
112
|
wasm do |w|
|
|
101
113
|
rc = w.fn(:uuid_new_v4).call(w.out)
|
|
102
|
-
raise
|
|
114
|
+
raise random_source_failure("uuid_new_v4") unless rc.zero?
|
|
103
115
|
w.memory.read(w.out, 16)
|
|
104
116
|
end
|
|
105
117
|
end
|
|
@@ -119,7 +131,7 @@ module HyperUuid
|
|
|
119
131
|
w.fn(:uuid_new_v5).call(w.in, name, name_bytes.bytesize, w.out)
|
|
120
132
|
end
|
|
121
133
|
end
|
|
122
|
-
raise
|
|
134
|
+
raise random_source_failure("uuid_new_v5") unless rc.zero?
|
|
123
135
|
w.memory.read(w.out, 16)
|
|
124
136
|
end
|
|
125
137
|
end
|
|
@@ -129,8 +141,8 @@ module HyperUuid
|
|
|
129
141
|
rc = w.fn(:uuid_new_v6).call(i64(unix_millis), w.out)
|
|
130
142
|
case rc
|
|
131
143
|
when 0 then w.memory.read(w.out, 16)
|
|
132
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
133
|
-
else raise
|
|
144
|
+
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
145
|
+
else raise random_source_failure("uuid_new_v6")
|
|
134
146
|
end
|
|
135
147
|
end
|
|
136
148
|
end
|
|
@@ -149,8 +161,8 @@ module HyperUuid
|
|
|
149
161
|
rc = w.fn(:uuid_new_v6_batch).call(i64(unix_millis), count, out)
|
|
150
162
|
case rc
|
|
151
163
|
when 0 then w.memory.read(out, count * 16)
|
|
152
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
153
|
-
else raise
|
|
164
|
+
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
165
|
+
else raise random_source_failure("uuid_new_v6_batch")
|
|
154
166
|
end
|
|
155
167
|
end
|
|
156
168
|
end
|
|
@@ -161,8 +173,8 @@ module HyperUuid
|
|
|
161
173
|
rc = w.fn(:uuid_new_v7).call(i64(unix_millis), w.out)
|
|
162
174
|
case rc
|
|
163
175
|
when 0 then w.memory.read(w.out, 16)
|
|
164
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
165
|
-
else raise
|
|
176
|
+
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
177
|
+
else raise random_source_failure("uuid_new_v7")
|
|
166
178
|
end
|
|
167
179
|
end
|
|
168
180
|
end
|
|
@@ -181,8 +193,8 @@ module HyperUuid
|
|
|
181
193
|
rc = w.fn(:uuid_new_v7_batch).call(i64(unix_millis), count, out)
|
|
182
194
|
case rc
|
|
183
195
|
when 0 then w.memory.read(out, count * 16)
|
|
184
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
185
|
-
else raise
|
|
196
|
+
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
197
|
+
else raise random_source_failure("uuid_new_v7_batch")
|
|
186
198
|
end
|
|
187
199
|
end
|
|
188
200
|
end
|
|
@@ -204,6 +216,10 @@ module HyperUuid
|
|
|
204
216
|
rewrite(:uuid_v6_to_rfc_order, bytes)
|
|
205
217
|
end
|
|
206
218
|
|
|
219
|
+
def packed_version
|
|
220
|
+
wasm { |w| w.fn(:hyperuuid_version).call }
|
|
221
|
+
end
|
|
222
|
+
|
|
207
223
|
private
|
|
208
224
|
|
|
209
225
|
# The in-place byte-order rewrites: copy the caller's frozen bytes into the guest,
|
|
@@ -228,7 +244,9 @@ module HyperUuid
|
|
|
228
244
|
|
|
229
245
|
# wasm has no unsigned integers: a u64 crosses as an i64. The core's own range checks
|
|
230
246
|
# (48-bit v7, 60-bit v6) still see the exact value the caller passed, they just see
|
|
231
|
-
# it through a two's-complement reinterpretation on the way in and back.
|
|
247
|
+
# it through a two's-complement reinterpretation on the way in and back. Exact only
|
|
248
|
+
# for a value that is a u64 to begin with, which the doors in hyperuuid.rb guarantee
|
|
249
|
+
# before any backend is called.
|
|
232
250
|
def i64(value)
|
|
233
251
|
value >= 2**63 ? value - 2**64 : value
|
|
234
252
|
end
|
data/lib/hyperuuid.rb
CHANGED
|
@@ -1,49 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
require_relative "hyperuuid/errors"
|
|
3
2
|
require_relative "hyperuuid/uuid"
|
|
4
3
|
require_relative "hyperuuid/namespaces"
|
|
5
4
|
require_relative "hyperuuid/native_platform"
|
|
6
5
|
require_relative "hyperuuid/runtime"
|
|
7
6
|
|
|
8
|
-
# RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
7
|
+
# RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation from one
|
|
8
|
+
# Rust core, reached through whichever of three backends this install can load: the core
|
|
9
|
+
# linked straight into a Magnus native extension (shipped precompiled in the platform gems),
|
|
10
|
+
# the native libhyperuuid shared library called through Fiddle (the universal gem bundles
|
|
11
|
+
# one per supported platform, picked by HyperUuid::NativePlatform), or the same core as a
|
|
12
|
+
# WebAssembly module inside the wasmtime gem. No runtime bridge in any of them, and nothing
|
|
13
|
+
# compiled at install time. HyperUuid::BACKEND names the one that loaded; the selection logic
|
|
14
|
+
# is at the bottom of this file.
|
|
15
|
+
#
|
|
16
|
+
# The doors below — with Uuid, Namespaces and the two exception classes — are the public
|
|
17
|
+
# surface, and are shared by every backend, argument validation included: a caller bug raises
|
|
18
|
+
# the same error whichever backend is live.
|
|
13
19
|
module HyperUuid
|
|
14
20
|
# This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v6/v7) the
|
|
15
21
|
# rest of this module generates.
|
|
16
|
-
VERSION = "0.
|
|
22
|
+
VERSION = "0.5.0"
|
|
23
|
+
|
|
24
|
+
# The widest batch count, and the longest v5 name in bytes, the native ABI carries (a u32).
|
|
25
|
+
# (The millisecond count is a u64; unix_millis_from refuses anything wider.)
|
|
26
|
+
U32_MAX = 0xFFFF_FFFF
|
|
27
|
+
private_constant :U32_MAX
|
|
17
28
|
|
|
18
29
|
# Creates a random UUID version 4 (RFC 9562 §5.4).
|
|
19
30
|
def self.new_v4
|
|
20
|
-
Uuid.new(Runtime.new_v4)
|
|
31
|
+
Uuid.new(Runtime.new_v4, true)
|
|
21
32
|
end
|
|
22
33
|
|
|
23
34
|
# Creates a deterministic UUID version 5 (RFC 9562 §5.5) from a namespace and a name. The
|
|
24
35
|
# same (namespace, name) pair always produces the same UUID. `name` may be a text String
|
|
25
|
-
# (encoded as UTF-8) or already-raw ASCII-8BIT bytes, which are used as-is.
|
|
36
|
+
# (encoded as UTF-8) or already-raw ASCII-8BIT bytes, which are used as-is; it may be empty.
|
|
37
|
+
#
|
|
38
|
+
# @raise [TypeError] if +namespace+ isn't a HyperUuid::Uuid or +name+ isn't a String.
|
|
26
39
|
def self.new_v5(namespace, name)
|
|
40
|
+
raise TypeError, "namespace must be a HyperUuid::Uuid; got #{namespace.class}" unless namespace.is_a?(Uuid)
|
|
41
|
+
raise TypeError, "name must be a String; got #{name.class}" unless name.is_a?(String)
|
|
42
|
+
|
|
27
43
|
name_bytes =
|
|
28
44
|
if name.encoding == Encoding::ASCII_8BIT
|
|
29
45
|
name
|
|
30
46
|
else
|
|
31
47
|
name.encode(Encoding::UTF_8).dup.force_encoding(Encoding::BINARY)
|
|
32
48
|
end
|
|
33
|
-
|
|
49
|
+
raise ArgumentError, "name must be at most #{U32_MAX} bytes" if name_bytes.bytesize > U32_MAX
|
|
50
|
+
|
|
51
|
+
Uuid.new(Runtime.new_v5(namespace.bytes, name_bytes), true)
|
|
34
52
|
end
|
|
35
53
|
|
|
36
54
|
# Converts +value+ to a Unix-epoch millisecond integer: +nil+ becomes the current time, a
|
|
37
55
|
# +Time+ is converted exactly (via its own Rational seconds, avoiding float rounding), and
|
|
38
|
-
#
|
|
56
|
+
# an Integer millisecond count passes through unchanged. Shared by every
|
|
39
57
|
# `new_v6`/`new_v7`/batch door below so a caller can pass either a `Time` or a raw
|
|
40
58
|
# millisecond count interchangeably.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
#
|
|
60
|
+
# This is also where a caller bug is caught once, for every backend: anything else is a
|
|
61
|
+
# TypeError, and a count that cannot cross the ABI as a u64 at all — negative (a `Time`
|
|
62
|
+
# before the epoch included) or past 64 bits — is the same TimestampOutOfRangeError, with
|
|
63
|
+
# the same +out_of_range+ message, the core raises for a value it can see but not embed.
|
|
64
|
+
# Left to the backends those cases diverged: a RangeError from the extension, a wrapped
|
|
65
|
+
# value from Fiddle, a silently truncated one from wasm.
|
|
66
|
+
#
|
|
67
|
+
# The two common arguments go first and cheapest: no argument is the clock, which needs no
|
|
68
|
+
# range check, and an Integer is checked by its bit length rather than against 2**64 - 1: a
|
|
69
|
+
# comparison with a number that size is a bignum comparison, and cost more than the mint.
|
|
70
|
+
private_class_method def self.unix_millis_from(value, out_of_range)
|
|
71
|
+
return Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) if value.nil?
|
|
72
|
+
|
|
73
|
+
millis =
|
|
74
|
+
if value.is_a?(Integer) then value
|
|
75
|
+
elsif value.is_a?(Time) then (value.to_r * 1000).floor
|
|
76
|
+
else raise TypeError, "unix_millis must be a Time, an Integer or nil; got #{value.class}"
|
|
77
|
+
end
|
|
78
|
+
raise TimestampOutOfRangeError, out_of_range unless millis >= 0 && millis.bit_length <= 64
|
|
79
|
+
|
|
80
|
+
millis
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Checks a batch +count+ once, for every backend: an Integer the ABI's u32 can carry.
|
|
84
|
+
private_class_method def self.batch_count(count)
|
|
85
|
+
raise TypeError, "count must be an Integer; got #{count.class}" unless count.is_a?(Integer)
|
|
86
|
+
raise ArgumentError, "count must be between 0 and #{U32_MAX}; got #{count}" unless count.between?(0, U32_MAX)
|
|
87
|
+
|
|
88
|
+
count
|
|
47
89
|
end
|
|
48
90
|
|
|
49
91
|
# Creates a time-sortable UUID version 6 (RFC 9562 §5.6), a field-compatible reordering of
|
|
@@ -51,49 +93,66 @@ module HyperUuid
|
|
|
51
93
|
# `Time` or Unix-epoch millisecond integer to embed a specific time instead. `clock_seq` and
|
|
52
94
|
# `node` are randomly generated on every call — unlike version 7, there is no monotonic
|
|
53
95
|
# counter, so calls within the same millisecond are not guaranteed to sort in creation order.
|
|
96
|
+
#
|
|
97
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
98
|
+
# @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
|
|
54
99
|
def self.new_v6(unix_millis = nil)
|
|
55
|
-
Uuid.new(Runtime.new_v6(unix_millis_from(unix_millis)))
|
|
100
|
+
Uuid.new(Runtime.new_v6(unix_millis_from(unix_millis, Runtime::V6_TIMESTAMP_OUT_OF_RANGE)), true)
|
|
56
101
|
end
|
|
57
102
|
|
|
58
103
|
# Creates `count` time-sortable version 6 UUIDs sharing one timestamp capture — one FFI call
|
|
59
104
|
# and one random-bytes fetch instead of `count` of each. Defaults to the current time; pass
|
|
60
105
|
# an explicit `Time` or Unix-epoch millisecond integer to embed a specific time instead.
|
|
106
|
+
#
|
|
107
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
108
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
61
109
|
def self.new_v6_batch(count, unix_millis = nil)
|
|
62
|
-
bytes =
|
|
63
|
-
Array.new(count) { |i| Uuid.new(bytes[i * 16, 16]) }
|
|
110
|
+
bytes = new_v6_batch_bytes(count, unix_millis)
|
|
111
|
+
Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
|
|
64
112
|
end
|
|
65
113
|
|
|
66
114
|
# Creates a time-sortable UUID version 7 (RFC 9562 §6.2). Defaults to the current time; pass
|
|
67
115
|
# an explicit `Time` or Unix-epoch millisecond integer (non-negative, fitting in 48 bits) to
|
|
68
116
|
# embed a specific time instead.
|
|
117
|
+
#
|
|
118
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
119
|
+
# @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
|
|
69
120
|
def self.new_v7(unix_millis = nil)
|
|
70
|
-
Uuid.new(Runtime.new_v7(unix_millis_from(unix_millis)))
|
|
121
|
+
Uuid.new(Runtime.new_v7(unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE)), true)
|
|
71
122
|
end
|
|
72
123
|
|
|
73
124
|
# Creates `count` time-sortable version 7 UUIDs sharing one timestamp capture and one
|
|
74
125
|
# contiguous block of the monotonic counter — one FFI call and one random-bytes fetch
|
|
75
126
|
# instead of `count` of each. Defaults to the current time; pass an explicit `Time` or
|
|
76
127
|
# Unix-epoch millisecond integer to embed a specific time instead.
|
|
128
|
+
#
|
|
129
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
130
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
77
131
|
def self.new_v7_batch(count, unix_millis = nil)
|
|
78
|
-
bytes =
|
|
79
|
-
Array.new(count) { |i| Uuid.new(bytes[i * 16, 16]) }
|
|
132
|
+
bytes = new_v7_batch_bytes(count, unix_millis)
|
|
133
|
+
Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
|
|
80
134
|
end
|
|
81
135
|
|
|
82
136
|
# Returns `count` version 7 UUIDs as one binary String of raw RFC 9562-ordered bytes,
|
|
83
137
|
# 16 per UUID, instead of an Array of Uuid objects.
|
|
84
138
|
#
|
|
85
|
-
#
|
|
86
|
-
# The difference is not the native call —
|
|
87
|
-
#
|
|
88
|
-
# bytes the native core already
|
|
139
|
+
# Far faster than #new_v7_batch for a large batch — the README's "Bulk generation into
|
|
140
|
+
# bytes" section carries the measured figures. The difference is not the native call —
|
|
141
|
+
# that is identical — it is that #new_v7_batch then allocates `count` Uuid objects and
|
|
142
|
+
# their byte Strings on top of it. This hands back the bytes the native core already
|
|
143
|
+
# produced, untouched.
|
|
89
144
|
#
|
|
90
145
|
# Use it when bytes are the destination: a BYTEA/uniqueidentifier bind parameter, a wire
|
|
91
146
|
# format, a bulk COPY. If you need Uuid objects, keep using #new_v7_batch — slicing this
|
|
92
147
|
# String into them yourself just moves the same allocations into your own code.
|
|
93
148
|
#
|
|
94
149
|
# Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally.
|
|
150
|
+
#
|
|
151
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
152
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
95
153
|
def self.new_v7_batch_bytes(count, unix_millis = nil)
|
|
96
|
-
|
|
154
|
+
count = batch_count(count)
|
|
155
|
+
Runtime.new_v7_batch(count, unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE))
|
|
97
156
|
end
|
|
98
157
|
|
|
99
158
|
# Returns `count` version 6 UUIDs as one binary String of raw RFC 9562-ordered bytes,
|
|
@@ -103,8 +162,42 @@ module HyperUuid
|
|
|
103
162
|
# clock_seq and node are independently random per item; unlike version 7 there is no
|
|
104
163
|
# monotonic counter, so items minted in the same millisecond are not guaranteed to sort in
|
|
105
164
|
# creation order.
|
|
165
|
+
#
|
|
166
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
167
|
+
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
106
168
|
def self.new_v6_batch_bytes(count, unix_millis = nil)
|
|
107
|
-
|
|
169
|
+
count = batch_count(count)
|
|
170
|
+
Runtime.new_v6_batch(count, unix_millis_from(unix_millis, Runtime::V6_TIMESTAMP_OUT_OF_RANGE))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# The version of the native core actually loaded, as "major.minor.patch" — read from the
|
|
174
|
+
# library itself (its `hyperuuid_version` export), not from this gem, so a consumer can
|
|
175
|
+
# prove the core behind the doors is the one this binding was built against (and name the
|
|
176
|
+
# mismatch against HyperUuid::VERSION when it is not). The cheapest possible probe that the
|
|
177
|
+
# backend resolved at all: takes nothing, mints nothing, cannot fail once the backend loads.
|
|
178
|
+
#
|
|
179
|
+
# @raise [LoadError] if no backend could be loaded for this platform.
|
|
180
|
+
def self.native_version
|
|
181
|
+
word = Runtime.packed_version
|
|
182
|
+
"#{word >> 16}.#{(word >> 8) & 0xFF}.#{word & 0xFF}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Whether a backend actually loaded and exports the ABI this binding was built against —
|
|
186
|
+
# what a consumer with a fallback of its own (SecureRandom.uuid, say) checks before
|
|
187
|
+
# committing to these doors. Probed once (a native_version round trip), cached, and never
|
|
188
|
+
# raises: a missing shared library, an unsupported platform, an older core without the
|
|
189
|
+
# version export, or a wasm module the wasmtime gem cannot instantiate all answer false.
|
|
190
|
+
# The doors themselves keep their own behavior — the first call on an unavailable backend
|
|
191
|
+
# raises its precise error — and so does require-time selection under HYPERUUID_WASM=1
|
|
192
|
+
# without wasmtime; this only answers the question quietly.
|
|
193
|
+
def self.available?
|
|
194
|
+
return @available unless @available.nil?
|
|
195
|
+
|
|
196
|
+
@available = begin
|
|
197
|
+
native_version.is_a?(String)
|
|
198
|
+
rescue LoadError, StandardError
|
|
199
|
+
false
|
|
200
|
+
end
|
|
108
201
|
end
|
|
109
202
|
end
|
|
110
203
|
|
|
@@ -122,6 +215,10 @@ end
|
|
|
122
215
|
# forces it (and fails loudly if wasmtime is missing); otherwise it is only ever chosen when
|
|
123
216
|
# there is no native library for this platform at all and wasmtime happens to be available,
|
|
124
217
|
# so no supported platform's behavior changes by its existence.
|
|
218
|
+
#
|
|
219
|
+
# Both variables are read for presence, not value — set to anything at all, "0" and the
|
|
220
|
+
# empty string included, they force their backend — and HYPERUUID_WASM wins when both are
|
|
221
|
+
# set.
|
|
125
222
|
HyperUuid::BACKEND =
|
|
126
223
|
if ENV["HYPERUUID_WASM"]
|
|
127
224
|
begin
|
|
@@ -138,9 +235,12 @@ HyperUuid::BACKEND =
|
|
|
138
235
|
# Two layouts, and both have to work. A released platform gem is a "fat" gem carrying one
|
|
139
236
|
# extension per supported Ruby ABI under lib/hyperuuid/<minor>/ (see the Rakefile's
|
|
140
237
|
# native:gem task for why an ABI-per-file is unavoidable — Magnus has no `abi3`
|
|
141
|
-
# equivalent)
|
|
142
|
-
#
|
|
143
|
-
#
|
|
238
|
+
# equivalent), and `rake native:dev` — the local dev loop — stages its own build at that
|
|
239
|
+
# same versioned path. CI's in-job staging additionally drops a single extension flat at
|
|
240
|
+
# lib/, which is also where a hand copy of `cargo ruby-ext`'s output goes (the alias only
|
|
241
|
+
# builds into rust/target/ruby/release/; nothing but that rake task copies it here).
|
|
242
|
+
# Trying the versioned path first and the flat one second means neither has to know the
|
|
243
|
+
# other exists.
|
|
144
244
|
#
|
|
145
245
|
# A miss on both is not an error: it means this Ruby/platform combination has no
|
|
146
246
|
# precompiled extension, which is precisely what the Fiddle backend below is for.
|
|
@@ -157,8 +257,9 @@ HyperUuid::BACKEND =
|
|
|
157
257
|
else
|
|
158
258
|
# No shared library for this platform either. wasmtime, if the consumer has it,
|
|
159
259
|
# is the only backend left that can run here; without it, stay on Fiddle so the
|
|
160
|
-
# first call raises its own precise "not found" LoadError
|
|
161
|
-
#
|
|
260
|
+
# first call raises its own precise error — a "not found" LoadError naming the
|
|
261
|
+
# path, or NativePlatform::UnsupportedPlatformError naming the platform — rather
|
|
262
|
+
# than a vaguer one from here. HyperUuid.available? is the quiet way to ask.
|
|
162
263
|
begin
|
|
163
264
|
require "wasmtime"
|
|
164
265
|
require_relative "hyperuuid/wasm_runtime"
|