require "base64" # `Digest` is the base type of hashing algorithms like `Digest::MD5`, # `Digest::SHA1`, `Digest::SHA256`, or `Digest::SHA512`. # # A `Digest` instance holds the state of an ongoing hash calculation. # It can receive new data to include in the hash via `#update`, `#<<`, or `#file`. # Once all data is included, use `#final` or `#hexfinal` to get the hash. This will mark the # ongoing calculation as finished. A finished calculation can't receive new data. # # A `digest.dup.final` call may be used to get an intermediate hash value. # # Use `#reset` to reuse the `Digest` instance for a new calculation. abstract class Digest class FinalizedError < Exception end # The `Digest::ClassMethods` module is used in the concrete subclass of `Digest` # that does not require arguments in its construction. # # The modules adds convenient class methods as `Digest::MD5.digest`, `Digest::MD5.hexdigest`. module ClassMethods # Returns the hash of *data*. *data* must respond to `#to_slice`. def digest(data) : Bytes digest do |ctx| ctx.update(data.to_slice) end end # Yields an instance of `self` which can receive calls to `#update(data : String | Bytes)` # and returns the finalized digest afterwards. # # ``` # require "digest/md5" # # digest = Digest::MD5.digest do |ctx| # ctx.update "f" # ctx.update "oo" # end # digest.hexstring # => "acbd18db4cc2f85cedef654fccc4a4d8" # ``` def digest(& : self ->) : Bytes context = new yield context context.final end # Returns the hexadecimal representation of the hash of *data*. # # ``` # require "digest/md5" # # Digest::MD5.hexdigest("foo") # => "acbd18db4cc2f85cedef654fccc4a4d8" # ``` def hexdigest(data) : String hexdigest &.update(data) end # Yields a context object with an `#update(data : String | Bytes)` # method available. Returns the resulting digest in hexadecimal representation # afterwards. # # ``` # require "digest/md5" # # Digest::MD5.hexdigest("foo") # => "acbd18db4cc2f85cedef654fccc4a4d8" # Digest::MD5.hexdigest do |ctx| # ctx.update "f" # ctx.update "oo" # end # # => "acbd18db4cc2f85cedef654fccc4a4d8" # ``` def hexdigest(& : self ->) : String hashsum = digest do |ctx| yield ctx end hashsum.to_slice.hexstring end # Returns the base64-encoded hash of *data*. # # ``` # require "digest/sha1" # # Digest::SHA1.base64digest("foo") # => "C+7Hteo/D9vJXQ3UfzxbwnXaijM=" # ``` def base64digest(data) : String base64digest &.update(data) end # Yields a context object with an `#update(data : String | Bytes)` # method available. Returns the resulting digest in base64 representation # afterwards. # # ``` # require "digest/sha1" # # Digest::SHA1.base64digest do |ctx| # ctx.update "f" # ctx.update "oo" # end # # => "C+7Hteo/D9vJXQ3UfzxbwnXaijM=" # ``` def base64digest(& : self -> _) : String hashsum = digest do |ctx| yield ctx end Base64.strict_encode(hashsum) end end @finished = false def update(data) : self update data.to_slice end def update(data : Bytes) : self check_finished update_impl data self end # Returns the final digest output. # # `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls. # # NOTE: `.dup.final` call may be used to get an intermediate hash value. def final : Bytes dst = Bytes.new digest_size final dst end # Puts the final digest output into `dst`. # # Faster than the `Bytes` allocating version. # Use when hashing in loops. # # `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls. # # NOTE: `.dup.final(dst)` call may be used to get an intermediate hash value. def final(dst : Bytes) : Bytes check_finished @finished = true final_impl dst dst end # Returns a hexadecimal-encoded digest in a new `String`. # # `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls. # # NOTE: `.dup.hexfinal` call may be used to get an intermediate hash value. def hexfinal : String dsize = digest_size string_size = dsize * 2 String.new(string_size) do |buffer| tmp = Slice.new(buffer + dsize, dsize) final tmp tmp.hexstring buffer {string_size, string_size} end end # Writes a hexadecimal-encoded digest to `dst`. # # Faster than the `String` allocating version. # Use when hashing in loops. # # `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls. # # NOTE: `.dup.final` call may be used to get an intermediate hash value. def hexfinal(dst : Bytes) : Nil dsize = digest_size unless dst.bytesize == dsize * 2 raise ArgumentError.new("Incorrect dst size: #{dst.bytesize}, expected: #{dsize * 2}") end tmp = dst[dsize, dsize] final tmp tmp.hexstring dst end # Writes a hexadecimal-encoded digest to `IO`. # # Faster than the `String` allocating version. # # `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls. # # NOTE: `.dup.final` call may be used to get an intermediate hash value. # # This method is restricted to a maximum digest size of 64 bytes. Implementations that allow # a larger digest size should override this method to use a larger buffer. def hexfinal(io : IO) : Nil if digest_size > 64 raise "Digest#hexfinal(IO) can't handle digest_size over 64 bytes" end sary = uninitialized StaticArray(UInt8, 128) tmp = sary.to_slice[0, digest_size * 2] hexfinal tmp io << tmp end # Resets the state of this object. Use to get another hash value without creating a new object. def reset : self reset_impl @finished = false self end # Reads the file's content and updates the digest with it. def file(file_name : Path | String) : self File.open(file_name) do |io| # `#update` works with big buffers so there's no need for additional read buffering in the file io.read_buffering = false self << io end end # Reads the io's data and updates the digest with it. def update(io : IO) : self buffer = uninitialized UInt8[IO::DEFAULT_BUFFER_SIZE] while (read_bytes = io.read(buffer.to_slice)) > 0 self << buffer.to_slice[0, read_bytes] end self end # :ditto: def <<(data) : self update(data) end private def check_finished : Nil raise FinalizedError.new("finish already called") if @finished end # Hashes data incrementally. abstract def update_impl(data : Bytes) : Nil # Stores the output digest of #digest_size bytes in dst. abstract def final_impl(dst : Bytes) : Nil # Resets the object to it's initial state. abstract def reset_impl : Nil # Returns the digest output size in bytes. abstract def digest_size : Int32 end