uuid-ncname 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +59 -0
- data/lib/uuid/ncname.rb +165 -53
- data/lib/uuid/ncname/version.rb +1 -1
- data/uuid-ncname.gemspec +3 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 709119ee194cd0d230580aa34906af6ffcf2ff1d271ec431396d5ab8395a150b
|
4
|
+
data.tar.gz: e21a0cdd45a3d905c85d20469b63f0978a154bd8e7e791b2780b575c8e8a994f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7885b27a99f48e644a1f4152bdd8919e9dacac8a3109c466d8b87ba61d4934aa1761f381c17563c7c228083ec9ede908154c16572f5291f05f5b3c87d8977875
|
7
|
+
data.tar.gz: f2c6b86bfc74481ba896fa6e8504c7f6f312ab39831775e92465d7f0b09cd3087950ba768e0828d4a8e24e69cb43caf71e70b0125dc420475bf053abf31eba79
|
data/README.md
CHANGED
@@ -28,6 +28,65 @@ the constraints of various other identifiers such as NCName, and create an
|
|
28
28
|
[isomorphic](http://en.wikipedia.org/wiki/Isomorphism) mapping between
|
29
29
|
them.
|
30
30
|
|
31
|
+
## _FORMAT DEPRECATION NOTICE_
|
32
|
+
|
33
|
+
After careful consideration, I have decided to change the UUID-NCName
|
34
|
+
format in a minor yet incompatible way. In particular, I have moved
|
35
|
+
the nybble containing
|
36
|
+
the [`variant`](https://tools.ietf.org/html/rfc4122#section-4.1.1) to
|
37
|
+
the very end of the identifier, whereas it previously was mixed into
|
38
|
+
the middle somewhere.
|
39
|
+
|
40
|
+
This can be considered an application
|
41
|
+
of [Postel's Law](https://en.wikipedia.org/wiki/Postel%27s_law), based
|
42
|
+
on the assumption that these identifiers will be generated through
|
43
|
+
other methods, and potentially naïvely. Like the `version` field, the
|
44
|
+
`variant` field has a limited acceptable range of values. If, for
|
45
|
+
example, one were to attempt to generate a conforming identifier by
|
46
|
+
simply generating a random Base32 or Base64 string, it will be
|
47
|
+
difficult to ensure that the `variant` field will indeed conform when
|
48
|
+
the identifier is converted to a standard UUID. By moving the
|
49
|
+
`variant` field out to the end of the identifier, everything between
|
50
|
+
the `version` and `variant` bookends can be generated randomly without
|
51
|
+
any further consideration, like so:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
B64_ALPHA = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + %w(- _)
|
55
|
+
|
56
|
+
def make_cheapo_b64_uuid_ncname
|
57
|
+
vals = (1..20).map { rand 64 } # generate the content
|
58
|
+
vals.push(rand(4) + 8) # last digit is special
|
59
|
+
'E' + vals.map { |v| B64_ALPHA[v] }.join('') # 'E' for UUID v4
|
60
|
+
end
|
61
|
+
|
62
|
+
# voilà:
|
63
|
+
|
64
|
+
cheap = make_cheapo_b64_uuid_ncname
|
65
|
+
# => "EXSVv8ezPbSKWoKOkBNWKL"
|
66
|
+
|
67
|
+
# now try changing it to a standard UUID:
|
68
|
+
|
69
|
+
UUID::NCName.from_ncname cheap, version: 1
|
70
|
+
# => "5d256ff1-eccf-46d2-b296-a0a3a404d58a"
|
71
|
+
```
|
72
|
+
|
73
|
+
Furthermore, since the default behaviour is to align the bits of the
|
74
|
+
last byte to the size of the encoding symbol, and since the `variant`
|
75
|
+
bits are masked, a compliant RFC4122 UUID will _always_ end with `I`,
|
76
|
+
`J`, `K`, or `L`, in _both_ Base32 (case-insensitive) and Base64
|
77
|
+
variants.
|
78
|
+
|
79
|
+
Since I have already released this gem prior to this format change, I
|
80
|
+
have added a `:version` parameter to both `to_ncname` and
|
81
|
+
`from_ncname`. The version currently defaults to `0`, the old one, but
|
82
|
+
will issue a warning if not explicitly set. Later I will change the
|
83
|
+
default to `1`, while keeping the warning, then later still, finally
|
84
|
+
remove the warning with 1 as the default. This should ensure that any
|
85
|
+
code written during the transition produces the correct results.
|
86
|
+
|
87
|
+
> Unless you have to support identifiers generated from version 0.1.3
|
88
|
+
> or newer, you should be running these methods with `version: 1`.
|
89
|
+
|
31
90
|
## Rationale & Method
|
32
91
|
|
33
92
|
The UUID is a generic identifier which is large enough to be globally
|
data/lib/uuid/ncname.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
1
2
|
require "uuid/ncname/version"
|
2
3
|
|
3
4
|
require 'base64'
|
@@ -8,34 +9,42 @@ module UUID::NCName
|
|
8
9
|
private
|
9
10
|
|
10
11
|
ENCODE = {
|
11
|
-
32 => -> bin {
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
32 => -> (bin, align = true) {
|
13
|
+
if align
|
14
|
+
bin = bin.unpack 'C*'
|
15
|
+
bin[-1] >>= 1
|
16
|
+
bin = bin.pack 'C*'
|
17
|
+
end
|
18
|
+
|
19
|
+
out = ::Base32.encode bin
|
15
20
|
|
16
21
|
out.downcase[0, 25]
|
17
22
|
},
|
18
|
-
64 => -> bin {
|
19
|
-
|
20
|
-
|
21
|
-
|
23
|
+
64 => -> (bin, align = true) {
|
24
|
+
if align
|
25
|
+
bin = bin.unpack 'C*'
|
26
|
+
bin[-1] >>= 2
|
27
|
+
bin = bin.pack 'C*'
|
28
|
+
end
|
29
|
+
|
30
|
+
out = ::Base64.urlsafe_encode64 bin
|
22
31
|
|
23
32
|
out[0, 21]
|
24
33
|
},
|
25
34
|
}
|
26
35
|
|
27
36
|
DECODE = {
|
28
|
-
32 => -> str {
|
37
|
+
32 => -> (str, align = true) {
|
29
38
|
str = str.upcase[0, 25] + 'A======'
|
30
39
|
out = ::Base32.decode(str).unpack 'C*'
|
31
|
-
out[-1] <<= 1
|
40
|
+
out[-1] <<= 1 if align
|
32
41
|
|
33
42
|
out.pack 'C*'
|
34
43
|
},
|
35
|
-
64 => -> str {
|
44
|
+
64 => -> (str, align = true) {
|
36
45
|
str = str[0, 21] + 'A=='
|
37
46
|
out = ::Base64.urlsafe_decode64(str).unpack 'C*'
|
38
|
-
out[-1] <<= 2
|
47
|
+
out[-1] <<= 2 if align
|
39
48
|
|
40
49
|
out.pack 'C*'
|
41
50
|
},
|
@@ -50,30 +59,62 @@ module UUID::NCName
|
|
50
59
|
bin: -> bin { bin },
|
51
60
|
}
|
52
61
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
62
|
+
TRANSFORM = [
|
63
|
+
# old version prior to shifting out the variant nybble
|
64
|
+
[
|
65
|
+
-> data {
|
66
|
+
list = data.unpack 'N4'
|
67
|
+
version = (list[1] & 0x0000f000) >> 12
|
68
|
+
list[1] = (list[1] & 0xffff0000) |
|
69
|
+
((list[1] & 0x00000fff) << 4) | (list[2] >> 28)
|
70
|
+
list[2] = (list[2] & 0x0fffffff) << 4 | (list[3] >> 28)
|
71
|
+
list[3] <<= 4
|
72
|
+
|
73
|
+
return version, list.pack('N4')
|
74
|
+
},
|
75
|
+
-> (version, data) {
|
76
|
+
version &= 0xf
|
77
|
+
|
78
|
+
list = data.unpack 'N4'
|
79
|
+
list[3] >>= 4
|
80
|
+
list[3] |= ((list[2] & 0xf) << 28)
|
81
|
+
list[2] >>= 4
|
82
|
+
list[2] |= ((list[1] & 0xf) << 28)
|
83
|
+
list[1] = (
|
84
|
+
list[1] & 0xffff0000) | (version << 12) | ((list[1] >> 4) & 0xfff)
|
85
|
+
|
86
|
+
list.pack 'N4'
|
87
|
+
},
|
88
|
+
],
|
89
|
+
# current version
|
90
|
+
[
|
91
|
+
-> data {
|
92
|
+
list = data.unpack 'N4'
|
93
|
+
version = (list[1] & 0x0000f000) >> 12
|
94
|
+
variant = (list[2] & 0xf0000000) >> 24
|
95
|
+
list[1] = (list[1] & 0xffff0000) |
|
96
|
+
((list[1] & 0x00000fff) << 4) | ((list[2] & 0x0fffffff) >> 24)
|
97
|
+
list[2] = (list[2] & 0x00ffffff) << 8 | (list[3] >> 24)
|
98
|
+
list[3] = (list[3] << 8) | variant
|
99
|
+
|
100
|
+
return version, list.pack('N4')
|
101
|
+
},
|
102
|
+
-> (version, data) {
|
103
|
+
version &= 0xf
|
104
|
+
|
105
|
+
list = data.unpack 'N4'
|
106
|
+
variant = (list[3] & 0xf0) << 24
|
107
|
+
list[3] >>= 8
|
108
|
+
list[3] |= ((list[2] & 0xff) << 24)
|
109
|
+
list[2] >>= 8
|
110
|
+
list[2] |= ((list[1] & 0xf) << 24) | variant
|
111
|
+
list[1] = (
|
112
|
+
list[1] & 0xffff0000) | (version << 12) | ((list[1] >> 4) & 0xfff)
|
113
|
+
|
114
|
+
list.pack 'N4'
|
115
|
+
},
|
116
|
+
],
|
117
|
+
]
|
77
118
|
|
78
119
|
def self.encode_version version
|
79
120
|
((version & 15) + 65).chr
|
@@ -83,6 +124,17 @@ module UUID::NCName
|
|
83
124
|
(version.upcase.ord - 65) % 16
|
84
125
|
end
|
85
126
|
|
127
|
+
def self.warn_version version
|
128
|
+
if version.nil?
|
129
|
+
warn 'Set an explicit :version to remove this warning. See documentation.'
|
130
|
+
version = 0
|
131
|
+
end
|
132
|
+
|
133
|
+
raise 'Version must be 0 or 1' unless [0, 1].include? version
|
134
|
+
|
135
|
+
version
|
136
|
+
end
|
137
|
+
|
86
138
|
public
|
87
139
|
|
88
140
|
# Converts a UUID (or object that when converted to a string looks
|
@@ -95,16 +147,36 @@ module UUID::NCName
|
|
95
147
|
#
|
96
148
|
# @param radix [32, 64] either the number 32 or the number 64.
|
97
149
|
#
|
150
|
+
# @param version [0, 1] An optional formatting version, where 0 is
|
151
|
+
# the naïve original version and 1 moves the `variant` nybble out
|
152
|
+
# to the end of the identifier. You will be warned if you do not
|
153
|
+
# set this parameter explicitly. The default is currently 0, but
|
154
|
+
# will change in the next version.
|
155
|
+
#
|
156
|
+
# @param align [true, false] Optional directive to treat the
|
157
|
+
# terminating character as aligned to the numerical base of the
|
158
|
+
# representation. Since the version nybble is removed from the
|
159
|
+
# string and the first 120 bits divide evenly into both Base32 and
|
160
|
+
# Base64, the overhang is only ever 4 bits. This means that when
|
161
|
+
# the terminating character is aligned, it will always be in the
|
162
|
+
# range of the letters A through P in (the RFC 3548/4648
|
163
|
+
# representations of) both Base32 and Base64. When `version` is 1
|
164
|
+
# and the terminating character is aligned, RFC4122-compliant UUIDs
|
165
|
+
# will always terminate with I, J, K, or L. Defaults to `true`.
|
166
|
+
#
|
98
167
|
# @return [String] The NCName-formatted UUID.
|
99
168
|
|
100
|
-
def self.to_ncname uuid, radix: 64
|
169
|
+
def self.to_ncname uuid, radix: 64, version: nil, align: true
|
101
170
|
raise 'Radix must be either 32 or 64' unless [32, 64].include? radix
|
102
171
|
raise 'UUID must be something stringable' if uuid.nil? or
|
103
172
|
not uuid.respond_to? :to_s
|
173
|
+
raise 'Align must be true or false' unless [true, false].include? align
|
104
174
|
|
105
|
-
|
175
|
+
# XXX remove this when appropriate
|
176
|
+
version = warn_version(version)
|
106
177
|
|
107
|
-
|
178
|
+
uuid = uuid.to_s
|
179
|
+
bin = nil
|
108
180
|
|
109
181
|
if uuid.length == 16
|
110
182
|
bin = uuid
|
@@ -113,7 +185,7 @@ module UUID::NCName
|
|
113
185
|
if (m = /^(?:urn:uuid:)?([0-9A-Fa-f-]{32,})$/.match(uuid))
|
114
186
|
bin = [m[1].tr('-', '')].pack 'H*'
|
115
187
|
elsif (m = /^([0-9A-Za-z+\/_-]+=*)$/.match(uuid))
|
116
|
-
match= m[1].tr('-_', '+/')
|
188
|
+
match = m[1].tr('-_', '+/')
|
117
189
|
bin = ::Base64.decode64(match)
|
118
190
|
else
|
119
191
|
raise "Not sure what to do with #{uuid}"
|
@@ -123,9 +195,9 @@ module UUID::NCName
|
|
123
195
|
raise 'Binary representation of UUID is shorter than 16 bytes' if
|
124
196
|
bin.length < 16
|
125
197
|
|
126
|
-
|
198
|
+
uuidver, content = TRANSFORM[version][0].call bin[0, 16]
|
127
199
|
|
128
|
-
encode_version(
|
200
|
+
encode_version(uuidver) + ENCODE[radix].call(content, align)
|
129
201
|
end
|
130
202
|
|
131
203
|
# Converts an NCName-encoded UUID back to its canonical
|
@@ -139,13 +211,25 @@ module UUID::NCName
|
|
139
211
|
#
|
140
212
|
# @param format [:str, :hex, :b64, :bin] An optional formatting
|
141
213
|
# parameter; defaults to `:str`, the canonical string representation.
|
214
|
+
#
|
215
|
+
# @param version [0, 1] See `to_ncname`. Defaults (for now) to 0.
|
142
216
|
#
|
217
|
+
# @param align [true, false, nil] See `to_ncname` for details.
|
218
|
+
# Setting this parameter to `nil`, the default, will cause the
|
219
|
+
# decoder to detect the alignment state from the identifier.
|
220
|
+
#
|
143
221
|
# @return [String, nil] The corresponding UUID or nil if the input
|
144
222
|
# is malformed.
|
145
223
|
|
146
|
-
def self.from_ncname ncname,
|
224
|
+
def self.from_ncname ncname,
|
225
|
+
radix: nil, format: :str, version: nil, align: nil
|
147
226
|
raise 'Format must be symbol-able' unless format.respond_to? :to_sym
|
148
227
|
raise "Invalid format #{format}" unless FORMAT[format]
|
228
|
+
raise 'Align must be true, false, or nil' unless
|
229
|
+
[true, false, nil].include? align
|
230
|
+
|
231
|
+
# XXX remove this when appropriate
|
232
|
+
version = warn_version version
|
149
233
|
|
150
234
|
return unless ncname and ncname.respond_to? :to_s
|
151
235
|
|
@@ -170,11 +254,13 @@ module UUID::NCName
|
|
170
254
|
end
|
171
255
|
end
|
172
256
|
|
173
|
-
|
174
|
-
|
175
|
-
content
|
257
|
+
uuidver, content = match.captures
|
258
|
+
|
259
|
+
align = !!(content =~ /[A-Pa-p]$/) if align.nil?
|
260
|
+
uuidver = decode_version uuidver
|
261
|
+
content = DECODE[radix].call content, align
|
176
262
|
|
177
|
-
bin =
|
263
|
+
bin = TRANSFORM[version][1].call uuidver, content
|
178
264
|
|
179
265
|
FORMAT[format].call bin
|
180
266
|
end
|
@@ -182,36 +268,62 @@ module UUID::NCName
|
|
182
268
|
# Shorthand for conversion to the Base64 version
|
183
269
|
#
|
184
270
|
# @param uuid [#to_s] The UUID
|
271
|
+
#
|
272
|
+
# @param version [0, 1] See `to_ncname`.
|
273
|
+
#
|
274
|
+
# @param align [true, false] See `to_ncname`.
|
275
|
+
#
|
185
276
|
# @return [String] The Base64-encoded NCName
|
186
277
|
|
187
|
-
def self.to_ncname_64 uuid
|
188
|
-
to_ncname uuid
|
278
|
+
def self.to_ncname_64 uuid, version: nil, align: true
|
279
|
+
to_ncname uuid, version: version, align: align
|
189
280
|
end
|
190
281
|
|
191
282
|
# Shorthand for conversion from the Base64 version
|
192
283
|
#
|
193
284
|
# @param ncname [#to_s] The Base64 variant of the NCName-encoded UUID
|
285
|
+
#
|
194
286
|
# @param format [:str, :hex, :b64, :bin] The format
|
287
|
+
#
|
288
|
+
# @param version [0, 1] See `to_ncname`.
|
289
|
+
#
|
290
|
+
# @param align [true, false] See `to_ncname`.
|
291
|
+
#
|
292
|
+
# @return [String, nil] The corresponding UUID or nil if the input
|
293
|
+
# is malformed.
|
195
294
|
|
196
|
-
def self.from_ncname_64 ncname, format: :str
|
295
|
+
def self.from_ncname_64 ncname, format: :str, version: nil, align: nil
|
197
296
|
from_ncname ncname, radix: 64, format: format
|
198
297
|
end
|
199
298
|
|
200
299
|
# Shorthand for conversion to the Base32 version
|
201
300
|
#
|
202
301
|
# @param uuid [#to_s] The UUID
|
302
|
+
#
|
303
|
+
# @param version [0, 1] See `to_ncname`.
|
304
|
+
#
|
305
|
+
# @param align [true, false] See `to_ncname`.
|
306
|
+
#
|
203
307
|
# @return [String] The Base32-encoded NCName
|
204
308
|
|
205
|
-
def self.to_ncname_32 uuid
|
206
|
-
to_ncname uuid, radix: 32
|
309
|
+
def self.to_ncname_32 uuid, version: nil, align: true
|
310
|
+
to_ncname uuid, radix: 32, version: version, align: align
|
207
311
|
end
|
208
312
|
|
209
313
|
# Shorthand for conversion from the Base32 version
|
210
314
|
#
|
211
315
|
# @param ncname [#to_s] The Base32 variant of the NCName-encoded UUID
|
316
|
+
#
|
212
317
|
# @param format [:str, :hex, :b64, :bin] The format
|
318
|
+
#
|
319
|
+
# @param version [0, 1] See `to_ncname`.
|
320
|
+
#
|
321
|
+
# @param align [true, false] See `to_ncname`.
|
322
|
+
#
|
323
|
+
# @return [String, nil] The corresponding UUID or nil if the input
|
324
|
+
# is malformed.
|
213
325
|
|
214
|
-
def self.from_ncname_32 ncname, format: :str
|
326
|
+
def self.from_ncname_32 ncname, format: :str, version: nil, align: nil
|
215
327
|
from_ncname ncname, radix: 32, format: format
|
216
328
|
end
|
217
329
|
|
data/lib/uuid/ncname/version.rb
CHANGED
data/uuid-ncname.gemspec
CHANGED
@@ -26,6 +26,9 @@ DESC
|
|
26
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
27
|
spec.require_paths = ["lib"]
|
28
28
|
|
29
|
+
# we use named parameters
|
30
|
+
spec.required_ruby_version = '~> 2.0'
|
31
|
+
|
29
32
|
# surprisingly do not need this
|
30
33
|
# spec.add_runtime_dependency 'uuidtools', '~> 2.1.5'
|
31
34
|
spec.add_runtime_dependency 'base32', '~> 0.3.2'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: uuid-ncname
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dorian Taylor
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base32
|
@@ -101,9 +101,9 @@ require_paths:
|
|
101
101
|
- lib
|
102
102
|
required_ruby_version: !ruby/object:Gem::Requirement
|
103
103
|
requirements:
|
104
|
-
- - "
|
104
|
+
- - "~>"
|
105
105
|
- !ruby/object:Gem::Version
|
106
|
-
version: '0'
|
106
|
+
version: '2.0'
|
107
107
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
109
|
- - ">="
|