win32-file 0.6.7 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGES CHANGED
@@ -1,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