lib/uuid/ncname.rb in uuid-ncname-0.1.3 vs lib/uuid/ncname.rb in uuid-ncname-0.2.0
- old
+ new
@@ -1,43 +1,52 @@
+# -*- coding: utf-8 -*-
require "uuid/ncname/version"
require 'base64'
require 'base32'
module UUID::NCName
private
ENCODE = {
- 32 => -> bin {
- bin = bin.unpack 'C*'
- bin[-1] >>= 1
- out = ::Base32.encode bin.pack('C*')
+ 32 => -> (bin, align = true) {
+ if align
+ bin = bin.unpack 'C*'
+ bin[-1] >>= 1
+ bin = bin.pack 'C*'
+ end
+ out = ::Base32.encode bin
+
out.downcase[0, 25]
},
- 64 => -> bin {
- bin = bin.unpack 'C*'
- bin[-1] >>= 2
- out = ::Base64.urlsafe_encode64 bin.pack('C*')
+ 64 => -> (bin, align = true) {
+ if align
+ bin = bin.unpack 'C*'
+ bin[-1] >>= 2
+ bin = bin.pack 'C*'
+ end
+ out = ::Base64.urlsafe_encode64 bin
+
out[0, 21]
},
}
DECODE = {
- 32 => -> str {
+ 32 => -> (str, align = true) {
str = str.upcase[0, 25] + 'A======'
out = ::Base32.decode(str).unpack 'C*'
- out[-1] <<= 1
+ out[-1] <<= 1 if align
out.pack 'C*'
},
- 64 => -> str {
+ 64 => -> (str, align = true) {
str = str[0, 21] + 'A=='
out = ::Base64.urlsafe_decode64(str).unpack 'C*'
- out[-1] <<= 2
+ out[-1] <<= 2 if align
out.pack 'C*'
},
}
@@ -48,43 +57,86 @@
hex: -> bin { bin.unpack 'H*' },
b64: -> bin { ::Base64.strict_encode64 bin },
bin: -> bin { bin },
}
- def self.bin_uuid_to_pair data
- list = data.unpack 'N4'
- version = (list[1] & 0x0000f000) >> 12
- list[1] = (list[1] & 0xffff0000) |
- ((list[1] & 0x00000fff) << 4) | (list[2] >> 28)
- list[2] = (list[2] & 0x0fffffff) << 4 | (list[3] >> 28)
- list[3] <<= 4
+ TRANSFORM = [
+ # old version prior to shifting out the variant nybble
+ [
+ -> data {
+ list = data.unpack 'N4'
+ version = (list[1] & 0x0000f000) >> 12
+ list[1] = (list[1] & 0xffff0000) |
+ ((list[1] & 0x00000fff) << 4) | (list[2] >> 28)
+ list[2] = (list[2] & 0x0fffffff) << 4 | (list[3] >> 28)
+ list[3] <<= 4
- return version, list.pack('N4')
- end
+ return version, list.pack('N4')
+ },
+ -> (version, data) {
+ version &= 0xf
- def self.pair_to_bin_uuid version, data
- version &= 0xf
+ list = data.unpack 'N4'
+ list[3] >>= 4
+ list[3] |= ((list[2] & 0xf) << 28)
+ list[2] >>= 4
+ list[2] |= ((list[1] & 0xf) << 28)
+ list[1] = (
+ list[1] & 0xffff0000) | (version << 12) | ((list[1] >> 4) & 0xfff)
- list = data.unpack 'N4'
- list[3] >>= 4
- list[3] |= ((list[2] & 0xf) << 28)
- list[2] >>= 4
- list[2] |= ((list[1] & 0xf) << 28)
- list[1] = (
- list[1] & 0xffff0000) | (version << 12) | ((list[1] >> 4) & 0xfff)
+ list.pack 'N4'
+ },
+ ],
+ # current version
+ [
+ -> data {
+ list = data.unpack 'N4'
+ version = (list[1] & 0x0000f000) >> 12
+ variant = (list[2] & 0xf0000000) >> 24
+ list[1] = (list[1] & 0xffff0000) |
+ ((list[1] & 0x00000fff) << 4) | ((list[2] & 0x0fffffff) >> 24)
+ list[2] = (list[2] & 0x00ffffff) << 8 | (list[3] >> 24)
+ list[3] = (list[3] << 8) | variant
- list.pack 'N4'
- end
+ return version, list.pack('N4')
+ },
+ -> (version, data) {
+ version &= 0xf
+ list = data.unpack 'N4'
+ variant = (list[3] & 0xf0) << 24
+ list[3] >>= 8
+ list[3] |= ((list[2] & 0xff) << 24)
+ list[2] >>= 8
+ list[2] |= ((list[1] & 0xf) << 24) | variant
+ list[1] = (
+ list[1] & 0xffff0000) | (version << 12) | ((list[1] >> 4) & 0xfff)
+
+ list.pack 'N4'
+ },
+ ],
+ ]
+
def self.encode_version version
((version & 15) + 65).chr
end
def self.decode_version version
(version.upcase.ord - 65) % 16
end
+ def self.warn_version version
+ if version.nil?
+ warn 'Set an explicit :version to remove this warning. See documentation.'
+ version = 0
+ end
+
+ raise 'Version must be 0 or 1' unless [0, 1].include? version
+
+ version
+ end
+
public
# Converts a UUID (or object that when converted to a string looks
# like a UUID) to an NCName. By default it produces the Base64
# variant.
@@ -93,41 +145,61 @@
# UUID. This includes UUID objects, URNs, 32-character hex strings,
# 16-byte binary strings, etc.
#
# @param radix [32, 64] either the number 32 or the number 64.
#
+ # @param version [0, 1] An optional formatting version, where 0 is
+ # the naïve original version and 1 moves the `variant` nybble out
+ # to the end of the identifier. You will be warned if you do not
+ # set this parameter explicitly. The default is currently 0, but
+ # will change in the next version.
+ #
+ # @param align [true, false] Optional directive to treat the
+ # terminating character as aligned to the numerical base of the
+ # representation. Since the version nybble is removed from the
+ # string and the first 120 bits divide evenly into both Base32 and
+ # Base64, the overhang is only ever 4 bits. This means that when
+ # the terminating character is aligned, it will always be in the
+ # range of the letters A through P in (the RFC 3548/4648
+ # representations of) both Base32 and Base64. When `version` is 1
+ # and the terminating character is aligned, RFC4122-compliant UUIDs
+ # will always terminate with I, J, K, or L. Defaults to `true`.
+ #
# @return [String] The NCName-formatted UUID.
- def self.to_ncname uuid, radix: 64
+ def self.to_ncname uuid, radix: 64, version: nil, align: true
raise 'Radix must be either 32 or 64' unless [32, 64].include? radix
raise 'UUID must be something stringable' if uuid.nil? or
not uuid.respond_to? :to_s
+ raise 'Align must be true or false' unless [true, false].include? align
+ # XXX remove this when appropriate
+ version = warn_version(version)
+
uuid = uuid.to_s
+ bin = nil
- bin = nil
-
if uuid.length == 16
bin = uuid
else
uuid.gsub!(/\s+/, '')
if (m = /^(?:urn:uuid:)?([0-9A-Fa-f-]{32,})$/.match(uuid))
bin = [m[1].tr('-', '')].pack 'H*'
elsif (m = /^([0-9A-Za-z+\/_-]+=*)$/.match(uuid))
- match= m[1].tr('-_', '+/')
+ match = m[1].tr('-_', '+/')
bin = ::Base64.decode64(match)
else
raise "Not sure what to do with #{uuid}"
end
end
raise 'Binary representation of UUID is shorter than 16 bytes' if
bin.length < 16
- version, content = bin_uuid_to_pair bin[0, 16]
+ uuidver, content = TRANSFORM[version][0].call bin[0, 16]
- encode_version(version) + ENCODE[radix].call(content)
+ encode_version(uuidver) + ENCODE[radix].call(content, align)
end
# Converts an NCName-encoded UUID back to its canonical
# representation. Will return nil if the input doesn't match the
# radix (if supplied) or is otherwise malformed. doesn't match
@@ -137,18 +209,30 @@
#
# @param radix [nil, 32, 64] Optional radix; will use heuristic if omitted.
#
# @param format [:str, :hex, :b64, :bin] An optional formatting
# parameter; defaults to `:str`, the canonical string representation.
+ #
+ # @param version [0, 1] See `to_ncname`. Defaults (for now) to 0.
#
+ # @param align [true, false, nil] See `to_ncname` for details.
+ # Setting this parameter to `nil`, the default, will cause the
+ # decoder to detect the alignment state from the identifier.
+ #
# @return [String, nil] The corresponding UUID or nil if the input
# is malformed.
- def self.from_ncname ncname, radix: nil, format: :str
+ def self.from_ncname ncname,
+ radix: nil, format: :str, version: nil, align: nil
raise 'Format must be symbol-able' unless format.respond_to? :to_sym
raise "Invalid format #{format}" unless FORMAT[format]
+ raise 'Align must be true, false, or nil' unless
+ [true, false, nil].include? align
+ # XXX remove this when appropriate
+ version = warn_version version
+
return unless ncname and ncname.respond_to? :to_s
ncname = ncname.to_s.strip.gsub(/\s+/, '')
match = /^([A-Za-z])([0-9A-Za-z_-]{21,})$/.match(ncname) or return
@@ -168,51 +252,79 @@
# uh will this ever get executed now that i put in that return?
raise "Not sure what to do with an identifier of length #{len}."
end
end
- version, content = match.captures
- version = decode_version version
- content = DECODE[radix].call content
+ uuidver, content = match.captures
- bin = pair_to_bin_uuid version, content
+ align = !!(content =~ /[A-Pa-p]$/) if align.nil?
+ uuidver = decode_version uuidver
+ content = DECODE[radix].call content, align
+ bin = TRANSFORM[version][1].call uuidver, content
+
FORMAT[format].call bin
end
# Shorthand for conversion to the Base64 version
#
# @param uuid [#to_s] The UUID
+ #
+ # @param version [0, 1] See `to_ncname`.
+ #
+ # @param align [true, false] See `to_ncname`.
+ #
# @return [String] The Base64-encoded NCName
- def self.to_ncname_64 uuid
- to_ncname uuid
+ def self.to_ncname_64 uuid, version: nil, align: true
+ to_ncname uuid, version: version, align: align
end
# Shorthand for conversion from the Base64 version
#
# @param ncname [#to_s] The Base64 variant of the NCName-encoded UUID
+ #
# @param format [:str, :hex, :b64, :bin] The format
+ #
+ # @param version [0, 1] See `to_ncname`.
+ #
+ # @param align [true, false] See `to_ncname`.
+ #
+ # @return [String, nil] The corresponding UUID or nil if the input
+ # is malformed.
- def self.from_ncname_64 ncname, format: :str
+ def self.from_ncname_64 ncname, format: :str, version: nil, align: nil
from_ncname ncname, radix: 64, format: format
end
# Shorthand for conversion to the Base32 version
#
# @param uuid [#to_s] The UUID
+ #
+ # @param version [0, 1] See `to_ncname`.
+ #
+ # @param align [true, false] See `to_ncname`.
+ #
# @return [String] The Base32-encoded NCName
- def self.to_ncname_32 uuid
- to_ncname uuid, radix: 32
+ def self.to_ncname_32 uuid, version: nil, align: true
+ to_ncname uuid, radix: 32, version: version, align: align
end
# Shorthand for conversion from the Base32 version
#
# @param ncname [#to_s] The Base32 variant of the NCName-encoded UUID
+ #
# @param format [:str, :hex, :b64, :bin] The format
+ #
+ # @param version [0, 1] See `to_ncname`.
+ #
+ # @param align [true, false] See `to_ncname`.
+ #
+ # @return [String, nil] The corresponding UUID or nil if the input
+ # is malformed.
- def self.from_ncname_32 ncname, format: :str
+ def self.from_ncname_32 ncname, format: :str, version: nil, align: nil
from_ncname ncname, radix: 32, format: format
end
end