Class: URI::NI

Inherits:
Generic
  • Object
show all
Defined in:
lib/uri/ni/version.rb,
lib/uri/ni.rb

Constant Summary collapse

VERSION =
"0.2.5"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.algorithms(truncated: false) ⇒ Array

Display the available algorithms.

Returns:

  • (Array)

    containing the symbols representing the available digest algorithms.



380
381
382
383
# File 'lib/uri/ni.rb', line 380

def self.algorithms truncated: false
  out = DIGESTS.keys.sort
  truncated ? out : out - TRUNCATED
end

.compute(data = nil, algorithm: :"sha-256", blocksize: 65536, authority: nil, query: nil) {|ctx, buf| ... } ⇒ URI::NI

Compute an RFC6920 URI from a data source.

Parameters:

  • data (#to_s, IO, Digest, nil) (defaults to: nil)
  • algorithm (Symbol) (defaults to: :"sha-256")

    See available algorithms. Default: :“sha-256”

  • blocksize (Integer) (defaults to: 65536)

    The number or bytes per call to the Digest

  • authority (String, nil) (defaults to: nil)

    Optional authority (user, host, port)

  • query (String, nil) (defaults to: nil)

    Optional query string

Yields:

  • (ctx, buf)

    Passes the Digest and (maybe) the buffer

Yield Parameters:

  • ctx (Digest::Instance)

    The digest instance to the block

  • buf (String, nil)

    The current read buffer (if data is set)

Yield Returns:

  • (nil)

    The result of the block is ignored

Returns:



285
286
287
288
289
290
# File 'lib/uri/ni.rb', line 285

def self.compute data = nil, algorithm: :"sha-256", blocksize: 65536,
    authority: nil, query: nil, &block

  build({ scheme: 'ni' }).compute data, algorithm: algorithm,
    blocksize: blocksize, authority: authority, query: query, &block
end

.context(algorithm) ⇒ Digest:Instance

Return a Digest::Instance for a supported algorithm.

Parameters:

  • The (#to_s, #to_sym)

    algorithm

Returns:

  • (Digest:Instance)

    The digest context

Raises:

  • (ArgumentError)

    if the algorithm is unrecognized



296
297
298
299
300
301
302
303
# File 'lib/uri/ni.rb', line 296

def self.context algorithm
  raise ArgumentError, "Cannot coerce #{algorithm} to a symbol" unless
    algorithm.respond_to? :to_s # cheat: symbols respond to to_s
  algorithm = algorithm.to_s.to_sym unless algorithm.is_a? Symbol
  raise ArgumentError, "Unsupported digest algorithm #{algorithm}" unless
    ctx = DIGESTS[algorithm]
  ctx.new
end

.ingest(algorithm = nil, digest) ⇒ URI::NI

Transform a digest for a known algorithm into a URI::NI. Takes a binary, Base64, Base32, or hexadecimal string.

Parameters:

  • algorithm (Symbol, #to_sym) (defaults to: nil)

    a supported algorithm

  • digest (String, #to_s, Digest::Instance)

    the digest (or context)

Returns:

  • (URI::NI)

    the transformed identifier

Raises:

  • (ArgumentError)

    if the algorithm is unsupported

  • (ArgumentError)

    if the input string is not the right length for the algorithm

  • (ArgumentError)

    if the input string is incorrectly encoded



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/uri/ni.rb', line 233

def self.ingest algorithm = nil, digest
  if digest.is_a? Digest::Instance
    # this will complain
    algo_for digest, algorithm
    return compute(digest)
  elsif tmp = try_uri(digest)
    raise ArgumentError,
      "digest algorithm #{tmp.algorithm} does not match #{algorithm}" if
      algorithm && algorithm != tmp.algorithm
    return tmp
  end

  # okay now we should have a string that's just the hash
  digest = digest.to_s

  # get the expected length of the digest for the algorithm
  len = LENGTHS[algorithm.to_s.downcase.to_sym] or raise ArgumentError,
    "unsupported algorithm #{algorithm}"

  # the length of the string is used to determine the expected encoding
  radix = case digest.length
          when len then 256
          when -> x { x >= (len * 8 / 6.0).ceil && x < (len * 8 / 5.0).ceil }
            # note that a length longer than base64 but shorter than base32
            # can be assumed to be base64 with padding
            64
          when (len * 8 / 5.0).ceil then 32
          when len * 2 then 16
          else
            raise ArgumentError, "invalid string length: #{digest.length}"
          end

  # then we lint it to see if it's correct
  digest = transcode digest, from: radix, to: 64

  # then we transcode
  URI("ni:///#{algorithm};#{digest}")
end

.truncated?(algorithm) ⇒ false, true

Returns true if the supplied algorithm is a truncated one.

Returns:

  • (false, true)


646
647
648
# File 'lib/uri/ni.rb', line 646

def self.truncated? algorithm
  valid_algo?(algorithm) && TRUNCATED.include?(algorithm.to_s.downcase.to_sym)
end

.try_uri(arg) ⇒ nil, URI::NI

This will attempt to coerce the input into a URI::NI if it’s possible.

Parameters:

  • arg (#to_s, URI::Generic)

    something that might be a URI

Returns:

  • (nil, URI::NI)

    the hash URI (maybe)

Raises:

  • (URI::Error)

    if the URI itself is malformed

  • (ArgumentError)

    if it is not a ‘ni:` URI or HTTP(S) with the `/.well-known/ni/…` construct



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/uri/ni.rb', line 195

def self.try_uri arg
  unless arg.is_a? URI
    arg = arg.to_s
    return unless %r{^(?i:ni|https?)://}.match? arg
    arg = URI(arg)
  end

  # normalize the uri
  arg = arg.normalize

  # if HTTP(S) URI with `/.well-known/ni/…`, convert it
  if m = %r{^/+\.well-known/+ni/+([^/]+)/+([0-9A-Za-z_-]+)$}.match(arg.path)
    arg = URI('ni:///%s;%s' % m.captures)
  end

  raise ArgumentError,
    "don't know what to do with a #{arg.scheme}: URI" unless
    arg.is_a? URI::NI

  # return it
  arg.dup
end

.valid_algo?(algorithm) ⇒ true, false

Returns true if the algorithm is supported.

Parameters:

  • algorithm (Symbol, String)

    the algorithm identifier to test

Returns:

  • (true, false)

    whether it is supported



638
639
640
# File 'lib/uri/ni.rb', line 638

def self.valid_algo? algorithm
  DIGESTS.has_key? algorithm.to_s.downcase.to_sym
end

Instance Method Details

#algorithmSymbol?

Obtain the algorithm of the digest. May be nil.

Returns:

  • (Symbol, nil)


389
390
391
392
# File 'lib/uri/ni.rb', line 389

def algorithm
  algo = assert_path.first
  return algo.to_sym if algo
end

#algorithm=(algo) ⇒ Symbol?

Set the algorithm of the digest. Will croak if the path is malformed.

Returns:

  • (Symbol, nil)


398
399
400
401
402
403
# File 'lib/uri/ni.rb', line 398

def algorithm= algo
  a, b = assert_path
  self.path   = "/#{algo}"
  self.set_digest(b, radix: 64) if b
  a.to_sym if a
end

#authorityString?

Obtain the authority (userinfo@host:port) if present.

Returns:

  • (String, nil)

    the authority



409
410
411
412
413
# File 'lib/uri/ni.rb', line 409

def authority
  out = userinfo ? "#{userinfo}@#{host}" : host
  out += "#{out}:#{port}" if port
  out
end

#authority=(authority) ⇒ String?

Set the authority of the URI.

Returns:

  • (String, nil)


419
420
421
422
423
424
425
426
# File 'lib/uri/ni.rb', line 419

def authority= authority
  old = self.authority
  u, h, p = assert_authority authority unless authority.nil?
  set_userinfo u
  set_host     h
  set_port     p
  old
end

#b32digest(alt: false) ⇒ String

Return the digest in its base32 notation. Optionally give alt: a truthy value to return an alternate (lowercase) representation. Note this method requires the base32 module.

Parameters:

  • alt (false, true) (defaults to: false)

    Return the alternative representation

Returns:

  • (String)

    The base32 digest



525
526
527
# File 'lib/uri/ni.rb', line 525

def b32digest alt: false
  transcode raw_digest, from: 64, to: 32, alt: alt
end

#b32digest=(value) ⇒ String, ...

Set the digest value, assuming a base32 input (requires base32).

Parameters:

  • value (String, nil, Digest::Instance)

    the new digest

Returns:

  • (String, nil, Digest::Instance)

    the value passed in



535
536
537
# File 'lib/uri/ni.rb', line 535

def b32digest= value
  set_digest value, radix: 32
end

#b64digest(alt: false) ⇒ String

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.

Parameters:

  • alt (false, true) (defaults to: false)

    Return the alternative representation

Returns:

  • (String)

    The base64 digest



548
549
550
# File 'lib/uri/ni.rb', line 548

def b64digest alt: false
  transcode raw_digest, from: 64, to: 64, alt: alt
end

#b64digest=(value) ⇒ String, ...

Set the digest value, assuming a base64 input.

Parameters:

  • value (String, nil, Digest::Instance)

    the new digest

Returns:

  • (String, nil, Digest::Instance)

    the value passed in



558
559
560
# File 'lib/uri/ni.rb', line 558

def b64digest= value
  set_digest value, radix: 64
end

#compute(data = nil, algorithm: nil, blocksize: 65536, authority: nil, query: nil, &block) ⇒ Object

(Re)-compute a digest using existing information from an instance.

Raises:

  • (ArgumentError)

See Also:



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/uri/ni.rb', line 307

def compute data = nil, algorithm: nil, blocksize: 65536,
    authority: nil, query: nil, &block

  # enforce block size
  raise ArgumentError,
    "Blocksize must be an integer >0, not #{blocksize}" unless
    blocksize.is_a? Integer and blocksize > 0

  # special case for when the data is a digest
  ctx = nil
  if data.is_a? Digest::Instance
    # note the self; this would have been a bug lol
    self.algorithm = algorithm ||= algo_for data, algorithm
    ctx  = data
    data = nil # unset data
  else
    # make sure we're all on the same page hurr
    self.algorithm = algorithm ||= self.algorithm || :"sha-256"
    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
  if authm = AUTH_RE.match(authority.to_s)
    userinfo, host, port = authm.captures
    set_userinfo userinfo
    set_host     host.to_s
    set_port     port
  end

  # coerce data to something non-null
  data = data.to_s if (data.class.ancestors & [String, IO, NilClass]).empty?
  if data
    data = StringIO.new data unless data.is_a? IO

    # give us a default block
    block ||= -> x, y { x << y } # unless block_given?

    while buf = data.read(blocksize)
      block.call ctx, buf
    end
  elsif block
    block.call ctx, nil
  end

  set_digest ctx.digest

  self
end

#digest(radix: 256, alt: false) ⇒ String

Return the digest in the hash. Optionally takes a radix: argument to specify binary, base64, base32, or hexadecimal representations. Another optional flag will return alternative representations for each: base64url (vanilla base64 is canonical), base32 in lowercase (uppercase is canonical), hexadecimal in uppercase (lowercase is canonical). The binary representation naturally has no alternative form. Base64/base32 values will be appropriately padded.

Parameters:

  • radix (256, 64, 32, 16) (defaults to: 256)

    The radix of the representation

  • alt (false, true) (defaults to: false)

    Return the alternative representation

Returns:

  • (String)

    The digest of the URI in the given representation



442
443
444
445
# File 'lib/uri/ni.rb', line 442

def digest radix: 256, alt: false
  assert_radix radix
  transcode raw_digest, from: 64, to: radix, alt: alt
end

#digest=(value) ⇒ String, ...

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.

Parameters:

  • value (String, nil, Digest::Instance)

    the new digest

Returns:

  • (String, nil, Digest::Instance)

    the value passed in



491
492
493
# File 'lib/uri/ni.rb', line 491

def digest= value
  return set_digest value
end

#hexdigest(alt: false) ⇒ String

Return the digest in its hexadecimal notation. Optionally give alt: a truthy value to return an alternate (uppercase) representation.

Parameters:

  • alt (false, true) (defaults to: false)

    Return the alternative representation

Returns:

  • (String)

    The hexadecimal digest



503
504
505
# File 'lib/uri/ni.rb', line 503

def hexdigest alt: false
  transcode raw_digest, from: 64, to: 16, alt: alt
end

#hexdigest=(value) ⇒ String, ...

Set the digest value, assuming a hexadecimal input.

Parameters:

  • value (String, nil, Digest::Instance)

    the new digest

Returns:

  • (String, nil, Digest::Instance)

    the value passed in



513
514
515
# File 'lib/uri/ni.rb', line 513

def hexdigest= value
  set_digest value, radix: 16
end

#set_digest(value, radix: 256) ⇒ String

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.

Parameters:

  • value (String, nil, Digest::Instance)

    The new digest

  • radix (256, 64, 32, 16) (defaults to: 256)

    The radix of the encoding (default 256)

Returns:

  • (String)

    The old digest in the given radix



457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/uri/ni.rb', line 457

def set_digest value, radix: 256
  assert_radix radix

  a, d = assert_path

  case value
  when Digest::Instance
    compute value
  when String
    value = transcode value, from: radix
    value = value[0, LENGTHS[a.to_sym]] # deal with truncation
    value = transcode value, to: 64

    self.path = a ? "/#{a};#{value}" : "/;#{value}"
  when nil
    self.path = a ? "/#{a}" : ?/
  else
    raise ArgumentError,
      "Value must be a string or Digest::Instance, not #{value.class}"
  end

  # bail out if nil
  return unless d
  transcode d, from: 64, to: radix
end

#to_http(authority: nil) ⇒ URI::HTTP

Unconditionally returns an HTTP URL.

Parameters:

  • authority (#to_s, URI) (defaults to: nil)

    Override the authority part of the URI

Returns:

  • (URI::HTTP)


627
628
629
# File 'lib/uri/ni.rb', line 627

def to_http authority: nil
  to_www https: false, authority: authority
end

#to_https(authority: nil) ⇒ URI::HTTPS

Unconditionally returns an HTTPS URL.

Parameters:

  • authority (#to_s, URI) (defaults to: nil)

    Override the authority part of the URI

Returns:

  • (URI::HTTPS)


616
617
618
619
# File 'lib/uri/ni.rb', line 616

def to_https authority: nil
  # note we don't simply alias this
  to_www authority: authority
end

#to_www(https: true, authority: nil) ⇒ URI::HTTPS, URI::HTTP

Returns a /.well-known/..., either HTTPS or HTTP URL, given the contents of the ni: URI.

Parameters:

  • authority (#to_s, URI) (defaults to: nil)

    Override the authority part of the URI

  • https (true, false) (defaults to: true)

    Whether the URL is to be HTTPS.

Returns:

  • (URI::HTTPS, URI::HTTP)

    The generated URL.



570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'lib/uri/ni.rb', line 570

def to_www https: true, authority: nil
  a, d = assert_path
  components = {
    scheme:   "http#{https ? ?s : ''}",
    userinfo: userinfo,
    host:     host,
    port:     port,
    path:     "/.well-known/ni/#{a}/#{d}",
    query:    query,
    fragment: fragment,
  }

  if authority
    uhp = []
    if authority.is_a? URI
      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 URI::InvalidComponentError,
        "Invalid authority #{authority}"
      uhp = uhp.captures
    end
    components[:userinfo] = uhp[0]
    components[:host]     = uhp[1]
    components[:port]     = uhp[2]
  end

  # pick the class
  cls = https ? URI::HTTPS : URI::HTTP

  # `normalize` should do this but doesn't
  components[:port] = nil if
    components[:port] and components[:port] == cls::DEFAULT_PORT

  cls.build(components).normalize
end

#truncated?false, true

Returns true if the algorithm is a truncated one.

Returns:

  • (false, true)


371
372
373
# File 'lib/uri/ni.rb', line 371

def truncated?
  TRUNCATED.include? algorithm
end

#valid?false, true

Returns true if the digest length matches the algorithm.

Returns:

  • (false, true)


363
364
365
# File 'lib/uri/ni.rb', line 363

def valid?
  LENGTHS[algorithm] and LENGTHS[algorithm] == digest.length
end