file-temp 1.2.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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