valeth/digest-ed2k-hash.rb

View on GitHub
lib/digest/ed2k.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true

require 'digest'
require 'openssl'

module Digest # :nodoc:
    # The ED2K hashing algorithm.
    class ED2K < Digest::Class
        # The ED2K chunk size (9500KB).
        CHUNK_SIZE = 9_728_000

        # Shortcut to the OpenSSL MD4 class.
        MD4 = OpenSSL::Digest::MD4

        # Create a new ED2K hash object.
        #
        # @return  self  to allow method chaining
        def initialize
            @md4 = MD4.new
            reset
        end

        # Hash an IO object.
        #
        # @param   [IO] io  the IO object that will be read
        # @return  self  to allow method chaining
        def io(io)
            self << io
        end

        # Calculate the hash of a file.
        #
        # @param   [String] path  the file that will be read
        # @return  self  to allow method chaining
        def file(path)
            io(File.open(path))
        end

        # Reset to the initial state.
        #
        # @return  self  the reset digest object
        def reset
            @md4.reset
            @finalized = false

            self
        end

        # Rehash with new data.
        #
        # @param  [String, IO] data  the chunk of data to add to the hash
        # @return  self  to allow method chaining
        # @raise  RuntimeError  if the digest object has been finalized
        def update(data)
            raise RuntimeError if @finalized

            # get the IO object
            buf = to_io(data)

            # if the chunk is smaller than CHUNK_SIZE just return the MD4 hash
            if buf.size < CHUNK_SIZE
                @md4 << buf.read
            else
                # read chunks from the IO object and update the MD4 hash
                while (chunk = buf.read(CHUNK_SIZE))
                    @md4 << MD4.digest(chunk)
                end

                # weird EDonkey bug requires multiples of CHUNK_SIZE
                # to append one additional MD4 hash
                @md4 << MD4.new.digest if multiple?(buf.size)
            end

            self
        end

        alias << update

        # Finalize the hash and return the digest.
        #
        # If no string is provided, the current hash is used
        #
        # @param  [String, IO] data  hash this chunk of data
        def digest(data = nil)
            if data.nil?
                finish
                @md4.digest
            else
                reset
                self << data
                digest
            end
        end

        # Finalize the hash and return the hexdigest.
        #
        # If no string is provided, the current hash is used
        #
        # @param  [String, IO] data  hash this chunk of data
        def hexdigest(data = nil)
            if data.nil?
                finish
                @md4.hexdigest
            else
                reset
                self << data
                hexdigest
            end
        end

        # Finalize the hash.
        #
        # @return  self  to allow method chaining
        def finish
            @finalized = true unless @finalized

            self
        end

        # Shows the current state and the digest class.
        # If the digest is finalized, it shows the hexdigest.
        def inspect
            dig = @finalized ? hexdigest : 'unfinalized'
            "#<#{self.class.name}: #{dig}>"
        end

        class << self
            # Calculate the digest of a string.
            #
            # @param  [String, IO] data  the string to digest
            # @return a finalized digest object
            def digest(data)
                new.digest(data)
            end

            # Calculate the hexdigest of a string.
            #
            # @param  [String, IO] data  the string to digest
            # @return a finalized digest object
            def hexdigest(data)
                new.hexdigest(data)
            end

            # Create a new digest object from a IO object.
            #
            # @param  [IO] io  the IO object to read
            # @return a new digest object
            def io(io)
                new.io(io)
            end

            # Create a new digest object from a file.
            #
            # @param  [String] path  the file to read
            # @return a new digest object
            def file(path)
                new.file(path)
            end
        end

        private

        def to_io(obj)
            if obj.is_a? String
                StringIO.new(obj)
            elsif obj.is_a? IO
                obj
            else
                raise ArgumentError, "cannot hash #{obj.class.name} object"
            end
        end

        def multiple?(buf_size)
            (buf_size % CHUNK_SIZE).zero?
        end
    end
end