win32-file 0.6.3 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,14 +1,17 @@
1
+ == 0.6.4 - 3-Jun-2010
2
+ * Redefined the File.join method so that it converts all forward slashes
3
+ to back slashes.
4
+ * Refactored the Rakefile and path tests a bit.
5
+ * Bumped the minimum test-unit version to 2.0.7.
6
+
1
7
  == 0.6.3 - 24-Aug-2009
2
8
  * Refactored the File.directory? method so that it checks against the
3
9
  INVALID_FILE_ATTRIBUTES constant instead of a hard coded value.
4
10
  * Updated windows-pr dependency to 1.0.8.
5
11
 
6
- == 0.6.2 - 21-Aug-2009
12
+ == 0.6.2 - 19-Dec-2009
7
13
  * Changed the license to Artistic 2.0.
8
14
  * Several gemspec updates, including the license and description.
9
- * Refactored the File.attributes method so that it isn't making a bunch
10
- of calls to File::Stat.
11
- * Some documentation updates.
12
15
 
13
16
  == 0.6.1 - 9-Feb-2009
14
17
  * Fixed a bug in the custom File.directory? method with regards to
data/Rakefile CHANGED
@@ -5,65 +5,64 @@ include Config
5
5
 
6
6
  desc 'Clean any text files that may have been left over from tests'
7
7
  task :clean do
8
- Dir['test/*'].each{ |file|
9
- rm file if File.extname(file) == '.txt'
10
- }
8
+ Dir['test/*'].each{ |file|
9
+ rm file if File.extname(file) == '.txt'
10
+ }
11
11
  end
12
12
 
13
- desc 'Install the win32-file library (non-gem)'
14
- task :install do
15
- sitelibdir = CONFIG['sitelibdir']
16
- installdir = File.join(sitelibdir, 'win32')
17
- file = 'lib\win32\file.rb'
13
+ namespace 'gem' do
14
+ desc 'Create the win32-file gem'
15
+ task :create => [:clean] do
16
+ spec = eval(IO.read('win32-file.gemspec'))
17
+ Gem::Builder.new(spec).build
18
+ end
18
19
 
19
- Dir.mkdir(installdir) unless File.exists?(installdir)
20
- FileUtils.cp(file, installdir, :verbose => true)
20
+ desc 'Install the win32-file gem'
21
+ task :install => [:create] do
22
+ file = Dir['win32-file*.gem'].first
23
+ sh "gem install #{file}"
24
+ end
21
25
  end
22
26
 
23
- desc 'Install the win32-file library as a gem'
24
- task :install_gem => [:clean] do
25
- ruby 'win32-file.gemspec'
26
- file = Dir['win32-file*.gem'].first
27
- sh "gem install #{file}"
28
- end
29
-
30
- Rake::TestTask.new("test") do |t|
31
- t.verbose = true
32
- t.warning = true
33
- end
27
+ namespace 'test' do
28
+ Rake::TestTask.new("all") do |t|
29
+ t.verbose = true
30
+ t.warning = true
31
+ end
34
32
 
35
- Rake::TestTask.new("test_attributes") do |t|
36
- t.verbose = true
37
- t.warning = true
38
- t.test_files = FileList['test/test_win32_file_attributes.rb']
39
- end
33
+ Rake::TestTask.new("attributes") do |t|
34
+ t.verbose = true
35
+ t.warning = true
36
+ t.test_files = FileList['test/test_win32_file_attributes.rb']
37
+ end
40
38
 
41
- Rake::TestTask.new("test_constants") do |t|
42
- t.verbose = true
43
- t.warning = true
44
- t.test_files = FileList['test/test_win32_file_constants.rb']
45
- end
39
+ Rake::TestTask.new("constants") do |t|
40
+ t.verbose = true
41
+ t.warning = true
42
+ t.test_files = FileList['test/test_win32_file_constants.rb']
43
+ end
46
44
 
47
- Rake::TestTask.new("test_encryption") do |t|
48
- t.verbose = true
49
- t.warning = true
50
- t.test_files = FileList['test/test_win32_file_encryption.rb']
51
- end
45
+ Rake::TestTask.new("encryption") do |t|
46
+ t.verbose = true
47
+ t.warning = true
48
+ t.test_files = FileList['test/test_win32_file_encryption.rb']
49
+ end
52
50
 
53
- Rake::TestTask.new("test_path") do |t|
54
- t.verbose = true
55
- t.warning = true
56
- t.test_files = FileList['test/test_win32_file_path.rb']
57
- end
51
+ Rake::TestTask.new("path") do |t|
52
+ t.verbose = true
53
+ t.warning = true
54
+ t.test_files = FileList['test/test_win32_file_path.rb']
55
+ end
58
56
 
59
- Rake::TestTask.new("test_security") do |t|
60
- t.verbose = true
61
- t.warning = true
62
- t.test_files = FileList['test/test_win32_file_security.rb']
63
- end
57
+ Rake::TestTask.new("security") do |t|
58
+ t.verbose = true
59
+ t.warning = true
60
+ t.test_files = FileList['test/test_win32_file_security.rb']
61
+ end
64
62
 
65
- Rake::TestTask.new("test_stat") do |t|
66
- t.verbose = true
67
- t.warning = true
68
- t.test_files = FileList['test/test_win32_file_stat.rb']
63
+ Rake::TestTask.new("stat") do |t|
64
+ t.verbose = true
65
+ t.warning = true
66
+ t.test_files = FileList['test/test_win32_file_stat.rb']
67
+ end
69
68
  end
