lib/uri/ni.rb in uri-ni-0.1.0 vs lib/uri/ni.rb in uri-ni-0.1.1
- old
+ new
@@ -1,5 +1,6 @@
+# -*- coding: utf-8 -*-
require 'uri/ni/version'
require 'uri'
require 'uri/generic'
require 'digest'
@@ -77,10 +78,69 @@
m = PATH_RE.match(path) or raise ArgumentError,
"Path #{path} does not match constraint"
m.captures
end
+ def assert_radix radix
+ raise ArgumentError,
+ "Radix must be 16, 32, 64, or 256, not #{radix.inspect}" unless
+ [256, 64, 32, 16].include? radix
+ radix
+ end
+
+ # assertions about data representation
+ ASSERT = {
+ 256 => [/.*/, ''],
+ 64 => [/^[0-9A-Za-z+\/_-]*=*$/, 'Data %s is not in base64'],
+ 32 => [/^[2-7A-Za-z]*=*$/, 'Data %s is not in base32'],
+ 16 => [/^[0-9A-Fa-f]*$/, 'Data %s is not in hexadecimal'],
+ }
+
+ def assert_repr data, radix
+ re, error = ASSERT[radix]
+ raise ArgumentError, error % data unless re.match data
+ end
+
+ # from whatever to binary
+ DECODE = {
+ 256 => -> x { x },
+ 64 => -> x { Base64.decode64 x.tr('-_', '+/') },
+ 32 => -> x { require 'base32'; Base32.decode x },
+ 16 => -> x { [x].pack 'H*' },
+ }
+
+ # from binary to whatever
+ ENCODE = {
+ 256 => -> x { x },
+ 64 => -> x { Base64.urlsafe_encode64(x).tr '=', '' },
+ 32 => -> x { require 'base32'; Base32.encode(x).tr '=', '' },
+ 16 => -> x { x.unpack1 'H*' },
+ }
+
+ # canonical and alternative representations
+ CANON = {
+ 256 => -> x { x },
+ 64 => -> x { x.tr('=', '').tr '+/', '-_' },
+ 32 => -> x { x.tr('=', '').upcase },
+ 16 => -> x { x.downcase },
+ }
+
+ # note if we put the padding here then we sanitize input as well
+
+ ALT = {
+ 256 => -> x { x },
+ 64 => -> x { x.tr('=', '').tr '-_', '+/' },
+ 32 => -> x { x.tr('=', '').downcase },
+ 16 => -> x { x.upcase },
+ }
+
+ def transcode data, from: 256, to: 256, alt: false
+ assert_repr data, from
+ data = ENCODE[to].call(DECODE[from].call data) unless from == to
+ alt ? ALT[to].call(data) : CANON[to].call(data)
+ end
+
protected
# holy crap you can override these?
# our host can be an empty string
@@ -125,12 +185,12 @@
ctx = data
data = nil # unset data
else
# make sure we're all on the same page hurr
self.algorithm = algorithm ||= self.algorithm
- raise ArgumentError,
- "#{algorithm} is not a supported digest algorithm." unless
+ raise URI::InvalidComponentError,
+ "Can't resolve a Digest context for the algorithm #{algorithm}." unless
ctx = DIGESTS[algorithm]
ctx = ctx.new
end
# deal with authority component
@@ -155,12 +215,11 @@
elsif block
block.call ctx, nil
end
self.set_path("/#{algorithm};" +
- ctx.base64digest.gsub(/[+\/]/, ?+ => ?-, ?/ => ?_).gsub(/=/, ''))
-
+ ctx.base64digest.tr('+/', '-_').tr('=', ''))
self
end
# Display the available algorithms.
#
@@ -182,11 +241,11 @@
#
# @return [Symbol, nil] the old algorithm
def algorithm= algo
a, b = assert_path
self.path = "/#{algo}"
- self.digest = b if b
+ self.set_digest(b, radix: 64) if b
a.to_sym if a
end
# Obtain the authority (userinfo@host:port) if present.
#
@@ -221,98 +280,118 @@
# @param radix [256, 64, 32, 16] The radix of the representation
# @param alt [false, true] Return the alternative representation
# @return [String] The digest of the URI in the given representation
#
def digest radix: 256, alt: false
- case radix
- when 256
- # XXX do not use urlsafe_decode64; it will complain if the
- # thingies aren't aligned
- Base64.decode64(raw_digest.tr('-_', '+/'))
- when 64
- b64digest alt: alt
- when 32
- b32digest alt: alt
- when 16
- hexdigest alt: alt
- else
- raise ArgumentError, "Radix must be 16, 32, 64, 256, not #{radix}"
- end
+ assert_radix radix
+ transcode raw_digest, from: 64, to: radix, alt: alt
end
- # Set the digest to the data. Data may either be a
- # +Digest::Instance+ or a base64 string. String representations will
- # be normalized to {https://tools.ietf.org/html/rfc3548#section-4
- # RFC 3548} base64url, i.e. +\+/+ will be replaced with +-_+ and
- # padding (+=+) will be removed. +Digest::Instance+ objects will
+ # Set the digest to the data, with an optional radix. Data may
+ # either be a +Digest::Instance+—in which case the radix is
+ # ignored–a string, or +nil+. +Digest::Instance+ objects will
# just be run through #compute, with all that entails.
- def digest= data
- a = assert_path.first
- case data
+ #
+ # @param value [String, nil, Digest::Instance] The new digest
+ # @param radix [256, 64, 32, 16] The radix of the encoding (default 256)
+ # @return [String] The _old_ digest in the given radix
+ #
+ def set_digest value, radix: 256
+ assert_radix radix
+
+ a, d = assert_path
+
+ case value
when Digest::Instance
- compute data
+ compute value
when String
- raise ArgumentError, "Data #{data} is not in base64" unless
- /^[0-9A-Za-z+\/_-]*=*$/.match(data)
- data = data.tr('+/', '-_').tr('=', '')
- self.path = a ? "/#{a};#{data}" : "/;#{data}"
+ value = transcode value, from: radix, to: 64
+ self.path = a ? "/#{a};#{value}" : "/;#{value}"
when nil
self.path = a ? "/#{a}" : ?/
else
raise ArgumentError,
- "Data must be a string or Digest::Instance, not #{data.class}"
+ "Value must be a string or Digest::Instance, not #{value.class}"
end
- data
+ # bail out if nil
+ return unless d
+ transcode d, from: 64, to: radix
end
+ # Set the digest to the data. Data may either be a
+ # +Digest::Instance+ or a _binary_ string. +Digest::Instance+
+ # objects will just be run through #compute, with all that entails.
+ #
+ # @param value [String, nil, Digest::Instance] the new digest
+ # @return [String, nil, Digest::Instance] the value passed in
+ #
+ def digest= value
+ return set_digest value
+ end
+
# Return the digest in its hexadecimal notation. Optionally give
# +alt:+ a truthy value to return an alternate (uppercase)
# representation.
#
# @param alt [false, true] Return the alternative representation
# @return [String] The hexadecimal digest
#
def hexdigest alt: false
- str = digest.unpack('H*').first
- return str.upcase if alt
- str
+ transcode raw_digest, from: 64, to: 16, alt: alt
end
+ # Set the digest value, assuming a hexadecimal input.
+ # @param value [String, nil, Digest::Instance] the new digest
+ # @return [String, nil, Digest::Instance] the value passed in
+ def hexdigest= value
+ set_digest value, radix: 16
+ end
+
# Return the digest in its base32 notation. Optionally give
# +alt:+ a truthy value to return an alternate (lowercase)
- # representation. Note this method requires
+ # representation. Note this method requires the base32 module.
#
# @param alt [false, true] Return the alternative representation
# @return [String] The base32 digest
#
def b32digest alt: false
- require 'base32'
- ret = Base32.encode(digest).gsub(/=+/, '')
- return ret.downcase if alt
- ret.upcase
+ transcode raw_digest, from: 64, to: 32, alt: alt
end
- # Return the digest in its base64 notation. Optionally give
- # +alt:+ a truthy value to return an alternate (URL-safe)
- # representation.
+ # Set the digest value, assuming a base32 input (requires base32).
+ # @param value [String, nil, Digest::Instance] the new digest
+ # @return [String, nil, Digest::Instance] the value passed in
+ def b32digest= value
+ set_digest value, radix: 32
+ end
+
+ # Return the digest in its base64 notation. Note it is the
+ # _default_ representation that is URL-safe, for parity with the
+ # identifier itself. Give +alt:+ a truthy value to return a plain
+ # (_non_-URL-safe) base64 representation.
#
# @param alt [false, true] Return the alternative representation
# @return [String] The base64 digest
#
def b64digest alt: false
- ret = raw_digest
- return ret.gsub(/[-_]/, ?- => ?+, ?_ => ?/) unless alt
- ret
+ transcode raw_digest, from: 64, to: 64, alt: alt
end
+ # Set the digest value, assuming a base64 input.
+ # @param value [String, nil, Digest::Instance] the new digest
+ # @return [String, nil, Digest::Instance] the value passed in
+ def b64digest= value
+ set_digest value, radix: 64
+ end
+
# Returns a +/.well-known/...+, either HTTPS or HTTP URL, given the
# contents of the +ni:+ URI.
#
# @param authority [#to_s, URI] Override the authority part of the URI
- # @param https [true, false] whether the URL is to be HTTPS.
- # @return [URI::HTTPS, URI::HTTP]
+ # @param https [true, false] Whether the URL is to be HTTPS.
+ # @return [URI::HTTPS, URI::HTTP] The generated URL.
#
def to_www https: true, authority: nil
a, d = assert_path
components = {
scheme: "http#{https ? ?s : ''}",
@@ -325,16 +404,16 @@
}
if authority
uhp = []
if authority.is_a? URI
- raise ArgumentError, "Bad authority #{authority}" unless
+ raise URI::InvalidComponentError, "Bad authority #{authority}" unless
%i[userinfo host port].all? {|c| authority.respond_to? c }
uhp = [authority.userinfo, authority.host, authority.port]
uhp[2] = nil if authority.port == authority.class::DEFAULT_PORT
else
authority = authority.to_s
- uhp = AUTH_RE.match(authority) or raise ArgumentError,
+ uhp = AUTH_RE.match(authority) or raise URI::InvalidComponentError,
"Invalid authority #{authority}"
uhp = uhp.captures
end
components[:userinfo] = uhp[0]
components[:host] = uhp[1]