file-temp 1.2.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ require_relative 'file/temp'
@@ -2,17 +2,14 @@ require 'java'
2
2
  import java.lang.System
3
3
 
4
4
  class File::Temp < File
5
- # The version of the file-temp library.
6
- VERSION = '1.2.1'
7
-
8
5
  # The temporary directory used on MS Windows or Unix.
9
6
  TMPDIR = java.lang.System.getProperties["java.io.tmpdir"]
10
7
 
11
8
  # The name of the temporary file.
12
9
  attr_reader :path
13
10
 
14
- # Creates a new, anonymous, temporary file in your File::Temp::TMPDIR
15
- # directory.
11
+ # Creates a new, anonymous, temporary file in your system's temporary
12
+ # directory, or whichever directory you specify.
16
13
  #
17
14
  # If the +delete+ option is set to true (the default) then the temporary file
18
15
  # will be deleted automatically as soon as all references to it are closed.
@@ -30,11 +27,11 @@ class File::Temp < File
30
27
  #
31
28
  # Example:
32
29
  #
33
- # fh = File::Temp.new(true, 'rb_file_temp_XXXXXX') => file
30
+ # fh = File::Temp.new(delete: true, template: 'rb_file_temp_XXXXXX')
34
31
  # fh.puts 'hello world'
35
32
  # fh.close
36
33
  #
37
- def initialize(delete = true, template = 'rb_file_temp_XXXXXX')
34
+ def initialize(delete: true, template: 'rb_file_temp_XXXXXX', directory: TMPDIR, options: {})
38
35
  raise TypeError unless template.is_a?(String)
39
36
 
40
37
  # Since Java uses a GUID extension to generate a unique file name
@@ -44,26 +41,27 @@ class File::Temp < File
44
41
  # For consistency between implementations, convert errors here
45
42
  # to Errno::EINVAL.
46
43
  begin
47
- @file = java.io.File.createTempFile(template, nil)
44
+ @file = java.io.File.createTempFile(template, nil, java.io.File.new(directory))
48
45
  rescue NativeException => err
49
46
  raise SystemCallError.new(22), template # 22 is EINVAL
50
47
  end
51
48
 
52
49
  @file.deleteOnExit if delete
50
+ options[:mode] ||= 'wb+'
53
51
 
54
- @path = @file.getName
52
+ path = @file.getName
53
+ super(path, options)
55
54
 
56
- super(@path, 'wb+')
55
+ @path = path unless delete
57
56
  end
58
57
 
59
- # Generates a unique file name.
58
+ # Generates a unique file name based on your tmpdir, or whichever
59
+ # directory you specify.
60
60
  #
61
- def self.temp_name
62
- file = java.io.File.createTempFile('rb_file_temp_', nil)
61
+ def self.temp_name(directory = TMPDIR)
62
+ file = java.io.File.createTempFile('rb_file_temp_', nil, java.io.File.new(directory))
63
63
  file.deleteOnExit
64
- name = file.getName
65
- file.finalize
66
- name
64
+ directory + file.getName
67
65
  end
68
66
 
69
67
  # Identical to the File#close method except that we also finalize
@@ -1,5 +1,14 @@
1
+ class File::Temp < File
2
+ # The version of the file-temp library
3
+ VERSION = '1.7.0'.freeze
4
+ end
5
+
1
6
  if RUBY_PLATFORM == 'java'
2
- require File.join(File.expand_path(File.dirname(__FILE__)), 'temp_java')
7
+ require_relative 'java/temp'
3
8
  else
4
- require File.join(File.expand_path(File.dirname(__FILE__)), 'temp_c')
9
+ if File::ALT_SEPARATOR
10
+ require_relative 'windows/temp'
11
+ else
12
+ require_relative 'unix/temp'
13
+ end
5
14
  end
