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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fc4b7e3db6125831483cce9cc94ff07241e7087163160d74b5b68bfada42745
4
- data.tar.gz: b82352826f8c74f755df56e07c77af3d807cb886e2c8e72807c2bc38ef5cce88
3
+ metadata.gz: 709119ee194cd0d230580aa34906af6ffcf2ff1d271ec431396d5ab8395a150b
4
+ data.tar.gz: e21a0cdd45a3d905c85d20469b63f0978a154bd8e7e791b2780b575c8e8a994f
5
5
  SHA512:
6
- metadata.gz: d8def5dba926479acf58efbb8f6c227fdb34b2b3ee375b027a189a318089114b981430d77b2cb855d1c67d42984fdd0a75c406287aa699c02f5ece9cd477694c
7
- data.tar.gz: 0b41190f230d497c58696feb317f18deb6ca22d1a9995fd10feb4fd3e38d81417b3d15293fe0ce1991189ebc762364b6b2fe4924063921bfa1730c8df3834aa3
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
@@ -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
- bin = bin.unpack 'C*'
13
- bin[-1] >>= 1
14
- out = ::Base32.encode bin.pack('C*')
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
- bin = bin.unpack 'C*'
20
- bin[-1] >>= 2
21
- out = ::Base64.urlsafe_encode64 bin.pack('C*')
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
- def self.bin_uuid_to_pair data
54
- list = data.unpack 'N4'
55
- version = (list[1] & 0x0000f000) >> 12
56
- list[1] = (list[1] & 0xffff0000) |
57
- ((list[1] & 0x00000fff) << 4) | (list[2] >> 28)
58
- list[2] = (list[2] & 0x0fffffff) << 4 | (list[3] >> 28)
59
- list[3] <<= 4
60
-
61
- return version, list.pack('N4')
62
- end
63
-
64
- def self.pair_to_bin_uuid version, data
65
- version &= 0xf
66
-
67
- list = data.unpack 'N4'
68
- list[3] >>= 4
69
- list[3] |= ((list[2] & 0xf) << 28)
70
- list[2] >>= 4
71
- list[2] |= ((list[1] & 0xf) << 28)
72
- list[1] = (
73
- list[1] & 0xffff0000) | (version << 12) | ((list[1] >> 4) & 0xfff)
74
-
75
- list.pack 'N4'
76
- end
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
- uuid = uuid.to_s
175
+ # XXX remove this when appropriate
176
+ version = warn_version(version)
106
177
 
107
- bin = nil
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
- version, content = bin_uuid_to_pair bin[0, 16]
198
+ uuidver, content = TRANSFORM[version][0].call bin[0, 16]
127
199
 
128
- encode_version(version) + ENCODE[radix].call(content)
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, radix: nil, format: :str
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
- version, content = match.captures
174
- version = decode_version version
175
- content = DECODE[radix].call 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 = pair_to_bin_uuid version, content
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
 
@@ -3,5 +3,5 @@ unless Module.const_defined? 'UUID'
3
3
  end
4
4
 
5
5
  module UUID::NCName
6
- VERSION = "0.1.3"
6
+ VERSION = "0.2.0"
7
7
  end
@@ -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.1.3
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-07-10 00:00:00.000000000 Z
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
  - - ">="