Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 5 additions & 99 deletions lib/tmpdir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,14 @@
# $Id$
#

require 'fileutils'
begin
require 'etc.so'
rescue LoadError # rescue LoadError for miniruby
end
require_relative './tmpdir/tmpname'

class Dir

@@systmpdir ||= defined?(Etc.systmpdir) ? Etc.systmpdir : '/tmp'

##
# Returns the operating system's temporary file path.

#
def self.tmpdir
['TMPDIR', 'TMP', 'TEMP', ['system temporary path', @@systmpdir], ['/tmp']*2, ['.']*2].find do |name, dir|
unless dir
next if !(dir = ENV[name]) or dir.empty?
end
dir = File.expand_path(dir)
stat = File.stat(dir) rescue next
case
when !stat.directory?
warn "#{name} is not a directory: #{dir}"
when !stat.writable?
warn "#{name} is not writable: #{dir}"
when stat.world_writable? && !stat.sticky?
warn "#{name} is world-writable: #{dir}"
else
break dir
end
end or raise ArgumentError, "could not find a temporary directory"
Dir::Tmpname.tmpdir
end

# Dir.mktmpdir creates a temporary directory.
Expand Down Expand Up @@ -83,78 +60,7 @@ def self.tmpdir
# FileUtils.remove_entry dir
# end
#
def self.mktmpdir(prefix_suffix=nil, *rest, **options)
base = nil
path = Tmpname.create(prefix_suffix || "d", *rest, **options) {|path, _, _, d|
base = d
mkdir(path, 0700)
}
if block_given?
begin
yield path.dup
ensure
unless base
stat = File.stat(File.dirname(path))
if stat.world_writable? and !stat.sticky?
raise ArgumentError, "parent directory is world writable but not sticky"
end
end
FileUtils.remove_entry path
end
else
path
end
end

# Temporary name generator
module Tmpname # :nodoc:
module_function

def tmpdir
Dir.tmpdir
end

# Unusable characters as path name
UNUSABLE_CHARS = "^,-.0-9A-Z_a-z~"

# Dedicated random number generator
RANDOM = Random.new
class << RANDOM # :nodoc:
# Maximum random number
MAX = 36**6 # < 0x100000000

# Returns new random string upto 6 bytes
def next
rand(MAX).to_s(36)
end
end
private_constant :RANDOM

# Generates and yields random names to create a temporary name
def create(basename, tmpdir=nil, max_try: nil, **opts)
origdir = tmpdir
tmpdir ||= tmpdir()
n = nil
prefix, suffix = basename
prefix = (String.try_convert(prefix) or
raise ArgumentError, "unexpected prefix: #{prefix.inspect}")
prefix = prefix.delete(UNUSABLE_CHARS)
suffix &&= (String.try_convert(suffix) or
raise ArgumentError, "unexpected suffix: #{suffix.inspect}")
suffix &&= suffix.delete(UNUSABLE_CHARS)
begin
t = Time.now.strftime("%Y%m%d")
path = "#{prefix}#{t}-#{$$}-#{RANDOM.next}"\
"#{n ? %[-#{n}] : ''}#{suffix||''}"
path = File.join(tmpdir, path)
yield(path, n, opts, origdir)
rescue Errno::EEXIST
n ||= 0
n += 1
retry if !max_try or n < max_try
raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'"
end
path
end
def self.mktmpdir(prefix_suffix=nil, *rest, **options, &block)
Dir::Tmpname.mktmpdir(prefix_suffix, *rest, **options, &block)
end
end
17 changes: 17 additions & 0 deletions lib/tmpdir/insecure_world_writable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

class Dir
module Tmpname
module InsecureWorldWritable
refine File::Stat do
def insecure_world_writable?
world_writable? && !sticky?
end

def not_insecure_world_writable?
!insecure_world_writable?
end
end
end
end
end
113 changes: 113 additions & 0 deletions lib/tmpdir/tmpname.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# frozen_string_literal: true
#
# tmpdir - retrieve temporary directory path
#
# $Id$
#

require 'fileutils'
begin
require 'etc.so'
rescue LoadError # rescue LoadError for miniruby
end
require_relative './insecure_world_writable'

class Dir
# Temporary name generator
module Tmpname # :nodoc:
using InsecureWorldWritable

module_function

SYSTMPDIR = Etc.systmpdir rescue '/tmp'
private_constant :SYSTMPDIR

TRY_ENVS = %w{TMPDIR TMP TEMP}.freeze
private_constant :TRY_ENVS

TRY_DIRS = [['system temporary path', SYSTMPDIR], ['/tmp']*2, ['.']*2].map(&:freeze).freeze
private_constant :TRY_DIRS

PERM_CHECKS = [
['not a directory', :directory? ],
['not writable' , :writable? ],
['world-writable' , :not_insecure_world_writable? ]].map(&:freeze).freeze
private_constant :PERM_CHECKS

def tmpdir
TRY_ENVS.each { |env| d = self.verify_permissions(env, ENV[env]); return d if d }
TRY_DIRS.each { |name, dir| d = self.verify_permissions(name, dir) ; return d if d }
raise ArgumentError, 'could not find a temporary directory'
end

private_class_method def verify_permissions(name, dir)
return if dir.to_s.empty?
dir = File.expand_path(dir)
stat = File.stat(dir) rescue return
dir if PERM_CHECKS.all? { |msg, ok| stat.send(ok) or warn "#{name} is #{msg}: #{dir}" }
end

def mktmpdir(prefix_suffix = nil, *rest, **options, &block)
base = nil
path = self.create(prefix_suffix || 'd', *rest, **options) do |path, _, _, d|
base = d
Dir.mkdir(path, 0700)
end
if block_given?
self.mktmpdir_with_block(base, path, block)
else
path
end
end

private_class_method def mktmpdir_with_block(base, path, block)
block.call(path.dup)
ensure
if !base && File.stat(File.dirname(path))&.insecure_world_writable?
raise ArgumentError, 'parent directory is world writable but not sticky'
end
FileUtils.remove_entry path
end

# Unusable characters as path name
UNUSABLE_CHARS = '^,-.0-9A-Z_a-z~'

# Generates and yields random names to create a temporary name
def create(basename, tmpdir = nil, max_try: nil, **opts)
raise ArgumentError, "empty parent path" if tmpdir&.empty?
n = nil
begin
path = generate_path(basename, tmpdir, n)
path = File.join(tmpdir || self.tmpdir, path)
yield(path, n, opts, tmpdir)
path
rescue Errno::EEXIST
n ||= 0
retry if max_try&.>=(n += 1)
raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'"
end
end

private_class_method def generate_path(basename, tmpdir, n)
prefix, suffix = self.make_prefix_suffix(basename)
time = Time.now.strftime('%Y%m%d')
rnd = Random.bytes(4).unpack1('L').to_s(36)[0..5]
'%s%s-%d-%s%s%s' % [prefix, time, $$, rnd, n&.-@, suffix]
end

private_class_method def make_prefix_suffix(basename)
prefix, suffix = basename
prefix = self.make_str('prefix', prefix)
suffix &&= self.make_str('suffix', suffix)
[prefix, suffix]
end

private_class_method def make_str(msg, var)
if x = String.try_convert(var)
x.delete(UNUSABLE_CHARS)
else
raise ArgumentError, "unexpected #{msg}: #{var.inspect}"
end
end
end
end
22 changes: 22 additions & 0 deletions test/test_tmpdir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ def test_mktmpdir_traversal_array
end
end

def test_mktmpdir_not_empty_parent
assert_raise(ArgumentError) do
Dir.mktmpdir("foo", "")
end
end

def assert_mktmpdir_traversal
Dir.mktmpdir do |target|
target = target.chomp('/') + '/'
Expand All @@ -115,4 +121,20 @@ def assert_mktmpdir_traversal
end
end
end

def test_ractor
assert_ractor(<<~'end;', require: "tmpdir")
r = Ractor.new do
Dir.mktmpdir() do |d|
Ractor.yield d
Ractor.receive
end
end
dir = r.take
assert_file.directory? dir
r.send true
r.take
assert_file.not_exist? dir
end;
end
end