diff --git a/lib/bundler/compact_index_client/cache_file.rb b/lib/bundler/compact_index_client/cache_file.rb index 299d683438b8..03659aeac19e 100644 --- a/lib/bundler/compact_index_client/cache_file.rb +++ b/lib/bundler/compact_index_client/cache_file.rb @@ -103,7 +103,7 @@ def open(write_mode = "wb", perm = @perm, &block) # Returns false without appending when no digests since appending is too error prone to do without digests. def append(data) return false unless digests? - open("a") {|f| f.write data } + open("ab") {|f| f.write data } verify && commit end diff --git a/lib/rubygems/compact_index_client/cache_file.rb b/lib/rubygems/compact_index_client/cache_file.rb index 3656645d8220..00e5f541ddf6 100644 --- a/lib/rubygems/compact_index_client/cache_file.rb +++ b/lib/rubygems/compact_index_client/cache_file.rb @@ -99,7 +99,7 @@ def open(write_mode = "wb", perm = @perm, &block) # Returns false without appending when no digests since appending is too error prone to do without digests. def append(data) return false unless digests? - open("a") {|f| f.write data } + open("ab") {|f| f.write data } verify && commit end diff --git a/spec/bundler/compact_index_client/cache_file_spec.rb b/spec/bundler/compact_index_client/cache_file_spec.rb new file mode 100644 index 000000000000..c9a87c3c2d44 --- /dev/null +++ b/spec/bundler/compact_index_client/cache_file_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "bundler/compact_index_client" +require "bundler/compact_index_client/cache_file" +require "tmpdir" + +RSpec.describe Bundler::CompactIndexClient::CacheFile do + let(:path) { Pathname.new(Dir.mktmpdir("localpath")).join("versions") } + + def sha256(data) + { "sha-256" => Digest::SHA256.base64digest(data) } + end + + it "appends in binary mode so line endings are preserved" do + path.binwrite "created_at: 2026-06-10\n---\n" + + appended = nil + described_class.copy(path) do |file| + file.digests = sha256("created_at: 2026-06-10\n---\nrake 13.0.0\n") + appended = file.append("rake 13.0.0\n") + end + + expect(appended).to be_truthy + # On Windows a text-mode append rewrites the appended LF as CRLF while the + # digest is computed over the pre-write bytes, so verify passes but the file + # on disk is corrupted. Read raw bytes to catch any stray carriage return. + expect(path.binread).not_to include("\r") + expect(path.binread).to eq("created_at: 2026-06-10\n---\nrake 13.0.0\n") + end +end diff --git a/spec/support/shards.rb b/spec/support/shards.rb index 5718e775b23a..8416190d000b 100644 --- a/spec/support/shards.rb +++ b/spec/support/shards.rb @@ -143,6 +143,7 @@ module Shards "spec/bundler/ci_detector_spec.rb", ], shard_d: [ + "spec/bundler/compact_index_client/cache_file_spec.rb", "spec/bundler/rubygems_ext_spec.rb", "spec/bundler/resolver/cooldown_spec.rb", "spec/install/cooldown_spec.rb", diff --git a/test/rubygems/test_gem_compact_index_client_cache_file.rb b/test/rubygems/test_gem_compact_index_client_cache_file.rb index 193ae327e9c2..f2e2633646b4 100644 --- a/test/rubygems/test_gem_compact_index_client_cache_file.rb +++ b/test/rubygems/test_gem_compact_index_client_cache_file.rb @@ -82,6 +82,23 @@ def test_append_with_matching_digests assert_equal "abcdef", @path.read end + def test_append_writes_in_binary_mode + @path.binwrite "created_at: 2026-06-10\n---\n" + + appended = nil + CacheFile.copy(@path) do |file| + file.digests = sha256("created_at: 2026-06-10\n---\nrake 13.0.0\n") + appended = file.append("rake 13.0.0\n") + end + + assert appended + # On Windows a text-mode append rewrites the appended LF as CRLF while the + # digest is computed over the pre-write bytes, so verify passes but the file + # on disk is corrupted. Read raw bytes to catch any stray carriage return. + refute_includes @path.binread, "\r" + assert_equal "created_at: 2026-06-10\n---\nrake 13.0.0\n", @path.binread + end + def test_append_with_mismatched_digests_keeps_original @path.binwrite "abc"