Skip to content

Commit 5e6440b

Browse files
committed
Handle case where a requirement conflicts with required rubygems version
1 parent 744939c commit 5e6440b

3 files changed

Lines changed: 80 additions & 11 deletions

File tree

app/models/version.rb

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -504,22 +504,45 @@ def self.platformed?(platform)
504504
private
505505

506506
def content_addressable_required_rubygems_version
507-
return if meets_content_addressable_rubygems_floor?
507+
floor = Gem::Requirement.new(CONTENT_ADDRESSABLE_REQUIRED_RUBYGEMS_VERSION).requirements.first.last
508+
requirement = begin
509+
Gem::Requirement.new(required_rubygems_version.presence&.split(/\s*,\s*/) || [">= 0"])
510+
rescue Gem::Requirement::BadRequirementError
511+
nil
512+
end
508513

509-
errors.add(:required_rubygems_version,
510-
"must be #{CONTENT_ADDRESSABLE_REQUIRED_RUBYGEMS_VERSION} for content-addressable gems (set required_rubygems_version in the gemspec)")
514+
if requirement.nil? || !meets_version_floor?(requirement, floor)
515+
errors.add(:required_rubygems_version,
516+
"must be #{CONTENT_ADDRESSABLE_REQUIRED_RUBYGEMS_VERSION} for content-addressable gems " \
517+
"(set required_rubygems_version in the gemspec)")
518+
elsif conflicts_content_addressable_rubygems_floor?(requirement, floor)
519+
errors.add(:required_rubygems_version,
520+
"must not conflict with RubyGems #{CONTENT_ADDRESSABLE_REQUIRED_RUBYGEMS_VERSION} required to install content-addressable gems " \
521+
"(remove or loosen the conflicting constraint in the gemspec)")
522+
end
511523
end
512524

513-
def meets_content_addressable_rubygems_floor?
514-
floor = Gem::Requirement.new(CONTENT_ADDRESSABLE_REQUIRED_RUBYGEMS_VERSION)
515-
requirements = required_rubygems_version.presence&.split(/\s*,\s*/) || [">= 0"]
516-
requirement = Gem::Requirement.new(requirements)
525+
def meets_version_floor?(requirement, floor)
526+
requirement.requirements.any? do |operator, version|
527+
[">=", "~>", "=", ">"].include?(operator) && version >= floor
528+
end
529+
end
517530

518-
requirement.requirements.any? do |operator, required_version|
519-
[">=", "~>", "=", ">"].include?(operator) && floor.satisfied_by?(required_version)
531+
def conflicts_version_floor?(requirement, floor)
532+
conflict = requirement.requirements.any? do |operator, version|
533+
case operator
534+
when "<" then version <= floor
535+
when "<=", "=" then version < floor
536+
when "~>" then version.bump <= floor.release
537+
else false
538+
end
539+
end
540+
541+
return true if conflict
542+
543+
!requirement.satisfied_by?(floor) && requirement.requirements.any? do |operator, version|
544+
["<=", "="].include?(operator) && version == floor
520545
end
521-
rescue Gem::Requirement::BadRequirementError
522-
false
523546
end
524547

525548
def update_prerelease

test/integration/pusher_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,22 @@ def two_cert_chain(signing_key:, root_not_before: Time.current, cert_not_before:
543543
"must be #{Version::CONTENT_ADDRESSABLE_REQUIRED_RUBYGEMS_VERSION} " \
544544
"for content-addressable gems (set required_rubygems_version in the gemspec)"
545545
end
546+
547+
should "reject with 403 when required_rubygems_version conflicts with the floor" do
548+
FeatureFlag.enable_for_actor(FeatureFlag::CONTENT_ADDRESSABLE_GEM_PUSHES, @user)
549+
spec = new_gemspec("ca-floor-conflict", "1.0.0", "CA floor test", "arm64-darwin-25",
550+
ruby_version: "~> 3.4.0",
551+
rubygems_version: [">= 4.2", "< 4.1.0.a"])
552+
cutter = Pusher.new(@api_key, build_gem(spec))
553+
cutter.logger.level = :info
554+
555+
assert cutter.pull_spec
556+
assert cutter.find
557+
refute cutter.validate
558+
559+
assert_equal 403, cutter.code
560+
assert_includes cutter.message, "must not conflict with RubyGems #{Version::CONTENT_ADDRESSABLE_REQUIRED_RUBYGEMS_VERSION}"
561+
end
546562
end
547563

548564
context "pushing a new version" do

test/models/version_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,36 @@ class VersionTest < ActiveSupport::TestCase
929929
assert_predicate @content_addressable_version, :valid?
930930
end
931931

932+
should "be invalid when an upper bound excludes the floor despite a satisfying lower bound" do
933+
@content_addressable_version.required_rubygems_version = ">= 4.2, < 4.1.0.a"
934+
935+
refute_predicate @content_addressable_version, :valid?
936+
end
937+
938+
should "be invalid when a <= upper bound excludes the floor despite a satisfying lower bound" do
939+
@content_addressable_version.required_rubygems_version = ">= 4.2, <= 4.0"
940+
941+
refute_predicate @content_addressable_version, :valid?
942+
end
943+
944+
should "be invalid when a ~> constraint caps the requirement below the floor" do
945+
@content_addressable_version.required_rubygems_version = ">= 4.2, ~> 4.0.0"
946+
947+
refute_predicate @content_addressable_version, :valid?
948+
end
949+
950+
should "be invalid when the only version satisfying the floor is excluded" do
951+
@content_addressable_version.required_rubygems_version = ">= 4.1.0.a, <= 4.1.0.a, != 4.1.0.a"
952+
953+
refute_predicate @content_addressable_version, :valid?
954+
end
955+
956+
should "be valid when an upper bound still allows versions satisfying the floor" do
957+
@content_addressable_version.required_rubygems_version = ">= 4.1.0.a, < 5"
958+
959+
assert_predicate @content_addressable_version, :valid?
960+
end
961+
932962
should "be valid when = operator value satisfies the floor" do
933963
@content_addressable_version.required_rubygems_version = "= 4.2"
934964

0 commit comments

Comments
 (0)