1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/lib/rubygems/dependency_resolver.rb
drbrain 8eb3918581 * lib/rubygems: Fix CVE-2013-4363. Miscellaneous minor improvements.
* test/rubygems:  Tests for the above.


git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@43039 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2013-09-25 00:53:19 +00:00

310 lines
8.7 KiB
Ruby

require 'rubygems'
require 'rubygems/dependency'
require 'rubygems/exceptions'
require 'rubygems/util/list'
require 'uri'
require 'net/http'
##
# Given a set of Gem::Dependency objects as +needed+ and a way to query the
# set of available specs via +set+, calculates a set of ActivationRequest
# objects which indicate all the specs that should be activated to meet the
# all the requirements.
class Gem::DependencyResolver
##
# Contains all the conflicts encountered while doing resolution
attr_reader :conflicts
attr_accessor :development
attr_reader :missing
##
# When a missing dependency, don't stop. Just go on and record what was
# missing.
attr_accessor :soft_missing
def self.compose_sets *sets
Gem::DependencyResolver::ComposedSet.new(*sets)
end
##
# Provide a DependencyResolver that queries only against the already
# installed gems.
def self.for_current_gems needed
new needed, Gem::DependencyResolver::CurrentSet.new
end
##
# Create DependencyResolver object which will resolve the tree starting
# with +needed+ Depedency objects.
#
# +set+ is an object that provides where to look for specifications to
# satisify the Dependencies. This defaults to IndexSet, which will query
# rubygems.org.
def initialize needed, set = nil
@set = set || Gem::DependencyResolver::IndexSet.new
@needed = needed
@conflicts = nil
@development = false
@missing = []
@soft_missing = false
end
def requests s, act, reqs=nil
s.dependencies.reverse_each do |d|
next if d.type == :development and not @development
reqs = Gem::List.new Gem::DependencyResolver::DependencyRequest.new(d, act), reqs
end
@set.prefetch reqs
reqs
end
##
# Proceed with resolution! Returns an array of ActivationRequest objects.
def resolve
@conflicts = []
needed = nil
@needed.reverse_each do |n|
request = Gem::DependencyResolver::DependencyRequest.new n, nil
needed = Gem::List.new request, needed
end
res = resolve_for needed, nil
raise Gem::DependencyResolutionError, res if
res.kind_of? Gem::DependencyResolver::DependencyConflict
res.to_a
end
##
# Finds the State in +states+ that matches the +conflict+ so that we can try
# other possible sets.
def find_conflict_state conflict, states # :nodoc:
until states.empty? do
if conflict.for_spec? states.last.spec
state = states.last
state.conflicts << [state.spec, conflict]
return state
else
states.pop
end
end
nil
end
##
# Extracts the specifications that may be able to fulfill +dependency+
def find_possible dependency # :nodoc:
possible = @set.find_all dependency
select_local_platforms possible
end
def handle_conflict(dep, existing)
# There is a conflict! We return the conflict
# object which will be seen by the caller and be
# handled at the right level.
# If the existing activation indicates that there
# are other possibles for it, then issue the conflict
# on the dep for the activation itself. Otherwise, issue
# it on the requester's request itself.
#
if existing.others_possible?
conflict =
Gem::DependencyResolver::DependencyConflict.new dep, existing
else
depreq = existing.request.requester.request
conflict =
Gem::DependencyResolver::DependencyConflict.new depreq, existing, dep
end
@conflicts << conflict
return conflict
end
# Contains the state for attempting activation of a set of possible specs.
# +needed+ is a Gem::List of DependencyRequest objects that, well, need
# to be satisfied.
# +specs+ is the List of ActivationRequest that are being tested.
# +dep+ is the DepedencyRequest that was used to generate this state.
# +spec+ is the Specification for this state.
# +possible+ is List of DependencyRequest objects that can be tried to
# find a complete set.
# +conflicts+ is a [DependencyRequest, DependencyConflict] hit tried to
# activate the state.
#
State = Struct.new(:needed, :specs, :dep, :spec, :possibles, :conflicts)
##
# The meat of the algorithm. Given +needed+ DependencyRequest objects and
# +specs+ being a list to ActivationRequest, calculate a new list of
# ActivationRequest objects.
def resolve_for needed, specs
# The State objects that are used to attempt the activation tree.
states = []
while needed
dep = needed.value
needed = needed.tail
# If there is already a spec activated for the requested name...
if specs && existing = specs.find { |s| dep.name == s.name }
# then we're done since this new dep matches the existing spec.
next if dep.matches_spec? existing
conflict = handle_conflict dep, existing
state = find_conflict_state conflict, states
return conflict unless state
needed, specs = resolve_for_conflict needed, specs, state
next
end
possible = find_possible dep
case possible.size
when 0
resolve_for_zero dep
when 1
needed, specs =
resolve_for_single needed, specs, dep, possible
else
needed, specs =
resolve_for_multiple needed, specs, states, dep, possible
end
end
specs
end
##
# Rewinds +needed+ and +specs+ to a previous state in +state+ for a conflict
# between +dep+ and +existing+.
def resolve_for_conflict needed, specs, state # :nodoc:
# We exhausted the possibles so it's definitely not going to work out,
# bail out.
raise Gem::ImpossibleDependenciesError.new state.dep, state.conflicts if
state.possibles.empty?
spec = state.possibles.pop
# Retry resolution with this spec and add it's dependencies
act = Gem::DependencyResolver::ActivationRequest.new spec, state.dep
needed = requests spec, act, state.needed
specs = Gem::List.prepend state.specs, act
return needed, specs
end
##
# There are multiple +possible+ specifications for this +dep+. Updates
# +needed+, +specs+ and +states+ for further resolution of the +possible+
# choices.
def resolve_for_multiple needed, specs, states, dep, possible # :nodoc:
# Sort them so that we try the highest versions first.
possible = possible.sort_by do |s|
[s.source, s.version, s.platform == Gem::Platform::RUBY ? -1 : 1]
end
# To figure out which to pick, we keep resolving given each one being
# activated and if there isn't a conflict, we know we've found a full set.
#
# We use an until loop rather than reverse_each to keep the stack short
# since we're using a recursive algorithm.
spec = possible.pop
# We may need to try all of +possible+, so we setup state to unwind back
# to current +needed+ and +specs+ so we can try another. This is code is
# what makes conflict resolution possible.
act = Gem::DependencyResolver::ActivationRequest.new spec, dep
states << State.new(needed, specs, dep, spec, possible, [])
needed = requests spec, act, needed
specs = Gem::List.prepend specs, act
return needed, specs
end
##
# Add the spec from the +possible+ list to +specs+ and process the spec's
# dependencies by adding them to +needed+.
def resolve_for_single needed, specs, dep, possible # :nodoc:
spec = possible.first
act = Gem::DependencyResolver::ActivationRequest.new spec, dep, false
specs = Gem::List.prepend specs, act
# Put the deps for at the beginning of needed
# rather than the end to match the depth first
# searching done by the multiple case code below.
#
# This keeps the error messages consistent.
needed = requests spec, act, needed
return needed, specs
end
##
# When there are no possible specifications for +dep+ our work is done.
def resolve_for_zero dep # :nodoc:
@missing << dep
unless @soft_missing
raise Gem::UnsatisfiableDependencyError, dep
end
end
##
# Returns the gems in +specs+ that match the local platform.
def select_local_platforms specs # :nodoc:
specs.select do |spec|
Gem::Platform.match spec.platform
end
end
end
require 'rubygems/dependency_resolver/api_set'
require 'rubygems/dependency_resolver/api_specification'
require 'rubygems/dependency_resolver/activation_request'
require 'rubygems/dependency_resolver/composed_set'
require 'rubygems/dependency_resolver/current_set'
require 'rubygems/dependency_resolver/dependency_conflict'
require 'rubygems/dependency_resolver/dependency_request'
require 'rubygems/dependency_resolver/index_set'
require 'rubygems/dependency_resolver/index_specification'
require 'rubygems/dependency_resolver/installed_specification'
require 'rubygems/dependency_resolver/installer_set'