summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreileencodes <eileencodes@gmail.com>2025-12-16 13:28:06 -0500
committergit <svn-admin@ruby-lang.org>2026-01-13 07:12:47 +0000
commitb7dbdfe23ad443ca796f471144be99c00d5ce583 (patch)
tree2c640f0d0676446e36ccfd5ed98182e6c6e28cb5
parent60cf8598b2f0e9548fd30761276f655569d3daf9 (diff)
[ruby/rubygems] Refactor atomic file write
This refactoring is based off the changes in test/rubygems/test_gem_remote_fetcher.rb. It no longer uses tempfile as a result. https://github.com/ruby/rubygems/commit/be6fd6550b
-rw-r--r--lib/rubygems/util/atomic_file_writer.rb84
1 files changed, 46 insertions, 38 deletions
diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb
index 7d1d6a7416..ea592407eb 100644
--- a/lib/rubygems/util/atomic_file_writer.rb
+++ b/lib/rubygems/util/atomic_file_writer.rb
@@ -12,55 +12,63 @@ module Gem
# want other processes or threads to see half-written files.
def self.open(file_name)
- temp_dir = File.dirname(file_name)
- require "tempfile" unless defined?(Tempfile)
+ require "securerandom" unless defined?(SecureRandom)
- Tempfile.create(".#{File.basename(file_name)}", temp_dir) do |temp_file|
- temp_file.binmode
- return_value = yield temp_file
- temp_file.close
+ old_stat = begin
+ File.stat(file_name)
+ rescue SystemCallError
+ nil
+ end
- original_permissions = if File.exist?(file_name)
- File.stat(file_name)
- else
- # If not possible, probe which are the default permissions in the
- # destination directory.
- probe_permissions_in(File.dirname(file_name))
- end
+ # Names can't be longer than 255B
+ tmp_suffix = ".tmp.#{SecureRandom.hex}"
+ dirname = File.dirname(file_name)
+ basename = File.basename(file_name)
+ tmp_path = File.join(dirname, ".#{basename.byteslice(0, 254 - tmp_suffix.bytesize)}#{tmp_suffix}")
+
+ flags = File::RDWR | File::CREAT | File::EXCL | File::BINARY
+ flags |= File::SHARE_DELETE if defined?(File::SHARE_DELETE)
- # Set correct permissions on new file
- if original_permissions
+ File.open(tmp_path, flags) do |temp_file|
+ if old_stat
+ # Set correct permissions on new file
begin
- File.chown(original_permissions.uid, original_permissions.gid, temp_file.path)
- File.chmod(original_permissions.mode, temp_file.path)
+ File.chown(old_stat.uid, old_stat.gid, temp_file.path)
+ # This operation will affect filesystem ACL's
+ File.chmod(old_stat.mode, temp_file.path)
rescue Errno::EPERM, Errno::EACCES
# Changing file ownership failed, moving on.
end
end
- # Overwrite original file with temp file
- File.rename(temp_file.path, file_name)
- return_value
- end
- end
+ return_val = yield temp_file
+ rescue StandardError => error
+ begin
+ temp_file.close
+ rescue StandardError
+ nil
+ end
- def self.probe_permissions_in(dir) # :nodoc:
- basename = [
- ".permissions_check",
- Thread.current.object_id,
- Process.pid,
- rand(1_000_000),
- ].join(".")
+ begin
+ File.unlink(temp_file.path)
+ rescue StandardError
+ nil
+ end
+
+ raise error
+ else
+ begin
+ File.rename(temp_file.path, file_name)
+ rescue StandardError
+ begin
+ File.unlink(temp_file.path)
+ rescue StandardError
+ end
+
+ raise
+ end
- file_name = File.join(dir, basename)
- File.open(file_name, "w") {}
- File.stat(file_name)
- rescue Errno::ENOENT
- nil
- ensure
- begin
- File.unlink(file_name) if File.exist?(file_name)
- rescue SystemCallError
+ return_val
end
end
end