@@ -0,0 +1,106 @@
1
+ require 'ffi'
2
+ require 'tmpdir'
3
+
4
+ class File::Temp < File
5
+ extend FFI::Library
6
+ ffi_lib FFI::Library::LIBC
7
+
8
+ # :stopdoc:
9
+
10
+ private
11
+
12
+ attach_function :fclose, [:pointer], :int
13
+ attach_function :_fileno, :fileno, [:pointer], :int
14
+ attach_function :strerror, [:int], :string
15
+ attach_function :tmpfile, [], :pointer
16
+ attach_function :tmpnam, [:pointer], :string
17
+ attach_function :mktemp, [:pointer], :string
18
+
19
+ private_class_method :mktemp, :strerror, :tmpfile
20
+ private_class_method :tmpnam, :fclose, :_fileno
21
+
22
+ public
23
+
24
+ # :startdoc:
25
+
26
+ # The temporary directory used on MS Windows or Unix by default.
27
+ TMPDIR = ENV['TEMP'] || ENV['TMP'] || ENV['TMPDIR'] || Dir.tmpdir
28
+
29
+ # The name of the temporary file. Set to nil if the +delete+ option to the
30
+ # constructor is true.
31
+ attr_reader :path
32
+
33
+ # Creates a new, anonymous, temporary file in your tmpdir, or whichever
34
+ # directory you specifiy.
35
+ #
36
+ # If the +delete+ option is set to true (the default) then the temporary file
37
+ # will be deleted automatically as soon as all references to it are closed.
38
+ # Otherwise, the file will live on in your tmpdir path.
39
+ #
40
+ # If the +delete+ option is set to false, then the file is not deleted. In
41
+ # addition, you can supply a string +template+ that the system replaces with
42
+ # a unique filename. This template should end with 3 to 6 'X' characters.
43
+ # The default template is 'rb_file_temp_XXXXXX'. In this case the temporary
44
+ # file lives in the directory where it was created.
45
+ #
46
+ # The +template+ argument is ignored if the +delete+ argument is true.
47
+ #
48
+ # Example:
49
+ #
50
+ # fh = File::Temp.new(delete: true, template: 'rb_file_temp_XXXXXX') => file
51
+ # fh.puts 'hello world'
52
+ # fh.close
53
+ #
54
+ def initialize(delete: true, template: 'rb_file_temp_XXXXXX', directory: TMPDIR, options: {})
55
+ @fptr = nil
56
+
57
+ if delete
58
+ @fptr = tmpfile()
59
+ raise SystemCallError.new('tmpfile', FFI.errno) if @fptr.null?
60
+ fd = _fileno(@fptr)
61
+ else
62
+ begin
63
+ omask = File.umask(077)
64
+ ptr = FFI::MemoryPointer.from_string(template)
65
+ str = mktemp(ptr)
66
+
67
+ if str.nil? || str.empty?
68
+ raise SystemCallError.new('mktemp', FFI.errno)
69
+ end
70
+
71
+ @path = File.join(directory, ptr.read_string)
72
+ ensure
73
+ File.umask(omask)
74
+ end
75
+ end
76
+
77
+ options[:mode] ||= 'wb+'
78
+
79
+ if delete
80
+ super(fd, options)
81
+ else
82
+ super(@path, options)
83
+ end
84
+ end
85
+
86
+ # The close method was overridden to ensure the internal file pointer that we
87
+ # potentially created in the constructor is closed. It is otherwise identical
88
+ # to the File#close method.
89
+ #--
90
+ # This is probably unnecessary since Ruby will close the fd, and in reality
91
+ # the fclose function probably fails with an Errno::EBADF. Consequently
92
+ # I will let it silently fail as a no-op.
93
+ #
94
+ def close
95
+ super
96
+ fclose(@fptr) if @fptr && !@fptr.null?
97
+ end
98
+
99
+ # Generates a unique file name.
100
+ #
101
+ # Note that a file is not actually generated on the filesystem.
102
+ #
103
+ def self.temp_name
104
+ tmpnam(nil) << '.tmp'
105
+ end
106
+ end
@@ -0,0 +1,217 @@
1
+ require 'ffi'
2
+ require 'tmpdir'
3
+
4
+ class File::Temp < File
5
+ extend FFI::Library
6
+ ffi_lib FFI::Library::LIBC
7
+
8
+ # :stopdoc:
9
+
10
+ private
11
+
12
+ attach_function :_close, [:int], :int
13
+ attach_function :fclose, [:pointer], :int
14
+ attach_function :_fdopen, [:int, :string], :pointer
15
+ attach_function :_fileno, [:pointer], :int
16
+ attach_function :_get_errno, [:pointer], :int
17
+ attach_function :_open, [:string, :int, :int], :int
18
+ attach_function :_open_osfhandle, [:long, :int], :int
19
+ attach_function :tmpnam_s, [:pointer, :size_t], :int
20
+ attach_function :mktemp_s, :_mktemp_s, [:pointer, :size_t], :int
21
+
22
+ private_class_method :_close, :fclose, :_fdopen, :_fileno, :_get_errno
23
+ private_class_method :_open, :_open_osfhandle, :mktemp_s, :tmpnam_s
24
+
25
+ ffi_lib :kernel32
26
+
27
+ attach_function :CloseHandle, [:long], :bool
28
+ attach_function :CreateFileW, [:buffer_in, :ulong, :ulong, :pointer, :ulong, :ulong, :ulong], :long
29
+ attach_function :DeleteFileW, [:string], :bool
30
+ attach_function :GetTempPathW, [:ulong, :buffer_out], :ulong
31
+ attach_function :GetTempFileNameW, [:buffer_in, :string, :uint, :buffer_out], :uint
32
+
33
+ private_class_method :_close, :_fdopen, :_open, :_open_osfhandle
34
+ private_class_method :CloseHandle, :CreateFileW, :DeleteFileW
35
+ private_class_method :GetTempPathW, :GetTempFileNameW
36
+
37
+ S_IWRITE = 128
38
+ S_IREAD = 256
39
+ BINARY = 0x8000
40
+ SHORT_LIVED = 0x1000
41
+ GENERIC_READ = 0x80000000
42
+ GENERIC_WRITE = 0x40000000
43
+ CREATE_ALWAYS = 2
44
+
45
+ FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
46
+ FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000
47
+
48
+ FILE_ATTRIBUTE_NORMAL = 0x00000080
49
+ FILE_FLAG_DELETE_ON_CLOSE = 0x04000000
50
+ INVALID_HANDLE_VALUE = -1
51
+
52
+ public
53
+
54
+ # :startdoc:
55
+
56
+ # The temporary directory used on MS Windows or Unix by default.
57
+ TMPDIR = ENV['TEMP'] || ENV['TMP'] || ENV['USERPROFILE'] || Dir.tmpdir
58
+
59
+ # The name of the temporary file. Set to nil if the +delete+ option to the
60
+ # constructor is true.
61
+ attr_reader :path
62
+
63
+ # Creates a new, anonymous, temporary file in your File::Temp::TMPDIR
64
+ # directory, or whichever directory you specify.
65
+ #
66
+ # If the +delete+ option is set to true (the default) then the temporary file
67
+ # will be deleted automatically as soon as all references to it are closed.
68
+ # Otherwise, the file will live on in your File::Temp::TMPDIR path.
69
+ #
70
+ # If the +delete+ option is set to false, then the file is not deleted. In
71
+ # addition, you can supply a string +template+ that the system replaces with
72
+ # a unique filename. This template should end with 3 to 6 'X' characters.
73
+ # The default template is 'rb_file_temp_XXXXXX'. In this case the temporary
74
+ # file lives in the directory where it was created.
75
+ #
76
+ # The +template+ argument is ignored if the +delete+ argument is true.
77
+ #
78
+ # Example:
79
+ #
80
+ # fh = File::Temp.new(delete: true, template: 'rb_file_temp_XXXXXX')
81
+ # fh.puts 'hello world'
82
+ # fh.close
83
+ #
84
+ def initialize(delete: true, template: 'rb_file_temp_XXXXXX', directory: TMPDIR, options: {})
85
+ @fptr = nil
86
+
87
+ if delete
88
+ @fptr = tmpfile()
89
+ fd = _fileno(@fptr)
90
+ else
91
+ begin
92
+ omask = File.umask(077)
93
+ ptr = FFI::MemoryPointer.from_string(template)
94
+ errno = mktemp_s(ptr, ptr.size)
95
+
96
+ raise SystemCallError.new('mktemp_s', errno) if errno != 0
97
+
98
+ @path = File.join(directory, ptr.read_string)
99
+ @path.tr!(File::SEPARATOR, File::ALT_SEPARATOR)
100
+ ensure
101
+ File.umask(omask)
102
+ end
103
+ end
104
+
105
+ options[:mode] ||= 'wb+'
106
+
107
+ if delete
108
+ super(fd, options)
109
+ else
110
+ super(@path, options)
111
+ end
112
+ end
113
+
114
+ # The close method was overridden to ensure the internal file pointer we
115
+ # created in the constructor is closed. It is otherwise identical to the
116
+ # File#close method.
117
+ #
118
+ def close
119
+ super
120
+ fclose(@fptr) if @fptr
121
+ end
122
+
123
+ # Generates a unique file name based on your default temporary directory,
124
+ # or whichever directory you specify.
125
+ #
126
+ # Note that a file is not actually generated on the filesystem.
127
+ #--
128
+ # NOTE: One quirk of the Windows function is that, after the first call, it
129
+ # adds a file extension of sequential numbers in base 32, e.g. .1-.1vvvvvu.
130
+ #
131
+ def self.temp_name(directory: TMPDIR)
132
+ ptr = FFI::MemoryPointer.new(:char, 1024)
133
+ errno = tmpnam_s(ptr, ptr.size)
134
+
135
+ raise SystemCallError.new('tmpnam_s', errno) if errno != 0
136
+
137
+ directory + ptr.read_string + 'tmp'
138
+ end
139
+
140
+ private
141
+
142
+ # For those times when we want the posix errno rather than a formatted string.
143
+ # This is necessary because FFI.errno appears to be using GetLastError() which
144
+ # does not always match what _get_errno() returns.
145
+ #
146
+ def get_posix_errno
147
+ ptr = FFI::MemoryPointer.new(:int)
148
+ _get_errno(ptr)
149
+ ptr.read_int
150
+ end
151
+
152
+ # Simple wrapper around the GetTempPath function.
153
+ #
154
+ def get_temp_path
155
+ buf = 0.chr * 1024
156
+ buf.encode!("UTF-16LE")
157
+
158
+ if GetTempPathW(buf.size, buf) == 0
159
+ raise SystemCallError, FFI.errno, 'GetTempPathW'
160
+ end
161
+
162
+ buf.strip.chop # remove trailing slash
163
+ end
164
+
165
+ # The version of tmpfile() implemented by Microsoft is unacceptable.
166
+ # It attempts to write to C:\ (root) instead of a temporary directory.
167
+ # This is not only bad behavior, it won't work on Windows 7 and later
168
+ # without admin rights due to security restrictions.
169
+ #
170
+ # This is a custom implementation based on some code from the Cairo
171
+ # project.
172
+ #
173
+ def tmpfile
174
+ file_name = get_temp_path()
175
+ buf = 0.chr * 1024
176
+ buf.encode!("UTF-16LE")
177
+
178
+ if GetTempFileNameW(file_name, 'rb_', 0, buf) == 0
179
+ raise SystemCallError, FFI.errno, 'GetTempFileNameW'
180
+ end
181
+
182
+ file_name = buf.strip
183
+
184
+ handle = CreateFileW(
185
+ file_name,
186
+ GENERIC_READ | GENERIC_WRITE,
187
+ 0,
188
+ nil,
189
+ CREATE_ALWAYS,
190
+ FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE,
191
+ 0
192
+ )
193
+
194
+ if handle == INVALID_HANDLE_VALUE
195
+ error = FFI.errno
196
+ DeleteFileW(file_name)
197
+ raise SystemCallError.new('CreateFileW', error)
198
+ end
199
+
200
+ fd = _open_osfhandle(handle, 0)
201
+
202
+ if fd < 0
203
+ CloseHandle(handle)
204
+ raise SystemCallError, get_posix_errno, '_open_osfhandle'
205
+ end
206
+
207
+ fp = _fdopen(fd, 'w+b')
208
+
209
+ if fp.nil?
210
+ _close(fd)
211
+ CloseHandle(handle)
212
+ raise SystemCallError, get_posix_errno, 'fdopen'
213
+ end
214
+
215
+ fp
216
+ end
217
+ end
@@ -0,0 +1,159 @@
1
+ ######################################################################
2
+ # file_temp_spec.rb
3
+ #
4
+ # Test suite for the file-temp library. These tests should be run
5
+ # via the 'rake spec' task.
6
+ ######################################################################
7
+ require 'rspec'
8
+ require 'file/temp'
9
+
10
+ RSpec.describe File::Temp do
11
+ let(:windows) { File::ALT_SEPARATOR }
12
+ let(:osx) { RbConfig::CONFIG['host_os'] =~ /darwin/i }
13
+
14
+ before do
15
+ @dir = File::Temp::TMPDIR
16
+ @template = 'file-temp-test-XXXXX'
17
+ @fh = nil
18
+
19
+ # Because Dir[] doesn't work right with backslashes
20
+ @dir = @dir.tr("\\", "/") if windows
21
+ end
22
+
23
+ context "constants" do
24
+ example "library version is set to expected value" do
25
+ expect( File::Temp::VERSION).to eq('1.7.0')
26
+ expect(File::Temp::VERSION).to be_frozen
27
+ end
28
+
29
+ example "TMPDIR constant is defined" do
30
+ expect(File::Temp::TMPDIR).to be_kind_of(String)
31
+ expect(File::Temp::TMPDIR.size).to be > 0
32
+ end
33
+ end
34
+
35
+ context "threads" do
36
+ example "library works as expected with multiple threads" do
37
+ threads = []
38
+ expect{ 100.times{ threads << Thread.new{ File::Temp.new }}}.not_to raise_error
39
+ expect{ threads.each{ |t| t.join }.not_to raise_error }
40
+ end
41
+ end
42
+
43
+ context "constructor" do
44
+ example "constructor works as expected with default auto delete option" do
45
+ expect{
46
+ @fh = File::Temp.new
47
+ @fh.print "hello"
48
+ @fh.close
49
+ }.not_to raise_error
50
+ end
51
+
52
+ example "constructor works as expected with false auto delete option" do
53
+ expect{
54
+ @fh = File::Temp.new(:delete => false)
55
+ @fh.print "hello"
56
+ @fh.close
57
+ }.not_to raise_error
58
+ end
59
+
60
+ example "constructor accepts and uses an optional template as expected" do
61
+ expect{ File::Temp.new(:delete => false, :template => 'temp_foo_XXXXXX').close }.not_to raise_error
62
+ expect(Dir["#{@dir}/temp_foo*"].length).to be >= 1
63
+ end
64
+
65
+ example "constructor with false auto delete and block works as expected" do
66
+ expect{
67
+ File::Temp.open(:delete => false, :template => 'temp_foo_XXXXXX'){ |fh| fh.puts "hello" }
68
+ }.not_to raise_error
69
+ expect(Dir["#{@dir}/temp_foo*"].length).to be >= 1
70
+ end
71
+
72
+ example "constructor accepts a maximum of three arguments" do
73
+ expect{
74
+ @fh = File::Temp.new(
75
+ :delete => true,
76
+ :template => 'temp_bar_XXXXX',
77
+ :directory => Dir.pwd,
78
+ :bogus => 1
79
+ )
80
+ }.to raise_error(ArgumentError)
81
+ end
82
+ end
83
+
84
+ context "template" do
85
+ example "template argument must be a string" do
86
+ expect{ @fh = File::Temp.new(:delete => false, :template => 1) }.to raise_error(TypeError)
87
+ end
88
+
89
+ example "an error is raised if a custom template is invalid" do
90
+ skip "skipped on OSX" if osx
91
+ expect{ File::Temp.new(:delete => false, :template => 'xx') }.to raise_error(Errno::EINVAL)
92
+ end
93
+ end
94
+
95
+ context "temp_name" do
96
+ example "temp_name basic functionality" do
97
+ expect(File::Temp).to respond_to(:temp_name)
98
+ expect{ File::Temp.temp_name }.not_to raise_error
99
+ expect(File::Temp.temp_name).to be_kind_of(String)
100
+ end
101
+
102
+ example "temp_name returns expected value" do
103
+ if windows
104
+ expect( File.extname(File::Temp.temp_name)).to match(/^.*?\d*?tmp/)
105
+ else
106
+ expect( File.extname(File::Temp.temp_name)).to eq('.tmp')
107
+ end
108
+ end
109
+ end
110
+
111
+ context "path" do
112
+ example "temp path basic functionality" do
113
+ @fh = File::Temp.new
114
+ expect(@fh).to respond_to(:path)
115
+ end
116
+
117
+ example "temp path is nil if delete option is true" do
118
+ @fh = File::Temp.new
119
+ expect(@fh.path).to be_nil
120
+ end
121
+
122
+ example "temp path is not nil if delete option is false" do
123
+ @fh = File::Temp.new(delete: false)
124
+ expect(@fh.path).not_to be_nil
125
+ end
126
+ end
127
+
128
+ context "ffi" do
129
+ example "ffi functions are private" do
130
+ methods = File::Temp.methods(false).map(&:to_s)
131
+ expect(methods).not_to include('_fileno')
132
+ expect(methods).not_to include('mkstemp')
133
+ expect(methods).not_to include('_umask')
134
+ expect(methods).not_to include('fclose')
135
+ expect(methods).not_to include('strerror')
136
+ expect(methods).not_to include('tmpnam')
137
+ expect(methods).not_to include('CloseHandle')
138
+ expect(methods).not_to include('CreateFileA')
139
+ expect(methods).not_to include('DeleteFileA')
140
+ expect(methods).not_to include('GetTempPathA')
141
+ expect(methods).not_to include('GetTempFileNameA')
142
+ end
143
+ end
144
+
145
+ after do
146
+ @dir = nil
147
+ @template = nil
148
+ @fh.close if @fh && !@fh.closed?
149
+ @fh = nil
150
+
151
+ Dir["temp_*"].each{ |f| File.delete(f) }
152
+ Dir["rb_file_temp_*"].each{ |f| File.delete(f) }
153
+
154
+ Dir.chdir(File::Temp::TMPDIR) do
155
+ Dir["temp_*"].each{ |f| File.delete(f) }
156
+ Dir["rb_file_temp_*"].each{ |f| File.delete(f) }
157
+ end
158
+ end
159
+ end