Class: Downloadable::VerificationCache Private

Inherits:
Object
  • Object
show all
Includes:
Context, Utils::Output::Mixin
Defined in:
downloadable.rb

Overview

This class is part of a private API. This class may only be used in the Homebrew/brew repository. Third parties should avoid using this class if possible, as it may be removed or changed without warning.

Remembers the SHA-256 digest of each file hashed in this process, keyed by its resolved path, size and modification time, so a file is hashed at most once per on-disk state no matter how many download objects or verifications reference it.

Defined Under Namespace

Classes: RepeatedHashingError

Constant Summary collapse

CACHE_MEDIATED_HASHING_KEY =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

:homebrew_verification_cache_mediated_hashing

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils::Output::Mixin

#issue_reporting_message, #odebug, #odeprecated, #odie, #odisabled, #ofail, #oh1, #oh1_title, #ohai, #ohai_title, #onoe, #opoo, #opoo_outside_github_actions, #opoo_without_github_actions_annotation, #pretty_cannot_install, #pretty_deprecated, #pretty_disabled, #pretty_duration, #pretty_install_status, #pretty_installed, #pretty_uninstalled, #pretty_unmarked, #pretty_upgradable, #pretty_warning

Methods included from Context

current, current=, #debug?, #deferred_environment_expansion?, #quiet?, #verbose?, #with_context

Constructor Details

#initializevoid

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.



82
83
84
85
86
# File 'downloadable.rb', line 82

def initialize
  require "concurrent/map"

  @digests = T.let(Concurrent::Map.new, Concurrent::Map)
end

Class Method Details

.check_repeated_hashing(filename) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Guards against repeated verification creeping in without this cache: every Pathname#sha256 call reports here and, in brew commands run by integration tests only, rehashing an unchanged file outside the cache raises. Hashing is cheap there while a repeat in real use would rehash a download that can be hundreds of megabytes.

Parameters:

Raises:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'downloadable.rb', line 53

def check_repeated_hashing(filename)
  return if ENV["HOMEBREW_CHECK_REPEATED_HASHING"].blank?

  state = on_disk_state(filename)
  return if state.nil?

  require "concurrent/set"
  @hashed_states ||= T.let(Concurrent::Set.new, T.nilable(Concurrent::Set))
  # `add?` atomically records the state and reports whether it was new.
  return unless @hashed_states.add?(state).nil?
  return if Thread.current[CACHE_MEDIATED_HASHING_KEY]

  raise RepeatedHashingError, <<~ERROR
    Refusing to hash '#{filename}' again: its unchanged contents were already hashed in this process.
    Verify downloads through `Downloadable#verify_download_integrity` so its digest cache reuses the existing hash.
    This check only runs when `$HOMEBREW_CHECK_REPEATED_HASHING` is set, e.g. in Homebrew's integration tests.
  ERROR
end

.on_disk_state(filename) ⇒ String?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

The identity and change metadata that determine whether a file can be considered unchanged on disk: the device, inode, size and nanosecond modification and change times ensure a replaced or rewritten file is treated as new, even when its size and modification time are restored, as the change time cannot be set from userland.

Parameters:

Returns:



39
40
41
42
43
44
45
# File 'downloadable.rb', line 39

def on_disk_state(filename)
  stat = filename.stat
  "#{stat.dev}|#{stat.ino}|#{stat.size}|" \
    "#{stat.mtime.to_i}.#{stat.mtime.nsec}|#{stat.ctime.to_i}.#{stat.ctime.nsec}"
rescue SystemCallError
  nil
end

.while_hashing_through_cache(&_block) ⇒ T.type_parameter(:U)

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

  • _block (T.proc.returns(T.type_parameter(:U)))

Returns:

  • (T.type_parameter(:U))


73
74
75
76
77
78
# File 'downloadable.rb', line 73

def while_hashing_through_cache(&_block)
  Thread.current[CACHE_MEDIATED_HASHING_KEY] = true
  yield
ensure
  Thread.current[CACHE_MEDIATED_HASHING_KEY] = nil
end

Instance Method Details

#invalidate!(filename) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Forgets the remembered digest for the file's current on-disk state, for callers that suspect its contents changed without the metadata that keys this cache changing, e.g. in-place corruption suggested by a failed extraction.

Parameters:



123
124
125
126
# File 'downloadable.rb', line 123

def invalidate!(filename)
  key = key_for(filename)
  @digests.delete(key) if key
end

#sha256(filename) ⇒ String

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

The file's SHA-256 digest, hashing its contents at most once per on-disk state in this process.

Parameters:

Returns:



104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'downloadable.rb', line 104

def sha256(filename)
  key = key_for(filename)
  if key && (digest = @digests[key])
    odebug "Skipping SHA-256 hashing for '#{filename.basename}' (unchanged since last hashed in this run)"
    return digest
  end

  digest = self.class.while_hashing_through_cache { filename.sha256 }
  # Only remember the digest when the file did not change while its
  # contents were being hashed.
  @digests[key] = digest if key && key == key_for(filename)
  digest
end

#verify(filename, checksum) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Verifies the file against the checksum. Repeated verifications of a file unchanged on disk only compare against its remembered digest.

Parameters:

Raises:



91
92
93
94
95
96
97
98
99
# File 'downloadable.rb', line 91

def verify(filename, checksum)
  raise ChecksumMissingError if checksum.blank?

  ohai "Verifying checksum for '#{filename.basename}'" if verbose?
  actual = Checksum.new(sha256(filename))
  return if checksum == actual

  raise ChecksumMismatchError.new(filename, checksum, actual)
end