globeglitter 1.0.0
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 +7 -0
- data/LICENSE +661 -0
- data/README.md +21 -0
- data/bin/are-we-unallocated-yet +10 -0
- data/bin/benchmark +70 -0
- data/bin/profile +15 -0
- data/bin/repl +8 -0
- data/bin/test-my-best +5 -0
- data/lib/globeglitter/chrono_diver.rb +200 -0
- data/lib/globeglitter/first_resolution.rb +24 -0
- data/lib/globeglitter/inner_spirit.rb +162 -0
- data/lib/globeglitter/rottel_da_sun.rb +0 -0
- data/lib/globeglitter/say_yeeeahh.rb +64 -0
- data/lib/globeglitter.rb +277 -0
- metadata +112 -0
data/README.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
- https://en.wikipedia.org/wiki/Surrogate_key
|
2
|
+
|
3
|
+
- https://devblogs.microsoft.com/oldnewthing/20190426-00/?p=102450
|
4
|
+
- https://devblogs.microsoft.com/oldnewthing/20190913-00/?p=102859
|
5
|
+
|
6
|
+
|
7
|
+
## Non-UUID specs
|
8
|
+
|
9
|
+
- https://github.com/ulid/spec
|
10
|
+
- https://github.com/rs/xid
|
11
|
+
- https://github.com/boundary/flake
|
12
|
+
- https://github.com/segmentio/ksuid
|
13
|
+
|
14
|
+
|
15
|
+
## Alternative Ruby Libraries
|
16
|
+
|
17
|
+
- `::SecureRandom::uuid` obviously
|
18
|
+
- https://github.com/sporkmonger/uuidtools
|
19
|
+
- https://github.com/assaf/uuid / https://www.rubydoc.info/gems/uuid/
|
20
|
+
- https://gist.github.com/brandur/1bddb5215540889983dc7e3a66ef4e41
|
21
|
+
- https://github.com/cassandra-rb/simple_uuid
|
data/bin/benchmark
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'globeglitter'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'benchmark/ips'
|
7
|
+
|
8
|
+
Benchmark.ips do |bm|
|
9
|
+
integer_buffer = ::Data::define(:lol, :rofl, :lmao) do
|
10
|
+
def self.new(msb, lsb, rofl: nil, lmao: nil) = self.allocate.tap { |gg|
|
11
|
+
gg.send(
|
12
|
+
:initialize,
|
13
|
+
**{
|
14
|
+
lol: ((msb << 64) | lsb),
|
15
|
+
rofl:,
|
16
|
+
lmao:,
|
17
|
+
}
|
18
|
+
)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
io_buffer = ::Data::define(:lol, :rofl, :lmao) do
|
23
|
+
def self.new(msb, lsb, rofl: nil, lmao: nil) = self.allocate.tap { |gg|
|
24
|
+
gg.send(
|
25
|
+
:initialize,
|
26
|
+
**{
|
27
|
+
lol: ::IO::Buffer::new(size=16).tap {
|
28
|
+
_1.set_value(:U64, 0, msb)
|
29
|
+
_1.set_value(:U64, 8, lsb)
|
30
|
+
},
|
31
|
+
rofl:,
|
32
|
+
lmao:,
|
33
|
+
}
|
34
|
+
)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
bm.report('Embedded buffer as `::Integer`') do
|
39
|
+
integer_buffer.new(
|
40
|
+
::SecureRandom::random_number(0xFFFFFFFF_FFFFFFFF),
|
41
|
+
::SecureRandom::random_number(0xFFFFFFFF_FFFFFFFF),
|
42
|
+
)
|
43
|
+
end
|
44
|
+
bm.report('Embedded buffer as `::IO::Buffer`') do
|
45
|
+
io_buffer.new(
|
46
|
+
::SecureRandom::random_number(0xFFFFFFFF_FFFFFFFF),
|
47
|
+
::SecureRandom::random_number(0xFFFFFFFF_FFFFFFFF),
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Benchmark.ips do |bm|
|
53
|
+
bm.report 'SecureRandom::uuid random String' do
|
54
|
+
::SecureRandom::uuid
|
55
|
+
end
|
56
|
+
bm.report 'SecureRandom::uuid random String to Integer' do
|
57
|
+
::SecureRandom::uuid.gsub(?-, '').to_i(16)
|
58
|
+
end
|
59
|
+
bm.report 'GlobeGlitter random from SecureRandom::uuid' do
|
60
|
+
::GlobeGlitter::new(::SecureRandom::uuid.gsub(?-, '').to_i(16))
|
61
|
+
end
|
62
|
+
bm.report 'GlobeGlitter random native' do
|
63
|
+
::GlobeGlitter::random
|
64
|
+
end
|
65
|
+
bm.report 'GlobeGlitter random native to String' do
|
66
|
+
::GlobeGlitter::random.to_s
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO: Benchmark `::IO::Buffer` vs `::Integer` immediates https://docs.ruby-lang.org/en/master/IO/Buffer.html
|
data/bin/profile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'globeglitter'
|
5
|
+
|
6
|
+
require 'ruby-prof'
|
7
|
+
|
8
|
+
|
9
|
+
result = RubyProf.profile(:track_allocations => true) do
|
10
|
+
1_234_56.times do
|
11
|
+
::GlobeGlitter::random
|
12
|
+
end
|
13
|
+
end
|
14
|
+
printer = RubyProf::GraphPrinter.new(result)
|
15
|
+
printer.print(STDOUT, :min_percent => 2)
|
data/bin/repl
ADDED
data/bin/test-my-best
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
require('securerandom') unless defined?(::SecureRandom)
|
2
|
+
require('socket') unless defined?(::Socket)
|
3
|
+
|
4
|
+
# Components for time-based UUIDs.
|
5
|
+
#
|
6
|
+
# See also:
|
7
|
+
# - https://pythonhosted.org/time-uuid/
|
8
|
+
module ::GlobeGlitter::CHRONO_DIVER; end
|
9
|
+
module ::GlobeGlitter::CHRONO_DIVER::PENDULUMS
|
10
|
+
|
11
|
+
# ITU-T Rec. X.667 sez —
|
12
|
+
#
|
13
|
+
# “The timestamp is a 60-bit value. For UUID version 1, this is
|
14
|
+
# represented by Coordinated Universal Time (UTC) as a count of 100-
|
15
|
+
# nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of
|
16
|
+
# Gregorian reform to the Christian calendar).“
|
17
|
+
# “For systems that do not have UTC available, but do have the local
|
18
|
+
# time, they may use that instead of UTC, as long as they do so
|
19
|
+
# consistently throughout the system. However, this is not recommended
|
20
|
+
# since generating the UTC from local time only needs a time zone offset.”
|
21
|
+
# “Since a UUID is a fixed size and contains a time field,
|
22
|
+
# it is possible for values to rollover (around A.D. 3400,
|
23
|
+
# depending on the specific algorithm used).”
|
24
|
+
# “NT keeps time in FILETIME format which is 100ns ticks since Jan 1, 1601.
|
25
|
+
# UUIDs use time in 100ns ticks since Oct 15, 1582.
|
26
|
+
# The difference is 17 Days in Oct + 30 (Nov) + 31 (Dec) + 18 years and 5 leap days.”
|
27
|
+
#
|
28
|
+
# See also —
|
29
|
+
# - https://medium.com/swlh/should-i-use-date-time-or-datetime-in-ruby-and-rails-9372ad20ca4f
|
30
|
+
# - https://stackoverflow.com/questions/11835193/how-do-i-use-ruby-date-constants-gregorian-julian-england-and-even-italy
|
31
|
+
#
|
32
|
+
# TODO: Figure out how to handle date rollover.
|
33
|
+
private def current_time = (((::Time::now.utc - ::Time::new(1582, 10, 15)) * 1_000_000_000) / 100).to_i
|
34
|
+
|
35
|
+
# ITU-T Rec. X.667 sez —
|
36
|
+
#
|
37
|
+
# “[O]btain a 47-bit cryptographic quality random number and use it as the low 47 bits of the node ID,
|
38
|
+
# with the least significant bit of the first octet of the node ID set to one.
|
39
|
+
# This bit is the unicast/multicast bit, which will never be set in IEEE 802 addresses
|
40
|
+
# obtained from network cards. Hence, there can never be a conflict between UUIDs
|
41
|
+
# generated by machines with and without network cards.”
|
42
|
+
private def random_node = (::SecureRandom::random_number(0xFFFFFFFFFFFF) | (0b1 << 40))
|
43
|
+
|
44
|
+
# General background info: https://beej.us/guide/bgnet/html/
|
45
|
+
#
|
46
|
+
# Ruby exposes `hwaddr` in `::Socket::getifaddrs`, e.g. —
|
47
|
+
#
|
48
|
+
# irb> Socket::getifaddrs.map(&:addr)
|
49
|
+
# =>
|
50
|
+
# [#<Addrinfo: PACKET[protocol=0 lo hatype=772 HOST hwaddr=00:00:00:00:00:00]>,
|
51
|
+
# #<Addrinfo: PACKET[protocol=0 enp1s0 hatype=1 HOST hwaddr=00:e0:4c:18:42:69]>,
|
52
|
+
# #<Addrinfo: PACKET[protocol=0 wlp2s0 hatype=1 HOST hwaddr=80:19:34:6f:13:37]>,
|
53
|
+
# #<Addrinfo: 127.0.0.1>,
|
54
|
+
# #<Addrinfo: 172.16.0.226>,
|
55
|
+
# #<Addrinfo: 172.16.0.88>,
|
56
|
+
# #<Addrinfo: ::1>,
|
57
|
+
# #<Addrinfo: fe80::d9d7:4890:9ebc:486%wlp2s0>]
|
58
|
+
#
|
59
|
+
# …but there seems to be no way to get it in Ruby code without parsing the `::String` output
|
60
|
+
# of `::Addrinfo#inspect`, confirmed when we look at `raddrinfo.c` and see it constructing a `str`:
|
61
|
+
# https://github.com/ruby/ruby/blob/ea8a7287e2b96b9c24e5e89fe863e5bfa60bfdda/ext/socket/raddrinfo.c#L1375
|
62
|
+
#
|
63
|
+
# MAYBE: Figure out how to use ioctl to get this without `::String` parsing.
|
64
|
+
# - https://stackoverflow.com/a/1779758
|
65
|
+
# - https://medium.com/geckoboard-under-the-hood/how-to-build-a-network-stack-in-ruby-f73aeb1b661b
|
66
|
+
private def hardware_node = ::Socket::getifaddrs.map(&:addr).map(&:inspect_sockaddr).map! {
|
67
|
+
_1.match(/hwaddr=(?<hwaddr>\h\h:\h\h:\h\h:\h\h:\h\h:\h\h)/)&.captures&.pop&.gsub(?:, '')&.to_i(16)
|
68
|
+
}.compact.tap {
|
69
|
+
# Remove `00:00:00:00:00:00` loopback address.
|
70
|
+
# TODO: Better logic. There can be multiple loopback interfaces or no loopback interface.
|
71
|
+
# Maybe alter the `::Regexp` above and remove this `tap`/`delete` section entirely.
|
72
|
+
_1.delete(0)
|
73
|
+
}&.max {
|
74
|
+
# Choose the hwaddr that gives up the most entropy for a UUID,
|
75
|
+
# rather than always the first-enumerated hwaddr.
|
76
|
+
_1.bit_length
|
77
|
+
}
|
78
|
+
|
79
|
+
# Fall back to random node ID if a hardware ID is unavailable.
|
80
|
+
private def current_node = self.hardware_node || self.random_node
|
81
|
+
|
82
|
+
# ITU-T Rec. X.667 sez —
|
83
|
+
#
|
84
|
+
# “For UUID version 1, the clock sequence is used to help avoid
|
85
|
+
# duplicates that could arise when the clock is set backwards in time
|
86
|
+
# or if the node ID changes.
|
87
|
+
#
|
88
|
+
# If the clock is set backwards, or might have been set backwards
|
89
|
+
# (e.g., while the system was powered off), and the UUID generator can
|
90
|
+
# not be sure that no UUIDs were generated with timestamps larger than
|
91
|
+
# the value to which the clock was set, then the clock sequence has to
|
92
|
+
# be changed. If the previous value of the clock sequence is known, it
|
93
|
+
# can just be incremented; otherwise it should be set to a random or
|
94
|
+
# high-quality pseudo-random value.
|
95
|
+
#
|
96
|
+
# Similarly, if the node ID changes (e.g., because a network card has
|
97
|
+
# been moved between machines), setting the clock sequence to a random
|
98
|
+
# number minimizes the probability of a duplicate due to slight
|
99
|
+
# differences in the clock settings of the machines. If the value of
|
100
|
+
# clock sequence associated with the changed node ID were known, then
|
101
|
+
# the clock sequence could just be incremented, but that is unlikely.
|
102
|
+
#
|
103
|
+
# The clock sequence MUST be originally (i.e., once in the lifetime of
|
104
|
+
# a system) initialized to a random number to minimize the correlation
|
105
|
+
# across systems. This provides maximum protection against node
|
106
|
+
# identifiers that may move or switch from system to system rapidly.
|
107
|
+
# The initial value MUST NOT be correlated to the node identifier.”
|
108
|
+
#
|
109
|
+
# TODO: Pick a single random sequence at startup and increment from there,
|
110
|
+
# maybe in a dedicated `::Ractor`.
|
111
|
+
def clock_sequence = ::SecureRandom::random_number(0xFFFF)
|
112
|
+
|
113
|
+
# TODO: Take arguments to generate identifiers for specific time/seq/node.
|
114
|
+
def from_time = self::new(
|
115
|
+
current_time,
|
116
|
+
clock_sequence,
|
117
|
+
current_node,
|
118
|
+
structure: ::GlobeGlitter::STRUCTURE_ITU_T_REC_X_667,
|
119
|
+
rules: ::GlobeGlitter::RULES_TIME_GREGORIAN,
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
module ::GlobeGlitter::CHRONO_DIVER::FRAGMENT
|
124
|
+
|
125
|
+
# Getters for fields defined in the specification.
|
126
|
+
def time_low = self.bits127–96
|
127
|
+
def time_mid = self.bits95–80
|
128
|
+
def time_high_and_version = self.bits79–64
|
129
|
+
def clock_seq_high_and_reserved = self.bits63–56 & case self.structure
|
130
|
+
# This field is overlayed by the backward-masked `structure` a.k.a. """variant""",
|
131
|
+
# so we return a different number of bits from the chunk depending on that value.
|
132
|
+
when 0 then 0b01111111
|
133
|
+
when 1 then 0b00111111
|
134
|
+
when 2, 3 then 0b00011111
|
135
|
+
else 0b11111111 # Non-compliant structure. This should never happen.
|
136
|
+
end
|
137
|
+
def clock_seq_low = self.bits55–48
|
138
|
+
def node = self.bits47–0
|
139
|
+
|
140
|
+
# Setters for fields defined in the specification.
|
141
|
+
def time_low=(otra); self.bits127–96=(otra); end
|
142
|
+
def time_mid=(otra); self.bits95–80=(otra); end
|
143
|
+
def time_high=(otra); self.bits79–64=(((self.inner_spirit >> 64) & 0xF000) | (otra & 0x0FFF)); end
|
144
|
+
def time=(otra)
|
145
|
+
self.bits79–64=(((self.inner_spirit >> 64) & 0x00000000_0000F000) | (otra & 0xFFFFFFFF_FFFF0FFF))
|
146
|
+
end
|
147
|
+
def clock_seq_high_and_reserved=(otra)
|
148
|
+
# This field is overlayed by the backward-masked `structure` a.k.a. """variant""",
|
149
|
+
# so we set a different number of bits from the chunk depending on that value
|
150
|
+
# along with saving the existing value.
|
151
|
+
self.bits63–56=(
|
152
|
+
(otra & case self.structure
|
153
|
+
when 0 then 0b01111111
|
154
|
+
when 1 then 0b00111111
|
155
|
+
when 2, 3 then 0b00011111
|
156
|
+
else 0b11111111 # Non-compliant structure. This should never happen.
|
157
|
+
end) | (case self.structure
|
158
|
+
when 0 then 0b00000000
|
159
|
+
when 1 then 0b10000000
|
160
|
+
when 2 then 0b11000000
|
161
|
+
when 3 then 0b11100000
|
162
|
+
else 0b00000000 # Non-compliant structure. This should never happen.
|
163
|
+
end)
|
164
|
+
)
|
165
|
+
end
|
166
|
+
def clock_seq_low=(otra)
|
167
|
+
self.bits55–48=(otra)
|
168
|
+
end
|
169
|
+
def node=(otra)
|
170
|
+
self.bits47–0=(otra)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Getter for Base-16 `::String` output where these two fields are combined.
|
174
|
+
def clock_seq = (self.clock_seq_high_and_reserved << 8) | self.clock_seq_low
|
175
|
+
def clock_seq=(otra)
|
176
|
+
self.with(inner_spirit: (
|
177
|
+
(self.inner_spirit & 0xFFFFFFFF_FFFFFFFF_0000FFFF_FFFFFFFF) |
|
178
|
+
(
|
179
|
+
case self.structure
|
180
|
+
when 0 then 0b00000000
|
181
|
+
when 1 then 0b10000000
|
182
|
+
when 2 then 0b11000000
|
183
|
+
when 3 then 0b11100000
|
184
|
+
end << 56
|
185
|
+
) | (otra << 48)
|
186
|
+
))
|
187
|
+
end
|
188
|
+
|
189
|
+
# Construct the full timestamp from the split chunk contents.
|
190
|
+
def time = (self.bits127–96 << 32) | (self.bits95–80 << 16) | (self.bits79–64 & 0x0FFF)
|
191
|
+
|
192
|
+
# ITU-T Rec. X.667 sez —
|
193
|
+
#
|
194
|
+
# “The timestamp is a 60-bit value. For UUID version 1, this is
|
195
|
+
# represented by Coordinated Universal Time (UTC) as a count of 100-
|
196
|
+
# nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of
|
197
|
+
# Gregorian reform to the Christian calendar).“
|
198
|
+
def to_time = ::Time::new(1582, 10, 15).utc + (self.time / 10000000)
|
199
|
+
|
200
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require('forwardable') unless defined?(::Forwardable)
|
2
|
+
require('comparable') unless defined?(::Comparable)
|
3
|
+
|
4
|
+
::GlobeGlitter::FIRST_RESOLUTION = ::Module::new do
|
5
|
+
|
6
|
+
#extend(::Forwardable)
|
7
|
+
#def_instance_delegators(:inner_spirit, :to_i, :eql?)
|
8
|
+
|
9
|
+
# https://devblogs.microsoft.com/oldnewthing/20190426-00/?p=102450
|
10
|
+
# https://devblogs.microsoft.com/oldnewthing/20190913-00/?p=102859
|
11
|
+
# https://bornsql.ca/blog/how-sql-server-stores-data-types-guid/
|
12
|
+
COMPARATOR_UUID = 1
|
13
|
+
include(::Comparable)
|
14
|
+
def <=>(otra, comparator: COMPARATOR_UUID)
|
15
|
+
case otra
|
16
|
+
when ::GlobeGlitter then
|
17
|
+
case comparator
|
18
|
+
when COMPARATOR_UUID then self.inner_spirit.<=>(otra.inner_spirit)
|
19
|
+
#else
|
20
|
+
end
|
21
|
+
else self.inner_spirit.<=>(::GlobeGlitter::try_convert(otra))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# Bitslicing components.
|
2
|
+
module ::GlobeGlitter::INNER_SPIRIT
|
3
|
+
|
4
|
+
# NOTE: These method names are based on big-endian representation of our buffer, i.e. MSB <-> LSB.
|
5
|
+
#
|
6
|
+
# ITU-T Rec. X.667 sez —
|
7
|
+
# “This Recommendation | International Standard specifies a sequence of octets for a UUID
|
8
|
+
# using the terms first and last. The first octet is also called "octet 15" and the last octet "octet 0".
|
9
|
+
# The bits within a UUID are also numbered as "bit 127" to "bit 0", with bit 127 as the most-
|
10
|
+
# significant bit of octet 15 and bit 0 as the least significant bit of octet 0.”
|
11
|
+
#
|
12
|
+
# I am going to 1-index the boundaries in these helper methods because I think the resulting numbers
|
13
|
+
# are easier to remember, but our structure is otherwise identical to what's described in the quote.
|
14
|
+
def bits127–96 = self.inner_spirit.get_value(self.structure.eql?(self.class::STRUCTURE_MICROSOFT) ? :u32 : :U32, 0)
|
15
|
+
def bits95–80 = self.inner_spirit.get_value(self.structure.eql?(self.class::STRUCTURE_MICROSOFT) ? :u16 : :U16, 4)
|
16
|
+
def bits79–64 = self.inner_spirit.get_value(self.structure.eql?(self.class::STRUCTURE_MICROSOFT) ? :u16 : :U16, 6)
|
17
|
+
def bits63–56 = self.inner_spirit.get_value(:U8, 8)
|
18
|
+
def bits55–48 = self.inner_spirit.get_value(:U8, 9)
|
19
|
+
def bits47–0 = (self.inner_spirit.get_value(:U16, 10) << 32) | self.inner_spirit.get_value(:U32, 12)
|
20
|
+
|
21
|
+
def bits127–96=(otra); self.inner_spirit.set_value(self.structure.eql?(self.class::STRUCTURE_MICROSOFT) ? :u32 : :U32, 0, otra); end
|
22
|
+
def bits95–80=(otra); self.inner_spirit.set_value(self.structure.eql?(self.class::STRUCTURE_MICROSOFT) ? :u16 : :U16, 4, otra); end
|
23
|
+
def bits79–64=(otra); self.inner_spirit.set_value(self.structure.eql?(self.class::STRUCTURE_MICROSOFT) ? :u16 : :U16, 6, otra); end
|
24
|
+
def bits63–56=(otra); self.inner_spirit.set_value(:U8, 8, otra); end
|
25
|
+
def bits55–48=(otra); self.inner_spirit.set_value(:U8, 9, otra); end
|
26
|
+
def bits47–0=(otra)
|
27
|
+
self.inner_spirit.set_value(:U16, 10, otra >> 32)
|
28
|
+
self.inner_spirit.set_value(:U32, 12, otra & 0xFFFFFFFF)
|
29
|
+
end
|
30
|
+
|
31
|
+
# ITU-T Rec. X.667 sez —
|
32
|
+
#
|
33
|
+
# “The version number is in the most significant 4 bits of the time
|
34
|
+
# stamp (bits 4 through 7 of the time_hi_and_version field).”
|
35
|
+
#
|
36
|
+
# “The following table lists the currently-defined versions for this UUID structure.”
|
37
|
+
#
|
38
|
+
# Msb0 Msb1 Msb2 Msb3 Version Description
|
39
|
+
#
|
40
|
+
# 0 0 0 1 1 The time-based version
|
41
|
+
# specified in this document.
|
42
|
+
#
|
43
|
+
# 0 0 1 0 2 DCE Security version, with
|
44
|
+
# embedded POSIX UIDs.
|
45
|
+
#
|
46
|
+
# 0 0 1 1 3 The name-based version
|
47
|
+
# specified in this document
|
48
|
+
# that uses MD5 hashing.
|
49
|
+
#
|
50
|
+
# 0 1 0 0 4 The randomly or pseudo-
|
51
|
+
# randomly generated version
|
52
|
+
# specified in this document.
|
53
|
+
#
|
54
|
+
# 0 1 0 1 5 The name-based version
|
55
|
+
# specified in this document
|
56
|
+
# that uses SHA-1 hashing.
|
57
|
+
#
|
58
|
+
# “The version is more accurately a sub-type; again, we retain the term for compatibility.”
|
59
|
+
def rules = (self.to_i >> 76) & 0xF
|
60
|
+
# NOTE: Assignment methods in Ruby can only return their argument, so we name this `replace` instead.
|
61
|
+
def replace_rules(otra)
|
62
|
+
raise ::ArgumentError::new("invalid version #{otra.to_s}") unless otra.is_a?(::Integer) and otra.between?(1, 8)
|
63
|
+
return self.with(rules: otra).tap {
|
64
|
+
_1.inner_spirit.set_value(:U8, 7, ((otra << 0xF) | (self.inner_spirit.get_value(:U8, 7) & 0xF)))
|
65
|
+
}
|
66
|
+
end
|
67
|
+
# This is just straight-up the same thing as "version" in the UUID specification,
|
68
|
+
# but I don't want to call it that because it's a terrible ambiguous word
|
69
|
+
# for anybody unfamiliar with the minutae of the specs.
|
70
|
+
# We should still provide it as `#version` because why not??
|
71
|
+
alias_method(:version, :rules)
|
72
|
+
|
73
|
+
# ITU-T Rec. X.667 sez —
|
74
|
+
#
|
75
|
+
# “The variant field determines the layout of the UUID.
|
76
|
+
# That is, the interpretation of all other bits in the UUID depends on the setting
|
77
|
+
# of the bits in the variant field. As such, it could more accurately be called a type field;
|
78
|
+
# we retain the original term for compatibility.
|
79
|
+
# The variant field consists of a variable number of the most significant bits of octet 8 of the UUID.
|
80
|
+
#
|
81
|
+
# “The following table lists the contents of the variant field, where
|
82
|
+
# the letter "x" indicates a "don't-care" value.
|
83
|
+
#
|
84
|
+
# Msb0 Msb1 Msb2 Description
|
85
|
+
#
|
86
|
+
# 0 x x Reserved, NCS backward compatibility.
|
87
|
+
#
|
88
|
+
# 1 0 x The structure specified in this document.
|
89
|
+
#
|
90
|
+
# 1 1 0 Reserved, Microsoft Corporation backward compatibility
|
91
|
+
#
|
92
|
+
# 1 1 1 Reserved for future definition.
|
93
|
+
#
|
94
|
+
#
|
95
|
+
# NOTE: Some libraries (like `java.util.UUID`) specify the variant value as if it were not backwards-masked:
|
96
|
+
# https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/util/UUID.html#variant()
|
97
|
+
#
|
98
|
+
# I think it makes more sense for it to count upward like `version` rather than use the raw bit value.
|
99
|
+
def structure
|
100
|
+
# Can't use getter for this since the getter return value will rely on this structure.
|
101
|
+
clock_seq_high_and_reserved = self.inner_spirit.get_value(:U8, 8)
|
102
|
+
# The structure is masked backwards, but with a variable number of bits,
|
103
|
+
# so we can't just swap it and mask.
|
104
|
+
case
|
105
|
+
when (clock_seq_high_and_reserved >> 7).zero? then 0
|
106
|
+
when (clock_seq_high_and_reserved >> 6).eql?(0b10) then 1
|
107
|
+
when (clock_seq_high_and_reserved >> 5).eql?(0b110) then 2
|
108
|
+
when (clock_seq_high_and_reserved >> 5).eql?(0b111) then 3
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# NOTE: Assignment methods in Ruby can only return their argument, so we name this `replace` instead.
|
113
|
+
def replace_structure(otra)
|
114
|
+
raise ::ArgumentError::new("invalid structure #{otra.to_s}") unless otra.respond_to?(:<) and otra.<(4)
|
115
|
+
return self.with(structure: otra).tap {
|
116
|
+
_1.inner_spirit.set_value(:U8, 9,
|
117
|
+
(
|
118
|
+
(case otra
|
119
|
+
when 0 then 0b00000000
|
120
|
+
when 1 then 0b10000000
|
121
|
+
when 2 then 0b11000000
|
122
|
+
when 3 then 0b11100000
|
123
|
+
else raise ::ArgumentError::new("invalid structure #{otra.to_s}")
|
124
|
+
end) |
|
125
|
+
(_1.inner_spirit.get_value(:U8, 9) & case otra
|
126
|
+
when 0 then 0b01111111
|
127
|
+
when 1 then 0b00111111
|
128
|
+
when 2, 3 then 0b00011111
|
129
|
+
else raise ::ArgumentError::new("invalid structure #{otra.to_s}")
|
130
|
+
end)
|
131
|
+
)
|
132
|
+
)
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
# Return `::Integer` representation of the contents of our embedded buffer.
|
137
|
+
# ITU-T Rec. X.667 sez —
|
138
|
+
# “A UUID can be represented as a single integer value. To obtain the single integer value of the UUID,
|
139
|
+
# the 16 octets of the binary representation shall be treated as an unsigned integer encoding with
|
140
|
+
# the most significant bit of the integer encoding as the most significant bit (bit 7) of the first
|
141
|
+
# of the sixteen octets (octet 15) and the least significant bit as the least significant bit (bit 0)
|
142
|
+
# of the last of the sixteen octets (octet 0).”
|
143
|
+
# “UUIDs forming a component of an OID are represented in ASN.1 value notation
|
144
|
+
# as the decimal representation of their integer value.”
|
145
|
+
def to_i = (self.inner_spirit.get_value(:U64, 0) << 64) | self.inner_spirit.get_value(:U64, 8)
|
146
|
+
|
147
|
+
# ITU-T Rec. X.667 sez —
|
148
|
+
# “An alternative URN format [alternative to `"urn:uuid:<hex-string>"`] is available,
|
149
|
+
# but is not recommended for URNs generated using UUIDs.
|
150
|
+
# This alternative format uses the single integer value of the UUID, and represents the UUID
|
151
|
+
# `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` as `urn:oid:2.25.329800735698586629295641978511506172918`.”
|
152
|
+
#
|
153
|
+
# Explicitly call `#to_i#to_s` to avoid `RangeError: bignum out of char range`.
|
154
|
+
def to_oid = ::String::new("urn:oid:2.25.".concat(self.to_i.to_s), encoding: ::Encoding::US_ASCII).-@
|
155
|
+
|
156
|
+
# Compare to dotNET `Guid.ToByteArray` https://learn.microsoft.com/en-us/dotnet/api/system.guid.tobytearray
|
157
|
+
# Match the naming of `::String#bytes` since we behave identically.
|
158
|
+
def bytes = self.inner_spirit.values
|
159
|
+
# Also keep the name `values` because why not lol
|
160
|
+
def values = self.inner_spirit.values
|
161
|
+
|
162
|
+
end
|
File without changes
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# `::String`-printing components.
|
2
|
+
module ::GlobeGlitter::SAY_YEEEAHH
|
3
|
+
|
4
|
+
# SAY YEEEAHH
|
5
|
+
# NOTE: Built-in Ruby classes emit `US-ASCII` as their `#to_s`, thus so shall we.
|
6
|
+
# Some relevant discussions:
|
7
|
+
# - In https://bugs.ruby-lang.org/issues/7752 `naruse` sez —
|
8
|
+
# “On current policy, strings which always include only US-ASCII characters are US-ASCII.
|
9
|
+
# If there is a practical issue, I may change the policy in the future.
|
10
|
+
# Note that US-ASCII string is faster than UTF-8 on getting length or index access.”
|
11
|
+
# - `::Time`: https://bugs.ruby-lang.org/issues/6820
|
12
|
+
# - `::Integer`: https://bugs.ruby-lang.org/issues/15876
|
13
|
+
# - `::BigDecimal`: https://bugs.ruby-lang.org/issues/17011
|
14
|
+
def to_s(base=16)
|
15
|
+
case base
|
16
|
+
when 2 then self.to_i.to_s(2).rjust(128, ?0)
|
17
|
+
when 16 then
|
18
|
+
# ITU-T Rec. X.667 sez —
|
19
|
+
#
|
20
|
+
# “Each field is treated as an integer and has its value printed as a
|
21
|
+
# zero-filled hexadecimal digit string with the most significant digit first.
|
22
|
+
# The hexadecimal values "a" through "f" are output as lower case characters.”
|
23
|
+
::Array[
|
24
|
+
# TODO: Handle swapping for String representation of MS-style GUIDs
|
25
|
+
self.bits127–96.to_s(16).rjust(8, ?0),
|
26
|
+
self.bits95–80.to_s(16).rjust(4, ?0),
|
27
|
+
self.bits79–64.to_s(16).rjust(4, ?0),
|
28
|
+
((self.bits63–56 << 8) | self.bits55–48).to_s(16).rjust(4, ?0),
|
29
|
+
self.bits47–0.to_s(16).rjust(12, ?0),
|
30
|
+
].join(?-).encode!(::Encoding::US_ASCII).-@
|
31
|
+
else
|
32
|
+
# Compare to `::Integer#to_s` behavior:
|
33
|
+
# irb> 333.to_s(666)
|
34
|
+
# (irb):in `to_s': invalid radix 666 (ArgumentError)
|
35
|
+
raise ::ArgumentError::new("invalid radix #{base.to_s}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# In Microsoft-land, GUIDs were ALL-CAPS hex packaged between curly braces
|
40
|
+
def to_guid = ::String::new("{#{self.to_s(16).upcase}}", encoding: ::Encoding::US_ASCII)
|
41
|
+
|
42
|
+
def inspect = ::String::new("#<#{self.class.name} #{self.to_s}>", encoding: ::Encoding::US_ASCII)
|
43
|
+
|
44
|
+
# ITU-T Rec. X.667 sez —
|
45
|
+
#
|
46
|
+
# “A UUID can be used as the primary integer value of a Joint UUID arc using the single integer value of the UUID.
|
47
|
+
# The hexadecimal representation of the UUID can also be used as a non-integer Unicode label for the arc.
|
48
|
+
# EXAMPLE — The following is an example of the use of a UUID to form an IRI/URI value:
|
49
|
+
# "oid:/UUID/f81d4fae-7dec-11d0-a765-00a0c91e6bf6"”
|
50
|
+
def to_oid_s = ::String::new("oid:/UUID/".concat(self.to_s(base=16)), encoding: ::Encoding::US_ASCII).-@
|
51
|
+
|
52
|
+
# ITU-T Rec. X.667 sez —
|
53
|
+
#
|
54
|
+
# “The string representation of a UUID is fully compatible with the URN syntax.
|
55
|
+
# When converting from a bit-oriented, in-memory representation of a UUID into a URN,
|
56
|
+
# care must be taken to strictly adhere to the byte order issues
|
57
|
+
# mentioned in the string representation section.”
|
58
|
+
# “The following is an example of the string representation of a UUID as a URN:
|
59
|
+
# urn:inner_spirit:f81d4fae-7dec-11d0-a765-00a0c91e6bf6”
|
60
|
+
def to_urn = ::String::new("urn:uuid:".concat(self.to_s(base=16)), encoding: ::Encoding::US_ASCII).-@
|
61
|
+
|
62
|
+
# TODO: `#to_clsid` https://www.w3.org/Addressing/clsid-scheme
|
63
|
+
|
64
|
+
end
|