lib/sprockets/digest_utils.rb
require 'digest/md5' require 'digest/sha1' require 'digest/sha2' require 'set' module Sprockets # Internal: Hash functions and digest related utilities. Mixed into # Environment. module DigestUtils extend self # Internal: Default digest class. # # Returns a Digest::Base subclass. def digest_class Digest::SHA256 end # Internal: Maps digest bytesize to the digest class. DIGEST_SIZES = { 16 => Digest::MD5, 20 => Digest::SHA1, 32 => Digest::SHA256, 48 => Digest::SHA384, 64 => Digest::SHA512 } # Internal: Detect digest class hash algorithm for digest bytes. # # While not elegant, all the supported digests have a unique bytesize. # # Returns Digest::Base or nil. def detect_digest_class(bytes) DIGEST_SIZES[bytes.bytesize] end # Internal: Generate a hexdigest for a nested JSON serializable object. # # This is used for generating cache keys, so its pretty important its # wicked fast. Microbenchmarks away! # # obj - A JSON serializable object. # # Returns a String digest of the object. def digest(obj) digest = digest_class.new queue = [obj] while queue.length > 0 obj = queue.shift klass = obj.class if klass == String digest << obj elsif klass == Symbol digest << 'Symbol' digest << obj.to_s elsif klass == Fixnum digest << 'Fixnum' digest << obj.to_s elsif klass == Bignum digest << 'Bignum' digest << obj.to_s elsif klass == TrueClass digest << 'TrueClass' elsif klass == FalseClass digest << 'FalseClass' elsif klass == NilClass digest << 'NilClass' elsif klass == Array digest << 'Array' queue.concat(obj) elsif klass == Hash digest << 'Hash' queue.concat(obj.sort) elsif klass == Set digest << 'Set' queue.concat(obj.to_a) elsif klass == Encoding digest << 'Encoding' digest << obj.name else raise TypeError, "couldn't digest #{klass}" end end digest.digest end # Internal: Pack a binary digest to a hex encoded string. # # bin - String bytes # # Returns hex String. def pack_hexdigest(bin) bin.unpack('H*').first end # Internal: Pack a binary digest to a base64 encoded string. # # bin - String bytes # # Returns base64 String. def pack_base64digest(bin) [bin].pack('m0') end # Internal: Pack a binary digest to a urlsafe base64 encoded string. # # bin - String bytes # # Returns urlsafe base64 String. def pack_urlsafe_base64digest(bin) str = pack_base64digest(bin) str.tr!('+/'.freeze, '-_'.freeze) str.tr!('='.freeze, ''.freeze) str end # Internal: Maps digest class to the named information hash algorithm name. # # http://www.iana.org/assignments/named-information/named-information.xhtml NI_HASH_ALGORITHMS = { Digest::SHA256 => 'sha-256'.freeze, Digest::SHA384 => 'sha-384'.freeze, Digest::SHA512 => 'sha-512'.freeze } # Internal: Generate a "named information" URI for use in the `integrity` # attribute of an asset tag as per the subresource integrity specification. # # digest - The String byte digest of the asset content. # content_type - The content-type the asset will be served with. This *must* # be accurate if provided. Otherwise, subresource integrity # will block the loading of the asset. # # Returns a String or nil if hash algorithm is incompatible. def integrity_uri(digest, content_type = nil) case digest when Digest::Base digest_class = digest.class digest = digest.digest when String digest_class = DIGEST_SIZES[digest.bytesize] else raise TypeError, "unknown digest: #{digest.inspect}" end if hash_name = NI_HASH_ALGORITHMS[digest_class] uri = "ni:///#{hash_name};#{pack_urlsafe_base64digest(digest)}" uri << "?ct=#{content_type}" if content_type uri end end end end