123456789_123456789_123456789_123456789_123456789_

Module: Gem::ContentAddress

Relationships & Source Files
Defined in: lib/rubygems/content_address.rb

Overview

ContentAddress is the single home for content-addressing semantics: what an address and a Ruby ABI look like, which specs are eligible, how addresses are generated and verified against gem files.

Constant Summary

Class Method Summary

Class Method Details

.address_for(bytes, length: DEFAULT_LENGTH)

Generates the content address for bytes: the first length characters of the hexadecimal SHA256 digest of the contents.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 135

def self.address_for(bytes, length: DEFAULT_LENGTH)
  require "digest"
  Digest::SHA256.hexdigest(bytes)[0, length]
end

.content_addressed?(spec, validate_ruby_abi: true) ⇒ Boolean

Whether spec is content-addressed: it is eligible for content addressing and has a valid content address set. See eligible? for when to pass validate_ruby_abi: false.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 98

def self.content_addressed?(spec, validate_ruby_abi: true)
  eligible?(spec, validate_ruby_abi: validate_ruby_abi) && match?(spec.content_address)
end

.content_addressed_row?(suffix, platform, required_ruby_version = nil, validate_ruby_abi: true) ⇒ Boolean

Whether an index row describes a content-addressed gem: an address-shaped suffix, a pinned platform, and a required_ruby_version pinning a single Ruby ABI. Rows missing any of these must not assign a content address, so specs cannot be constructed half content-addressed. See eligible? for when to pass validate_ruby_abi: false.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 110

def self.content_addressed_row?(suffix, platform, required_ruby_version = nil, validate_ruby_abi: true)
  return false unless match?(suffix) && platform_eligible?(platform)
  return true unless validate_ruby_abi

  !ruby_abi_for(required_ruby_version).nil?
end

.eligible?(spec, validate_ruby_abi: true) ⇒ Boolean

Whether spec is eligible for content addressing. A gem must pin its required_ruby_version to a single Ruby ABI ("~> X.Y.0") and declare a non-RUBY platform to be content addressed. This makes content_addressed? implies ruby_abi present structural: no spec can count as content addressed without an ABI to scope it by.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 86

def self.eligible?(spec, validate_ruby_abi: true)
  return false unless platform_eligible?(spec.platform)
  return true unless validate_ruby_abi

  !ruby_abi_for(spec.required_ruby_version).nil?
end

.file_name_claim(filename, spec)

The content address claimed by a gem file name, or nil when the name makes no claim. filename is the file's base name without the ".gem" extension ("name-version"). A name claims an address when its suffix is address-shaped and is not just spec's own platform: a platform string that happens to look like hexadecimal (both normalized and original spellings) is a platform name, not a claim.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 148

def self.file_name_claim(filename, spec)
  suffix = filename.delete_prefix("#{spec.name}-#{spec.version}-")
  return nil if suffix == filename
  return nil unless match?(suffix)
  return nil if [spec.platform.to_s, spec.original_platform.to_s].include?(suffix)

  suffix
end

.match?(value) ⇒ Boolean

Whether value is a valid content address (a string of 8-64 lowercase hexadecimal characters). This only checks the shape of a string: use content_addressed? to ask whether a spec is actually content addressed, and file_name_claim to ask whether a file name claims an address.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 33

def self.match?(value)
  value.is_a?(String) && PATTERN.match?(value)
end

.platform_eligible?(platform) ⇒ Boolean

Whether platform is eligible for content addressing: present and not the generic RUBY platform.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 75

def self.platform_eligible?(platform)
  !platform.nil? && platform != Gem::Platform::RUBY
end

.ruby_abi_compatible?(spec, ruby_abi) ⇒ Boolean

Whether spec's required_ruby_version permits building for Gem.ruby_abi: an unset or default requirement can still be pinned to the ABI, and anything else must already pin exactly that ABI. Used at build time, before the requirement is injected, where eligible? would be premature.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 124

def self.ruby_abi_compatible?(spec, ruby_abi)
  required_ruby_version = spec.required_ruby_version
  return true if required_ruby_version.nil? || required_ruby_version.none?

  ruby_abi_for(required_ruby_version) == ruby_abi
end

.ruby_abi_for(required_ruby_version)

Derives the Ruby ABI ("X.Y") from required_ruby_version. Only a single pessimistic requirement with three segments ending in zero ("~> X.Y.0") pins an ABI. Returns nil for any other shape.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 49

def self.ruby_abi_for(required_ruby_version)
  return nil if required_ruby_version.nil?

  requirements = required_ruby_version.requirements
  return nil if requirements.size != 1

  op, version = requirements.first
  segments = version.segments
  return nil if op != "~>" || segments.size != 3 || segments[2] != 0
  return nil unless segments[0].is_a?(Integer) && segments[1].is_a?(Integer)

  segments[0..1].join(".")
end

.ruby_abi_requirement(ruby_abi)

The required_ruby_version that pins Gem.ruby_abi ("X.Y" to "~> X.Y.0"). Inverse of .ruby_abi_for.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 67

def self.ruby_abi_requirement(ruby_abi)
  Gem::Requirement.new("~> #{ruby_abi}.0")
end

.ruby_abi_specificity_match(spec, ruby_version = Gem.ruby_version)

Ranks spec for candidate selection against Gem.ruby_version: a content-addressed spec built for that Ruby ranks first (0), any non-content-addressed spec next (1), and a content-addressed spec built for another Ruby last (2), since its binary cannot load there.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 185

def self.ruby_abi_specificity_match(spec, ruby_version = Gem.ruby_version)
  return 1 unless match?(spec.content_address)

  if spec.required_ruby_version.satisfied_by?(ruby_version)
    0
  else
    2
  end
end

.valid_ruby_abi?(value) ⇒ Boolean

Whether value is a well-formed Ruby ABI ("X.Y").

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 40

def self.valid_ruby_abi?(value)
  value.is_a?(String) && RUBY_ABI_PATTERN.match?(value)
end

.verified_file_name_claim(path, spec)

Verifies the content address claimed by the gem file at Gem.path against the SHA256 digest of its contents. Returns the verified address, or nil when the file name makes no claim. Raises InstallError when the contents do not match the claim, regardless of whether the packaged spec is eligible, so swapped contents cannot hide behind an ineligible specification.

[ GitHub ]

  
# File 'lib/rubygems/content_address.rb', line 165

def self.verified_file_name_claim(path, spec)
  basename = File.basename(path, ".gem")
  address = file_name_claim(basename, spec)
  return nil unless address

  require "digest"
  digest = Digest::SHA256.file(path).hexdigest
  unless digest.start_with?(address)
    raise Gem::InstallError, "content address mismatch for #{File.basename(path)}"
  end

  address
end