win32-file-stat 1.3.6 → 1.4.0

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.
@@ -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