Skip to content

Open compact index cache in binary mode when appending#9679

Open
hsbt wants to merge 1 commit into
masterfrom
compact-index-binary-append
Open

Open compact index cache in binary mode when appending#9679
hsbt wants to merge 1 commit into
masterfrom
compact-index-binary-append

Conversation

@hsbt

@hsbt hsbt commented Jul 9, 2026

Copy link
Copy Markdown
Member

CompactIndexClient::CacheFile#append opened the temporary cache file with mode "a". On Windows that is a text-mode stream, so every "\n" written into the appended range is silently rewritten as "\r\n". Full writes already use the binary "wb" mode, so only the incremental append path is affected.

The corruption is invisible to the integrity check. append writes through Gem::Package::DigestIO, which hashes the logical bytes handed to write, not the bytes the operating system actually lands on disk. The digest therefore matches, verify returns true, and the mangled file is committed to the cache as if it were correct.

The failure compounds on the next update. The stray carriage returns inflate file.size, so the following ranged request (file.size - 1) restarts at the wrong offset and the reassembled versions index no longer matches the digest the server advertises, forcing a full multi-megabyte re-download. The per-gem info files keep failing their MD5 comparison against the raw index bytes, so they are refetched on every resolve. A full replace with "wb" heals the file momentarily, but the next incremental append corrupts it again, leaving Windows users in a permanent re-download loop.

The fix opens the append stream with "ab" so the appended range is written verbatim, matching the binary mode already used for full writes. The change is applied to both the Bundler and RubyGems copies of CacheFile.

Tests

test/rubygems/test_gem_compact_index_client_cache_file.rb gains test_append_writes_in_binary_mode, which appends content containing an "\n" and asserts via binread that no "\r" reaches disk. On the unfixed code this fails on Windows with "rake 13.0.0\r\n"; with the fix it passes. A matching spec/bundler/compact_index_client/cache_file_spec.rb covers the Bundler copy. The existing append specs used newline-free payloads, which is why the corruption went unnoticed.

Copilot AI review requested due to automatic review settings July 9, 2026 23:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Windows-specific corruption when incrementally appending to Compact Index cache files by ensuring the append path uses binary mode (matching the existing full-write path). This prevents LF→CRLF translation during append, which could previously bypass digest verification and lead to persistent cache invalidation/redownload loops on Windows.

Changes:

  • Switch CompactIndexClient::CacheFile#append from "a" to binary append "ab" in both RubyGems and Bundler copies.
  • Add a RubyGems minitest covering binary-safe append behavior (ensuring no "\r" reaches disk).
  • Add a Bundler RSpec covering the same binary append behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
lib/rubygems/compact_index_client/cache_file.rb Use binary append mode ("ab") so appended bytes are written verbatim on Windows.
lib/bundler/compact_index_client/cache_file.rb Mirror the RubyGems fix in Bundler’s CacheFile implementation.
test/rubygems/test_gem_compact_index_client_cache_file.rb Add regression test asserting append preserves LF-only line endings on disk.
spec/bundler/compact_index_client/cache_file_spec.rb Add Bundler-side regression spec for binary append behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3 to +5
require "bundler/compact_index_client"
require "bundler/compact_index_client/cache_file"

CacheFile#append opened the temp file with mode "a", which on Windows is
a text-mode stream that rewrites every "\n" as "\r\n". The corruption
goes undetected because DigestIO hashes the logical bytes handed to
write, not the bytes the OS actually lands on disk, so verify passes and
the mangled file is committed.

The damage compounds on the next update: the extra carriage returns
shift file.size, so the ranged request restarts at the wrong offset and
the versions index no longer matches its expected digest, forcing a full
re-download every time. The info files keep failing their MD5 check
against the raw index bytes, so they are refetched on every resolve.

Open the append stream with "ab" so the appended range is written
verbatim, matching the binary "wb" used for full writes.
@hsbt hsbt force-pushed the compact-index-binary-append branch from 6808889 to 9568d96 Compare July 10, 2026 01:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants