[rubygems/rubygems] Use a constant empty tar header to avoid extra allocations
[ruby.git] / lib / tmpdir.rb
blobfe3e0e19d1f00cbbfb20e72a049bfaa5ca0b9bb8
1 # frozen_string_literal: true
3 # tmpdir - retrieve temporary directory path
5 # $Id$
8 require 'fileutils'
9 begin
10   require 'etc.so'
11 rescue LoadError # rescue LoadError for miniruby
12 end
14 class Dir
16   # Class variables are inaccessible from non-main Ractor.
17   # And instance variables too, in Ruby 3.0.
19   # System-wide temporary directory path
20   SYSTMPDIR = (defined?(Etc.systmpdir) ? Etc.systmpdir.freeze : '/tmp')
21   private_constant :SYSTMPDIR
23   ##
24   # Returns the operating system's temporary file path.
25   #
26   #   require 'tmpdir'
27   #   Dir.tmpdir # => "/tmp"
29   def self.tmpdir
30     ['TMPDIR', 'TMP', 'TEMP', ['system temporary path', SYSTMPDIR], ['/tmp']*2, ['.']*2].find do |name, dir|
31       unless dir
32         next if !(dir = ENV[name] rescue next) or dir.empty?
33       end
34       dir = File.expand_path(dir)
35       stat = File.stat(dir) rescue next
36       case
37       when !stat.directory?
38         warn "#{name} is not a directory: #{dir}"
39       when !stat.writable?
40         warn "#{name} is not writable: #{dir}"
41       when stat.world_writable? && !stat.sticky?
42         warn "#{name} is world-writable: #{dir}"
43       else
44         break dir
45       end
46     end or raise ArgumentError, "could not find a temporary directory"
47   end
49   # Dir.mktmpdir creates a temporary directory.
50   #
51   #   require 'tmpdir'
52   #   Dir.mktmpdir {|dir|
53   #     # use the directory
54   #   }
55   #
56   # The directory is created with 0700 permission.
57   # Application should not change the permission to make the temporary directory accessible from other users.
58   #
59   # The prefix and suffix of the name of the directory is specified by
60   # the optional first argument, <i>prefix_suffix</i>.
61   # - If it is not specified or nil, "d" is used as the prefix and no suffix is used.
62   # - If it is a string, it is used as the prefix and no suffix is used.
63   # - If it is an array, first element is used as the prefix and second element is used as a suffix.
64   #
65   #  Dir.mktmpdir {|dir| dir is ".../d..." }
66   #  Dir.mktmpdir("foo") {|dir| dir is ".../foo..." }
67   #  Dir.mktmpdir(["foo", "bar"]) {|dir| dir is ".../foo...bar" }
68   #
69   # The directory is created under Dir.tmpdir or
70   # the optional second argument <i>tmpdir</i> if non-nil value is given.
71   #
72   #  Dir.mktmpdir {|dir| dir is "#{Dir.tmpdir}/d..." }
73   #  Dir.mktmpdir(nil, "/var/tmp") {|dir| dir is "/var/tmp/d..." }
74   #
75   # If a block is given,
76   # it is yielded with the path of the directory.
77   # The directory and its contents are removed
78   # using FileUtils.remove_entry before Dir.mktmpdir returns.
79   # The value of the block is returned.
80   #
81   #  Dir.mktmpdir {|dir|
82   #    # use the directory...
83   #    open("#{dir}/foo", "w") { something using the file }
84   #  }
85   #
86   # If a block is not given,
87   # The path of the directory is returned.
88   # In this case, Dir.mktmpdir doesn't remove the directory.
89   #
90   #  dir = Dir.mktmpdir
91   #  begin
92   #    # use the directory...
93   #    open("#{dir}/foo", "w") { something using the file }
94   #  ensure
95   #    # remove the directory.
96   #    FileUtils.remove_entry dir
97   #  end
98   #
99   def self.mktmpdir(prefix_suffix=nil, *rest, **options)
100     base = nil
101     path = Tmpname.create(prefix_suffix || "d", *rest, **options) {|path, _, _, d|
102       base = d
103       mkdir(path, 0700)
104     }
105     if block_given?
106       begin
107         yield path.dup
108       ensure
109         unless base
110           base = File.dirname(path)
111           stat = File.stat(base)
112           if stat.world_writable? and !stat.sticky?
113             raise ArgumentError, "parent directory is world writable but not sticky: #{base}"
114           end
115         end
116         FileUtils.remove_entry path
117       end
118     else
119       path
120     end
121   end
123   # Temporary name generator
124   module Tmpname # :nodoc:
125     module_function
127     def tmpdir
128       Dir.tmpdir
129     end
131     # Unusable characters as path name
132     UNUSABLE_CHARS = "^,-.0-9A-Z_a-z~"
134     # Dedicated random number generator
135     RANDOM = Object.new
136     class << RANDOM # :nodoc:
137       # Maximum random number
138       MAX = 36**6 # < 0x100000000
140       # Returns new random string upto 6 bytes
141       def next
142         (::Random.urandom(4).unpack1("L")%MAX).to_s(36)
143       end
144     end
145     RANDOM.freeze
146     private_constant :RANDOM
148     # Generates and yields random names to create a temporary name
149     def create(basename, tmpdir=nil, max_try: nil, **opts)
150       origdir = tmpdir
151       tmpdir ||= tmpdir()
152       n = nil
153       prefix, suffix = basename
154       prefix = (String.try_convert(prefix) or
155                 raise ArgumentError, "unexpected prefix: #{prefix.inspect}")
156       prefix = prefix.delete(UNUSABLE_CHARS)
157       suffix &&= (String.try_convert(suffix) or
158                   raise ArgumentError, "unexpected suffix: #{suffix.inspect}")
159       suffix &&= suffix.delete(UNUSABLE_CHARS)
160       begin
161         t = Time.now.strftime("%Y%m%d")
162         path = "#{prefix}#{t}-#{$$}-#{RANDOM.next}"\
163                "#{n ? %[-#{n}] : ''}#{suffix||''}"
164         path = File.join(tmpdir, path)
165         yield(path, n, opts, origdir)
166       rescue Errno::EEXIST
167         n ||= 0
168         n += 1
169         retry if !max_try or n < max_try
170         raise "cannot generate temporary name using '#{basename}' under '#{tmpdir}'"
171       end
172       path
173     end
174   end