win32-file 0.6.8 → 0.6.9

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