Module: Philiprehberger::Etag::Generator

Defined in:
lib/philiprehberger/etag/generator.rb

Overview

Generates strong and weak ETags from content using cryptographic hashes.

Constant Summary collapse

ALGORITHMS =
{
  sha256: Digest::SHA256,
  sha512: Digest::SHA512,
  md5: Digest::MD5,
  sha1: Digest::SHA1
}.freeze
OPENSSL_ALGORITHMS =

Algorithms that delegate to OpenSSL::Digest rather than the stdlib ‘digest/*` classes.

{ sha3_256: 'SHA3-256' }.freeze

Class Method Summary collapse

Class Method Details

.for_file(path, algorithm: :sha256) ⇒ String

Generates a strong ETag for a file based on its mtime and size. Does not read file content.

Parameters:

  • path (String)

    the file path

  • algorithm (Symbol) (defaults to: :sha256)

    the hash algorithm (:sha256, :sha512, :md5, :sha1, :sha3_256)

Returns:

  • (String)

    a quoted ETag string

Raises:

  • (Errno::ENOENT)

    if the file does not exist

  • (ArgumentError)

    if the algorithm is not supported



48
49
50
51
52
53
# File 'lib/philiprehberger/etag/generator.rb', line 48

def self.for_file(path, algorithm: :sha256)
  stat = File.stat(path)
  fingerprint = "#{stat.mtime.to_i}-#{stat.size}"
  digest = compute_digest(fingerprint, algorithm)
  %("#{digest}")
end

.for_io(io, algorithm: :sha256, chunk_size: 65_536) ⇒ String

Generates a strong ETag from an IO stream by reading it in chunks. Unlike strong, the entire payload is never materialised in memory at once, making this suitable for hashing large files and rack response bodies.

The IO is read from its current position to EOF. Callers that need to consume the IO afterwards should ‘rewind` it first.

Parameters:

  • io (IO, #read)

    an IO or IO-like object responding to ‘read(n)`

  • algorithm (Symbol) (defaults to: :sha256)

    the hash algorithm (:sha256, :sha512, :md5, :sha1, :sha3_256)

  • chunk_size (Integer) (defaults to: 65_536)

    bytes to read per iteration (default: 65_536)

Returns:

  • (String)

    a quoted ETag string

Raises:

  • (ArgumentError)

    if the algorithm is not supported, or chunk_size <= 0



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/philiprehberger/etag/generator.rb', line 67

def self.for_io(io, algorithm: :sha256, chunk_size: 65_536)
  raise ArgumentError, 'chunk_size must be positive' unless chunk_size.positive?

  digest = digest_for(algorithm)
  while (chunk = io.read(chunk_size))
    break if chunk.empty?

    digest.update(chunk)
  end
  %("#{digest.hexdigest}")
end

.strong(content, algorithm: :sha256) ⇒ String

Generates a strong ETag from content using the specified algorithm.

Parameters:

  • content (String)

    the content to hash

  • algorithm (Symbol) (defaults to: :sha256)

    the hash algorithm (:sha256, :sha512, :md5, :sha1, :sha3_256)

Returns:

  • (String)

    a quoted ETag string, e.g. ‘“"a1b2c3…"”`

Raises:

  • (ArgumentError)

    if the algorithm is not supported



26
27
28
29
# File 'lib/philiprehberger/etag/generator.rb', line 26

def self.strong(content, algorithm: :sha256)
  digest = compute_digest(content, algorithm)
  %("#{digest}")
end

.weak(content) ⇒ String

Generates a weak ETag from content using MD5.

Parameters:

  • content (String)

    the content to hash

Returns:

  • (String)

    a weak ETag string, e.g. ‘“W/"a1b2c3…"”`



35
36
37
38
# File 'lib/philiprehberger/etag/generator.rb', line 35

def self.weak(content)
  digest = Digest::MD5.hexdigest(content.to_s)
  %(W/"#{digest}")
end