win32-file 0.6.8 → 0.6.9

Sign up to get free protection for your applications and to get access to all the features.
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