uuid-ncname 0.1.3 → 0.2.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 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
  - - ">="