win32-file 0.6.3 → 0.6.4

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.
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")