win32-file-stat 1.3.6 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,755 +1,934 @@
1
- require 'windows/msvcrt/buffer'
2
- require 'windows/msvcrt/file'
3
- require 'windows/filesystem'
4
- require 'windows/device_io'
5
- require 'windows/path'
6
- require 'windows/file'
7
- require 'windows/error'
8
- require 'windows/handle'
9
- require 'windows/volume'
10
- require 'windows/process'
11
- require 'windows/security'
12
- require 'windows/time'
13
- require 'windows/ntfs/winternl'
14
- require 'pp'
15
-
16
- class File::Stat
17
- include Windows::MSVCRT::Buffer
18
- include Windows::MSVCRT::File
19
- include Windows::DeviceIO
20
- include Windows::FileSystem
21
- include Windows::Path
22
- include Windows::File
23
- include Windows::Error
24
- include Windows::Handle
25
- include Windows::Volume
26
- include Windows::Process
27
- include Windows::Security
28
- include Windows::Time
29
- include Windows::NTFS::Winternl
30
- include Comparable
31
-
32
- # The version of the win32-file-stat library
33
- VERSION = '1.3.6'
34
-
35
- private
36
-
37
- # :stopdoc:
38
-
39
- # Defined in Ruby's win32.h. Not meant for public consumption.
40
- S_IWGRP = 0020
41
- S_IWOTH = 0002
42
-
43
- # This is the only way to avoid a -w warning for initialize. We remove
44
- # it later, after we've defined our initialize method.
45
- alias old_init initialize
46
-
47
- # Make this library -w clean
48
- undef_method(:atime, :blksize, :blockdev?, :blocks, :chardev?, :ctime)
49
- undef_method(:dev, :directory?, :executable?, :file?, :ftype, :gid, :ino)
50
- undef_method(:executable_real?, :grpowned?, :mode, :mtime, :nlink, :owned?)
51
- undef_method(:pipe?, :readable?, :rdev, :readable_real?, :setgid?, :setuid?)
52
- undef_method(:size, :size?, :socket?, :sticky?, :symlink?, :uid, :writable?)
53
- undef_method(:dev_major, :dev_minor, :rdev_major, :rdev_minor)
54
- undef_method(:writable_real?, :zero?)
55
- undef_method(:pretty_print, :inspect, :<=>)
56
-
57
- public
58
-
59
- # Always nil. Provided for interface compatibility only.
60
- attr_reader :dev_major
61
- attr_reader :dev_minor
62
- attr_reader :rdev_major
63
- attr_reader :rdev_minor
64
-
65
- # :startdoc:
66
-
67
- # Creates and returns a File::Stat object, which encapsulate common status
68
- # information for File objects on MS Windows sytems. The information is
69
- # recorded at the moment the File::Stat object is created; changes made to
70
- # the file after that point will not be reflected.
71
- #
72
- def initialize(file)
73
- @file = File.expand_path(file)
74
- @file = @file.tr('/', "\\")
75
- @file = multi_to_wide(@file)
76
-
77
- @file_type = get_file_type(@file)
78
- @chardev = @file_type == FILE_TYPE_CHAR
79
-
80
- case GetDriveTypeW(@file)
81
- when DRIVE_REMOVABLE, DRIVE_CDROM, DRIVE_RAMDISK
82
- @blockdev = true
83
- else
84
- @blockdev = false
85
- end
86
-
87
- # The stat struct in stat.h only has 11 members on Windows
88
- stat_buf = [0,0,0,0,0,0,0,0,0,0,0].pack('ISSsssIQQQQ')
89
-
90
- # The stat64 function doesn't seem to like character devices
91
- if wstat64(@file, stat_buf) != 0
92
- raise ArgumentError, get_last_error unless @chardev
93
- end
94
-
95
- # Some bytes skipped (padding for struct alignment)
96
- @dev = stat_buf[0, 4].unpack('I').first # Drive number
97
- @ino = stat_buf[4, 2].unpack('S').first # Meaningless
98
- @mode = stat_buf[6, 2].unpack('S').first # File mode bit mask
99
- @nlink = stat_buf[8, 2].unpack('s').first # Always 1
100
- @uid = stat_buf[10, 2].unpack('s').first # Always 0
101
- @gid = stat_buf[12, 2].unpack('s').first # Always 0
102
- @rdev = stat_buf[16, 4].unpack('I').first # Same as dev
103
- @size = stat_buf[24, 8].unpack('Q').first # Size of file in bytes
104
-
105
- # This portion can fail in rare, FS related instances. If it does, set
106
- # the various times to Time.at(0).
107
- begin
108
- @atime = Time.at(stat_buf[32, 8].unpack('Q').first) # Access time
109
- @mtime = Time.at(stat_buf[40, 8].unpack('Q').first) # Mod time
110
- @ctime = Time.at(stat_buf[48, 8].unpack('Q').first) # Creation time
111
- rescue
112
- @atime = Time.at(0)
113
- @mtime = Time.at(0)
114
- @ctime = Time.at(0)
115
- end
116
-
117
- @mode = 33188 if @chardev
118
-
119
- attributes = GetFileAttributesW(@file)
120
- error_num = GetLastError()
121
-
122
- # Locked files.
123
- if error_num == ERROR_SHARING_VIOLATION
124
- buffer = 0.chr * 512
125
-
126
- begin
127
- handle = FindFirstFileW(@file, buffer)
128
-
129
- if handle == INVALID_HANDLE_VALUE
130
- raise SystemCallError, get_last_error()
131
- end
132
- ensure
133
- FindClose(handle) if handle != INVALID_HANDLE_VALUE
134
- end
135
-
136
- attributes = buffer[0,4].unpack('L').first
137
- st = 0.chr * 16
138
- FileTimeToSystemTime(buffer[4,8],st)
139
- y,m,w,d,h,n,s,i = st.unpack('SSSSSSSS')
140
- @ctime = Time.local(y,m,d,h,n,s)
141
-
142
- st = 0.chr * 16
143
- FileTimeToSystemTime(buffer[12,8],st)
144
- y,m,w,d,h,n,s,i = st.unpack('SSSSSSSS')
145
- @atime = Time.local(y,m,d,h,n,s)
146
-
147
- st = 0.chr * 16
148
- FileTimeToSystemTime(buffer[20,8],st)
149
- y,m,w,d,h,n,s,i = st.unpack('SSSSSSSS')
150
- @mtime = Time.local(y,m,d,h,n,s)
151
- end
152
-
153
- # Ignore errors caused by empty/open/used block devices.
154
- if attributes == INVALID_FILE_ATTRIBUTES
155
- unless error_num == ERROR_NOT_READY
156
- raise ArgumentError, get_last_error(error_num)
157
- end
158
- end
159
-
160
- @blksize = get_blksize(@file)
161
-
162
- # This is a reasonable guess
163
- case @blksize
164
- when nil
165
- @blocks = nil
166
- when 0
167
- @blocks = 0
168
- else
169
- @blocks = (@size.to_f / @blksize.to_f).ceil
170
- end
171
-
172
- @readonly = attributes & FILE_ATTRIBUTE_READONLY > 0
173
- @hidden = attributes & FILE_ATTRIBUTE_HIDDEN > 0
174
- @system = attributes & FILE_ATTRIBUTE_SYSTEM > 0
175
- @archive = attributes & FILE_ATTRIBUTE_ARCHIVE > 0
176
- @directory = attributes & FILE_ATTRIBUTE_DIRECTORY > 0
177
- @encrypted = attributes & FILE_ATTRIBUTE_ENCRYPTED > 0
178
- @normal = attributes & FILE_ATTRIBUTE_NORMAL > 0
179
- @temporary = attributes & FILE_ATTRIBUTE_TEMPORARY > 0
180
- @sparse = attributes & FILE_ATTRIBUTE_SPARSE_FILE > 0
181
- @reparse_point = attributes & FILE_ATTRIBUTE_REPARSE_POINT > 0
182
- @compressed = attributes & FILE_ATTRIBUTE_COMPRESSED > 0
183
- @offline = attributes & FILE_ATTRIBUTE_OFFLINE > 0
184
- @indexed = attributes & ~FILE_ATTRIBUTE_NOT_CONTENT_INDEXED > 0
185
-
186
- @executable = GetBinaryTypeW(@file, '')
187
- @regular = @file_type == FILE_TYPE_DISK
188
- @pipe = @file_type == FILE_TYPE_PIPE
189
-
190
- # Not supported and/or meaningless
191
- @dev_major = nil
192
- @dev_minor = nil
193
- @grpowned = true
194
- @owned = true
195
- @readable = true
196
- @readable_real = true
197
- @rdev_major = nil
198
- @rdev_minor = nil
199
- @setgid = false
200
- @setuid = false
201
- @sticky = false
202
- @symlink = false
203
- @writable = true
204
- @writable_real = true
205
- end
206
-
207
- ## Comparable
208
-
209
- # Compares two File::Stat objects. Comparsion is based on mtime only.
210
- #
211
- def <=>(other)
212
- @mtime.to_i <=> other.mtime.to_i
213
- end
214
-
215
- ## Miscellaneous
216
-
217
- # Returns whether or not the file is a block device. For MS Windows a
218
- # block device is a removable drive, cdrom or ramdisk.
219
- #
220
- def blockdev?
221
- @blockdev
222
- end
223
-
224
- # Returns whether or not the file is a character device.
225
- #
226
- def chardev?
227
- @chardev
228
- end
229
-
230
- # Returns whether or not the file is executable. Generally speaking, this
231
- # means .bat, .cmd, .com, and .exe files.
232
- #
233
- def executable?
234
- @executable
235
- end
236
-
237
- alias :executable_real? :executable?
238
-
239
- # Returns whether or not the file is a regular file, as opposed to a pipe,
240
- # socket, etc.
241
- #
242
- def file?
243
- @regular
244
- end
245
-
246
- # Identifies the type of file. The return string is one of 'file',
247
- # 'directory', 'characterSpecial', 'socket' or 'unknown'.
248
- #
249
- def ftype
250
- return 'directory' if directory?
251
- case @file_type
252
- when FILE_TYPE_CHAR
253
- 'characterSpecial'
254
- when FILE_TYPE_DISK
255
- 'file'
256
- when FILE_TYPE_PIPE
257
- 'socket'
258
- else
259
- if blockdev?
260
- 'blockSpecial'
261
- else
262
- 'unknown'
263
- end
264
- end
265
- end
266
-
267
- # Meaningless on Windows.
268
- #
269
- def grpowned?
270
- @grpowned
271
- end
272
-
273
- # Always true on Windows
274
- def owned?
275
- @owned
276
- end
277
-
278
- # Returns whether or not the file is a pipe.
279
- #
280
- def pipe?
281
- @pipe
282
- end
283
-
284
- alias :socket? :pipe?
285
-
286
- # Meaningless on Windows
287
- #
288
- def readable?
289
- @readable
290
- end
291
-
292
- # Meaningless on Windows
293
- #
294
- def readable_real?
295
- @readable_real
296
- end
297
-
298
- # Meaningless on Windows
299
- #
300
- def setgid?
301
- @setgid
302
- end
303
-
304
- # Meaningless on Windows
305
- #
306
- def setuid?
307
- @setuid
308
- end
309
-
310
- # Returns nil if statfile is a zero-length file; otherwise, returns the
311
- # file size. Usable as a condition in tests.
312
- #
313
- def size?
314
- @size > 0 ? @size : nil
315
- end
316
-
317
- # Meaningless on Windows.
318
- #
319
- def sticky?
320
- @sticky
321
- end
322
-
323
- # Meaningless on Windows at the moment. This may change in the future.
324
- #
325
- def symlink?
326
- @symlink
327
- end
328
-
329
- # Meaningless on Windows.
330
- #
331
- def writable?
332
- @writable
333
- end
334
-
335
- # Meaningless on Windows.
336
- #
337
- def writable_real?
338
- @writable_real
339
- end
340
-
341
- # Returns whether or not the file size is zero.
342
- #
343
- def zero?
344
- @size == 0
345
- end
346
-
347
- ## Attribute members
348
-
349
- # Returns whether or not the file is an archive file.
350
- #
351
- def archive?
352
- @archive
353
- end
354
-
355
- # Returns whether or not the file is compressed.
356
- #
357
- def compressed?
358
- @compressed
359
- end
360
-
361
- # Returns whether or not the file is a directory.
362
- #
363
- def directory?
364
- @directory
365
- end
366
-
367
- # Returns whether or not the file in encrypted.
368
- #
369
- def encrypted?
370
- @encrypted
371
- end
372
-
373
- # Returns whether or not the file is hidden.
374
- #
375
- def hidden?
376
- @hidden
377
- end
378
-
379
- # Returns whether or not the file is content indexed.
380
- #
381
- def indexed?
382
- @indexed
383
- end
384
-
385
- alias :content_indexed? :indexed?
386
-
387
- # Returns whether or not the file is 'normal'. This is only true if
388
- # virtually all other attributes are false.
389
- #
390
- def normal?
391
- @normal
392
- end
393
-
394
- # Returns whether or not the file is offline.
395
- #
396
- def offline?
397
- @offline
398
- end
399
-
400
- # Returns whether or not the file is readonly.
401
- #
402
- def readonly?
403
- @readonly
404
- end
405
-
406
- alias :read_only? :readonly?
407
-
408
- # Returns whether or not the file is a reparse point.
409
- #
410
- def reparse_point?
411
- @reparse_point
412
- end
413
-
414
- # Returns whether or not the file is a sparse file. In most cases a sparse
415
- # file is an image file.
416
- #
417
- def sparse?
418
- @sparse
419
- end
420
-
421
- # Returns whether or not the file is a system file.
422
- #
423
- def system?
424
- @system
425
- end
426
-
427
- # Returns whether or not the file is being used for temporary storage.
428
- #
429
- def temporary?
430
- @temporary
431
- end
432
-
433
- ## Standard stat members
434
-
435
- # Returns a Time object containing the last access time.
436
- #
437
- def atime
438
- @atime
439
- end
440
-
441
- # Returns the file system's block size, or nil if it cannot be determined.
442
- #
443
- def blksize
444
- @blksize
445
- end
446
-
447
- # Returns the number of blocks used by the file, where a block is defined
448
- # as size divided by blksize, rounded up.
449
- #
450
- #--
451
- # This is a fudge. A search of the internet reveals different ways people
452
- # have defined st_blocks on MS Windows.
453
- #
454
- def blocks
455
- @blocks
456
- end
457
-
458
- # Returns a Time object containing the time that the file status associated
459
- # with the file was changed.
460
- #
461
- def ctime
462
- @ctime
463
- end
464
-
465
- # Drive letter (A-Z) of the disk containing the file. If the path is a
466
- # UNC path then the drive number (probably -1) is returned instead.
467
- #
468
- def dev
469
- if PathIsUNCW(@file)
470
- @dev
471
- else
472
- if RUBY_VERSION.to_f >= 1.9
473
- (@dev + 'A'.ord).chr + ':'
474
- else
475
- (@dev + ?A).chr + ':'
476
- end
477
- end
478
- end
479
-
480
- # Group ID. Always 0.
481
- #
482
- def gid
483
- @gid
484
- end
485
-
486
- # Inode number. Meaningless on NTFS.
487
- #
488
- def ino
489
- @ino
490
- end
491
-
492
- # Bit mask for file-mode information.
493
- #
494
- # :no-doc:
495
- # This was taken from rb_win32_stat() in win32.c. I'm not entirely
496
- # sure what the point is.
497
- #
498
- def mode
499
- @mode &= ~(S_IWGRP | S_IWOTH)
500
- end
501
-
502
- # Returns a Time object containing the modification time.
503
- #
504
- def mtime
505
- @mtime
506
- end
507
-
508
- # Drive number of the disk containing the file.
509
- #
510
- def rdev
511
- @rdev
512
- end
513
-
514
- # Always 1
515
- #
516
- def nlink
517
- @nlink
518
- end
519
-
520
- # Returns the size of the file, in bytes.
521
- #
522
- def size
523
- @size
524
- end
525
-
526
- # User ID. Always 0.
527
- #
528
- def uid
529
- @uid
530
- end
531
-
532
- # Returns a stringified version of a File::Stat object.
533
- #
534
- def inspect
535
- members = %w[
536
- archive? atime blksize blockdev? blocks compressed? ctime dev
537
- encrypted? gid hidden? indexed? ino mode mtime rdev nlink normal?
538
- offline? readonly? reparse_point? size sparse? system? temporary?
539
- uid
540
- ]
541
-
542
- str = "#<#{self.class}"
543
-
544
- members.sort.each{ |mem|
545
- if mem == 'mode'
546
- str << " #{mem}=" << sprintf("0%o", send(mem.intern))
547
- elsif mem[-1].chr == '?'
548
- str << " #{mem.chop}=" << send(mem.intern).to_s
549
- else
550
- str << " #{mem}=" << send(mem.intern).to_s
551
- end
552
- }
553
-
554
- str
555
- end
556
-
557
- # A custom pretty print method. This was necessary not only to handle
558
- # the additional attributes, but to work around an error caused by the
559
- # builtin method for the current File::Stat class (see pp.rb).
560
- #
561
- def pretty_print(q)
562
- members = %w[
563
- archive? atime blksize blockdev? blocks compressed? ctime dev
564
- encrypted? gid hidden? indexed? ino mode mtime rdev nlink normal?
565
- offline? readonly? reparse_point? size sparse? system? temporary?
566
- uid
567
- ]
568
-
569
- q.object_group(self){
570
- q.breakable
571
- members.each{ |mem|
572
- q.group{
573
- q.text("#{mem}".ljust(15) + "=> ")
574
- if mem == 'mode'
575
- q.text(sprintf("0%o", send(mem.intern)))
576
- else
577
- val = self.send(mem.intern)
578
- if val.nil?
579
- q.text('nil')
580
- else
581
- q.text(val.to_s)
582
- end
583
- end
584
- }
585
- q.comma_breakable unless mem == members.last
586
- }
587
- }
588
- end
589
-
590
- # Since old_init was added strictly to avoid a warning, we remove it now.
591
- remove_method(:old_init)
592
-
593
- private
594
-
595
- # Returns the file system's block size.
596
- #
597
- def get_blksize(file)
598
- size = nil
599
-
600
- sectors = [0].pack('L')
601
- bytes = [0].pack('L')
602
- free = [0].pack('L')
603
- total = [0].pack('L')
604
-
605
- # If there's a drive letter it must contain a trailing backslash.
606
- # The dup is necessary here because the function modifies the argument.
607
- file = file.dup
608
-
609
- if PathStripToRootA(wide_to_multi(file))
610
- file = file[/^[^\0]*/] << ':'
611
- file << "\\" unless file[-1].chr == "\\"
612
- else
613
- file = nil # Default to the root drive on relative paths
614
- end
615
-
616
- # Don't check for an error here. Just default to nil.
617
- if GetDiskFreeSpaceA(file, sectors, bytes, free, total)
618
- size = sectors.unpack('L').first * bytes.unpack('L').first
619
- end
620
-
621
- size
622
- end
623
-
624
- # Private method to get a HANDLE when CreateFile() won't cut it.
625
- #
626
- def get_handle(file)
627
- file = file.upcase
628
-
629
- begin
630
- hdlTokenHandle = 0.chr * 4
631
-
632
- OpenProcessToken(
633
- GetCurrentProcess(),
634
- TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
635
- hdlTokenHandle
636
- )
637
-
638
- hdlTokenHandle = hdlTokenHandle.unpack('L').first
639
-
640
- # Get the LUID for shutdown privilege.
641
- tmpLuid = 0.chr * 8
642
- LookupPrivilegeValue("", "SeDebugPrivilege", tmpLuid)
643
- tkp = [1].pack('L') + tmpLuid + [SE_PRIVILEGE_ENABLED].pack('L')
644
-
645
- # Enable the shutdown privilege in the access token of this process.
646
- AdjustTokenPrivileges(hdlTokenHandle, 0,tkp, tkp.length , nil, nil)
647
- ensure
648
- CloseHandle(hdlTokenHandle)
649
- end
650
-
651
- # First call is to get the required length
652
- handle_info = 0.chr * 4096
653
- required = 0.chr * 4
654
- NtQuerySystemInformation(16, handle_info, 4096, required)
655
-
656
- # Second call is the actual call
657
- handle_info = 0.chr * required.unpack('L').first
658
- NtQuerySystemInformation(16, handle_info, handle_info.length, required)
659
-
660
- count = handle_info[0,4].unpack('L').first
661
-
662
- for i in 0...count
663
- info = handle_info[4+i*16,16].unpack('LSSLL')
664
-
665
- # Split out like this to silence Ruby 1.9 warnings
666
- pid = info[0]
667
- handle = info[2]
668
- access = info[4]
669
-
670
- if access & 0xffff == 3
671
- begin
672
- process = OpenProcess(0x40,1,pid)
673
- dup_handle = 0.chr * 4
674
-
675
- DuplicateHandle(
676
- process,
677
- handle,
678
- GetCurrentProcess(),
679
- dup_handle,
680
- 0,
681
- 1,
682
- 2
683
- )
684
- ensure
685
- CloseHandle(process)
686
- end
687
-
688
- handle = dup_handle.unpack('L').first
689
- buffer = 0.chr * 0x2000
690
- NtQueryObject(handle, 1, buffer, 0x2000, nil)
691
- len = buffer[0,2].unpack('S').first
692
-
693
- if len>0
694
- if buffer[8..-1].upcase[file]
695
- return handle
696
- end
697
- end
698
- CloseHandle(handle)
699
- end
700
- end
701
-
702
- return 0
703
- end
704
-
705
- # Returns the file's type (as a numeric).
706
- #
707
- def get_file_type(file)
708
- begin
709
- handle = CreateFileW(
710
- file,
711
- 0,
712
- 0,
713
- nil,
714
- OPEN_EXISTING,
715
- FILE_FLAG_BACKUP_SEMANTICS, # Need this for directories
716
- nil
717
- )
718
-
719
- error_num = GetLastError()
720
-
721
- # CreateFile() chokes on locked files
722
- if error_num == ERROR_SHARING_VIOLATION
723
- drive = file[0,4] + 0.chr * 2
724
- device = 0.chr * 512
725
- QueryDosDeviceW(drive, device, 256)
726
- file = device.strip + 0.chr + file[4..-1]
727
- handle = get_handle(file)
728
- end
729
-
730
- # We raise a SystemCallError explicitly here in order to maintain
731
- # compatibility with the FileUtils module.
732
- if handle == INVALID_HANDLE_VALUE
733
- raise SystemCallError, get_last_error(error_num)
734
- end
735
-
736
- file_type = GetFileType(handle)
737
- error_num = GetLastError()
738
- ensure
739
- CloseHandle(handle)
740
- end
741
-
742
- if file_type == FILE_TYPE_UNKNOWN && error_num != NO_ERROR
743
- raise SystemCallError, get_last_error(error_num)
744
- end
745
-
746
- file_type
747
- end
748
-
749
- private
750
-
751
- # Verifies that a value is either true or false
752
- def check_bool(val)
753
- raise TypeError unless val == true || val == false
754
- end
755
- end
1
+ require File.join(File.dirname(__FILE__), 'windows', 'helper')
2
+ require File.join(File.dirname(__FILE__), 'windows', 'constants')
3
+ require File.join(File.dirname(__FILE__), 'windows', 'structs')
4
+ require File.join(File.dirname(__FILE__), 'windows', 'functions')
5
+ require 'pp'
6
+
7
+ class File::Stat
8
+ include Windows::Stat::Constants
9
+ include Windows::Stat::Structs
10
+ include Windows::Stat::Functions
11
+ include Comparable
12
+
13
+ # We have to undefine these first in order to avoid redefinition warnings.
14
+ undef_method :atime, :ctime, :mtime, :blksize, :blockdev?, :blocks, :chardev?
15
+ undef_method :dev, :dev_major, :dev_minor, :directory?, :executable?
16
+ undef_method :executable_real?, :file?
17
+ undef_method :ftype, :gid, :grpowned?, :ino, :mode, :nlink, :owned?
18
+ undef_method :pipe?, :readable?, :readable_real?, :rdev, :rdev_major
19
+ undef_method :rdev_minor, :setuid?, :setgid?
20
+ undef_method :size, :size?, :socket?, :sticky?, :symlink?, :uid
21
+ undef_method :world_readable?, :world_writable?, :writable?, :writable_real?
22
+ undef_method :<=>, :inspect, :pretty_print, :zero?
23
+
24
+ # A Time object containing the last access time.
25
+ attr_reader :atime
26
+
27
+ # A Time object indicating when the file was last changed.
28
+ attr_reader :ctime
29
+
30
+ # A Time object containing the last modification time.
31
+ attr_reader :mtime
32
+
33
+ # The native filesystems' block size.
34
+ attr_reader :blksize
35
+
36
+ # The number of native filesystem blocks allocated for this file.
37
+ attr_reader :blocks
38
+
39
+ # The serial number of the file's volume.
40
+ attr_reader :rdev
41
+
42
+ # The file's unique identifier. Only valid for regular files.
43
+ attr_reader :ino
44
+
45
+ # Integer representing the permission bits of the file.
46
+ attr_reader :mode
47
+
48
+ # The number of hard links to the file.
49
+ attr_reader :nlink
50
+
51
+ # The size of the file in bytes.
52
+ attr_reader :size
53
+
54
+ # Nil on Windows
55
+ attr_reader :dev_major, :dev_minor, :rdev_major, :rdev_minor
56
+
57
+ # The version of the win32-file-stat library
58
+ WIN32_FILE_STAT_VERSION = '1.4.0'
59
+
60
+ # Creates and returns a File::Stat object, which encapsulate common status
61
+ # information for File objects on MS Windows sytems. The information is
62
+ # recorded at the moment the File::Stat object is created; changes made to
63
+ # the file after that point will not be reflected.
64
+ #
65
+ def initialize(file)
66
+ raise TypeError unless file.is_a?(String)
67
+
68
+ path = file.tr('/', "\\")
69
+ @path = path
70
+
71
+ @user_sid = get_file_sid(file, OWNER_SECURITY_INFORMATION)
72
+ @grp_sid = get_file_sid(file, GROUP_SECURITY_INFORMATION)
73
+
74
+ @uid = @user_sid.split('-').last.to_i
75
+ @gid = @grp_sid.split('-').last.to_i
76
+
77
+ @owned = @user_sid == get_current_process_sid(TokenUser)
78
+ @grpowned = @grp_sid == get_current_process_sid(TokenGroups)
79
+
80
+ begin
81
+ # The handle returned will be used by other functions
82
+ handle = get_handle(path)
83
+
84
+ @blockdev = get_blockdev(path)
85
+ @blksize = get_blksize(path)
86
+
87
+ if handle
88
+ @filetype = get_filetype(handle)
89
+ @chardev = @filetype == FILE_TYPE_CHAR
90
+ @regular = @filetype == FILE_TYPE_DISK
91
+ @pipe = @filetype == FILE_TYPE_PIPE
92
+ else
93
+ @chardev = false
94
+ @regular = false
95
+ @pipe = false
96
+ end
97
+
98
+ fpath = path.wincode
99
+
100
+ if handle == nil || ((@blockdev || @chardev || @pipe) && GetDriveType(fpath) != DRIVE_REMOVABLE)
101
+ data = WIN32_FIND_DATA.new
102
+ CloseHandle(handle) if handle
103
+
104
+ handle = FindFirstFile(fpath, data)
105
+
106
+ if handle == INVALID_HANDLE_VALUE
107
+ raise SystemCallError.new('FindFirstFile', FFI.errno)
108
+ end
109
+
110
+ FindClose(handle)
111
+ handle = nil
112
+
113
+ @nlink = 1 # Default from stat/wstat function.
114
+ @ino = nil
115
+ @rdev = nil
116
+ else
117
+ data = BY_HANDLE_FILE_INFORMATION.new
118
+
119
+ unless GetFileInformationByHandle(handle, data)
120
+ raise SystemCallError.new('GetFileInformationByHandle', FFI.errno)
121
+ end
122
+
123
+ @nlink = data[:nNumberOfLinks]
124
+ @ino = (data[:nFileIndexHigh] << 32) | data[:nFileIndexLow]
125
+ @rdev = data[:dwVolumeSerialNumber]
126
+ end
127
+
128
+ @readable = access_check(path, GENERIC_READ)
129
+ @readable_real = @readable
130
+
131
+ @writable = access_check(path, GENERIC_WRITE)
132
+ @writable_real = @writable
133
+
134
+ @world_readable = access_check_world(path, FILE_READ_DATA)
135
+ @world_writable = access_check_world(path, FILE_WRITE_DATA)
136
+
137
+ # Not supported and/or meaningless on MS Windows
138
+ @dev_major = nil
139
+ @dev_minor = nil
140
+ @rdev_major = nil
141
+ @rdev_minor = nil
142
+ @setgid = false
143
+ @setuid = false
144
+ @sticky = false
145
+
146
+ # Originally used GetBinaryType, but it only worked
147
+ # for .exe files, and it could return false positives.
148
+ @executable = %w[.bat .cmd .com .exe].include?(File.extname(@path).downcase)
149
+
150
+ # Set blocks equal to size / blksize, rounded up
151
+ case @blksize
152
+ when nil
153
+ @blocks = nil
154
+ when 0
155
+ @blocks = 0
156
+ else
157
+ @blocks = (data.size.to_f / @blksize.to_f).ceil
158
+ end
159
+
160
+ @attr = data[:dwFileAttributes]
161
+ @atime = Time.at(data.atime)
162
+ @ctime = Time.at(data.ctime)
163
+ @mtime = Time.at(data.mtime)
164
+ @size = data.size
165
+
166
+ @archive = @attr & FILE_ATTRIBUTE_ARCHIVE > 0
167
+ @compressed = @attr & FILE_ATTRIBUTE_COMPRESSED > 0
168
+ @directory = @attr & FILE_ATTRIBUTE_DIRECTORY > 0
169
+ @encrypted = @attr & FILE_ATTRIBUTE_ENCRYPTED > 0
170
+ @hidden = @attr & FILE_ATTRIBUTE_HIDDEN > 0
171
+ @indexed = @attr & ~FILE_ATTRIBUTE_NOT_CONTENT_INDEXED > 0
172
+ @normal = @attr & FILE_ATTRIBUTE_NORMAL > 0
173
+ @offline = @attr & FILE_ATTRIBUTE_OFFLINE > 0
174
+ @readonly = @attr & FILE_ATTRIBUTE_READONLY > 0
175
+ @reparse_point = @attr & FILE_ATTRIBUTE_REPARSE_POINT > 0
176
+ @sparse = @attr & FILE_ATTRIBUTE_SPARSE_FILE > 0
177
+ @system = @attr & FILE_ATTRIBUTE_SYSTEM > 0
178
+ @temporary = @attr & FILE_ATTRIBUTE_TEMPORARY > 0
179
+
180
+ @mode = get_mode
181
+
182
+ if @reparse_point
183
+ @symlink = get_symlink(path)
184
+ else
185
+ @symlink = false
186
+ end
187
+ ensure
188
+ CloseHandle(handle) if handle
189
+ end
190
+ end
191
+
192
+ ## Comparable
193
+
194
+ # Compares two File::Stat objects using modification time.
195
+ #--
196
+ # Custom implementation necessary since we altered File::Stat.
197
+ #
198
+ def <=>(other)
199
+ @mtime.to_i <=> other.mtime.to_i
200
+ end
201
+
202
+ ## Other
203
+
204
+ # Returns whether or not the file is an archive file.
205
+ #
206
+ def archive?
207
+ @archive
208
+ end
209
+
210
+ # Returns whether or not the file is a block device. For MS Windows a
211
+ # block device is a removable drive, cdrom or ramdisk.
212
+ #
213
+ def blockdev?
214
+ @blockdev
215
+ end
216
+
217
+ # Returns whether or not the file is a character device.
218
+ #
219
+ def chardev?
220
+ @chardev
221
+ end
222
+
223
+ # Returns whether or not the file is compressed.
224
+ #
225
+ def compressed?
226
+ @compressed
227
+ end
228
+
229
+ # Returns whether or not the file is a directory.
230
+ #
231
+ def directory?
232
+ @directory
233
+ end
234
+
235
+ # Returns whether or not the file in encrypted.
236
+ #
237
+ def encrypted?
238
+ @encrypted
239
+ end
240
+
241
+ # Returns whether or not the file is executable. Generally speaking, this
242
+ # means .bat, .cmd, .com, and .exe files.
243
+ #
244
+ def executable?
245
+ @executable
246
+ end
247
+
248
+ alias executable_real? executable?
249
+
250
+ # Returns whether or not the file is a regular file, as opposed to a pipe,
251
+ # socket, etc.
252
+ #
253
+ def file?
254
+ @regular
255
+ end
256
+
257
+ # Returns the user ID of the file. If full_sid is true, then the full
258
+ # string sid is returned instead.
259
+ #--
260
+ # The user id is the RID of the SID.
261
+ #
262
+ def gid(full_sid = false)
263
+ full_sid ? @grp_sid : @gid
264
+ end
265
+
266
+ # Returns true if the process owner's ID is the same as one of the file's groups.
267
+ #--
268
+ # Internally we're checking the process sid against the TokenGroups sid.
269
+ #
270
+ def grpowned?
271
+ @grpowned
272
+ end
273
+
274
+ # Returns whether or not the file is hidden.
275
+ #
276
+ def hidden?
277
+ @hidden
278
+ end
279
+
280
+ # Returns whether or not the file is content indexed.
281
+ #
282
+ def indexed?
283
+ @indexed
284
+ end
285
+
286
+ alias content_indexed? indexed?
287
+
288
+ # Returns whether or not the file is 'normal'. This is only true if
289
+ # virtually all other attributes are false.
290
+ #
291
+ def normal?
292
+ @normal
293
+ end
294
+
295
+ # Returns whether or not the file is offline.
296
+ #
297
+ def offline?
298
+ @offline
299
+ end
300
+
301
+ # Returns whether or not the current process owner is the owner of the file.
302
+ #--
303
+ # Internally we're checking the process sid against the owner's sid.
304
+ def owned?
305
+ @owned
306
+ end
307
+
308
+ # Returns the drive number of the disk containing the file, or -1 if there
309
+ # is no associated drive number.
310
+ #
311
+ # If the +letter+ option is true, returns the drive letter instead. If there
312
+ # is no drive letter, it will return nil.
313
+ #--
314
+ # This differs slightly from MRI in that it will return -1 if the path
315
+ # does not have a drive letter.
316
+ #
317
+ # Note: Bug in JRuby as of JRuby 1.7.8, which does not expand NUL properly.
318
+ #
319
+ def dev(letter = false)
320
+ fpath = File.expand_path(@path).wincode
321
+ num = PathGetDriveNumber(fpath)
322
+
323
+ if letter
324
+ if num == -1
325
+ nil
326
+ else
327
+ (num + 'A'.ord).chr + ':'
328
+ end
329
+ else
330
+ num
331
+ end
332
+ end
333
+
334
+ # Returns whether or not the file is readable by the process owner.
335
+ #--
336
+ # In Windows terms, we're checking for GENERIC_READ privileges.
337
+ #
338
+ def readable?
339
+ @readable
340
+ end
341
+
342
+ # A synonym for File::Stat#readable?
343
+ #
344
+ def readable_real?
345
+ @readable_real
346
+ end
347
+
348
+ # Returns whether or not the file is readonly.
349
+ #
350
+ def readonly?
351
+ @readonly
352
+ end
353
+
354
+ alias read_only? readonly?
355
+
356
+ # Returns whether or not the file is a pipe.
357
+ #
358
+ def pipe?
359
+ @pipe
360
+ end
361
+
362
+ alias socket? pipe?
363
+
364
+ # Returns whether or not the file is a reparse point.
365
+ #
366
+ def reparse_point?
367
+ @reparse_point
368
+ end
369
+
370
+ # Returns false on MS Windows.
371
+ #--
372
+ # I had to explicitly define this because of a bug in JRuby.
373
+ #
374
+ def setgid?
375
+ @setgid
376
+ end
377
+
378
+ # Returns false on MS Windows.
379
+ #--
380
+ # I had to explicitly define this because of a bug in JRuby.
381
+ #
382
+ def setuid?
383
+ @setuid
384
+ end
385
+
386
+ # Returns whether or not the file size is zero.
387
+ #
388
+ def size?
389
+ @size > 0 ? @size : nil
390
+ end
391
+
392
+ # Returns whether or not the file is a sparse file. In most cases a sparse
393
+ # file is an image file.
394
+ #
395
+ def sparse?
396
+ @sparse
397
+ end
398
+
399
+ # Returns false on MS Windows.
400
+ #--
401
+ # I had to explicitly define this because of a bug in JRuby.
402
+ #
403
+ def sticky?
404
+ @sticky
405
+ end
406
+
407
+ # Returns whether or not the file is a symlink.
408
+ #
409
+ def symlink?
410
+ @symlink
411
+ end
412
+
413
+ # Returns whether or not the file is a system file.
414
+ #
415
+ def system?
416
+ @system
417
+ end
418
+
419
+ # Returns whether or not the file is being used for temporary storage.
420
+ #
421
+ def temporary?
422
+ @temporary
423
+ end
424
+
425
+ # Returns the user ID of the file. If the +full_sid+ is true, then the
426
+ # full string sid is returned instead.
427
+ #--
428
+ # The user id is the RID of the SID.
429
+ #
430
+ def uid(full_sid = false)
431
+ full_sid ? @user_sid : @uid
432
+ end
433
+
434
+ # Returns whether or not the file is readable by others. Note that this
435
+ # merely returns true or false, not permission bits (or nil).
436
+ #--
437
+ # In Windows terms, this is checking the access right FILE_READ_DATA against
438
+ # the well-known SID "S-1-1-0", aka "Everyone".
439
+ #
440
+ #
441
+ def world_readable?
442
+ @world_readable
443
+ end
444
+
445
+ # Returns whether or not the file is writable by others. Note that this
446
+ # merely returns true or false, not permission bits (or nil).
447
+ #--
448
+ # In Windows terms, this is checking the access right FILE_WRITE_DATA against
449
+ # the well-known SID "S-1-1-0", aka "Everyone".
450
+ #
451
+ def world_writable?
452
+ @world_writable
453
+ end
454
+
455
+ # Returns whether or not the file is writable by the current process owner.
456
+ #--
457
+ # In Windows terms, we're checking for GENERIC_WRITE privileges.
458
+ #
459
+ def writable?
460
+ @writable
461
+ end
462
+
463
+ # A synonym for File::Stat#readable?
464
+ #
465
+ def writable_real?
466
+ @writable_real
467
+ end
468
+
469
+ # Returns whether or not the file size is zero.
470
+ #
471
+ def zero?
472
+ @size == 0
473
+ end
474
+
475
+ # Identifies the type of file. The return string is one of 'file',
476
+ # 'directory', 'characterSpecial', 'socket' or 'unknown'.
477
+ #
478
+ def ftype
479
+ return 'directory' if @directory
480
+
481
+ case @filetype
482
+ when FILE_TYPE_CHAR
483
+ 'characterSpecial'
484
+ when FILE_TYPE_DISK
485
+ 'file'
486
+ when FILE_TYPE_PIPE
487
+ 'socket'
488
+ else
489
+ if blockdev?
490
+ 'blockSpecial'
491
+ else
492
+ 'unknown'
493
+ end
494
+ end
495
+ end
496
+
497
+ # Returns a stringified version of a File::Stat object.
498
+ #
499
+ def inspect
500
+ members = %w[
501
+ archive? atime blksize blockdev? blocks compressed? ctime dev
502
+ encrypted? gid hidden? indexed? ino mode mtime rdev nlink normal?
503
+ offline? readonly? reparse_point? size sparse? system? temporary?
504
+ uid
505
+ ]
506
+
507
+ str = "#<#{self.class}"
508
+
509
+ members.sort.each{ |mem|
510
+ if mem == 'mode'
511
+ str << " #{mem}=" << sprintf("0%o", send(mem.intern))
512
+ elsif mem[-1].chr == '?' # boolean methods
513
+ str << " #{mem.chop}=" << send(mem.intern).to_s
514
+ else
515
+ str << " #{mem}=" << send(mem.intern).to_s
516
+ end
517
+ }
518
+
519
+ str
520
+ end
521
+
522
+ # A custom pretty print method. This was necessary not only to handle
523
+ # the additional attributes, but to work around an error caused by the
524
+ # builtin method for the current File::Stat class (see pp.rb).
525
+ #
526
+ def pretty_print(q)
527
+ members = %w[
528
+ archive? atime blksize blockdev? blocks compressed? ctime dev
529
+ encrypted? gid hidden? indexed? ino mode mtime rdev nlink normal?
530
+ offline? readonly? reparse_point? size sparse? system? temporary?
531
+ uid
532
+ ]
533
+
534
+ q.object_group(self){
535
+ q.breakable
536
+ members.each{ |mem|
537
+ q.group{
538
+ q.text("#{mem}".ljust(15) + "=> ")
539
+ if mem == 'mode'
540
+ q.text(sprintf("0%o", send(mem.intern)))
541
+ else
542
+ val = self.send(mem.intern)
543
+ if val.nil?
544
+ q.text('nil')
545
+ else
546
+ q.text(val.to_s)
547
+ end
548
+ end
549
+ }
550
+ q.comma_breakable unless mem == members.last
551
+ }
552
+ }
553
+ end
554
+
555
+ private
556
+
557
+ # This is based on fileattr_to_unixmode in win32.c
558
+ #
559
+ def get_mode
560
+ mode = 0
561
+
562
+ s_iread = 0x0100; s_iwrite = 0x0080; s_iexec = 0x0040
563
+ s_ifreg = 0x8000; s_ifdir = 0x4000; s_iwusr = 0200
564
+ s_iwgrp = 0020; s_iwoth = 0002;
565
+
566
+ if @readonly
567
+ mode |= s_iread
568
+ else
569
+ mode |= s_iread | s_iwrite | s_iwusr
570
+ end
571
+
572
+ if @directory
573
+ mode |= s_ifdir | s_iexec
574
+ else
575
+ mode |= s_ifreg
576
+ end
577
+
578
+ if @executable
579
+ mode |= s_iexec
580
+ end
581
+
582
+ mode |= (mode & 0700) >> 3;
583
+ mode |= (mode & 0700) >> 6;
584
+
585
+ mode &= ~(s_iwgrp | s_iwoth)
586
+
587
+ mode
588
+ end
589
+
590
+ # Returns whether or not +path+ is a block device.
591
+ #
592
+ def get_blockdev(path)
593
+ ptr = FFI::MemoryPointer.from_string(path.wincode)
594
+
595
+ if PathStripToRoot(ptr)
596
+ fpath = ptr.read_bytes(path.size * 2).split("\000\000").first
597
+ else
598
+ fpath = nil
599
+ end
600
+
601
+ case GetDriveType(fpath)
602
+ when DRIVE_REMOVABLE, DRIVE_CDROM, DRIVE_RAMDISK
603
+ true
604
+ else
605
+ false
606
+ end
607
+ end
608
+
609
+ # Returns the blksize for +path+.
610
+ #---
611
+ # The jruby-ffi gem (as of 1.9.3) reports a failure here where it shouldn't.
612
+ # Consequently, this method returns 4096 automatically for now on JRuby.
613
+ #
614
+ def get_blksize(path)
615
+ return 4096 if RUBY_PLATFORM == 'java' # Bug in jruby-ffi
616
+
617
+ ptr = FFI::MemoryPointer.from_string(path.wincode)
618
+
619
+ if PathStripToRoot(ptr)
620
+ fpath = ptr.read_bytes(path.size * 2).split("\000\000").first
621
+ else
622
+ fpath = nil
623
+ end
624
+
625
+ size = nil
626
+
627
+ sectors = FFI::MemoryPointer.new(:ulong)
628
+ bytes = FFI::MemoryPointer.new(:ulong)
629
+ free = FFI::MemoryPointer.new(:ulong)
630
+ total = FFI::MemoryPointer.new(:ulong)
631
+
632
+ if GetDiskFreeSpace(fpath, sectors, bytes, free, total)
633
+ size = sectors.read_ulong * bytes.read_ulong
634
+ else
635
+ unless PathIsUNC(fpath)
636
+ raise SystemCallError.new('GetDiskFreeSpace', FFI.errno)
637
+ end
638
+ end
639
+
640
+ size
641
+ end
642
+
643
+ # Generic method for retrieving a handle.
644
+ #
645
+ def get_handle(path)
646
+ fpath = path.wincode
647
+
648
+ handle = CreateFile(
649
+ fpath,
650
+ GENERIC_READ,
651
+ FILE_SHARE_READ,
652
+ nil,
653
+ OPEN_EXISTING,
654
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
655
+ 0
656
+ )
657
+
658
+ if handle == INVALID_HANDLE_VALUE
659
+ return nil if FFI.errno == 32 # ERROR_SHARING_VIOLATION. Locked files.
660
+ raise SystemCallError.new('CreateFile', FFI.errno)
661
+ end
662
+
663
+ handle
664
+ end
665
+
666
+ # Determines whether or not +file+ is a symlink.
667
+ #
668
+ def get_symlink(file)
669
+ bool = false
670
+ fpath = File.expand_path(file).wincode
671
+
672
+ begin
673
+ data = WIN32_FIND_DATA.new
674
+ handle = FindFirstFile(fpath, data)
675
+
676
+ if handle == INVALID_HANDLE_VALUE
677
+ raise SystemCallError.new('FindFirstFile', FFI.errno)
678
+ end
679
+
680
+ if data[:dwReserved0] == IO_REPARSE_TAG_SYMLINK
681
+ bool = true
682
+ end
683
+ ensure
684
+ FindClose(handle) if handle
685
+ end
686
+
687
+ bool
688
+ end
689
+
690
+ # Returns the filetype for the given +handle+.
691
+ #
692
+ def get_filetype(handle)
693
+ file_type = GetFileType(handle)
694
+
695
+ if file_type == FILE_TYPE_UNKNOWN && FFI.errno != NO_ERROR
696
+ raise SystemCallError.new('GetFileType', FFI.errno)
697
+ end
698
+
699
+ file_type
700
+ end
701
+
702
+ # Return a sid of the file's owner.
703
+ #
704
+ def get_file_sid(file, info)
705
+ wfile = file.wincode
706
+ size_needed_ptr = FFI::MemoryPointer.new(:ulong)
707
+
708
+ # First pass, get the size needed
709
+ bool = GetFileSecurity(wfile, info, nil, 0, size_needed_ptr)
710
+
711
+ size_needed = size_needed_ptr.read_ulong
712
+ security_ptr = FFI::MemoryPointer.new(size_needed)
713
+
714
+ # Second pass, this time with the appropriately sized security pointer
715
+ bool = GetFileSecurity(wfile, info, security_ptr, security_ptr.size, size_needed_ptr)
716
+
717
+ unless bool
718
+ error = FFI.errno
719
+ return "S-1-5-80-0" if error == 32 # ERROR_SHARING_VIOLATION. Locked files, etc.
720
+ raise SystemCallError.new("GetFileSecurity", error)
721
+ end
722
+
723
+ sid_ptr = FFI::MemoryPointer.new(:pointer)
724
+ defaulted = FFI::MemoryPointer.new(:bool)
725
+
726
+ if info == OWNER_SECURITY_INFORMATION
727
+ bool = GetSecurityDescriptorOwner(security_ptr, sid_ptr, defaulted)
728
+ meth = "GetSecurityDescriptorOwner"
729
+ else
730
+ bool = GetSecurityDescriptorGroup(security_ptr, sid_ptr, defaulted)
731
+ meth = "GetSecurityDescriptorGroup"
732
+ end
733
+
734
+ raise SystemCallError.new(meth, FFI.errno) unless bool
735
+
736
+ ptr = FFI::MemoryPointer.new(:string)
737
+
738
+ unless ConvertSidToStringSid(sid_ptr.read_pointer, ptr)
739
+ raise SystemCallError.new("ConvertSidToStringSid")
740
+ end
741
+
742
+ ptr.read_pointer.read_string
743
+ end
744
+
745
+ # Return the sid of the current process.
746
+ #
747
+ def get_current_process_sid(token_type)
748
+ token = FFI::MemoryPointer.new(:uintptr_t)
749
+ sid = nil
750
+
751
+ begin
752
+ # Get the current process sid
753
+ unless OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token)
754
+ raise SystemCallError.new("OpenProcessToken", FFI.errno)
755
+ end
756
+
757
+ token = token.read_pointer.to_i
758
+ rlength = FFI::MemoryPointer.new(:pointer)
759
+
760
+ if token_type == TokenUser
761
+ buf = 0.chr * 512
762
+ else
763
+ buf = TOKEN_GROUP.new
764
+ end
765
+
766
+ unless GetTokenInformation(token, token_type, buf, buf.size, rlength)
767
+ raise SystemCallError.new("GetTokenInformation", FFI.errno)
768
+ end
769
+
770
+ if token_type == TokenUser
771
+ tsid = buf[FFI.type_size(:pointer)*2, (rlength.read_ulong - FFI.type_size(:pointer)*2)]
772
+ else
773
+ tsid = buf[:Groups][0][:Sid]
774
+ end
775
+
776
+ ptr = FFI::MemoryPointer.new(:string)
777
+
778
+ unless ConvertSidToStringSid(tsid, ptr)
779
+ raise SystemCallError.new("ConvertSidToStringSid")
780
+ end
781
+
782
+ sid = ptr.read_pointer.read_string
783
+ ensure
784
+ CloseHandle(token) if token
785
+ end
786
+
787
+ sid
788
+ end
789
+
790
+ # Returns whether or not the current process has given access rights for +path+.
791
+ #
792
+ def access_check(path, access_rights)
793
+ wfile = path.wincode
794
+ check = false
795
+ size_needed_ptr = FFI::MemoryPointer.new(:ulong)
796
+
797
+ flags = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION |
798
+ DACL_SECURITY_INFORMATION
799
+
800
+ # First attempt, get the size needed
801
+ bool = GetFileSecurity(wfile, flags, nil, 0, size_needed_ptr)
802
+
803
+ # If it fails horribly here, assume the answer is no.
804
+ if !bool && FFI.errno != ERROR_INSUFFICIENT_BUFFER
805
+ return false
806
+ end
807
+
808
+ size_needed = size_needed_ptr.read_ulong
809
+ security_ptr = FFI::MemoryPointer.new(size_needed)
810
+
811
+ # Second attempt, now with the needed size
812
+ if GetFileSecurity(wfile, flags, security_ptr, size_needed, size_needed_ptr)
813
+ token = FFI::MemoryPointer.new(:uintptr_t)
814
+
815
+ pflags = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ
816
+
817
+ if OpenProcessToken(GetCurrentProcess(), pflags, token)
818
+ begin
819
+ token = token.read_pointer.to_i
820
+ token2 = FFI::MemoryPointer.new(:uintptr_t)
821
+
822
+ if DuplicateToken(token, SecurityImpersonation, token2)
823
+ begin
824
+ token2 = token2.read_pointer.to_i
825
+ mapping = GENERIC_MAPPING.new
826
+ privileges = PRIVILEGE_SET.new
827
+ privileges[:PrivilegeCount] = 0
828
+ privileges_length = privileges.size
829
+
830
+ mapping[:GenericRead] = FILE_GENERIC_READ
831
+ mapping[:GenericWrite] = FILE_GENERIC_WRITE
832
+ mapping[:GenericExecute] = FILE_GENERIC_EXECUTE
833
+ mapping[:GenericAll] = FILE_ALL_ACCESS
834
+
835
+ rights_ptr = FFI::MemoryPointer.new(:ulong)
836
+ rights_ptr.write_ulong(access_rights)
837
+
838
+ MapGenericMask(rights_ptr, mapping)
839
+ rights = rights_ptr.read_ulong
840
+
841
+ result_ptr = FFI::MemoryPointer.new(:ulong)
842
+ privileges_length_ptr = FFI::MemoryPointer.new(:ulong)
843
+ privileges_length_ptr.write_ulong(privileges_length)
844
+ granted_access_ptr = FFI::MemoryPointer.new(:ulong)
845
+
846
+ bool = AccessCheck(
847
+ security_ptr,
848
+ token2,
849
+ rights,
850
+ mapping,
851
+ privileges,
852
+ privileges_length_ptr,
853
+ granted_access_ptr,
854
+ result_ptr
855
+ )
856
+
857
+ if bool
858
+ check = result_ptr.read_ulong == 1
859
+ else
860
+ raise SystemCallError.new('AccessCheck', FFI.errno)
861
+ end
862
+ ensure
863
+ CloseHandle(token2)
864
+ end
865
+ end
866
+ ensure
867
+ CloseHandle(token)
868
+ end
869
+ end
870
+ end
871
+
872
+ check
873
+ end
874
+
875
+ # Returns whether or not the Everyone has given access rights for +path+.
876
+ #
877
+ def access_check_world(path, access_rights)
878
+ wfile = path.wincode
879
+ check = false
880
+ size_needed_ptr = FFI::MemoryPointer.new(:ulong)
881
+
882
+ flags = DACL_SECURITY_INFORMATION
883
+
884
+ # First attempt, get the size needed
885
+ bool = GetFileSecurity(wfile, flags, nil, 0, size_needed_ptr)
886
+
887
+ # If it fails horribly here, assume the answer is no.
888
+ if !bool && FFI.errno != ERROR_INSUFFICIENT_BUFFER
889
+ return false
890
+ end
891
+
892
+ size_needed = size_needed_ptr.read_ulong
893
+ security_ptr = FFI::MemoryPointer.new(size_needed)
894
+
895
+ # Second attempt, now with the needed size
896
+ if GetFileSecurity(wfile, flags, security_ptr, size_needed, size_needed_ptr)
897
+ present_ptr = FFI::MemoryPointer.new(:ulong)
898
+ pdacl_ptr = FFI::MemoryPointer.new(:pointer)
899
+ defaulted_ptr = FFI::MemoryPointer.new(:ulong)
900
+
901
+ bool = GetSecurityDescriptorDacl(
902
+ security_ptr,
903
+ present_ptr,
904
+ pdacl_ptr,
905
+ defaulted_ptr
906
+ )
907
+
908
+ # If it fails, or the dacl isn't present, return false.
909
+ if !bool || present_ptr.read_ulong == 0
910
+ return false
911
+ end
912
+
913
+ pdacl = pdacl_ptr.read_pointer
914
+ psid_ptr = FFI::MemoryPointer.new(:pointer)
915
+
916
+ # S-1-1-0 is the well known SID for "Everyone".
917
+ ConvertStringSidToSid('S-1-1-0', psid_ptr)
918
+
919
+ psid = psid_ptr.read_pointer
920
+ trustee_ptr = FFI::MemoryPointer.new(TRUSTEE)
921
+
922
+ BuildTrusteeWithSid(trustee_ptr, psid)
923
+
924
+ rights_ptr = FFI::MemoryPointer.new(:ulong)
925
+
926
+ if GetEffectiveRightsFromAcl(pdacl, trustee_ptr, rights_ptr) == NO_ERROR
927
+ rights = rights_ptr.read_ulong
928
+ check = (rights & access_rights) == access_rights
929
+ end
930
+ end
931
+
932
+ check
933
+ end
934
+ end