data/lib/win32/file.rb CHANGED
@@ -1,1266 +1,1285 @@
1
- require 'windows/security'
2
- require 'windows/limits'
3
- require 'win32/file/stat'
4
-
5
- class File
6
- include Windows::Error
7
- include Windows::File
8
- include Windows::Security
9
- include Windows::Limits
10
- include Windows::DeviceIO
11
- include Windows::Handle
12
-
13
- extend Windows::Error
14
- extend Windows::File
15
- extend Windows::Path
16
- extend Windows::Security
17
- extend Windows::MSVCRT::Buffer
18
- extend Windows::Limits
19
- extend Windows::Handle
20
-
21
- # The version of the win32-file library
22
- WIN32_FILE_VERSION = '0.6.2'
23
-
24
- # Abbreviated attribute constants for convenience
25
-
26
- # The file or directory is an archive. Typically used to mark files for
27
- # backup or removal.
28
- ARCHIVE = FILE_ATTRIBUTE_ARCHIVE
29
-
30
- # The file or directory is encrypted. For a file, this means that all
31
- # data in the file is encrypted. For a directory, this means that
32
- # encryption is # the default for newly created files and subdirectories.
33
- COMPRESSED = FILE_ATTRIBUTE_COMPRESSED
34
-
35
- # The file is hidden. Not included in an ordinary directory listing.
36
- HIDDEN = FILE_ATTRIBUTE_HIDDEN
37
-
38
- # A file that does not have any other attributes set.
39
- NORMAL = FILE_ATTRIBUTE_NORMAL
40
-
41
- # The data of a file is not immediately available. This attribute indicates
42
- # that file data is physically moved to offline storage.
43
- OFFLINE = FILE_ATTRIBUTE_OFFLINE
44
-
45
- # The file is read only. Apps can read it, but not write to it or delete it.
46
- READONLY = FILE_ATTRIBUTE_READONLY
47
-
48
- # The file is part of or used exclusively by an operating system.
49
- SYSTEM = FILE_ATTRIBUTE_SYSTEM
50
-
51
- # The file is being used for temporary storage.
52
- TEMPORARY = FILE_ATTRIBUTE_TEMPORARY
53
-
54
- # The file or directory is to be indexed by the content indexing service.
55
- # Note that we have inverted the traditional definition.
56
- INDEXED = 0x0002000
57
-
58
- # Synonym for File::INDEXED.
59
- CONTENT_INDEXED = INDEXED
60
-
61
- # Custom Security rights
62
-
63
- # Full security rights - read, write, append, execute, and delete.
64
- FULL = STANDARD_RIGHTS_ALL | FILE_READ_DATA | FILE_WRITE_DATA |
65
- FILE_APPEND_DATA | FILE_READ_EA | FILE_WRITE_EA | FILE_EXECUTE |
66
- FILE_DELETE_CHILD | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES
67
-
68
- # Generic write, generic read, execute and delete privileges
69
- CHANGE = FILE_GENERIC_WRITE | FILE_GENERIC_READ | FILE_EXECUTE | DELETE
70
-
71
- # Read and execute privileges
72
- READ = FILE_GENERIC_READ | FILE_EXECUTE
73
-
74
- # Add privileges
75
- ADD = 0x001201bf
76
-
77
- # :stopdoc:
78
-
79
- SECURITY_RIGHTS = {
80
- 'FULL' => FULL,
81
- 'DELETE' => DELETE,
82
- 'READ' => READ,
83
- 'CHANGE' => CHANGE,
84
- 'ADD' => ADD
85
- }
86
-
87
- # :startdoc:
88
-
89
- ### Class Methods
90
-
91
- class << self
92
-
93
- # :stopdoc:
94
-
95
- # Strictly for making this code -w clean. They are removed later.
96
- alias basename_orig basename
97
- alias blockdev_orig blockdev?
98
- alias chardev_orig chardev?
99
- alias directory_orig? directory?
100
- alias dirname_orig dirname
101
- alias lstat_orig lstat
102
- alias readlink_orig readlink
103
- alias size_orig size
104
- alias split_orig split
105
- alias stat_orig stat
106
- alias symlink_orig symlink
107
- alias symlink_orig? symlink?
108
-
109
- # :startdoc:
110
-
111
- ## Security
112
-
113
- # Sets the file permissions for the given file name. The 'permissions'
114
- # argument is a hash with an account name as the key, and the various
115
- # permission constants as possible values. The possible constant values
116
- # are:
117
- #
118
- # * FILE_READ_DATA
119
- # * FILE_WRITE_DATA
120
- # * FILE_APPEND_DATA
121
- # * FILE_READ_EA
122
- # * FILE_WRITE_EA
123
- # * FILE_EXECUTE
124
- # * FILE_DELETE_CHILD
125
- # * FILE_READ_ATTRIBUTES
126
- # * FILE_WRITE_ATTRIBUTES
127
- # * STANDARD_RIGHTS_ALL
128
- # * FULL
129
- # * READ
130
- # * ADD
131
- # * CHANGE
132
- # * DELETE
133
- # * READ_CONTROL
134
- # * WRITE_DAC
135
- # * WRITE_OWNER
136
- # * SYNCHRONIZE
137
- # * STANDARD_RIGHTS_REQUIRED
138
- # * STANDARD_RIGHTS_READ
139
- # * STANDARD_RIGHTS_WRITE
140
- # * STANDARD_RIGHTS_EXECUTE
141
- # * STANDARD_RIGHTS_ALL
142
- # * SPECIFIC_RIGHTS_ALL
143
- # * ACCESS_SYSTEM_SECURITY
144
- # * MAXIMUM_ALLOWED
145
- # * GENERIC_READ
146
- # * GENERIC_WRITE
147
- # * GENERIC_EXECUTE
148
- # * GENERIC_ALL
149
- #
150
- def set_permissions(file, perms)
151
- raise TypeError unless file.is_a?(String)
152
- raise TypeError unless perms.kind_of?(Hash)
153
-
154
- file = multi_to_wide(file)
155
-
156
- account_rights = 0
157
- sec_desc = 0.chr * SECURITY_DESCRIPTOR_MIN_LENGTH
158
-
159
- unless InitializeSecurityDescriptor(sec_desc, 1)
160
- raise ArgumentError, get_last_error
161
- end
162
-
163
- cb_acl = 1024
164
- cb_sid = 1024
165
-
166
- acl_new = 0.chr * cb_acl
167
-
168
- unless InitializeAcl(acl_new, cb_acl, ACL_REVISION2)
169
- raise ArgumentError, get_last_error
170
- end
171
-
172
- sid = 0.chr * cb_sid
173
- snu_type = 0.chr * cb_sid
174
-
175
- all_ace = 0.chr * ALLOW_ACE_LENGTH
176
- all_ace_ptr = memset(all_ace, 0, 0) # address of all_ace
177
-
178
- # all_ace_ptr->Header.AceType = ACCESS_ALLOWED_ACE_TYPE
179
- all_ace[0] = 0
180
-
181
- perms.each{ |account, mask|
182
- next if mask.nil?
183
-
184
- cch_domain = [80].pack('L')
185
- cb_sid = [1024].pack('L')
186
- domain_buf = 0.chr * 80
187
-
188
- server, account = account.split("\\")
189
-
190
- if ['BUILTIN', 'NT AUTHORITY'].include?(server.upcase)
191
- server = nil
192
- end
193
-
194
- val = LookupAccountName(
195
- server,
196
- account,
197
- sid,
198
- cb_sid,
199
- domain_buf,
200
- cch_domain,
201
- snu_type
202
- )
203
-
204
- if val == 0
205
- raise ArgumentError, get_last_error
206
- end
207
-
208
- size = [0,0,0,0,0].pack('CCSLL').length # sizeof(ACCESS_ALLOWED_ACE)
209
-
210
- val = CopySid(
211
- ALLOW_ACE_LENGTH - size,
212
- all_ace_ptr + 8, # address of all_ace_ptr->SidStart
213
- sid
214
- )
215
-
216
- if val == 0
217
- raise ArgumentError, get_last_error
218
- end
219
-
220
- if (GENERIC_ALL & mask).nonzero?
221
- account_rights = GENERIC_ALL & mask
222
- elsif (GENERIC_RIGHTS_CHK & mask).nonzero?
223
- account_rights = GENERIC_RIGHTS_MASK & mask
224
- end
225
-
226
- # all_ace_ptr->Header.AceFlags = INHERIT_ONLY_ACE|OBJECT_INHERIT_ACE
227
- all_ace[1] = INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE
228
-
229
- 2.times{
230
- if account_rights != 0
231
- all_ace[2,2] = [12 - 4 + GetLengthSid(sid)].pack('S')
232
- all_ace[4,4] = [account_rights].pack('L')
233
-
234
- val = AddAce(
235
- acl_new,
236
- ACL_REVISION2,
237
- MAXDWORD,
238
- all_ace_ptr,
239
- all_ace[2,2].unpack('S').first
240
- )
241
-
242
- if val == 0
243
- raise ArgumentError, get_last_error
244
- end
245
-
246
- # all_ace_ptr->Header.AceFlags = CONTAINER_INHERIT_ACE
247
- all_ace[1] = CONTAINER_INHERIT_ACE
248
- else
249
- # all_ace_ptr->Header.AceFlags = 0
250
- all_ace[1] = 0
251
- end
252
-
253
- account_rights = REST_RIGHTS_MASK & mask
254
- }
255
- }
256
-
257
- unless SetSecurityDescriptorDacl(sec_desc, 1, acl_new, 0)
258
- raise ArgumentError, get_last_error
259
- end
260
-
261
- unless SetFileSecurityW(file, DACL_SECURITY_INFORMATION, sec_desc)
262
- raise ArgumentError, get_last_error
263
- end
264
-
265
- self
266
- end
267
-
268
- # Returns an array of human-readable strings that correspond to the
269
- # permission flags.
270
- #
271
- def securities(mask)
272
- sec_array = []
273
- if mask == 0
274
- sec_array.push('NONE')
275
- else
276
- if (mask & FULL) ^ FULL == 0
277
- sec_array.push('FULL')
278
- else
279
- SECURITY_RIGHTS.each{ |string, numeric|
280
- if (numeric & mask) ^ numeric == 0
281
- sec_array.push(string)
282
- end
283
- }
284
- end
285
- end
286
- sec_array
287
- end
288
-
289
- # Returns a hash describing the current file permissions for the given
290
- # file. The account name is the key, and the value is an integer
291
- # representing an or'd value that corresponds to the security
292
- # permissions for that file.
293
- #
294
- # To get a human readable version of the permissions, pass the value to
295
- # the +File.securities+ method.
296
- #
297
- def get_permissions(file, host=nil)
298
- current_length = 0
299
- length_needed = [0].pack('L')
300
- sec_buf = ''
301
-
302
- loop do
303
- bool = GetFileSecurityW(
304
- multi_to_wide(file),
305
- DACL_SECURITY_INFORMATION,
306
- sec_buf,
307
- sec_buf.length,
308
- length_needed
309
- )
310
-
311
- if bool == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER
312
- raise ArgumentError, get_last_error
313
- end
314
-
315
- break if sec_buf.length >= length_needed.unpack('L').first
316
- sec_buf += ' ' * length_needed.unpack('L').first
317
- end
318
-
319
- control = [0].pack('L')
320
- revision = [0].pack('L')
321
-
322
- unless GetSecurityDescriptorControl(sec_buf, control, revision)
323
- raise ArgumentError, get_last_error
324
- end
325
-
326
- # No DACL exists
327
- if (control.unpack('L').first & SE_DACL_PRESENT) == 0
328
- raise ArgumentError, 'No DACL present: explicit deny all'
329
- end
330
-
331
- dacl_present = [0].pack('L')
332
- dacl_defaulted = [0].pack('L')
333
- dacl_ptr = [0].pack('L')
334
-
335
- val = GetSecurityDescriptorDacl(
336
- sec_buf,
337
- dacl_present,
338
- dacl_ptr,
339
- dacl_defaulted
340
- )
341
-
342
- if val == 0
343
- raise ArgumentError, get_last_error
344
- end
345
-
346
- acl_buf = 0.chr * 8 # byte, byte, word, word, word (struct ACL)
347
- memcpy(acl_buf, dacl_ptr.unpack('L').first, acl_buf.size)
348
-
349
- if acl_buf.unpack('CCSSS').first == 0
350
- raise ArgumentError, 'DACL is NULL: implicit access grant'
351
- end
352
-
353
- ace_ptr = [0].pack('L')
354
- ace_count = acl_buf.unpack('CCSSS')[3]
355
-
356
- perms_hash = {}
357
- 0.upto(ace_count - 1){ |i|
358
- unless GetAce(dacl_ptr.unpack('L').first, i, ace_ptr)
359
- next
360
- end
361
-
362
- ace_buf = 0.chr * 12 # ACE_HEADER, dword, dword (ACCESS_ALLOWED_ACE)
363
- memcpy(ace_buf, ace_ptr.unpack('L').first, ace_buf.size)
364
-
365
- if ace_buf.unpack('CCS').first == ACCESS_ALLOWED_ACE_TYPE
366
- name = 0.chr * MAXPATH
367
- name_size = [name.size].pack('L')
368
- domain = 0.chr * MAXPATH
369
- domain_size = [domain.size].pack('L')
370
- snu_ptr = 0.chr * 4
371
-
372
- val = LookupAccountSid(
373
- host,
374
- ace_ptr.unpack('L').first + 8, # address of ace_ptr->SidStart
375
- name,
376
- name_size,
377
- domain,
378
- domain_size,
379
- snu_ptr
380
- )
381
-
382
- if val == 0
383
- raise ArgumentError, get_last_error
384
- end
385
-
386
- name = name[0..name_size.unpack('L').first].split(0.chr)[0]
387
- domain = domain[0..domain_size.unpack('L').first].split(0.chr)[0]
388
- mask = ace_buf.unpack('LLL')[1]
389
-
390
- unless domain.nil? || domain.empty?
391
- name = domain + '\\' + name
392
- end
393
-
394
- perms_hash[name] = mask
395
- end
396
- }
397
- perms_hash
398
- end
399
-
400
- ## Encryption
401
-
402
- # Encrypts a file or directory. All data streams in a file are encrypted.
403
- # All new files created in an encrypted directory are encrypted.
404
- #
405
- # The caller must have the FILE_READ_DATA, FILE_WRITE_DATA,
406
- # FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES, and SYNCHRONIZE access
407
- # rights.
408
- #
409
- # Requires exclusive access to the file being encrypted, and will fail if
410
- # another process is using the file. If the file is compressed,
411
- # EncryptFile will decompress the file before encrypting it.
412
- #
413
- def encrypt(file)
414
- unless EncryptFileW(multi_to_wide(file))
415
- raise ArgumentError, get_last_error
416
- end
417
- self
418
- end
419
-
420
- # Decrypts an encrypted file or directory.
421
- #
422
- # The caller must have the FILE_READ_DATA, FILE_WRITE_DATA,
423
- # FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES, and SYNCHRONIZE access
424
- # rights.
425
- #
426
- # Requires exclusive access to the file being decrypted, and will fail if
427
- # another process is using the file. If the file is not encrypted an error
428
- # is NOT raised.
429
- #
430
- # Windows 2000 or later only.
431
- #
432
- def decrypt(file)
433
- unless DecryptFileW(multi_to_wide(file), 0)
434
- raise ArgumentError, get_last_error
435
- end
436
- self
437
- end
438
-
439
- ## Path methods
440
-
441
- # Returns the last component of the filename given in +filename+. If
442
- # +suffix+ is given and present at the end of +filename+, it is removed.
443
- # Any extension can be removed by giving an extension of ".*".
444
- #
445
- # This was reimplemented because the current version does not handle UNC
446
- # paths properly, i.e. it should not return anything less than the root.
447
- # In all other respects it is identical to the current implementation.
448
- #
449
- # Unlike MRI, this version will convert all forward slashes to
450
- # backslashes automatically.
451
- #
452
- # Examples:
453
- #
454
- # File.basename("C:\\foo\\bar.txt") -> "bar.txt"
455
- # File.basename("C:\\foo\\bar.txt", ".txt") -> "bar"
456
- # File.basename("\\\\foo\\bar") -> "\\\\foo\\bar"
457
- #
458
- def basename(file, suffix = nil)
459
- raise TypeError unless file.is_a?(String)
460
- raise TypeError unless suffix.is_a?(String) if suffix
461
-
462
- return file if file.empty? # Return an empty path as-is.
463
- file = multi_to_wide(file)
464
-
465
- # Required for Windows API functions to work properly.
466
- file.tr!(File::SEPARATOR, File::ALT_SEPARATOR)
467
-
468
- # Return a root path as-is.
469
- return wide_to_multi(file) if PathIsRootW(file)
470
-
471
- PathStripPathW(file) # Gives us the basename
472
-
473
- if suffix
474
- if suffix == '.*'
475
- PathRemoveExtensionW(file)
476
- else
477
- if PathFindExtensionA(wide_to_multi(file)) == suffix
478
- PathRemoveExtensionW(file)
479
- end
480
- end
481
- end
482
-
483
- file = wide_to_multi(file)
484
- file.chop! while file[-1].chr == "\\" # Trim trailing slashes
485
-
486
- file
487
- end
488
-
489
- # Returns true if +file+ is a directory, false otherwise.
490
- #--
491
- # This method was redefined to handle wide character strings.
492
- #
493
- def directory?(file)
494
- file = multi_to_wide(file)
495
- attributes = GetFileAttributesW(file)
496
- (attributes != INVALID_FILE_ATTRIBUTES) && (attributes & FILE_ATTRIBUTE_DIRECTORY > 0)
497
- end
498
-
499
- # Returns all components of the filename given in +filename+ except the
500
- # last one.
501
- #
502
- # This was reimplemented because the current version does not handle UNC
503
- # paths properly, i.e. it should not return anything less than the root.
504
- # In all other respects it is identical to the current implementation.
505
- #
506
- # Also, this method will convert all forward slashes to backslashes.
507
- #
508
- # Examples:
509
- #
510
- # File.dirname("C:\\foo\\bar\\baz.txt") -> "C:\\foo\\bar"
511
- # File.dirname("\\\\foo\\bar") -> "\\\\foo\\bar"
512
- #
513
- def dirname(file)
514
- raise TypeError unless file.is_a?(String)
515
- file = multi_to_wide(file)
516
-
517
- # Convert slashes to backslashes for the Windows API functions
518
- file.tr!(File::SEPARATOR, File::ALT_SEPARATOR)
519
-
520
- # Return a root path as-is.
521
- return wide_to_multi(file) if PathIsRootW(file)
522
-
523
- # Remove trailing file name and backslash, if present
524
- PathRemoveFileSpecW(file)
525
-
526
- file = wide_to_multi(file)
527
-
528
- # Empty paths, short relative paths
529
- if file.nil? || (file && file.empty?)
530
- return '.'
531
- end
532
-
533
- file
534
- end
535
-
536
- # Returns +path+ in long format. For example, if 'SOMEFI~1.TXT'
537
- # was the argument provided, and the short representation for
538
- # 'somefile.txt', then this method would return 'somefile.txt'.
539
- #
540
- # Note that certain file system optimizations may prevent this method
541
- # from working as expected. In that case, you will get back the file
542
- # name in 8.3 format.
543
- #
544
- def long_path(path)
545
- buf = 0.chr * MAXPATH
546
- if GetLongPathNameW(multi_to_wide(path), buf, buf.size) == 0
547
- raise ArgumentError, get_last_error
548
- end
549
- wide_to_multi(buf)
550
- end
551
-
552
- # Returns the path of the of the symbolic link referred to by +file+.
553
- #
554
- # Requires Windows Vista or later. On older versions of Windows it
555
- # will raise a NotImplementedError, as per MRI.
556
- #
557
- def readlink(file)
558
- if defined? GetFinalPathNameByHandle
559
- file = multi_to_wide(file)
560
-
561
- begin
562
- handle = CreateFileW(
563
- file,
564
- GENERIC_READ,
565
- FILE_SHARE_READ,
566
- nil,
567
- OPEN_EXISTING,
568
- FILE_ATTRIBUTE_NORMAL,
569
- nil
570
- )
571
-
572
- if handle == INVALID_HANDLE_VALUE
573
- raise ArgumentError, get_last_error
574
- end
575
-
576
- path = 0.chr * MAXPATH
577
-
578
- GetFinalPathNameByHandleW(handle, path, path.size, 0)
579
- ensure
580
- CloseHandle(handle)
581
- end
582
-
583
- wide_to_multi(path).strip[4..-1] # get rid of prepending "\\?\"
584
- else
585
- msg = "readlink() function is unimplemented on this machine"
586
- raise NotImplementedError, msg
587
- end
588
- end
589
-
590
- # Returns +path+ in 8.3 format. For example, 'c:\documentation.doc'
591
- # would be returned as 'c:\docume~1.doc'.
592
- #
593
- def short_path(path)
594
- buf = 0.chr * MAXPATH
595
- if GetShortPathNameW(multi_to_wide(path), buf, buf.size) == 0
596
- raise ArgumentError, get_last_error
597
- end
598
- wide_to_multi(buf)
599
- end
600
-
601
- # Splits the given string into a directory and a file component and
602
- # returns them in a two element array. This was reimplemented because
603
- # the current version does not handle UNC paths properly.
604
- #
605
- def split(file)
606
- array = []
607
-
608
- if file.empty? || PathIsRootW(multi_to_wide(file))
609
- array.push(file, '')
610
- else
611
- array.push(File.dirname(file), File.basename(file))
612
- end
613
- array
614
- end
615
-
616
- # Creates a symbolic link called +new_name+ for the file or directory
617
- # +old_name+.
618
- #
619
- # This method requires Windows Vista or later to work. Otherwise, it
620
- # returns nil as per MRI.
621
- #
622
- def symlink(old_name, new_name)
623
- if defined? CreateSymbolicLink
624
- old_name = multi_to_wide(old_name)
625
- new_name = multi_to_wide(new_name)
626
- flags = File.directory?(wide_to_multi(old_name)) ? 1 : 0
627
-
628
- unless CreateSymbolicLinkW(new_name, old_name, flags)
629
- raise ArgumentError, get_last_error
630
- end
631
- else
632
- nil
633
- end
634
- end
635
-
636
- # Return true if the named file is a symbolic link, false otherwise.
637
- #
638
- # This method requires Windows Vista or later to work. Otherwise, it
639
- # always returns false as per MRI.
640
- #
641
- def symlink?(file)
642
- bool = false
643
- file = multi_to_wide(file)
644
- attr = GetFileAttributesW(file)
645
-
646
- # Differentiate between a symlink and other kinds of reparse points
647
- if attr & FILE_ATTRIBUTE_REPARSE_POINT > 0
648
- begin
649
- buffer = 0.chr * 278 # WIN32_FIND_DATA
650
- handle = FindFirstFileW(file, buffer)
651
-
652
- if handle == INVALID_HANDLE_VALUE
653
- raise ArgumentError, get_last_error
654
- end
655
-
656
- if buffer[36,4].unpack('L')[0] == IO_REPARSE_TAG_SYMLINK
657
- bool = true
658
- end
659
- ensure
660
- CloseHandle(handle)
661
- end
662
- end
663
-
664
- bool
665
- end
666
-
667
- ## Stat methods
668
-
669
- # Returns a File::Stat object, as defined in the win32-file-stat package.
670
- #
671
- def stat(file)
672
- File::Stat.new(file)
673
- end
674
-
675
- # Identical to File.stat on Windows.
676
- #
677
- def lstat(file)
678
- File::Stat.new(file)
679
- end
680
-
681
- # Returns the file system's block size.
682
- #
683
- def blksize(file)
684
- File::Stat.new(file).blksize
685
- end
686
-
687
- # Returns whether or not +file+ is a block device. For MS Windows this
688
- # means a removable drive, cdrom or ramdisk.
689
- #
690
- def blockdev?(file)
691
- File::Stat.new(file).blockdev?
692
- end
693
-
694
- # Returns true if the file is a character device. This replaces the
695
- # current Ruby implementation which always returns false.
696
- #
697
- def chardev?(file)
698
- File::Stat.new(file).chardev?
699
- end
700
-
701
- # Returns the size of the file in bytes.
702
- #
703
- # This was reimplemented because the current version does not handle file
704
- # sizes greater than 2gb.
705
- #
706
- def size(file)
707
- File::Stat.new(file).size
708
- end
709
-
710
- # We no longer need the aliases, so remove them
711
- remove_method(:basename_orig, :blockdev_orig, :chardev_orig)
712
- remove_method(:dirname_orig, :lstat_orig, :size_orig, :split_orig)
713
- remove_method(:stat_orig, :symlink_orig, :symlink_orig?, :readlink_orig)
714
- remove_method(:directory_orig?)
715
- end # class << self
716
-
717
- ## Attribute methods
718
-
719
- # Returns true if the file or directory is an archive file. Applications
720
- # use this attribute to mark files for backup or removal.
721
- #
722
- def self.archive?(file)
723
- File::Stat.new(file).archive?
724
- end
725
-
726
- # Returns true if the file or directory is compressed. For a file, this
727
- # means that all of the data in the file is compressed. For a directory,
728
- # this means that compression is the default for newly created files and
729
- # subdirectories.
730
- #
731
- def self.compressed?(file)
732
- File::Stat.new(file).compressed?
733
- end
734
-
735
- # Returns true if the file or directory is encrypted. For a file, this
736
- # means that all data in the file is encrypted. For a directory, this
737
- # means that encryption is the default for newly created files and
738
- # subdirectories.
739
- #
740
- def self.encrypted?(file)
741
- File::Stat.new(file).encrypted?
742
- end
743
-
744
- # Returns true if the file or directory is hidden. It is not included
745
- # in an ordinary directory listing.
746
- #
747
- def self.hidden?(file)
748
- File::Stat.new(file).hidden?
749
- end
750
-
751
- # Returns true if the file or directory is indexed by the content indexing
752
- # service.
753
- #
754
- def self.indexed?(file)
755
- File::Stat.new(file).indexed?
756
- end
757
-
758
- # Returns true if the file or directory has no other attributes set.
759
- #
760
- def self.normal?(file)
761
- File::Stat.new(file).normal?
762
- end
763
-
764
- # Returns true if the data of the file is not immediately available. This
765
- # attribute indicates that the file data has been physically moved to
766
- # offline storage. This attribute is used by Remote Storage, the
767
- # hierarchical storage management software. Applications should not
768
- # arbitrarily change this attribute.
769
- #
770
- def self.offline?(file)
771
- File::Stat.new(file).offline?
772
- end
773
-
774
- # Returns true if The file or directory is read-only. Applications can
775
- # read the file but cannot write to it or delete it. In the case of a
776
- # directory, applications cannot delete it.
777
- #
778
- def self.readonly?(file)
779
- File::Stat.new(file).readonly?
780
- end
781
-
782
- # Returns true if the file or directory has an associated reparse point. A
783
- # reparse point is a collection of user defined data associated with a file
784
- # or directory. For more on reparse points, search
785
- # http://msdn.microsoft.com.
786
- #
787
- def self.reparse_point?(file)
788
- File::Stat.new(file).reparse_point?
789
- end
790
-
791
- # Returns true if the file is a sparse file. A sparse file is a file in
792
- # which much of the data is zeros, typically image files. See
793
- # http://msdn.microsoft.com for more details.
794
- #
795
- def self.sparse?(file)
796
- File::Stat.new(file).sparse?
797
- end
798
-
799
- # Returns true if the file or directory is part of the operating system
800
- # or is used exclusively by the operating system.
801
- #
802
- def self.system?(file)
803
- File::Stat.new(file).system?
804
- end
805
-
806
- # Returns true if the file is being used for temporary storage.
807
- #
808
- # File systems avoid writing data back to mass storage if sufficient cache
809
- # memory is available, because often the application deletes the temporary
810
- # file shortly after the handle is closed. In that case, the system can
811
- # entirely avoid writing the data. Otherwise, the data will be written after
812
- # the handle is closed.
813
- #
814
- def self.temporary?(file)
815
- File::Stat.new(file).temporary?
816
- end
817
-
818
- # Returns an array of strings indicating the attributes for that file. The
819
- # possible values are:
820
- #
821
- # archive
822
- # compressed
823
- # directory
824
- # encrypted
825
- # hidden
826
- # indexed
827
- # normal
828
- # offline
829
- # readonly
830
- # reparse_point
831
- # sparse
832
- # system
833
- # temporary
834
- #
835
- def self.attributes(file)
836
- attributes = GetFileAttributesW(multi_to_wide(file))
837
-
838
- if attributes == INVALID_FILE_ATTRIBUTES
839
- raise ArgumentError, get_last_error
840
- end
841
-
842
- arr = []
843
-
844
- arr << 'archive' if attributes & FILE_ATTRIBUTE_ARCHIVE > 0
845
- arr << 'compressed' if attributes & FILE_ATTRIBUTE_COMPRESSED > 0
846
- arr << 'directory' if attributes & FILE_ATTRIBUTE_DIRECTORY > 0
847
- arr << 'encrypted' if attributes & FILE_ATTRIBUTE_ENCRYPTED > 0
848
- arr << 'hidden' if attributes & FILE_ATTRIBUTE_HIDDEN > 0
849
- arr << 'indexed' if attributes & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED == 0
850
- arr << 'normal' if attributes & FILE_ATTRIBUTE_NORMAL > 0
851
- arr << 'offline' if attributes & FILE_ATTRIBUTE_OFFLINE > 0
852
- arr << 'readonly' if attributes & FILE_ATTRIBUTE_READONLY > 0
853
- arr << 'reparse_point' if attributes & FILE_ATTRIBUTE_REPARSE_POINT > 0
854
- arr << 'sparse' if attributes & FILE_ATTRIBUTE_SPARSE_FILE > 0
855
- arr << 'system' if attributes & FILE_ATTRIBUTE_SYSTEM > 0
856
- arr << 'temporary' if attributes & FILE_ATTRIBUTE_TEMPORARY > 0
857
-
858
- arr
859
- end
860
-
861
- # Sets the file attributes based on the given (numeric) +flags+. This does
862
- # not remove existing attributes, it merely adds to them.
863
- #
864
- def self.set_attributes(file, flags)
865
- file = multi_to_wide(file)
866
- attributes = GetFileAttributesW(file)
867
-
868
- if attributes == INVALID_FILE_ATTRIBUTES
869
- raise ArgumentError, get_last_error
870
- end
871
-
872
- attributes |= flags
873
-
874
- if SetFileAttributesW(file, attributes) == 0
875
- raise ArgumentError, get_last_error
876
- end
877
-
878
- self
879
- end
880
-
881
- # Removes the file attributes based on the given (numeric) +flags+.
882
- #
883
- def self.remove_attributes(file, flags)
884
- file = multi_to_wide(file)
885
- attributes = GetFileAttributesW(file)
886
-
887
- if attributes == INVALID_FILE_ATTRIBUTES
888
- raise ArgumentError, get_last_error
889
- end
890
-
891
- attributes &= ~flags
892
-
893
- if SetFileAttributesW(file, attributes) == 0
894
- raise ArgumentError, get_last_error
895
- end
896
-
897
- self
898
- end
899
-
900
- # Instance methods
901
-
902
- def stat
903
- File::Stat.new(self.path)
904
- end
905
-
906
- # Sets whether or not the file is an archive file.
907
- #
908
- def archive=(bool)
909
- wide_path = multi_to_wide(self.path)
910
- attributes = GetFileAttributesW(wide_path)
911
-
912
- if attributes == INVALID_FILE_ATTRIBUTES
913
- raise ArgumentError, get_last_error
914
- end
915
-
916
- if bool
917
- attributes |= FILE_ATTRIBUTE_ARCHIVE;
918
- else
919
- attributes &= ~FILE_ATTRIBUTE_ARCHIVE;
920
- end
921
-
922
- if SetFileAttributesW(wide_path, attributes) == 0
923
- raise ArgumentError, get_last_error
924
- end
925
-
926
- self
927
- end
928
-
929
- # Sets whether or not the file is a compressed file.
930
- #
931
- def compressed=(bool)
932
- in_buf = bool ? COMPRESSION_FORMAT_DEFAULT : COMPRESSION_FORMAT_NONE
933
- in_buf = [in_buf].pack('L')
934
- bytes = [0].pack('L')
935
-
936
- # We can't use get_osfhandle here because we need specific attributes
937
- handle = CreateFileW(
938
- multi_to_wide(self.path),
939
- FILE_READ_DATA | FILE_WRITE_DATA,
940
- FILE_SHARE_READ | FILE_SHARE_WRITE,
941
- 0,
942
- OPEN_EXISTING,
943
- 0,
944
- 0
945
- )
946
-
947
- if handle == INVALID_HANDLE_VALUE
948
- raise ArgumentError, get_last_error
949
- end
950
-
951
- begin
952
- bool = DeviceIoControl(
953
- handle,
954
- FSCTL_SET_COMPRESSION(),
955
- in_buf,
956
- in_buf.length,
957
- 0,
958
- 0,
959
- bytes,
960
- 0
961
- )
962
-
963
- unless bool
964
- raise ArgumentError, get_last_error
965
- end
966
- ensure
967
- CloseHandle(handle)
968
- end
969
-
970
- self
971
- end
972
-
973
- # Sets the hidden attribute to true or false. Setting this attribute to
974
- # true means that the file is not included in an ordinary directory listing.
975
- #
976
- def hidden=(bool)
977
- wide_path = multi_to_wide(self.path)
978
- attributes = GetFileAttributesW(wide_path)
979
-
980
- if attributes == INVALID_FILE_ATTRIBUTES
981
- raise ArgumentError, get_last_error
982
- end
983
-
984
- if bool
985
- attributes |= FILE_ATTRIBUTE_HIDDEN;
986
- else
987
- attributes &= ~FILE_ATTRIBUTE_HIDDEN;
988
- end
989
-
990
- if SetFileAttributesW(wide_path, attributes) == 0
991
- raise ArgumentError, get_last_error
992
- end
993
-
994
- self
995
- end
996
-
997
- # Sets the 'indexed' attribute to true or false. Setting this to
998
- # false means that the file will not be indexed by the content indexing
999
- # service.
1000
- #
1001
- def indexed=(bool)
1002
- wide_path = multi_to_wide(self.path)
1003
- attributes = GetFileAttributesW(wide_path)
1004
-
1005
- if attributes == INVALID_FILE_ATTRIBUTES
1006
- raise ArgumentError, get_last_error
1007
- end
1008
-
1009
- if bool
1010
- attributes &= ~FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
1011
- else
1012
- attributes |= FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
1013
- end
1014
-
1015
- if SetFileAttributes(wide_path, attributes) == 0
1016
- raise ArgumentError, get_last_error
1017
- end
1018
-
1019
- self
1020
- end
1021
-
1022
- alias :content_indexed= :indexed=
1023
-
1024
- # Sets the normal attribute. Note that only 'true' is a valid argument,
1025
- # which has the effect of removing most other attributes. Attempting to
1026
- # pass any value except true will raise an ArgumentError.
1027
- #
1028
- def normal=(bool)
1029
- unless bool
1030
- raise ArgumentError, "only 'true' may be passed as an argument"
1031
- end
1032
-
1033
- if SetFileAttributesW(multi_to_wide(self.path),FILE_ATTRIBUTE_NORMAL) == 0
1034
- raise ArgumentError, get_last_error
1035
- end
1036
-
1037
- self
1038
- end
1039
-
1040
- # Sets whether or not a file is online or not. Setting this to false means
1041
- # that the data of the file is not immediately available. This attribute
1042
- # indicates that the file data has been physically moved to offline storage.
1043
- # This attribute is used by Remote Storage, the hierarchical storage
1044
- # management software.
1045
- #
1046
- # Applications should not arbitrarily change this attribute.
1047
- #
1048
- def offline=(bool)
1049
- wide_path = multi_to_wide(self.path)
1050
- attributes = GetFileAttributesW(wide_path)
1051
-
1052
- if attributes == INVALID_FILE_ATTRIBUTES
1053
- raise ArgumentError, get_last_error
1054
- end
1055
-
1056
- if bool
1057
- attributes |= FILE_ATTRIBUTE_OFFLINE;
1058
- else
1059
- attributes &= ~FILE_ATTRIBUTE_OFFLINE;
1060
- end
1061
-
1062
- if SetFileAttributesW(wide_path, attributes) == 0
1063
- raise ArgumentError, get_last_error
1064
- end
1065
-
1066
- self
1067
- end
1068
-
1069
- # Sets the readonly attribute. If set to true the the file or directory is
1070
- # readonly. Applications can read the file but cannot write to it or delete
1071
- # it. In the case of a directory, applications cannot delete it.
1072
- #
1073
- def readonly=(bool)
1074
- wide_path = multi_to_wide(self.path)
1075
- attributes = GetFileAttributesW(wide_path)
1076
-
1077
- if attributes == INVALID_FILE_ATTRIBUTES
1078
- raise ArgumentError, get_last_error
1079
- end
1080
-
1081
- if bool
1082
- attributes |= FILE_ATTRIBUTE_READONLY;
1083
- else
1084
- attributes &= ~FILE_ATTRIBUTE_READONLY;
1085
- end
1086
-
1087
- if SetFileAttributesW(wide_path, attributes) == 0
1088
- raise ArgumentError, get_last_error
1089
- end
1090
-
1091
- self
1092
- end
1093
-
1094
- # Sets the file to a sparse (usually image) file. Note that you cannot
1095
- # remove the sparse property from a file.
1096
- #
1097
- def sparse=(bool)
1098
- unless bool
1099
- warn 'cannot remove sparse property from a file - operation ignored'
1100
- return
1101
- end
1102
-
1103
- bytes = [0].pack('L')
1104
-
1105
- handle = CreateFileW(
1106
- multi_to_wide(self.path),
1107
- FILE_READ_DATA | FILE_WRITE_DATA,
1108
- FILE_SHARE_READ | FILE_SHARE_WRITE,
1109
- 0,
1110
- OPEN_EXISTING,
1111
- FSCTL_SET_SPARSE(),
1112
- 0
1113
- )
1114
-
1115
- if handle == INVALID_HANDLE_VALUE
1116
- raise ArgumentError, get_last_error
1117
- end
1118
-
1119
- begin
1120
- bool = DeviceIoControl(
1121
- handle,
1122
- FSCTL_SET_SPARSE(),
1123
- 0,
1124
- 0,
1125
- 0,
1126
- 0,
1127
- bytes,
1128
- 0
1129
- )
1130
-
1131
- unless bool == 0
1132
- raise ArgumentError, get_last_error
1133
- end
1134
- ensure
1135
- CloseHandle(handle)
1136
- end
1137
-
1138
- self
1139
- end
1140
-
1141
- # Set whether or not the file is a system file. A system file is a file
1142
- # that is part of the operating system or is used exclusively by it.
1143
- #
1144
- def system=(bool)
1145
- wide_path = multi_to_wide(self.path)
1146
- attributes = GetFileAttributesW(wide_path)
1147
-
1148
- if attributes == INVALID_FILE_ATTRIBUTES
1149
- raise ArgumentError, get_last_error
1150
- end
1151
-
1152
- if bool
1153
- attributes |= FILE_ATTRIBUTE_SYSTEM;
1154
- else
1155
- attributes &= ~FILE_ATTRIBUTE_SYSTEM;
1156
- end
1157
-
1158
- if SetFileAttributesW(wide_path, attributes) == 0
1159
- raise ArgumentError, get_last_error
1160
- end
1161
-
1162
- self
1163
- end
1164
-
1165
- # Sets whether or not the file is being used for temporary storage.
1166
- #
1167
- # File systems avoid writing data back to mass storage if sufficient cache
1168
- # memory is available, because often the application deletes the temporary
1169
- # file shortly after the handle is closed. In that case, the system can
1170
- # entirely avoid writing the data. Otherwise, the data will be written
1171
- # after the handle is closed.
1172
- #
1173
- def temporary=(bool)
1174
- wide_path = multi_to_wide(self.path)
1175
- attributes = GetFileAttributesW(wide_path)
1176
-
1177
- if attributes == INVALID_FILE_ATTRIBUTES
1178
- raise ArgumentError, get_last_error
1179
- end
1180
-
1181
- if bool
1182
- attributes |= FILE_ATTRIBUTE_TEMPORARY;
1183
- else
1184
- attributes &= ~FILE_ATTRIBUTE_TEMPORARY;
1185
- end
1186
-
1187
- if SetFileAttributesW(wide_path, attributes) == 0
1188
- raise ArgumentError, get_last_error
1189
- end
1190
-
1191
- self
1192
- end
1193
-
1194
- # Singleton aliases, mostly for backwards compatibility
1195
- class << self
1196
- alias :read_only? :readonly?
1197
- alias :content_indexed? :indexed?
1198
- alias :set_attr :set_attributes
1199
- alias :unset_attr :remove_attributes
1200
- end
1201
-
1202
- private
1203
-
1204
- # Get the owner of a given file by name.
1205
- #
1206
- def get_owner(file)
1207
- handle = CreateFile(
1208
- file,
1209
- GENERIC_READ,
1210
- FILE_SHARE_READ,
1211
- 0,
1212
- OPEN_EXISTING,
1213
- 0,
1214
- 0
1215
- )
1216
-
1217
- if handle == INVALID_HANDLE_VALUE
1218
- raise ArgumentError, get_last_error
1219
- end
1220
-
1221
- sid = 0.chr * 28
1222
- sid_ptr = [0].pack('L')
1223
-
1224
- begin
1225
- rv = GetSecurityInfo(
1226
- handle,
1227
- SE_FILE_OBJECT,
1228
- OWNER_SECURITY_INFORMATION,
1229
- sid_ptr,
1230
- nil,
1231
- nil,
1232
- nil,
1233
- nil
1234
- )
1235
-
1236
- if rv != 0
1237
- raise get_last_error
1238
- end
1239
-
1240
- name_buf = 0.chr * 80
1241
- name_cch = [name_buf.size].pack('L')
1242
-
1243
- domain_buf = 0.chr * 80
1244
- domain_cch = [domain_buf.size].pack('L')
1245
- sid_name = 0.chr * 4
1246
-
1247
- bool = LookupAccountSid(
1248
- nil,
1249
- sid_ptr.unpack('L').first,
1250
- name_buf,
1251
- name_cch,
1252
- domain_buf,
1253
- domain_cch,
1254
- sid_name
1255
- )
1256
-
1257
- unless bool
1258
- raise get_last_error
1259
- end
1260
- ensure
1261
- CloseHandle(handle)
1262
- end
1263
-
1264
- name_buf.strip
1265
- end
1266
- end
1
+ require 'windows/security'
2
+ require 'windows/limits'
3
+ require 'win32/file/stat'
4
+
5
+ class File
6
+ include Windows::Error
7
+ include Windows::File
8
+ include Windows::Security
9
+ include Windows::Limits
10
+ include Windows::DeviceIO
11
+ include Windows::Handle
12
+
13
+ extend Windows::Error
14
+ extend Windows::File
15
+ extend Windows::Path
16
+ extend Windows::Security
17
+ extend Windows::MSVCRT::Buffer
18
+ extend Windows::Limits
19
+ extend Windows::Handle
20
+
21
+ # The version of the win32-file library
22
+ WIN32_FILE_VERSION = '0.6.4'
23
+
24
+ # Abbreviated attribute constants for convenience
25
+
26
+ # The file or directory is an archive. Typically used to mark files for
27
+ # backup or removal.
28
+ ARCHIVE = FILE_ATTRIBUTE_ARCHIVE
29
+
30
+ # The file or directory is encrypted. For a file, this means that all
31
+ # data in the file is encrypted. For a directory, this means that
32
+ # encryption is # the default for newly created files and subdirectories.
33
+ COMPRESSED = FILE_ATTRIBUTE_COMPRESSED
34
+
35
+ # The file is hidden. Not included in an ordinary directory listing.
36
+ HIDDEN = FILE_ATTRIBUTE_HIDDEN
37
+
38
+ # A file that does not have any other attributes set.
39
+ NORMAL = FILE_ATTRIBUTE_NORMAL
40
+
41
+ # The data of a file is not immediately available. This attribute indicates
42
+ # that file data is physically moved to offline storage.
43
+ OFFLINE = FILE_ATTRIBUTE_OFFLINE
44
+
45
+ # The file is read only. Apps can read it, but not write to it or delete it.
46
+ READONLY = FILE_ATTRIBUTE_READONLY
47
+
48
+ # The file is part of or used exclusively by an operating system.
49
+ SYSTEM = FILE_ATTRIBUTE_SYSTEM
50
+
51
+ # The file is being used for temporary storage.
52
+ TEMPORARY = FILE_ATTRIBUTE_TEMPORARY
53
+
54
+ # The file or directory is to be indexed by the content indexing service.
55
+ # Note that we have inverted the traditional definition.
56
+ INDEXED = 0x0002000
57
+
58
+ # Synonym for File::INDEXED.
59
+ CONTENT_INDEXED = INDEXED
60
+
61
+ # Custom Security rights
62
+
63
+ # Full security rights - read, write, append, execute, and delete.
64
+ FULL = STANDARD_RIGHTS_ALL | FILE_READ_DATA | FILE_WRITE_DATA |
65
+ FILE_APPEND_DATA | FILE_READ_EA | FILE_WRITE_EA | FILE_EXECUTE |
66
+ FILE_DELETE_CHILD | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES
67
+
68
+ # Generic write, generic read, execute and delete privileges
69
+ CHANGE = FILE_GENERIC_WRITE | FILE_GENERIC_READ | FILE_EXECUTE | DELETE
70
+
71
+ # Read and execute privileges
72
+ READ = FILE_GENERIC_READ | FILE_EXECUTE
73
+
74
+ # Add privileges
75
+ ADD = 0x001201bf
76
+
77
+ # :stopdoc:
78
+
79
+ SECURITY_RIGHTS = {
80
+ 'FULL' => FULL,
81
+ 'DELETE' => DELETE,
82
+ 'READ' => READ,
83
+ 'CHANGE' => CHANGE,
84
+ 'ADD' => ADD
85
+ }
86
+
87
+ # :startdoc:
88
+
89
+ ### Class Methods
90
+
91
+ class << self
92
+
93
+ # :stopdoc:
94
+
95
+ # Strictly for making this code -w clean. They are removed later.
96
+ alias basename_orig basename
97
+ alias blockdev_orig blockdev?
98
+ alias chardev_orig chardev?
99
+ alias directory_orig? directory?
100
+ alias dirname_orig dirname
101
+ alias join_orig join
102
+ alias lstat_orig lstat
103
+ alias readlink_orig readlink
104
+ alias size_orig size
105
+ alias split_orig split
106
+ alias stat_orig stat
107
+ alias symlink_orig symlink
108
+ alias symlink_orig? symlink?
109
+
110
+ # :startdoc:
111
+
112
+ ## Security
113
+
114
+ # Sets the file permissions for the given file name. The 'permissions'
115
+ # argument is a hash with an account name as the key, and the various
116
+ # permission constants as possible values. The possible constant values
117
+ # are:
118
+ #
119
+ # * FILE_READ_DATA
120
+ # * FILE_WRITE_DATA
121
+ # * FILE_APPEND_DATA
122
+ # * FILE_READ_EA
123
+ # * FILE_WRITE_EA
124
+ # * FILE_EXECUTE
125
+ # * FILE_DELETE_CHILD
126
+ # * FILE_READ_ATTRIBUTES
127
+ # * FILE_WRITE_ATTRIBUTES
128
+ # * STANDARD_RIGHTS_ALL
129
+ # * FULL
130
+ # * READ
131
+ # * ADD
132
+ # * CHANGE
133
+ # * DELETE
134
+ # * READ_CONTROL
135
+ # * WRITE_DAC
136
+ # * WRITE_OWNER
137
+ # * SYNCHRONIZE
138
+ # * STANDARD_RIGHTS_REQUIRED
139
+ # * STANDARD_RIGHTS_READ
140
+ # * STANDARD_RIGHTS_WRITE
141
+ # * STANDARD_RIGHTS_EXECUTE
142
+ # * STANDARD_RIGHTS_ALL
143
+ # * SPECIFIC_RIGHTS_ALL
144
+ # * ACCESS_SYSTEM_SECURITY
145
+ # * MAXIMUM_ALLOWED
146
+ # * GENERIC_READ
147
+ # * GENERIC_WRITE
148
+ # * GENERIC_EXECUTE
149
+ # * GENERIC_ALL
150
+ #
151
+ def set_permissions(file, perms)
152
+ raise TypeError unless file.is_a?(String)
153
+ raise TypeError unless perms.kind_of?(Hash)
154
+
155
+ file = multi_to_wide(file)
156
+
157
+ account_rights = 0
158
+ sec_desc = 0.chr * SECURITY_DESCRIPTOR_MIN_LENGTH
159
+
160
+ unless InitializeSecurityDescriptor(sec_desc, 1)
161
+ raise ArgumentError, get_last_error
162
+ end
163
+
164
+ cb_acl = 1024
165
+ cb_sid = 1024
166
+
167
+ acl_new = 0.chr * cb_acl
168
+
169
+ unless InitializeAcl(acl_new, cb_acl, ACL_REVISION2)
170
+ raise ArgumentError, get_last_error
171
+ end
172
+
173
+ sid = 0.chr * cb_sid
174
+ snu_type = 0.chr * cb_sid
175
+
176
+ all_ace = 0.chr * ALLOW_ACE_LENGTH
177
+ all_ace_ptr = memset(all_ace, 0, 0) # address of all_ace
178
+
179
+ # all_ace_ptr->Header.AceType = ACCESS_ALLOWED_ACE_TYPE
180
+ all_ace[0] = 0
181
+
182
+ perms.each{ |account, mask|
183
+ next if mask.nil?
184
+
185
+ cch_domain = [80].pack('L')
186
+ cb_sid = [1024].pack('L')
187
+ domain_buf = 0.chr * 80
188
+
189
+ server, account = account.split("\\")
190
+
191
+ if ['BUILTIN', 'NT AUTHORITY'].include?(server.upcase)
192
+ server = nil
193
+ end
194
+
195
+ val = LookupAccountName(
196
+ server,
197
+ account,
198
+ sid,
199
+ cb_sid,
200
+ domain_buf,
201
+ cch_domain,
202
+ snu_type
203
+ )
204
+
205
+ if val == 0
206
+ raise ArgumentError, get_last_error
207
+ end
208
+
209
+ size = [0,0,0,0,0].pack('CCSLL').length # sizeof(ACCESS_ALLOWED_ACE)
210
+
211
+ val = CopySid(
212
+ ALLOW_ACE_LENGTH - size,
213
+ all_ace_ptr + 8, # address of all_ace_ptr->SidStart
214
+ sid
215
+ )
216
+
217
+ if val == 0
218
+ raise ArgumentError, get_last_error
219
+ end
220
+
221
+ if (GENERIC_ALL & mask).nonzero?
222
+ account_rights = GENERIC_ALL & mask
223
+ elsif (GENERIC_RIGHTS_CHK & mask).nonzero?
224
+ account_rights = GENERIC_RIGHTS_MASK & mask
225
+ end
226
+
227
+ # all_ace_ptr->Header.AceFlags = INHERIT_ONLY_ACE|OBJECT_INHERIT_ACE
228
+ all_ace[1] = INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE
229
+
230
+ 2.times{
231
+ if account_rights != 0
232
+ all_ace[2,2] = [12 - 4 + GetLengthSid(sid)].pack('S')
233
+ all_ace[4,4] = [account_rights].pack('L')
234
+
235
+ val = AddAce(
236
+ acl_new,
237
+ ACL_REVISION2,
238
+ MAXDWORD,
239
+ all_ace_ptr,
240
+ all_ace[2,2].unpack('S').first
241
+ )
242
+
243
+ if val == 0
244
+ raise ArgumentError, get_last_error
245
+ end
246
+
247
+ # all_ace_ptr->Header.AceFlags = CONTAINER_INHERIT_ACE
248
+ all_ace[1] = CONTAINER_INHERIT_ACE
249
+ else
250
+ # all_ace_ptr->Header.AceFlags = 0
251
+ all_ace[1] = 0
252
+ end
253
+
254
+ account_rights = REST_RIGHTS_MASK & mask
255
+ }
256
+ }
257
+
258
+ unless SetSecurityDescriptorDacl(sec_desc, 1, acl_new, 0)
259
+ raise ArgumentError, get_last_error
260
+ end
261
+
262
+ unless SetFileSecurityW(file, DACL_SECURITY_INFORMATION, sec_desc)
263
+ raise ArgumentError, get_last_error
264
+ end
265
+
266
+ self
267
+ end
268
+
269
+ # Returns an array of human-readable strings that correspond to the
270
+ # permission flags.
271
+ #
272
+ def securities(mask)
273
+ sec_array = []
274
+ if mask == 0
275
+ sec_array.push('NONE')
276
+ else
277
+ if (mask & FULL) ^ FULL == 0
278
+ sec_array.push('FULL')
279
+ else
280
+ SECURITY_RIGHTS.each{ |string, numeric|
281
+ if (numeric & mask) ^ numeric == 0
282
+ sec_array.push(string)
283
+ end
284
+ }
285
+ end
286
+ end
287
+ sec_array
288
+ end
289
+
290
+ # Returns a hash describing the current file permissions for the given
291
+ # file. The account name is the key, and the value is an integer
292
+ # representing an or'd value that corresponds to the security
293
+ # permissions for that file.
294
+ #
295
+ # To get a human readable version of the permissions, pass the value to
296
+ # the +File.securities+ method.
297
+ #
298
+ def get_permissions(file, host=nil)
299
+ current_length = 0
300
+ length_needed = [0].pack('L')
301
+ sec_buf = ''
302
+
303
+ loop do
304
+ bool = GetFileSecurityW(
305
+ multi_to_wide(file),
306
+ DACL_SECURITY_INFORMATION,
307
+ sec_buf,
308
+ sec_buf.length,
309
+ length_needed
310
+ )
311
+
312
+ if bool == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER
313
+ raise ArgumentError, get_last_error
314
+ end
315
+
316
+ break if sec_buf.length >= length_needed.unpack('L').first
317
+ sec_buf += ' ' * length_needed.unpack('L').first
318
+ end
319
+
320
+ control = [0].pack('L')
321
+ revision = [0].pack('L')
322
+
323
+ unless GetSecurityDescriptorControl(sec_buf, control, revision)
324
+ raise ArgumentError, get_last_error
325
+ end
326
+
327
+ # No DACL exists
328
+ if (control.unpack('L').first & SE_DACL_PRESENT) == 0
329
+ raise ArgumentError, 'No DACL present: explicit deny all'
330
+ end
331
+
332
+ dacl_present = [0].pack('L')
333
+ dacl_defaulted = [0].pack('L')
334
+ dacl_ptr = [0].pack('L')
335
+
336
+ val = GetSecurityDescriptorDacl(
337
+ sec_buf,
338
+ dacl_present,
339
+ dacl_ptr,
340
+ dacl_defaulted
341
+ )
342
+
343
+ if val == 0
344
+ raise ArgumentError, get_last_error
345
+ end
346
+
347
+ acl_buf = 0.chr * 8 # byte, byte, word, word, word (struct ACL)
348
+ memcpy(acl_buf, dacl_ptr.unpack('L').first, acl_buf.size)
349
+
350
+ if acl_buf.unpack('CCSSS').first == 0
351
+ raise ArgumentError, 'DACL is NULL: implicit access grant'
352
+ end
353
+
354
+ ace_ptr = [0].pack('L')
355
+ ace_count = acl_buf.unpack('CCSSS')[3]
356
+
357
+ perms_hash = {}
358
+
359
+ 0.upto(ace_count - 1){ |i|
360
+ unless GetAce(dacl_ptr.unpack('L').first, i, ace_ptr)
361
+ next
362
+ end
363
+
364
+ ace_buf = 0.chr * 12 # ACE_HEADER, dword, dword (ACCESS_ALLOWED_ACE)
365
+ memcpy(ace_buf, ace_ptr.unpack('L').first, ace_buf.size)
366
+
367
+ if ace_buf.unpack('CCS').first == ACCESS_ALLOWED_ACE_TYPE
368
+ name = 0.chr * MAXPATH
369
+ name_size = [name.size].pack('L')
370
+ domain = 0.chr * MAXPATH
371
+ domain_size = [domain.size].pack('L')
372
+ snu_ptr = 0.chr * 4
373
+
374
+ val = LookupAccountSid(
375
+ host,
376
+ ace_ptr.unpack('L').first + 8, # address of ace_ptr->SidStart
377
+ name,
378
+ name_size,
379
+ domain,
380
+ domain_size,
381
+ snu_ptr
382
+ )
383
+
384
+ if val == 0
385
+ raise ArgumentError, get_last_error
386
+ end
387
+
388
+ name = name[0..name_size.unpack('L').first].split(0.chr)[0]
389
+ domain = domain[0..domain_size.unpack('L').first].split(0.chr)[0]
390
+ mask = ace_buf.unpack('LLL')[1]
391
+
392
+ unless domain.nil? || domain.empty?
393
+ name = domain + '\\' + name
394
+ end
395
+
396
+ perms_hash[name] = mask
397
+ end
398
+ }
399
+ perms_hash
400
+ end
401
+
402
+ ## Encryption
403
+
404
+ # Encrypts a file or directory. All data streams in a file are encrypted.
405
+ # All new files created in an encrypted directory are encrypted.
406
+ #
407
+ # The caller must have the FILE_READ_DATA, FILE_WRITE_DATA,
408
+ # FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES, and SYNCHRONIZE access
409
+ # rights.
410
+ #
411
+ # Requires exclusive access to the file being encrypted, and will fail if
412
+ # another process is using the file. If the file is compressed,
413
+ # EncryptFile will decompress the file before encrypting it.
414
+ #
415
+ def encrypt(file)
416
+ unless EncryptFileW(multi_to_wide(file))
417
+ raise ArgumentError, get_last_error
418
+ end
419
+ self
420
+ end
421
+
422
+ # Decrypts an encrypted file or directory.
423
+ #
424
+ # The caller must have the FILE_READ_DATA, FILE_WRITE_DATA,
425
+ # FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES, and SYNCHRONIZE access
426
+ # rights.
427
+ #
428
+ # Requires exclusive access to the file being decrypted, and will fail if
429
+ # another process is using the file. If the file is not encrypted an error
430
+ # is NOT raised.
431
+ #
432
+ # Windows 2000 or later only.
433
+ #
434
+ def decrypt(file)
435
+ unless DecryptFileW(multi_to_wide(file), 0)
436
+ raise ArgumentError, get_last_error
437
+ end
438
+ self
439
+ end
440
+
441
+ ## Path methods
442
+
443
+ # Returns the last component of the filename given in +filename+. If
444
+ # +suffix+ is given and present at the end of +filename+, it is removed.
445
+ # Any extension can be removed by giving an extension of ".*".
446
+ #
447
+ # This was reimplemented because the current version does not handle UNC
448
+ # paths properly, i.e. it should not return anything less than the root.
449
+ # In all other respects it is identical to the current implementation.
450
+ #
451
+ # Unlike MRI, this version will convert all forward slashes to
452
+ # backslashes automatically.
453
+ #
454
+ # Examples:
455
+ #
456
+ # File.basename("C:\\foo\\bar.txt") -> "bar.txt"
457
+ # File.basename("C:\\foo\\bar.txt", ".txt") -> "bar"
458
+ # File.basename("\\\\foo\\bar") -> "\\\\foo\\bar"
459
+ #
460
+ def basename(file, suffix = nil)
461
+ raise TypeError unless file.is_a?(String)
462
+ raise TypeError unless suffix.is_a?(String) if suffix
463
+
464
+ return file if file.empty? # Return an empty path as-is.
465
+ file = multi_to_wide(file)
466
+
467
+ # Required for Windows API functions to work properly.
468
+ file.tr!(File::SEPARATOR, File::ALT_SEPARATOR)
469
+
470
+ # Return a root path as-is.
471
+ return wide_to_multi(file) if PathIsRootW(file)
472
+
473
+ PathStripPathW(file) # Gives us the basename
474
+
475
+ if suffix
476
+ if suffix == '.*'
477
+ PathRemoveExtensionW(file)
478
+ else
479
+ if PathFindExtensionA(wide_to_multi(file)) == suffix
480
+ PathRemoveExtensionW(file)
481
+ end
482
+ end
483
+ end
484
+
485
+ file = wide_to_multi(file)
486
+ file.chop! while file[-1].chr == "\\" # Trim trailing slashes
487
+
488
+ file
489
+ end
490
+
491
+ # Returns true if +file+ is a directory, false otherwise.
492
+ #--
493
+ # This method was redefined to handle wide character strings.
494
+ #
495
+ def directory?(file)
496
+ file = multi_to_wide(file)
497
+ attributes = GetFileAttributesW(file)
498
+ (attributes != INVALID_FILE_ATTRIBUTES) && (attributes & FILE_ATTRIBUTE_DIRECTORY > 0)
499
+ end
500
+
501
+ # Returns all components of the filename given in +filename+ except the
502
+ # last one.
503
+ #
504
+ # This was reimplemented because the current version does not handle UNC
505
+ # paths properly, i.e. it should not return anything less than the root.
506
+ # In all other respects it is identical to the current implementation.
507
+ #
508
+ # Also, this method will convert all forward slashes to backslashes.
509
+ #
510
+ # Examples:
511
+ #
512
+ # File.dirname("C:\\foo\\bar\\baz.txt") -> "C:\\foo\\bar"
513
+ # File.dirname("\\\\foo\\bar") -> "\\\\foo\\bar"
514
+ #
515
+ def dirname(file)
516
+ raise TypeError unless file.is_a?(String)
517
+ file = multi_to_wide(file)
518
+
519
+ # Convert slashes to backslashes for the Windows API functions
520
+ file.tr!(File::SEPARATOR, File::ALT_SEPARATOR)
521
+
522
+ # Return a root path as-is.
523
+ return wide_to_multi(file) if PathIsRootW(file)
524
+
525
+ # Remove trailing file name and backslash, if present
526
+ PathRemoveFileSpecW(file)
527
+
528
+ file = wide_to_multi(file)
529
+
530
+ # Empty paths, short relative paths
531
+ if file.nil? || (file && file.empty?)
532
+ return '.'
533
+ end
534
+
535
+ file
536
+ end
537
+
538
+ # Join path string components together into a single string.
539
+ #
540
+ # This method was reimplemented so that it automatically converts
541
+ # forward slashes to backslashes. It is otherwise identical to
542
+ # the core File.join method.
543
+ #
544
+ # Examples:
545
+ #
546
+ # File.join("C:", "foo", "bar") # => C:\foo\bar
547
+ # File.join("foo", "bar") # => foo\bar
548
+ #
549
+ def join(*args)
550
+ return join_orig(*args).tr("/", "\\")
551
+ end
552
+
553
+ # Returns +path+ in long format. For example, if 'SOMEFI~1.TXT'
554
+ # was the argument provided, and the short representation for
555
+ # 'somefile.txt', then this method would return 'somefile.txt'.
556
+ #
557
+ # Note that certain file system optimizations may prevent this method
558
+ # from working as expected. In that case, you will get back the file
559
+ # name in 8.3 format.
560
+ #
561
+ def long_path(path)
562
+ buf = 0.chr * MAXPATH
563
+ if GetLongPathNameW(multi_to_wide(path), buf, buf.size) == 0
564
+ raise ArgumentError, get_last_error
565
+ end
566
+ wide_to_multi(buf)
567
+ end
568
+
569
+ # Returns the path of the of the symbolic link referred to by +file+.
570
+ #
571
+ # Requires Windows Vista or later. On older versions of Windows it
572
+ # will raise a NotImplementedError, as per MRI.
573
+ #
574
+ def readlink(file)
575
+ if defined? GetFinalPathNameByHandle
576
+ file = multi_to_wide(file)
577
+
578
+ begin
579
+ handle = CreateFileW(
580
+ file,
581
+ GENERIC_READ,
582
+ FILE_SHARE_READ,
583
+ nil,
584
+ OPEN_EXISTING,
585
+ FILE_ATTRIBUTE_NORMAL,
586
+ nil
587
+ )
588
+
589
+ if handle == INVALID_HANDLE_VALUE
590
+ raise ArgumentError, get_last_error
591
+ end
592
+
593
+ path = 0.chr * MAXPATH
594
+
595
+ GetFinalPathNameByHandleW(handle, path, path.size, 0)
596
+ ensure
597
+ CloseHandle(handle)
598
+ end
599
+
600
+ wide_to_multi(path).strip[4..-1] # get rid of prepending "\\?\"
601
+ else
602
+ msg = "readlink() function is unimplemented on this machine"
603
+ raise NotImplementedError, msg
604
+ end
605
+ end
606
+
607
+ # Returns +path+ in 8.3 format. For example, 'c:\documentation.doc'
608
+ # would be returned as 'c:\docume~1.doc'.
609
+ #
610
+ def short_path(path)
611
+ buf = 0.chr * MAXPATH
612
+ if GetShortPathNameW(multi_to_wide(path), buf, buf.size) == 0
613
+ raise ArgumentError, get_last_error
614
+ end
615
+ wide_to_multi(buf)
616
+ end
617
+
618
+ # Splits the given string into a directory and a file component and
619
+ # returns them in a two element array. This was reimplemented because
620
+ # the current version does not handle UNC paths properly.
621
+ #
622
+ def split(file)
623
+ array = []
624
+
625
+ if file.empty? || PathIsRootW(multi_to_wide(file))
626
+ array.push(file, '')
627
+ else
628
+ array.push(File.dirname(file), File.basename(file))
629
+ end
630
+ array
631
+ end
632
+
633
+ # Creates a symbolic link called +new_name+ for the file or directory
634
+ # +old_name+.
635
+ #
636
+ # This method requires Windows Vista or later to work. Otherwise, it
637
+ # returns nil as per MRI.
638
+ #
639
+ def symlink(old_name, new_name)
640
+ if defined? CreateSymbolicLink
641
+ old_name = multi_to_wide(old_name)
642
+ new_name = multi_to_wide(new_name)
643
+ flags = File.directory?(wide_to_multi(old_name)) ? 1 : 0
644
+
645
+ unless CreateSymbolicLinkW(new_name, old_name, flags)
646
+ raise ArgumentError, get_last_error
647
+ end
648
+ else
649
+ nil
650
+ end
651
+ end
652
+
653
+ # Return true if the named file is a symbolic link, false otherwise.
654
+ #
655
+ # This method requires Windows Vista or later to work. Otherwise, it
656
+ # always returns false as per MRI.
657
+ #
658
+ def symlink?(file)
659
+ bool = false
660
+ file = multi_to_wide(file)
661
+ attr = GetFileAttributesW(file)
662
+
663
+ # Differentiate between a symlink and other kinds of reparse points
664
+ if attr & FILE_ATTRIBUTE_REPARSE_POINT > 0
665
+ begin
666
+ buffer = 0.chr * 278 # WIN32_FIND_DATA
667
+ handle = FindFirstFileW(file, buffer)
668
+
669
+ if handle == INVALID_HANDLE_VALUE
670
+ raise ArgumentError, get_last_error
671
+ end
672
+
673
+ if buffer[36,4].unpack('L')[0] == IO_REPARSE_TAG_SYMLINK
674
+ bool = true
675
+ end
676
+ ensure
677
+ CloseHandle(handle)
678
+ end
679
+ end
680
+
681
+ bool
682
+ end
683
+
684
+ ## Stat methods
685
+
686
+ # Returns a File::Stat object, as defined in the win32-file-stat package.
687
+ #
688
+ def stat(file)
689
+ File::Stat.new(file)
690
+ end
691
+
692
+ # Identical to File.stat on Windows.
693
+ #
694
+ def lstat(file)
695
+ File::Stat.new(file)
696
+ end
697
+
698
+ # Returns the file system's block size.
699
+ #
700
+ def blksize(file)
701
+ File::Stat.new(file).blksize
702
+ end
703
+
704
+ # Returns whether or not +file+ is a block device. For MS Windows this
705
+ # means a removable drive, cdrom or ramdisk.
706
+ #
707
+ def blockdev?(file)
708
+ File::Stat.new(file).blockdev?
709
+ end
710
+
711
+ # Returns true if the file is a character device. This replaces the
712
+ # current Ruby implementation which always returns false.
713
+ #
714
+ def chardev?(file)
715
+ File::Stat.new(file).chardev?
716
+ end
717
+
718
+ # Returns the size of the file in bytes.
719
+ #
720
+ # This was reimplemented because the current version does not handle file
721
+ # sizes greater than 2gb.
722
+ #
723
+ def size(file)
724
+ File::Stat.new(file).size
725
+ end
726
+
727
+ # We no longer need the aliases, so remove them
728
+ remove_method(:basename_orig, :blockdev_orig, :chardev_orig)
729
+ remove_method(:dirname_orig, :lstat_orig, :size_orig, :split_orig)
730
+ remove_method(:stat_orig, :symlink_orig, :symlink_orig?, :readlink_orig)
731
+ remove_method(:directory_orig?)
732
+ end # class << self
733
+
734
+ ## Attribute methods
735
+
736
+ # Returns true if the file or directory is an archive file. Applications
737
+ # use this attribute to mark files for backup or removal.
738
+ #
739
+ def self.archive?(file)
740
+ File::Stat.new(file).archive?
741
+ end
742
+
743
+ # Returns true if the file or directory is compressed. For a file, this
744
+ # means that all of the data in the file is compressed. For a directory,
745
+ # this means that compression is the default for newly created files and
746
+ # subdirectories.
747
+ #
748
+ def self.compressed?(file)
749
+ File::Stat.new(file).compressed?
750
+ end
751
+
752
+ # Returns true if the file or directory is encrypted. For a file, this
753
+ # means that all data in the file is encrypted. For a directory, this
754
+ # means that encryption is the default for newly created files and
755
+ # subdirectories.
756
+ #
757
+ def self.encrypted?(file)
758
+ File::Stat.new(file).encrypted?
759
+ end
760
+
761
+ # Returns true if the file or directory is hidden. It is not included
762
+ # in an ordinary directory listing.
763
+ #
764
+ def self.hidden?(file)
765
+ File::Stat.new(file).hidden?
766
+ end
767
+
768
+ # Returns true if the file or directory is indexed by the content indexing
769
+ # service.
770
+ #
771
+ def self.indexed?(file)
772
+ File::Stat.new(file).indexed?
773
+ end
774
+
775
+ # Returns true if the file or directory has no other attributes set.
776
+ #
777
+ def self.normal?(file)
778
+ File::Stat.new(file).normal?
779
+ end
780
+
781
+ # Returns true if the data of the file is not immediately available. This
782
+ # attribute indicates that the file data has been physically moved to
783
+ # offline storage. This attribute is used by Remote Storage, the
784
+ # hierarchical storage management software. Applications should not
785
+ # arbitrarily change this attribute.
786
+ #
787
+ def self.offline?(file)
788
+ File::Stat.new(file).offline?
789
+ end
790
+
791
+ # Returns true if The file or directory is read-only. Applications can
792
+ # read the file but cannot write to it or delete it. In the case of a
793
+ # directory, applications cannot delete it.
794
+ #
795
+ def self.readonly?(file)
796
+ File::Stat.new(file).readonly?
797
+ end
798
+
799
+ # Returns true if the file or directory has an associated reparse point. A
800
+ # reparse point is a collection of user defined data associated with a file
801
+ # or directory. For more on reparse points, search
802
+ # http://msdn.microsoft.com.
803
+ #
804
+ def self.reparse_point?(file)
805
+ File::Stat.new(file).reparse_point?
806
+ end
807
+
808
+ # Returns true if the file is a sparse file. A sparse file is a file in
809
+ # which much of the data is zeros, typically image files. See
810
+ # http://msdn.microsoft.com for more details.
811
+ #
812
+ def self.sparse?(file)
813
+ File::Stat.new(file).sparse?
814
+ end
815
+
816
+ # Returns true if the file or directory is part of the operating system
817
+ # or is used exclusively by the operating system.
818
+ #
819
+ def self.system?(file)
820
+ File::Stat.new(file).system?
821
+ end
822
+
823
+ # Returns true if the file is being used for temporary storage.
824
+ #
825
+ # File systems avoid writing data back to mass storage if sufficient cache
826
+ # memory is available, because often the application deletes the temporary
827
+ # file shortly after the handle is closed. In that case, the system can
828
+ # entirely avoid writing the data. Otherwise, the data will be written after
829
+ # the handle is closed.
830
+ #
831
+ def self.temporary?(file)
832
+ File::Stat.new(file).temporary?
833
+ end
834
+
835
+ # Returns an array of strings indicating the attributes for that file. The
836
+ # possible values are:
837
+ #
838
+ # archive
839
+ # compressed
840
+ # directory
841
+ # encrypted
842
+ # hidden
843
+ # indexed
844
+ # normal
845
+ # offline
846
+ # readonly
847
+ # reparse_point
848
+ # sparse
849
+ # system
850
+ # temporary
851
+ #
852
+ def self.attributes(file)
853
+ attributes = GetFileAttributesW(multi_to_wide(file))
854
+
855
+ if attributes == INVALID_FILE_ATTRIBUTES
856
+ raise ArgumentError, get_last_error
857
+ end
858
+
859
+ arr = []
860
+
861
+ arr << 'archive' if attributes & FILE_ATTRIBUTE_ARCHIVE > 0
862
+ arr << 'compressed' if attributes & FILE_ATTRIBUTE_COMPRESSED > 0
863
+ arr << 'directory' if attributes & FILE_ATTRIBUTE_DIRECTORY > 0
864
+ arr << 'encrypted' if attributes & FILE_ATTRIBUTE_ENCRYPTED > 0
865
+ arr << 'hidden' if attributes & FILE_ATTRIBUTE_HIDDEN > 0
866
+ arr << 'indexed' if attributes & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED == 0
867
+ arr << 'normal' if attributes & FILE_ATTRIBUTE_NORMAL > 0
868
+ arr << 'offline' if attributes & FILE_ATTRIBUTE_OFFLINE > 0
869
+ arr << 'readonly' if attributes & FILE_ATTRIBUTE_READONLY > 0
870
+ arr << 'reparse_point' if attributes & FILE_ATTRIBUTE_REPARSE_POINT > 0
871
+ arr << 'sparse' if attributes & FILE_ATTRIBUTE_SPARSE_FILE > 0
872
+ arr << 'system' if attributes & FILE_ATTRIBUTE_SYSTEM > 0
873
+ arr << 'temporary' if attributes & FILE_ATTRIBUTE_TEMPORARY > 0
874
+
875
+ arr
876
+ end
877
+
878
+ # Sets the file attributes based on the given (numeric) +flags+. This does
879
+ # not remove existing attributes, it merely adds to them.
880
+ #
881
+ def self.set_attributes(file, flags)
882
+ file = multi_to_wide(file)
883
+ attributes = GetFileAttributesW(file)
884
+
885
+ if attributes == INVALID_FILE_ATTRIBUTES
886
+ raise ArgumentError, get_last_error
887
+ end
888
+
889
+ attributes |= flags
890
+
891
+ if SetFileAttributesW(file, attributes) == 0
892
+ raise ArgumentError, get_last_error
893
+ end
894
+
895
+ self
896
+ end
897
+
898
+ # Removes the file attributes based on the given (numeric) +flags+.
899
+ #
900
+ def self.remove_attributes(file, flags)
901
+ file = multi_to_wide(file)
902
+ attributes = GetFileAttributesW(file)
903
+
904
+ if attributes == INVALID_FILE_ATTRIBUTES
905
+ raise ArgumentError, get_last_error
906
+ end
907
+
908
+ attributes &= ~flags
909
+
910
+ if SetFileAttributesW(file, attributes) == 0
911
+ raise ArgumentError, get_last_error
912
+ end
913
+
914
+ self
915
+ end
916
+
917
+ # Instance methods
918
+
919
+ def stat
920
+ File::Stat.new(self.path)
921
+ end
922
+
923
+ # Sets whether or not the file is an archive file.
924
+ #
925
+ def archive=(bool)
926
+ wide_path = multi_to_wide(self.path)
927
+ attributes = GetFileAttributesW(wide_path)
928
+
929
+ if attributes == INVALID_FILE_ATTRIBUTES
930
+ raise ArgumentError, get_last_error
931
+ end
932
+
933
+ if bool
934
+ attributes |= FILE_ATTRIBUTE_ARCHIVE;
935
+ else
936
+ attributes &= ~FILE_ATTRIBUTE_ARCHIVE;
937
+ end
938
+
939
+ if SetFileAttributesW(wide_path, attributes) == 0
940
+ raise ArgumentError, get_last_error
941
+ end
942
+
943
+ self
944
+ end
945
+
946
+ # Sets whether or not the file is a compressed file.
947
+ #
948
+ def compressed=(bool)
949
+ in_buf = bool ? COMPRESSION_FORMAT_DEFAULT : COMPRESSION_FORMAT_NONE
950
+ in_buf = [in_buf].pack('L')
951
+ bytes = [0].pack('L')
952
+
953
+ # We can't use get_osfhandle here because we need specific attributes
954
+ handle = CreateFileW(
955
+ multi_to_wide(self.path),
956
+ FILE_READ_DATA | FILE_WRITE_DATA,
957
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
958
+ 0,
959
+ OPEN_EXISTING,
960
+ 0,
961
+ 0
962
+ )
963
+
964
+ if handle == INVALID_HANDLE_VALUE
965
+ raise ArgumentError, get_last_error
966
+ end
967
+
968
+ begin
969
+ bool = DeviceIoControl(
970
+ handle,
971
+ FSCTL_SET_COMPRESSION(),
972
+ in_buf,
973
+ in_buf.length,
974
+ 0,
975
+ 0,
976
+ bytes,
977
+ 0
978
+ )
979
+
980
+ unless bool
981
+ raise ArgumentError, get_last_error
982
+ end
983
+ ensure
984
+ CloseHandle(handle)
985
+ end
986
+
987
+ self
988
+ end
989
+
990
+ # Sets the hidden attribute to true or false. Setting this attribute to
991
+ # true means that the file is not included in an ordinary directory listing.
992
+ #
993
+ def hidden=(bool)
994
+ wide_path = multi_to_wide(self.path)
995
+ attributes = GetFileAttributesW(wide_path)
996
+
997
+ if attributes == INVALID_FILE_ATTRIBUTES
998
+ raise ArgumentError, get_last_error
999
+ end
1000
+
1001
+ if bool
1002
+ attributes |= FILE_ATTRIBUTE_HIDDEN;
1003
+ else
1004
+ attributes &= ~FILE_ATTRIBUTE_HIDDEN;
1005
+ end
1006
+
1007
+ if SetFileAttributesW(wide_path, attributes) == 0
1008
+ raise ArgumentError, get_last_error
1009
+ end
1010
+
1011
+ self
1012
+ end
1013
+
1014
+ # Sets the 'indexed' attribute to true or false. Setting this to
1015
+ # false means that the file will not be indexed by the content indexing
1016
+ # service.
1017
+ #
1018
+ def indexed=(bool)
1019
+ wide_path = multi_to_wide(self.path)
1020
+ attributes = GetFileAttributesW(wide_path)
1021
+
1022
+ if attributes == INVALID_FILE_ATTRIBUTES
1023
+ raise ArgumentError, get_last_error
1024
+ end
1025
+
1026
+ if bool
1027
+ attributes &= ~FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
1028
+ else
1029
+ attributes |= FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
1030
+ end
1031
+
1032
+ if SetFileAttributes(wide_path, attributes) == 0
1033
+ raise ArgumentError, get_last_error
1034
+ end
1035
+
1036
+ self
1037
+ end
1038
+
1039
+ alias :content_indexed= :indexed=
1040
+
1041
+ # Sets the normal attribute. Note that only 'true' is a valid argument,
1042
+ # which has the effect of removing most other attributes. Attempting to
1043
+ # pass any value except true will raise an ArgumentError.
1044
+ #
1045
+ def normal=(bool)
1046
+ unless bool
1047
+ raise ArgumentError, "only 'true' may be passed as an argument"
1048
+ end
1049
+
1050
+ if SetFileAttributesW(multi_to_wide(self.path),FILE_ATTRIBUTE_NORMAL) == 0
1051
+ raise ArgumentError, get_last_error
1052
+ end
1053
+
1054
+ self
1055
+ end
1056
+
1057
+ # Sets whether or not a file is online or not. Setting this to false means
1058
+ # that the data of the file is not immediately available. This attribute
1059
+ # indicates that the file data has been physically moved to offline storage.
1060
+ # This attribute is used by Remote Storage, the hierarchical storage
1061
+ # management software.
1062
+ #
1063
+ # Applications should not arbitrarily change this attribute.
1064
+ #
1065
+ def offline=(bool)
1066
+ wide_path = multi_to_wide(self.path)
1067
+ attributes = GetFileAttributesW(wide_path)
1068
+
1069
+ if attributes == INVALID_FILE_ATTRIBUTES
1070
+ raise ArgumentError, get_last_error
1071
+ end
1072
+
1073
+ if bool
1074
+ attributes |= FILE_ATTRIBUTE_OFFLINE;
1075
+ else
1076
+ attributes &= ~FILE_ATTRIBUTE_OFFLINE;
1077
+ end
1078
+
1079
+ if SetFileAttributesW(wide_path, attributes) == 0
1080
+ raise ArgumentError, get_last_error
1081
+ end
1082
+
1083
+ self
1084
+ end
1085
+
1086
+ # Sets the readonly attribute. If set to true the the file or directory is
1087
+ # readonly. Applications can read the file but cannot write to it or delete
1088
+ # it. In the case of a directory, applications cannot delete it.
1089
+ #
1090
+ def readonly=(bool)
1091
+ wide_path = multi_to_wide(self.path)
1092
+ attributes = GetFileAttributesW(wide_path)
1093
+
1094
+ if attributes == INVALID_FILE_ATTRIBUTES
1095
+ raise ArgumentError, get_last_error
1096
+ end
1097
+
1098
+ if bool
1099
+ attributes |= FILE_ATTRIBUTE_READONLY;
1100
+ else
1101
+ attributes &= ~FILE_ATTRIBUTE_READONLY;
1102
+ end
1103
+
1104
+ if SetFileAttributesW(wide_path, attributes) == 0
1105
+ raise ArgumentError, get_last_error
1106
+ end
1107
+
1108
+ self
1109
+ end
1110
+
1111
+ # Sets the file to a sparse (usually image) file. Note that you cannot
1112
+ # remove the sparse property from a file.
1113
+ #
1114
+ def sparse=(bool)
1115
+ unless bool
1116
+ warn 'cannot remove sparse property from a file - operation ignored'
1117
+ return
1118
+ end
1119
+
1120
+ bytes = [0].pack('L')
1121
+
1122
+ handle = CreateFileW(
1123
+ multi_to_wide(self.path),
1124
+ FILE_READ_DATA | FILE_WRITE_DATA,
1125
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
1126
+ 0,
1127
+ OPEN_EXISTING,
1128
+ FSCTL_SET_SPARSE(),
1129
+ 0
1130
+ )
1131
+
1132
+ if handle == INVALID_HANDLE_VALUE
1133
+ raise ArgumentError, get_last_error
1134
+ end
1135
+
1136
+ begin
1137
+ bool = DeviceIoControl(
1138
+ handle,
1139
+ FSCTL_SET_SPARSE(),
1140
+ 0,
1141
+ 0,
1142
+ 0,
1143
+ 0,
1144
+ bytes,
1145
+ 0
1146
+ )
1147
+
1148
+ unless bool == 0
1149
+ raise ArgumentError, get_last_error
1150
+ end
1151
+ ensure
1152
+ CloseHandle(handle)
1153
+ end
1154
+
1155
+ self
1156
+ end
1157
+
1158
+ # Set whether or not the file is a system file. A system file is a file
1159
+ # that is part of the operating system or is used exclusively by it.
1160
+ #
1161
+ def system=(bool)
1162
+ wide_path = multi_to_wide(self.path)
1163
+ attributes = GetFileAttributesW(wide_path)
1164
+
1165
+ if attributes == INVALID_FILE_ATTRIBUTES
1166
+ raise ArgumentError, get_last_error
1167
+ end
1168
+
1169
+ if bool
1170
+ attributes |= FILE_ATTRIBUTE_SYSTEM;
1171
+ else
1172
+ attributes &= ~FILE_ATTRIBUTE_SYSTEM;
1173
+ end
1174
+
1175
+ if SetFileAttributesW(wide_path, attributes) == 0
1176
+ raise ArgumentError, get_last_error
1177
+ end
1178
+
1179
+ self
1180
+ end
1181
+
1182
+ # Sets whether or not the file is being used for temporary storage.
1183
+ #
1184
+ # File systems avoid writing data back to mass storage if sufficient cache
1185
+ # memory is available, because often the application deletes the temporary
1186
+ # file shortly after the handle is closed. In that case, the system can
1187
+ # entirely avoid writing the data. Otherwise, the data will be written
1188
+ # after the handle is closed.
1189
+ #
1190
+ def temporary=(bool)
1191
+ wide_path = multi_to_wide(self.path)
1192
+ attributes = GetFileAttributesW(wide_path)
1193
+
1194
+ if attributes == INVALID_FILE_ATTRIBUTES
1195
+ raise ArgumentError, get_last_error
1196
+ end
1197
+
1198
+ if bool
1199
+ attributes |= FILE_ATTRIBUTE_TEMPORARY;
1200
+ else
1201
+ attributes &= ~FILE_ATTRIBUTE_TEMPORARY;
1202
+ end
1203
+
1204
+ if SetFileAttributesW(wide_path, attributes) == 0
1205
+ raise ArgumentError, get_last_error
1206
+ end
1207
+
1208
+ self
1209
+ end
1210
+
1211
+ # Singleton aliases, mostly for backwards compatibility
1212
+ class << self
1213
+ alias :read_only? :readonly?
1214
+ alias :content_indexed? :indexed?
1215
+ alias :set_attr :set_attributes
1216
+ alias :unset_attr :remove_attributes
1217
+ end
1218
+
1219
+ private
1220
+
1221
+ # Get the owner of a given file by name.
1222
+ #
1223
+ def get_owner(file)
1224
+ handle = CreateFile(
1225
+ file,
1226
+ GENERIC_READ,
1227
+ FILE_SHARE_READ,
1228
+ 0,
1229
+ OPEN_EXISTING,
1230
+ 0,
1231
+ 0
1232
+ )
1233
+
1234
+ if handle == INVALID_HANDLE_VALUE
1235
+ raise ArgumentError, get_last_error
1236
+ end
1237
+
1238
+ sid = 0.chr * 28
1239
+ sid_ptr = [0].pack('L')
1240
+
1241
+ begin
1242
+ rv = GetSecurityInfo(
1243
+ handle,
1244
+ SE_FILE_OBJECT,
1245
+ OWNER_SECURITY_INFORMATION,
1246
+ sid_ptr,
1247
+ nil,
1248
+ nil,
1249
+ nil,
1250
+ nil
1251
+ )
1252
+
1253
+ if rv != 0
1254
+ raise get_last_error
1255
+ end
1256
+
1257
+ name_buf = 0.chr * 80
1258
+ name_cch = [name_buf.size].pack('L')
1259
+
1260
+ domain_buf = 0.chr * 80
1261
+ domain_cch = [domain_buf.size].pack('L')
1262
+ sid_name = 0.chr * 4
1263
+
1264
+ bool = LookupAccountSid(
1265
+ nil,
1266
+ sid_ptr.unpack('L').first,
1267
+ name_buf,
1268
+ name_cch,
1269
+ domain_buf,
1270
+ domain_cch,
1271
+ sid_name
1272
+ )
1273
+
1274
+ unless bool
1275
+ raise get_last_error
1276
+ end
1277
+ ensure
1278
+ CloseHandle(handle)
1279
+ end
1280
+
1281
+ name_buf.strip
1282
+ end
1283
+ end
1284
+
1285
+ puts File.join("C:", "foo", "bar")