win32-file 0.6.7 → 0.6.8

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