win32-file-stat 1.5.1 → 1.5.2

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/Rakefile CHANGED
@@ -1,31 +1,31 @@
1
- require 'rake'
2
- require 'rake/clean'
3
- require 'rake/testtask'
4
-
5
- CLEAN.include("**/*.gem", "**/*.rbx", "**/*.rbc", "**/*.log", "**/*.exe", "**/*.txt")
6
-
7
- namespace :gem do
8
- desc "Create the win32-file-stat gem"
9
- task :create => [:clean] do
10
- spec = eval(IO.read("win32-file-stat.gemspec"))
11
- if Gem::VERSION < "2.0.0"
12
- Gem::Builder.new(spec).build
13
- else
14
- require 'rubygems/package'
15
- Gem::Package.build(spec)
16
- end
17
- end
18
-
19
- desc "Install the win32-file-stat gem"
20
- task :install => [:create] do
21
- file = Dir["win32-file-stat*.gem"].first
22
- sh "gem install -l #{file}"
23
- end
24
- end
25
-
26
- Rake::TestTask.new do |t|
27
- t.verbose = true
28
- t.warning = true
29
- end
30
-
31
- task :default => :test
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rake/testtask'
4
+
5
+ CLEAN.include("**/*.gem", "**/*.rbx", "**/*.rbc", "**/*.log", "**/*.exe", "**/*.txt")
6
+
7
+ namespace :gem do
8
+ desc "Create the win32-file-stat gem"
9
+ task :create => [:clean] do
10
+ spec = eval(IO.read("win32-file-stat.gemspec"))
11
+ if Gem::VERSION < "2.0.0"
12
+ Gem::Builder.new(spec).build
13
+ else
14
+ require 'rubygems/package'
15
+ Gem::Package.build(spec)
16
+ end
17
+ end
18
+
19
+ desc "Install the win32-file-stat gem"
20
+ task :install => [:create] do
21
+ file = Dir["win32-file-stat*.gem"].first
22
+ sh "gem install -l #{file}"
23
+ end
24
+ end
25
+
26
+ Rake::TestTask.new do |t|
27
+ t.verbose = true
28
+ t.warning = true
29
+ end
30
+
31
+ task :default => :test
@@ -1,984 +1,994 @@
1
- require_relative 'windows/helper'
2
- require_relative 'windows/constants'
3
- require_relative 'windows/structs'
4
- require_relative '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
- # Alternate streams
58
- attr_reader :streams
59
-
60
- # The version of the win32-file-stat library
61
- WIN32_FILE_STAT_VERSION = '1.5.1'
62
-
63
- # Creates and returns a File::Stat object, which encapsulate common status
64
- # information for File objects on MS Windows sytems. The information is
65
- # recorded at the moment the File::Stat object is created; changes made to
66
- # the file after that point will not be reflected.
67
- #
68
- def initialize(file)
69
- file = string_check(file)
70
-
71
- path = file.tr('/', "\\")
72
- @path = path
73
-
74
- @user_sid = get_file_sid(file, OWNER_SECURITY_INFORMATION)
75
- @grp_sid = get_file_sid(file, GROUP_SECURITY_INFORMATION)
76
-
77
- @uid = @user_sid.split('-').last.to_i
78
- @gid = @grp_sid.split('-').last.to_i
79
-
80
- @owned = @user_sid == get_current_process_sid(TokenUser)
81
- @grpowned = @grp_sid == get_current_process_sid(TokenGroups)
82
-
83
- begin
84
- # The handle returned will be used by other functions
85
- handle = get_handle(path)
86
-
87
- @blockdev = get_blockdev(path)
88
- @blksize = get_blksize(path)
89
-
90
- if handle
91
- @filetype = get_filetype(handle)
92
- @streams = get_streams(handle)
93
- @chardev = @filetype == FILE_TYPE_CHAR
94
- @regular = @filetype == FILE_TYPE_DISK
95
- @pipe = @filetype == FILE_TYPE_PIPE
96
- else
97
- @chardev = false
98
- @regular = false
99
- @pipe = false
100
- end
101
-
102
- fpath = path.wincode
103
-
104
- if handle == nil || ((@blockdev || @chardev || @pipe) && GetDriveType(fpath) != DRIVE_REMOVABLE)
105
- data = WIN32_FIND_DATA.new
106
- CloseHandle(handle) if handle
107
-
108
- handle = FindFirstFile(fpath, data)
109
-
110
- if handle == INVALID_HANDLE_VALUE
111
- raise SystemCallError.new('FindFirstFile', FFI.errno)
112
- end
113
-
114
- FindClose(handle)
115
- handle = nil
116
-
117
- @nlink = 1 # Default from stat/wstat function.
118
- @ino = nil
119
- @rdev = nil
120
- else
121
- data = BY_HANDLE_FILE_INFORMATION.new
122
-
123
- unless GetFileInformationByHandle(handle, data)
124
- raise SystemCallError.new('GetFileInformationByHandle', FFI.errno)
125
- end
126
-
127
- @nlink = data[:nNumberOfLinks]
128
- @ino = (data[:nFileIndexHigh] << 32) | data[:nFileIndexLow]
129
- @rdev = data[:dwVolumeSerialNumber]
130
- end
131
-
132
- # Not supported and/or meaningless on MS Windows
133
- @dev_major = nil
134
- @dev_minor = nil
135
- @rdev_major = nil
136
- @rdev_minor = nil
137
- @setgid = false
138
- @setuid = false
139
- @sticky = false
140
-
141
- # Originally used GetBinaryType, but it only worked
142
- # for .exe files, and it could return false positives.
143
- @executable = %w[.bat .cmd .com .exe].include?(File.extname(@path).downcase)
144
-
145
- # Set blocks equal to size / blksize, rounded up
146
- case @blksize
147
- when nil
148
- @blocks = nil
149
- when 0
150
- @blocks = 0
151
- else
152
- @blocks = (data.size.to_f / @blksize.to_f).ceil
153
- end
154
-
155
- @attr = data[:dwFileAttributes]
156
- @atime = Time.at(data.atime)
157
- @ctime = Time.at(data.ctime)
158
- @mtime = Time.at(data.mtime)
159
- @size = data.size
160
-
161
- @archive = @attr & FILE_ATTRIBUTE_ARCHIVE > 0
162
- @compressed = @attr & FILE_ATTRIBUTE_COMPRESSED > 0
163
- @directory = @attr & FILE_ATTRIBUTE_DIRECTORY > 0
164
- @encrypted = @attr & FILE_ATTRIBUTE_ENCRYPTED > 0
165
- @hidden = @attr & FILE_ATTRIBUTE_HIDDEN > 0
166
- @indexed = @attr & ~FILE_ATTRIBUTE_NOT_CONTENT_INDEXED > 0
167
- @normal = @attr & FILE_ATTRIBUTE_NORMAL > 0
168
- @offline = @attr & FILE_ATTRIBUTE_OFFLINE > 0
169
- @readonly = @attr & FILE_ATTRIBUTE_READONLY > 0
170
- @reparse_point = @attr & FILE_ATTRIBUTE_REPARSE_POINT > 0
171
- @sparse = @attr & FILE_ATTRIBUTE_SPARSE_FILE > 0
172
- @system = @attr & FILE_ATTRIBUTE_SYSTEM > 0
173
- @temporary = @attr & FILE_ATTRIBUTE_TEMPORARY > 0
174
-
175
- @mode = get_mode
176
-
177
- @readable = access_check(path, GENERIC_READ)
178
- @readable_real = @readable
179
-
180
- # The MSDN docs say that the readonly attribute is honored for directories
181
- if @directory
182
- @writable = access_check(path, GENERIC_WRITE)
183
- else
184
- @writable = access_check(path, GENERIC_WRITE) && !@readonly
185
- end
186
-
187
- @writable_real = @writable
188
-
189
- @world_readable = access_check_world(path, FILE_READ_DATA)
190
- @world_writable = access_check_world(path, FILE_WRITE_DATA) && !@readonly
191
-
192
- if @reparse_point
193
- @symlink = get_symlink(path)
194
- else
195
- @symlink = false
196
- end
197
- ensure
198
- CloseHandle(handle) if handle
199
- end
200
- end
201
-
202
- ## Comparable
203
-
204
- # Compares two File::Stat objects using modification time.
205
- #--
206
- # Custom implementation necessary since we altered File::Stat.
207
- #
208
- def <=>(other)
209
- @mtime.to_i <=> other.mtime.to_i
210
- end
211
-
212
- ## Other
213
-
214
- # Returns whether or not the file is an archive file.
215
- #
216
- def archive?
217
- @archive
218
- end
219
-
220
- # Returns whether or not the file is a block device. For MS Windows a
221
- # block device is a removable drive, cdrom or ramdisk.
222
- #
223
- def blockdev?
224
- @blockdev
225
- end
226
-
227
- # Returns whether or not the file is a character device.
228
- #
229
- def chardev?
230
- @chardev
231
- end
232
-
233
- # Returns whether or not the file is compressed.
234
- #
235
- def compressed?
236
- @compressed
237
- end
238
-
239
- # Returns whether or not the file is a directory.
240
- #
241
- def directory?
242
- @directory
243
- end
244
-
245
- # Returns whether or not the file in encrypted.
246
- #
247
- def encrypted?
248
- @encrypted
249
- end
250
-
251
- # Returns whether or not the file is executable. Generally speaking, this
252
- # means .bat, .cmd, .com, and .exe files.
253
- #
254
- def executable?
255
- @executable
256
- end
257
-
258
- alias executable_real? executable?
259
-
260
- # Returns whether or not the file is a regular file, as opposed to a pipe,
261
- # socket, etc.
262
- #
263
- def file?
264
- @regular
265
- end
266
-
267
- # Returns the user ID of the file. If full_sid is true, then the full
268
- # string sid is returned instead.
269
- #--
270
- # The user id is the RID of the SID.
271
- #
272
- def gid(full_sid = false)
273
- full_sid ? @grp_sid : @gid
274
- end
275
-
276
- # Returns true if the process owner's ID is the same as one of the file's groups.
277
- #--
278
- # Internally we're checking the process sid against the TokenGroups sid.
279
- #
280
- def grpowned?
281
- @grpowned
282
- end
283
-
284
- # Returns whether or not the file is hidden.
285
- #
286
- def hidden?
287
- @hidden
288
- end
289
-
290
- # Returns whether or not the file is content indexed.
291
- #
292
- def indexed?
293
- @indexed
294
- end
295
-
296
- alias content_indexed? indexed?
297
-
298
- # Returns whether or not the file is 'normal'. This is only true if
299
- # virtually all other attributes are false.
300
- #
301
- def normal?
302
- @normal
303
- end
304
-
305
- # Returns whether or not the file is offline.
306
- #
307
- def offline?
308
- @offline
309
- end
310
-
311
- # Returns whether or not the current process owner is the owner of the file.
312
- #--
313
- # Internally we're checking the process sid against the owner's sid.
314
- def owned?
315
- @owned
316
- end
317
-
318
- # Returns the drive number of the disk containing the file, or -1 if there
319
- # is no associated drive number.
320
- #
321
- # If the +letter+ option is true, returns the drive letter instead. If there
322
- # is no drive letter, it will return nil.
323
- #--
324
- # This differs slightly from MRI in that it will return -1 if the path
325
- # does not have a drive letter.
326
- #
327
- # Note: Bug in JRuby as of JRuby 1.7.8, which does not expand NUL properly.
328
- #
329
- def dev(letter = false)
330
- fpath = File.expand_path(@path).wincode
331
- num = PathGetDriveNumber(fpath)
332
-
333
- if letter
334
- if num == -1
335
- nil
336
- else
337
- (num + 'A'.ord).chr + ':'
338
- end
339
- else
340
- num
341
- end
342
- end
343
-
344
- # Returns whether or not the file is readable by the process owner.
345
- #--
346
- # In Windows terms, we're checking for GENERIC_READ privileges.
347
- #
348
- def readable?
349
- @readable
350
- end
351
-
352
- # A synonym for File::Stat#readable?
353
- #
354
- def readable_real?
355
- @readable_real
356
- end
357
-
358
- # Returns whether or not the file is readonly.
359
- #
360
- def readonly?
361
- @readonly
362
- end
363
-
364
- alias read_only? readonly?
365
-
366
- # Returns whether or not the file is a pipe.
367
- #
368
- def pipe?
369
- @pipe
370
- end
371
-
372
- alias socket? pipe?
373
-
374
- # Returns whether or not the file is a reparse point.
375
- #
376
- def reparse_point?
377
- @reparse_point
378
- end
379
-
380
- # Returns false on MS Windows.
381
- #--
382
- # I had to explicitly define this because of a bug in JRuby.
383
- #
384
- def setgid?
385
- @setgid
386
- end
387
-
388
- # Returns false on MS Windows.
389
- #--
390
- # I had to explicitly define this because of a bug in JRuby.
391
- #
392
- def setuid?
393
- @setuid
394
- end
395
-
396
- # Returns whether or not the file size is zero.
397
- #
398
- def size?
399
- @size > 0 ? @size : nil
400
- end
401
-
402
- # Returns whether or not the file is a sparse file. In most cases a sparse
403
- # file is an image file.
404
- #
405
- def sparse?
406
- @sparse
407
- end
408
-
409
- # Returns false on MS Windows.
410
- #--
411
- # I had to explicitly define this because of a bug in JRuby.
412
- #
413
- def sticky?
414
- @sticky
415
- end
416
-
417
- # Returns whether or not the file is a symlink.
418
- #
419
- def symlink?
420
- @symlink
421
- end
422
-
423
- # Returns whether or not the file is a system file.
424
- #
425
- def system?
426
- @system
427
- end
428
-
429
- # Returns whether or not the file is being used for temporary storage.
430
- #
431
- def temporary?
432
- @temporary
433
- end
434
-
435
- # Returns the user ID of the file. If the +full_sid+ is true, then the
436
- # full string sid is returned instead.
437
- #--
438
- # The user id is the RID of the SID.
439
- #
440
- def uid(full_sid = false)
441
- full_sid ? @user_sid : @uid
442
- end
443
-
444
- # Returns whether or not the file is readable by others. Note that this
445
- # merely returns true or false, not permission bits (or nil).
446
- #--
447
- # In Windows terms, this is checking the access right FILE_READ_DATA against
448
- # the well-known SID "S-1-1-0", aka "Everyone".
449
- #
450
- #
451
- def world_readable?
452
- @world_readable
453
- end
454
-
455
- # Returns whether or not the file is writable by others. Note that this
456
- # merely returns true or false, not permission bits (or nil).
457
- #--
458
- # In Windows terms, this is checking the access right FILE_WRITE_DATA against
459
- # the well-known SID "S-1-1-0", aka "Everyone".
460
- #
461
- def world_writable?
462
- @world_writable
463
- end
464
-
465
- # Returns whether or not the file is writable by the current process owner.
466
- #--
467
- # In Windows terms, we're checking for GENERIC_WRITE privileges.
468
- #
469
- def writable?
470
- @writable
471
- end
472
-
473
- # A synonym for File::Stat#readable?
474
- #
475
- def writable_real?
476
- @writable_real
477
- end
478
-
479
- # Returns whether or not the file size is zero.
480
- #
481
- def zero?
482
- @size == 0
483
- end
484
-
485
- # Identifies the type of file. The return string is one of 'file',
486
- # 'directory', 'characterSpecial', 'socket' or 'unknown'.
487
- #
488
- def ftype
489
- return 'directory' if @directory
490
-
491
- case @filetype
492
- when FILE_TYPE_CHAR
493
- 'characterSpecial'
494
- when FILE_TYPE_DISK
495
- 'file'
496
- when FILE_TYPE_PIPE
497
- 'socket'
498
- else
499
- if blockdev?
500
- 'blockSpecial'
501
- else
502
- 'unknown'
503
- end
504
- end
505
- end
506
-
507
- # Returns a stringified version of a File::Stat object.
508
- #
509
- def inspect
510
- members = %w[
511
- archive? atime blksize blockdev? blocks compressed? ctime dev
512
- encrypted? gid hidden? indexed? ino mode mtime rdev nlink normal?
513
- offline? readonly? reparse_point? size sparse? system? streams
514
- temporary? uid
515
- ]
516
-
517
- str = "#<#{self.class}"
518
-
519
- members.sort.each{ |mem|
520
- if mem == 'mode'
521
- str << " #{mem}=" << sprintf("0%o", send(mem.intern))
522
- elsif mem[-1].chr == '?' # boolean methods
523
- str << " #{mem.chop}=" << send(mem.intern).to_s
524
- else
525
- str << " #{mem}=" << send(mem.intern).to_s
526
- end
527
- }
528
-
529
- str
530
- end
531
-
532
- # A custom pretty print method. This was necessary not only to handle
533
- # the additional attributes, but to work around an error caused by the
534
- # builtin method for the current File::Stat class (see pp.rb).
535
- #
536
- def pretty_print(q)
537
- members = %w[
538
- archive? atime blksize blockdev? blocks compressed? ctime dev
539
- encrypted? gid hidden? indexed? ino mode mtime rdev nlink normal?
540
- offline? readonly? reparse_point? size sparse? streams system? temporary?
541
- uid
542
- ]
543
-
544
- q.object_group(self){
545
- q.breakable
546
- members.each{ |mem|
547
- q.group{
548
- q.text("#{mem}".ljust(15) + "=> ")
549
- if mem == 'mode'
550
- q.text(sprintf("0%o", send(mem.intern)))
551
- else
552
- val = self.send(mem.intern)
553
- if val.nil?
554
- q.text('nil')
555
- else
556
- q.text(val.to_s)
557
- end
558
- end
559
- }
560
- q.comma_breakable unless mem == members.last
561
- }
562
- }
563
- end
564
-
565
- private
566
-
567
- # Workaround for bug in 64-big JRuby. Hope to remove it some day.
568
- def get_ptr_type
569
- if RUBY_PLATFORM == 'java' && ENV_JAVA['sun.arch.data.model'] == '64'
570
- :ulong_long
571
- else
572
- :uintptr_t
573
- end
574
- end
575
-
576
- # Allow stringy arguments
577
- def string_check(arg)
578
- return arg if arg.is_a?(String)
579
- return arg.send(:to_str) if arg.respond_to?(:to_str, true) # MRI honors private to_str
580
- return arg.to_path if arg.respond_to?(:to_path)
581
- raise TypeError
582
- end
583
-
584
- # This is based on fileattr_to_unixmode in win32.c
585
- #
586
- def get_mode
587
- mode = 0
588
-
589
- s_iread = 0x0100; s_iwrite = 0x0080; s_iexec = 0x0040
590
- s_ifreg = 0x8000; s_ifdir = 0x4000; s_iwusr = 0200
591
- s_iwgrp = 0020; s_iwoth = 0002;
592
-
593
- if @readonly
594
- mode |= s_iread
595
- else
596
- mode |= s_iread | s_iwrite | s_iwusr
597
- end
598
-
599
- if @directory
600
- mode |= s_ifdir | s_iexec
601
- else
602
- mode |= s_ifreg
603
- end
604
-
605
- if @executable
606
- mode |= s_iexec
607
- end
608
-
609
- mode |= (mode & 0700) >> 3;
610
- mode |= (mode & 0700) >> 6;
611
-
612
- mode &= ~(s_iwgrp | s_iwoth)
613
-
614
- mode
615
- end
616
-
617
- # Returns whether or not +path+ is a block device.
618
- #
619
- def get_blockdev(path)
620
- ptr = FFI::MemoryPointer.from_string(path.wincode)
621
-
622
- if PathStripToRoot(ptr)
623
- fpath = ptr.read_bytes(path.size * 2).split("\000\000").first
624
- else
625
- fpath = nil
626
- end
627
-
628
- case GetDriveType(fpath)
629
- when DRIVE_REMOVABLE, DRIVE_CDROM, DRIVE_RAMDISK
630
- true
631
- else
632
- false
633
- end
634
- end
635
-
636
- # Returns the blksize for +path+.
637
- #---
638
- # The jruby-ffi gem (as of 1.9.3) reports a failure here where it shouldn't.
639
- # Consequently, this method returns 4096 automatically for now on JRuby.
640
- #
641
- def get_blksize(path)
642
- return 4096 if RUBY_PLATFORM == 'java' # Bug in jruby-ffi
643
-
644
- ptr = FFI::MemoryPointer.from_string(path.wincode)
645
-
646
- if PathStripToRoot(ptr)
647
- fpath = ptr.read_bytes(path.size * 2).split("\000\000").first
648
- else
649
- fpath = nil
650
- end
651
-
652
- size = nil
653
-
654
- sectors = FFI::MemoryPointer.new(:ulong)
655
- bytes = FFI::MemoryPointer.new(:ulong)
656
- free = FFI::MemoryPointer.new(:ulong)
657
- total = FFI::MemoryPointer.new(:ulong)
658
-
659
- if GetDiskFreeSpace(fpath, sectors, bytes, free, total)
660
- size = sectors.read_ulong * bytes.read_ulong
661
- else
662
- unless PathIsUNC(fpath)
663
- raise SystemCallError.new('GetDiskFreeSpace', FFI.errno)
664
- end
665
- end
666
-
667
- size
668
- end
669
-
670
- # Generic method for retrieving a handle.
671
- #
672
- def get_handle(path)
673
- fpath = path.wincode
674
-
675
- handle = CreateFile(
676
- fpath,
677
- GENERIC_READ,
678
- FILE_SHARE_READ,
679
- nil,
680
- OPEN_EXISTING,
681
- FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
682
- 0
683
- )
684
-
685
- if handle == INVALID_HANDLE_VALUE
686
- return nil if FFI.errno == 32 # ERROR_SHARING_VIOLATION. Locked files.
687
- raise SystemCallError.new('CreateFile', FFI.errno)
688
- end
689
-
690
- handle
691
- end
692
-
693
- # Determines whether or not +file+ is a symlink.
694
- #
695
- def get_symlink(file)
696
- bool = false
697
- fpath = File.expand_path(file).wincode
698
-
699
- begin
700
- data = WIN32_FIND_DATA.new
701
- handle = FindFirstFile(fpath, data)
702
-
703
- if handle == INVALID_HANDLE_VALUE
704
- raise SystemCallError.new('FindFirstFile', FFI.errno)
705
- end
706
-
707
- if data[:dwReserved0] == IO_REPARSE_TAG_SYMLINK
708
- bool = true
709
- end
710
- ensure
711
- FindClose(handle) if handle
712
- end
713
-
714
- bool
715
- end
716
-
717
- # Returns the filetype for the given +handle+.
718
- #
719
- def get_filetype(handle)
720
- file_type = GetFileType(handle)
721
-
722
- if file_type == FILE_TYPE_UNKNOWN && FFI.errno != NO_ERROR
723
- raise SystemCallError.new('GetFileType', FFI.errno)
724
- end
725
-
726
- file_type
727
- end
728
-
729
- def get_streams(handle)
730
- io_status = IO_STATUS_BLOCK.new
731
- ptr = FFI::MemoryPointer.new(:uchar, 1024 * 64)
732
-
733
- NtQueryInformationFile(handle, io_status, ptr, ptr.size, FileStreamInformation)
734
-
735
- if FFI.errno != 0
736
- raise SystemCallError.new('NtQueryInformationFile', FFI.errno)
737
- end
738
-
739
- arr = []
740
-
741
- while true
742
- info = FILE_STREAM_INFORMATION.new(ptr)
743
- break if info[:StreamNameLength] == 0
744
- arr << info[:StreamName].to_ptr.read_bytes(info[:StreamNameLength]).delete(0.chr)
745
- break if info[:NextEntryOffset] == 0
746
- info = FILE_STREAM_INFORMATION.new(ptr += info[:NextEntryOffset])
747
- end
748
-
749
- arr
750
- end
751
-
752
- # Return a sid of the file's owner.
753
- #
754
- def get_file_sid(file, info)
755
- wfile = file.wincode
756
- size_needed_ptr = FFI::MemoryPointer.new(:ulong)
757
-
758
- # First pass, get the size needed
759
- bool = GetFileSecurity(wfile, info, nil, 0, size_needed_ptr)
760
-
761
- size_needed = size_needed_ptr.read_ulong
762
- security_ptr = FFI::MemoryPointer.new(size_needed)
763
-
764
- # Second pass, this time with the appropriately sized security pointer
765
- bool = GetFileSecurity(wfile, info, security_ptr, security_ptr.size, size_needed_ptr)
766
-
767
- unless bool
768
- error = FFI.errno
769
- return "S-1-5-80-0" if error == 32 # ERROR_SHARING_VIOLATION. Locked files, etc.
770
- raise SystemCallError.new("GetFileSecurity", error)
771
- end
772
-
773
- sid_ptr = FFI::MemoryPointer.new(:pointer)
774
- defaulted = FFI::MemoryPointer.new(:bool)
775
-
776
- if info == OWNER_SECURITY_INFORMATION
777
- bool = GetSecurityDescriptorOwner(security_ptr, sid_ptr, defaulted)
778
- meth = "GetSecurityDescriptorOwner"
779
- else
780
- bool = GetSecurityDescriptorGroup(security_ptr, sid_ptr, defaulted)
781
- meth = "GetSecurityDescriptorGroup"
782
- end
783
-
784
- raise SystemCallError.new(meth, FFI.errno) unless bool
785
-
786
- ptr = FFI::MemoryPointer.new(:string)
787
-
788
- unless ConvertSidToStringSid(sid_ptr.read_pointer, ptr)
789
- raise SystemCallError.new("ConvertSidToStringSid")
790
- end
791
-
792
- ptr.read_pointer.read_string
793
- end
794
-
795
- # Return the sid of the current process.
796
- #
797
- def get_current_process_sid(token_type)
798
- token = FFI::MemoryPointer.new(get_ptr_type)
799
- sid = nil
800
-
801
- # Get the current process sid
802
- unless OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token)
803
- raise SystemCallError.new("OpenProcessToken", FFI.errno)
804
- end
805
-
806
- begin
807
- token = token.read_pointer.to_i
808
- rlength = FFI::MemoryPointer.new(:pointer)
809
-
810
- if token_type == TokenUser
811
- buf = 0.chr * 512
812
- else
813
- buf = TOKEN_GROUP.new
814
- end
815
-
816
- unless GetTokenInformation(token, token_type, buf, buf.size, rlength)
817
- raise SystemCallError.new("GetTokenInformation", FFI.errno)
818
- end
819
-
820
- if token_type == TokenUser
821
- tsid = buf[FFI.type_size(:pointer)*2, (rlength.read_ulong - FFI.type_size(:pointer)*2)]
822
- else
823
- tsid = buf[:Groups][0][:Sid]
824
- end
825
-
826
- ptr = FFI::MemoryPointer.new(:string)
827
-
828
- unless ConvertSidToStringSid(tsid, ptr)
829
- raise SystemCallError.new("ConvertSidToStringSid")
830
- end
831
-
832
- sid = ptr.read_pointer.read_string
833
- ensure
834
- CloseHandle(token)
835
- end
836
-
837
- sid
838
- end
839
-
840
- # Returns whether or not the current process has given access rights for +path+.
841
- #
842
- def access_check(path, access_rights)
843
- wfile = path.wincode
844
- check = false
845
- size_needed_ptr = FFI::MemoryPointer.new(:ulong)
846
-
847
- flags = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION |
848
- DACL_SECURITY_INFORMATION
849
-
850
- # First attempt, get the size needed
851
- bool = GetFileSecurity(wfile, flags, nil, 0, size_needed_ptr)
852
-
853
- # If it fails horribly here, assume the answer is no.
854
- if !bool && FFI.errno != ERROR_INSUFFICIENT_BUFFER
855
- return false
856
- end
857
-
858
- size_needed = size_needed_ptr.read_ulong
859
- security_ptr = FFI::MemoryPointer.new(size_needed)
860
-
861
- # Second attempt, now with the needed size
862
- if GetFileSecurity(wfile, flags, security_ptr, size_needed, size_needed_ptr)
863
- token = FFI::MemoryPointer.new(get_ptr_type)
864
-
865
- pflags = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ
866
-
867
- if OpenProcessToken(GetCurrentProcess(), pflags, token)
868
- begin
869
- token = token.read_pointer.to_i
870
- token2 = FFI::MemoryPointer.new(get_ptr_type)
871
-
872
- if DuplicateToken(token, SecurityImpersonation, token2)
873
- begin
874
- token2 = token2.read_pointer.to_i
875
- mapping = GENERIC_MAPPING.new
876
- privileges = PRIVILEGE_SET.new
877
- privileges[:PrivilegeCount] = 0
878
- privileges_length = privileges.size
879
-
880
- mapping[:GenericRead] = FILE_GENERIC_READ
881
- mapping[:GenericWrite] = FILE_GENERIC_WRITE
882
- mapping[:GenericExecute] = FILE_GENERIC_EXECUTE
883
- mapping[:GenericAll] = FILE_ALL_ACCESS
884
-
885
- rights_ptr = FFI::MemoryPointer.new(:ulong)
886
- rights_ptr.write_ulong(access_rights)
887
-
888
- MapGenericMask(rights_ptr, mapping)
889
- rights = rights_ptr.read_ulong
890
-
891
- result_ptr = FFI::MemoryPointer.new(:ulong)
892
- privileges_length_ptr = FFI::MemoryPointer.new(:ulong)
893
- privileges_length_ptr.write_ulong(privileges_length)
894
- granted_access_ptr = FFI::MemoryPointer.new(:ulong)
895
-
896
- bool = AccessCheck(
897
- security_ptr,
898
- token2,
899
- rights,
900
- mapping,
901
- privileges,
902
- privileges_length_ptr,
903
- granted_access_ptr,
904
- result_ptr
905
- )
906
-
907
- if bool
908
- check = result_ptr.read_ulong == 1
909
- else
910
- raise SystemCallError.new('AccessCheck', FFI.errno)
911
- end
912
- ensure
913
- CloseHandle(token2)
914
- end
915
- end
916
- ensure
917
- CloseHandle(token)
918
- end
919
- end
920
- end
921
-
922
- check
923
- end
924
-
925
- # Returns whether or not the Everyone has given access rights for +path+.
926
- #
927
- def access_check_world(path, access_rights)
928
- wfile = path.wincode
929
- check = false
930
- size_needed_ptr = FFI::MemoryPointer.new(:ulong)
931
-
932
- flags = DACL_SECURITY_INFORMATION
933
-
934
- # First attempt, get the size needed
935
- bool = GetFileSecurity(wfile, flags, nil, 0, size_needed_ptr)
936
-
937
- # If it fails horribly here, assume the answer is no.
938
- if !bool && FFI.errno != ERROR_INSUFFICIENT_BUFFER
939
- return false
940
- end
941
-
942
- size_needed = size_needed_ptr.read_ulong
943
- security_ptr = FFI::MemoryPointer.new(size_needed)
944
-
945
- # Second attempt, now with the needed size
946
- if GetFileSecurity(wfile, flags, security_ptr, size_needed, size_needed_ptr)
947
- present_ptr = FFI::MemoryPointer.new(:ulong)
948
- pdacl_ptr = FFI::MemoryPointer.new(:pointer)
949
- defaulted_ptr = FFI::MemoryPointer.new(:ulong)
950
-
951
- bool = GetSecurityDescriptorDacl(
952
- security_ptr,
953
- present_ptr,
954
- pdacl_ptr,
955
- defaulted_ptr
956
- )
957
-
958
- # If it fails, or the dacl isn't present, return false.
959
- if !bool || present_ptr.read_ulong == 0
960
- return false
961
- end
962
-
963
- pdacl = pdacl_ptr.read_pointer
964
- psid_ptr = FFI::MemoryPointer.new(:pointer)
965
-
966
- # S-1-1-0 is the well known SID for "Everyone".
967
- ConvertStringSidToSid('S-1-1-0', psid_ptr)
968
-
969
- psid = psid_ptr.read_pointer
970
- trustee_ptr = FFI::MemoryPointer.new(TRUSTEE)
971
-
972
- BuildTrusteeWithSid(trustee_ptr, psid)
973
-
974
- rights_ptr = FFI::MemoryPointer.new(:ulong)
975
-
976
- if GetEffectiveRightsFromAcl(pdacl, trustee_ptr, rights_ptr) == NO_ERROR
977
- rights = rights_ptr.read_ulong
978
- check = (rights & access_rights) == access_rights
979
- end
980
- end
981
-
982
- check
983
- end
984
- end
1
+ require_relative 'windows/helper'
2
+ require_relative 'windows/constants'
3
+ require_relative 'windows/structs'
4
+ require_relative '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
+ # Alternate streams
58
+ attr_reader :streams
59
+
60
+ # The version of the win32-file-stat library
61
+ WIN32_FILE_STAT_VERSION = '1.5.2'
62
+
63
+ # Creates and returns a File::Stat object, which encapsulate common status
64
+ # information for File objects on MS Windows sytems. The information is
65
+ # recorded at the moment the File::Stat object is created; changes made to
66
+ # the file after that point will not be reflected.
67
+ #
68
+ def initialize(file)
69
+ file = string_check(file)
70
+
71
+ path = file.tr('/', "\\")
72
+ @path = path
73
+
74
+ @user_sid = get_file_sid(file, OWNER_SECURITY_INFORMATION)
75
+ @grp_sid = get_file_sid(file, GROUP_SECURITY_INFORMATION)
76
+
77
+ @uid = @user_sid.split('-').last.to_i
78
+ @gid = @grp_sid.split('-').last.to_i
79
+
80
+ @owned = @user_sid == get_current_process_sid(TokenUser)
81
+ @grpowned = @grp_sid == get_current_process_sid(TokenGroups)
82
+
83
+ begin
84
+ # The handle returned will be used by other functions
85
+ handle = get_handle(path)
86
+
87
+ @blockdev = get_blockdev(path)
88
+ @blksize = get_blksize(path)
89
+
90
+ if handle
91
+ @filetype = get_filetype(handle)
92
+ @streams = get_streams(handle)
93
+ @chardev = @filetype == FILE_TYPE_CHAR
94
+ @regular = @filetype == FILE_TYPE_DISK
95
+ @pipe = @filetype == FILE_TYPE_PIPE
96
+
97
+ if @pipe
98
+ @socket = !GetNamedPipeInfo(handle, nil, nil, nil, nil)
99
+ else
100
+ @socket = false
101
+ end
102
+ else
103
+ @chardev = false
104
+ @regular = false
105
+ @pipe = false
106
+ @socket = false
107
+ end
108
+
109
+ fpath = path.wincode
110
+
111
+ if handle == nil || ((@blockdev || @chardev || @pipe) && GetDriveType(fpath) != DRIVE_REMOVABLE)
112
+ data = WIN32_FIND_DATA.new
113
+ CloseHandle(handle) if handle
114
+
115
+ handle = FindFirstFile(fpath, data)
116
+
117
+ if handle == INVALID_HANDLE_VALUE
118
+ raise SystemCallError.new('FindFirstFile', FFI.errno)
119
+ end
120
+
121
+ FindClose(handle)
122
+ handle = nil
123
+
124
+ @nlink = 1 # Default from stat/wstat function.
125
+ @ino = nil
126
+ @rdev = nil
127
+ else
128
+ data = BY_HANDLE_FILE_INFORMATION.new
129
+
130
+ unless GetFileInformationByHandle(handle, data)
131
+ raise SystemCallError.new('GetFileInformationByHandle', FFI.errno)
132
+ end
133
+
134
+ @nlink = data[:nNumberOfLinks]
135
+ @ino = (data[:nFileIndexHigh] << 32) | data[:nFileIndexLow]
136
+ @rdev = data[:dwVolumeSerialNumber]
137
+ end
138
+
139
+ # Not supported and/or meaningless on MS Windows
140
+ @dev_major = nil
141
+ @dev_minor = nil
142
+ @rdev_major = nil
143
+ @rdev_minor = nil
144
+ @setgid = false
145
+ @setuid = false
146
+ @sticky = false
147
+
148
+ # Originally used GetBinaryType, but it only worked
149
+ # for .exe files, and it could return false positives.
150
+ @executable = %w[.bat .cmd .com .exe].include?(File.extname(@path).downcase)
151
+
152
+ # Set blocks equal to size / blksize, rounded up
153
+ case @blksize
154
+ when nil
155
+ @blocks = nil
156
+ when 0
157
+ @blocks = 0
158
+ else
159
+ @blocks = (data.size.to_f / @blksize.to_f).ceil
160
+ end
161
+
162
+ @attr = data[:dwFileAttributes]
163
+ @atime = Time.at(data.atime)
164
+ @ctime = Time.at(data.ctime)
165
+ @mtime = Time.at(data.mtime)
166
+ @size = data.size
167
+
168
+ @archive = @attr & FILE_ATTRIBUTE_ARCHIVE > 0
169
+ @compressed = @attr & FILE_ATTRIBUTE_COMPRESSED > 0
170
+ @directory = @attr & FILE_ATTRIBUTE_DIRECTORY > 0
171
+ @encrypted = @attr & FILE_ATTRIBUTE_ENCRYPTED > 0
172
+ @hidden = @attr & FILE_ATTRIBUTE_HIDDEN > 0
173
+ @indexed = @attr & ~FILE_ATTRIBUTE_NOT_CONTENT_INDEXED > 0
174
+ @normal = @attr & FILE_ATTRIBUTE_NORMAL > 0
175
+ @offline = @attr & FILE_ATTRIBUTE_OFFLINE > 0
176
+ @readonly = @attr & FILE_ATTRIBUTE_READONLY > 0
177
+ @reparse_point = @attr & FILE_ATTRIBUTE_REPARSE_POINT > 0
178
+ @sparse = @attr & FILE_ATTRIBUTE_SPARSE_FILE > 0
179
+ @system = @attr & FILE_ATTRIBUTE_SYSTEM > 0
180
+ @temporary = @attr & FILE_ATTRIBUTE_TEMPORARY > 0
181
+
182
+ @mode = get_mode
183
+
184
+ @readable = access_check(path, GENERIC_READ)
185
+ @readable_real = @readable
186
+
187
+ # The MSDN docs say that the readonly attribute is honored for directories
188
+ if @directory
189
+ @writable = access_check(path, GENERIC_WRITE)
190
+ else
191
+ @writable = access_check(path, GENERIC_WRITE) && !@readonly
192
+ end
193
+
194
+ @writable_real = @writable
195
+
196
+ @world_readable = access_check_world(path, FILE_READ_DATA)
197
+ @world_writable = access_check_world(path, FILE_WRITE_DATA) && !@readonly
198
+
199
+ if @reparse_point
200
+ @symlink = get_symlink(path)
201
+ else
202
+ @symlink = false
203
+ end
204
+ ensure
205
+ CloseHandle(handle) if handle
206
+ end
207
+ end
208
+
209
+ ## Comparable
210
+
211
+ # Compares two File::Stat objects using modification time.
212
+ #--
213
+ # Custom implementation necessary since we altered File::Stat.
214
+ #
215
+ def <=>(other)
216
+ @mtime.to_i <=> other.mtime.to_i
217
+ end
218
+
219
+ ## Other
220
+
221
+ # Returns whether or not the file is an archive file.
222
+ #
223
+ def archive?
224
+ @archive
225
+ end
226
+
227
+ # Returns whether or not the file is a block device. For MS Windows a
228
+ # block device is a removable drive, cdrom or ramdisk.
229
+ #
230
+ def blockdev?
231
+ @blockdev
232
+ end
233
+
234
+ # Returns whether or not the file is a character device.
235
+ #
236
+ def chardev?
237
+ @chardev
238
+ end
239
+
240
+ # Returns whether or not the file is compressed.
241
+ #
242
+ def compressed?
243
+ @compressed
244
+ end
245
+
246
+ # Returns whether or not the file is a directory.
247
+ #
248
+ def directory?
249
+ @directory
250
+ end
251
+
252
+ # Returns whether or not the file in encrypted.
253
+ #
254
+ def encrypted?
255
+ @encrypted
256
+ end
257
+
258
+ # Returns whether or not the file is executable. Generally speaking, this
259
+ # means .bat, .cmd, .com, and .exe files.
260
+ #
261
+ def executable?
262
+ @executable
263
+ end
264
+
265
+ alias executable_real? executable?
266
+
267
+ # Returns whether or not the file is a regular file, as opposed to a pipe,
268
+ # socket, etc.
269
+ #
270
+ def file?
271
+ @regular && !@directory && !@reparse_point
272
+ end
273
+
274
+ # Returns the user ID of the file. If full_sid is true, then the full
275
+ # string sid is returned instead.
276
+ #--
277
+ # The user id is the RID of the SID.
278
+ #
279
+ def gid(full_sid = false)
280
+ full_sid ? @grp_sid : @gid
281
+ end
282
+
283
+ # Returns true if the process owner's ID is the same as one of the file's groups.
284
+ #--
285
+ # Internally we're checking the process sid against the TokenGroups sid.
286
+ #
287
+ def grpowned?
288
+ @grpowned
289
+ end
290
+
291
+ # Returns whether or not the file is hidden.
292
+ #
293
+ def hidden?
294
+ @hidden
295
+ end
296
+
297
+ # Returns whether or not the file is content indexed.
298
+ #
299
+ def indexed?
300
+ @indexed
301
+ end
302
+
303
+ alias content_indexed? indexed?
304
+
305
+ # Returns whether or not the file is 'normal'. This is only true if
306
+ # virtually all other attributes are false.
307
+ #
308
+ def normal?
309
+ @normal
310
+ end
311
+
312
+ # Returns whether or not the file is offline.
313
+ #
314
+ def offline?
315
+ @offline
316
+ end
317
+
318
+ # Returns whether or not the current process owner is the owner of the file.
319
+ #--
320
+ # Internally we're checking the process sid against the owner's sid.
321
+ def owned?
322
+ @owned
323
+ end
324
+
325
+ # Returns the drive number of the disk containing the file, or -1 if there
326
+ # is no associated drive number.
327
+ #
328
+ # If the +letter+ option is true, returns the drive letter instead. If there
329
+ # is no drive letter, it will return nil.
330
+ #--
331
+ # This differs slightly from MRI in that it will return -1 if the path
332
+ # does not have a drive letter.
333
+ #
334
+ # Note: Bug in JRuby as of JRuby 1.7.8, which does not expand NUL properly.
335
+ #
336
+ def dev(letter = false)
337
+ fpath = File.expand_path(@path).wincode
338
+ num = PathGetDriveNumber(fpath)
339
+
340
+ if letter
341
+ if num == -1
342
+ nil
343
+ else
344
+ (num + 'A'.ord).chr + ':'
345
+ end
346
+ else
347
+ num
348
+ end
349
+ end
350
+
351
+ # Returns whether or not the file is readable by the process owner.
352
+ #--
353
+ # In Windows terms, we're checking for GENERIC_READ privileges.
354
+ #
355
+ def readable?
356
+ @readable
357
+ end
358
+
359
+ # A synonym for File::Stat#readable?
360
+ #
361
+ def readable_real?
362
+ @readable_real
363
+ end
364
+
365
+ # Returns whether or not the file is readonly.
366
+ #
367
+ def readonly?
368
+ @readonly
369
+ end
370
+
371
+ alias read_only? readonly?
372
+
373
+ # Returns whether or not the file is a pipe.
374
+ #
375
+ def pipe?
376
+ @pipe
377
+ end
378
+
379
+ # Returns whether or not the file is a socket.
380
+ def socket?
381
+ @socket
382
+ end
383
+
384
+ # Returns whether or not the file is a reparse point.
385
+ #
386
+ def reparse_point?
387
+ @reparse_point
388
+ end
389
+
390
+ # Returns false on MS Windows.
391
+ #--
392
+ # I had to explicitly define this because of a bug in JRuby.
393
+ #
394
+ def setgid?
395
+ @setgid
396
+ end
397
+
398
+ # Returns false on MS Windows.
399
+ #--
400
+ # I had to explicitly define this because of a bug in JRuby.
401
+ #
402
+ def setuid?
403
+ @setuid
404
+ end
405
+
406
+ # Returns whether or not the file size is zero.
407
+ #
408
+ def size?
409
+ @size > 0 ? @size : nil
410
+ end
411
+
412
+ # Returns whether or not the file is a sparse file. In most cases a sparse
413
+ # file is an image file.
414
+ #
415
+ def sparse?
416
+ @sparse
417
+ end
418
+
419
+ # Returns false on MS Windows.
420
+ #--
421
+ # I had to explicitly define this because of a bug in JRuby.
422
+ #
423
+ def sticky?
424
+ @sticky
425
+ end
426
+
427
+ # Returns whether or not the file is a symlink.
428
+ #
429
+ def symlink?
430
+ @symlink
431
+ end
432
+
433
+ # Returns whether or not the file is a system file.
434
+ #
435
+ def system?
436
+ @system
437
+ end
438
+
439
+ # Returns whether or not the file is being used for temporary storage.
440
+ #
441
+ def temporary?
442
+ @temporary
443
+ end
444
+
445
+ # Returns the user ID of the file. If the +full_sid+ is true, then the
446
+ # full string sid is returned instead.
447
+ #--
448
+ # The user id is the RID of the SID.
449
+ #
450
+ def uid(full_sid = false)
451
+ full_sid ? @user_sid : @uid
452
+ end
453
+
454
+ # Returns whether or not the file is readable by others. Note that this
455
+ # merely returns true or false, not permission bits (or nil).
456
+ #--
457
+ # In Windows terms, this is checking the access right FILE_READ_DATA against
458
+ # the well-known SID "S-1-1-0", aka "Everyone".
459
+ #
460
+ #
461
+ def world_readable?
462
+ @world_readable
463
+ end
464
+
465
+ # Returns whether or not the file is writable by others. Note that this
466
+ # merely returns true or false, not permission bits (or nil).
467
+ #--
468
+ # In Windows terms, this is checking the access right FILE_WRITE_DATA against
469
+ # the well-known SID "S-1-1-0", aka "Everyone".
470
+ #
471
+ def world_writable?
472
+ @world_writable
473
+ end
474
+
475
+ # Returns whether or not the file is writable by the current process owner.
476
+ #--
477
+ # In Windows terms, we're checking for GENERIC_WRITE privileges.
478
+ #
479
+ def writable?
480
+ @writable
481
+ end
482
+
483
+ # A synonym for File::Stat#readable?
484
+ #
485
+ def writable_real?
486
+ @writable_real
487
+ end
488
+
489
+ # Returns whether or not the file size is zero.
490
+ #
491
+ def zero?
492
+ @size == 0
493
+ end
494
+
495
+ # Identifies the type of file. The return string is one of 'file',
496
+ # 'directory', 'characterSpecial', 'socket' or 'unknown'.
497
+ #
498
+ def ftype
499
+ return 'directory' if @directory
500
+
501
+ case @filetype
502
+ when FILE_TYPE_CHAR
503
+ 'characterSpecial'
504
+ when FILE_TYPE_DISK
505
+ 'file'
506
+ when FILE_TYPE_PIPE
507
+ 'socket'
508
+ else
509
+ if blockdev?
510
+ 'blockSpecial'
511
+ else
512
+ 'unknown'
513
+ end
514
+ end
515
+ end
516
+
517
+ # Returns a stringified version of a File::Stat object.
518
+ #
519
+ def inspect
520
+ members = %w[
521
+ archive? atime blksize blockdev? blocks compressed? ctime dev
522
+ encrypted? gid hidden? indexed? ino mode mtime rdev nlink normal?
523
+ offline? readonly? reparse_point? size sparse? system? streams
524
+ temporary? uid
525
+ ]
526
+
527
+ str = "#<#{self.class}"
528
+
529
+ members.sort.each{ |mem|
530
+ if mem == 'mode'
531
+ str << " #{mem}=" << sprintf("0%o", send(mem.intern))
532
+ elsif mem[-1].chr == '?' # boolean methods
533
+ str << " #{mem.chop}=" << send(mem.intern).to_s
534
+ else
535
+ str << " #{mem}=" << send(mem.intern).to_s
536
+ end
537
+ }
538
+
539
+ str
540
+ end
541
+
542
+ # A custom pretty print method. This was necessary not only to handle
543
+ # the additional attributes, but to work around an error caused by the
544
+ # builtin method for the current File::Stat class (see pp.rb).
545
+ #
546
+ def pretty_print(q)
547
+ members = %w[
548
+ archive? atime blksize blockdev? blocks compressed? ctime dev
549
+ encrypted? gid hidden? indexed? ino mode mtime rdev nlink normal?
550
+ offline? readonly? reparse_point? size sparse? streams system? temporary?
551
+ uid
552
+ ]
553
+
554
+ q.object_group(self){
555
+ q.breakable
556
+ members.each{ |mem|
557
+ q.group{
558
+ q.text("#{mem}".ljust(15) + "=> ")
559
+ if mem == 'mode'
560
+ q.text(sprintf("0%o", send(mem.intern)))
561
+ else
562
+ val = self.send(mem.intern)
563
+ if val.nil?
564
+ q.text('nil')
565
+ else
566
+ q.text(val.to_s)
567
+ end
568
+ end
569
+ }
570
+ q.comma_breakable unless mem == members.last
571
+ }
572
+ }
573
+ end
574
+
575
+ private
576
+
577
+ # Workaround for bug in 64-big JRuby. Hope to remove it some day.
578
+ def get_ptr_type
579
+ if RUBY_PLATFORM == 'java' && ENV_JAVA['sun.arch.data.model'] == '64'
580
+ :ulong_long
581
+ else
582
+ :uintptr_t
583
+ end
584
+ end
585
+
586
+ # Allow stringy arguments
587
+ def string_check(arg)
588
+ return arg if arg.is_a?(String)
589
+ return arg.send(:to_str) if arg.respond_to?(:to_str, true) # MRI honors private to_str
590
+ return arg.to_path if arg.respond_to?(:to_path)
591
+ raise TypeError
592
+ end
593
+
594
+ # This is based on fileattr_to_unixmode in win32.c
595
+ #
596
+ def get_mode
597
+ mode = 0
598
+
599
+ s_iread = 0x0100; s_iwrite = 0x0080; s_iexec = 0x0040
600
+ s_ifreg = 0x8000; s_ifdir = 0x4000; s_iwusr = 0200
601
+ s_iwgrp = 0020; s_iwoth = 0002;
602
+
603
+ if @readonly
604
+ mode |= s_iread
605
+ else
606
+ mode |= s_iread | s_iwrite | s_iwusr
607
+ end
608
+
609
+ if @directory
610
+ mode |= s_ifdir | s_iexec
611
+ else
612
+ mode |= s_ifreg
613
+ end
614
+
615
+ if @executable
616
+ mode |= s_iexec
617
+ end
618
+
619
+ mode |= (mode & 0700) >> 3;
620
+ mode |= (mode & 0700) >> 6;
621
+
622
+ mode &= ~(s_iwgrp | s_iwoth)
623
+
624
+ mode
625
+ end
626
+
627
+ # Returns whether or not +path+ is a block device.
628
+ #
629
+ def get_blockdev(path)
630
+ ptr = FFI::MemoryPointer.from_string(path.wincode)
631
+
632
+ if PathStripToRoot(ptr)
633
+ fpath = ptr.read_bytes(path.size * 2).split("\000\000").first
634
+ else
635
+ fpath = nil
636
+ end
637
+
638
+ case GetDriveType(fpath)
639
+ when DRIVE_REMOVABLE, DRIVE_CDROM, DRIVE_RAMDISK
640
+ true
641
+ else
642
+ false
643
+ end
644
+ end
645
+
646
+ # Returns the blksize for +path+.
647
+ #---
648
+ # The jruby-ffi gem (as of 1.9.3) reports a failure here where it shouldn't.
649
+ # Consequently, this method returns 4096 automatically for now on JRuby.
650
+ #
651
+ def get_blksize(path)
652
+ return 4096 if RUBY_PLATFORM == 'java' # Bug in jruby-ffi
653
+
654
+ ptr = FFI::MemoryPointer.from_string(path.wincode)
655
+
656
+ if PathStripToRoot(ptr)
657
+ fpath = ptr.read_bytes(path.size * 2).split("\000\000").first
658
+ else
659
+ fpath = nil
660
+ end
661
+
662
+ size = nil
663
+
664
+ sectors = FFI::MemoryPointer.new(:ulong)
665
+ bytes = FFI::MemoryPointer.new(:ulong)
666
+ free = FFI::MemoryPointer.new(:ulong)
667
+ total = FFI::MemoryPointer.new(:ulong)
668
+
669
+ if GetDiskFreeSpace(fpath, sectors, bytes, free, total)
670
+ size = sectors.read_ulong * bytes.read_ulong
671
+ else
672
+ unless PathIsUNC(fpath)
673
+ raise SystemCallError.new('GetDiskFreeSpace', FFI.errno)
674
+ end
675
+ end
676
+
677
+ size
678
+ end
679
+
680
+ # Generic method for retrieving a handle.
681
+ #
682
+ def get_handle(path)
683
+ fpath = path.wincode
684
+
685
+ handle = CreateFile(
686
+ fpath,
687
+ GENERIC_READ,
688
+ FILE_SHARE_READ,
689
+ nil,
690
+ OPEN_EXISTING,
691
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
692
+ 0
693
+ )
694
+
695
+ if handle == INVALID_HANDLE_VALUE
696
+ return nil if FFI.errno == 32 # ERROR_SHARING_VIOLATION. Locked files.
697
+ raise SystemCallError.new('CreateFile', FFI.errno)
698
+ end
699
+
700
+ handle
701
+ end
702
+
703
+ # Determines whether or not +file+ is a symlink.
704
+ #
705
+ def get_symlink(file)
706
+ bool = false
707
+ fpath = File.expand_path(file).wincode
708
+
709
+ begin
710
+ data = WIN32_FIND_DATA.new
711
+ handle = FindFirstFile(fpath, data)
712
+
713
+ if handle == INVALID_HANDLE_VALUE
714
+ raise SystemCallError.new('FindFirstFile', FFI.errno)
715
+ end
716
+
717
+ if data[:dwReserved0] == IO_REPARSE_TAG_SYMLINK
718
+ bool = true
719
+ end
720
+ ensure
721
+ FindClose(handle) if handle
722
+ end
723
+
724
+ bool
725
+ end
726
+
727
+ # Returns the filetype for the given +handle+.
728
+ #
729
+ def get_filetype(handle)
730
+ file_type = GetFileType(handle)
731
+
732
+ if file_type == FILE_TYPE_UNKNOWN && FFI.errno != NO_ERROR
733
+ raise SystemCallError.new('GetFileType', FFI.errno)
734
+ end
735
+
736
+ file_type
737
+ end
738
+
739
+ def get_streams(handle)
740
+ io_status = IO_STATUS_BLOCK.new
741
+ ptr = FFI::MemoryPointer.new(:uchar, 1024 * 64)
742
+
743
+ NtQueryInformationFile(handle, io_status, ptr, ptr.size, FileStreamInformation)
744
+
745
+ if FFI.errno != 0
746
+ raise SystemCallError.new('NtQueryInformationFile', FFI.errno)
747
+ end
748
+
749
+ arr = []
750
+
751
+ while true
752
+ info = FILE_STREAM_INFORMATION.new(ptr)
753
+ break if info[:StreamNameLength] == 0
754
+ arr << info[:StreamName].to_ptr.read_bytes(info[:StreamNameLength]).delete(0.chr)
755
+ break if info[:NextEntryOffset] == 0
756
+ info = FILE_STREAM_INFORMATION.new(ptr += info[:NextEntryOffset])
757
+ end
758
+
759
+ arr
760
+ end
761
+
762
+ # Return a sid of the file's owner.
763
+ #
764
+ def get_file_sid(file, info)
765
+ wfile = file.wincode
766
+ size_needed_ptr = FFI::MemoryPointer.new(:ulong)
767
+
768
+ # First pass, get the size needed
769
+ bool = GetFileSecurity(wfile, info, nil, 0, size_needed_ptr)
770
+
771
+ size_needed = size_needed_ptr.read_ulong
772
+ security_ptr = FFI::MemoryPointer.new(size_needed)
773
+
774
+ # Second pass, this time with the appropriately sized security pointer
775
+ bool = GetFileSecurity(wfile, info, security_ptr, security_ptr.size, size_needed_ptr)
776
+
777
+ unless bool
778
+ error = FFI.errno
779
+ return "S-1-5-80-0" if error == 32 # ERROR_SHARING_VIOLATION. Locked files, etc.
780
+ raise SystemCallError.new("GetFileSecurity", error)
781
+ end
782
+
783
+ sid_ptr = FFI::MemoryPointer.new(:pointer)
784
+ defaulted = FFI::MemoryPointer.new(:bool)
785
+
786
+ if info == OWNER_SECURITY_INFORMATION
787
+ bool = GetSecurityDescriptorOwner(security_ptr, sid_ptr, defaulted)
788
+ meth = "GetSecurityDescriptorOwner"
789
+ else
790
+ bool = GetSecurityDescriptorGroup(security_ptr, sid_ptr, defaulted)
791
+ meth = "GetSecurityDescriptorGroup"
792
+ end
793
+
794
+ raise SystemCallError.new(meth, FFI.errno) unless bool
795
+
796
+ ptr = FFI::MemoryPointer.new(:string)
797
+
798
+ unless ConvertSidToStringSid(sid_ptr.read_pointer, ptr)
799
+ raise SystemCallError.new("ConvertSidToStringSid")
800
+ end
801
+
802
+ ptr.read_pointer.read_string
803
+ end
804
+
805
+ # Return the sid of the current process.
806
+ #
807
+ def get_current_process_sid(token_type)
808
+ token = FFI::MemoryPointer.new(get_ptr_type)
809
+ sid = nil
810
+
811
+ # Get the current process sid
812
+ unless OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token)
813
+ raise SystemCallError.new("OpenProcessToken", FFI.errno)
814
+ end
815
+
816
+ begin
817
+ token = token.read_pointer.to_i
818
+ rlength = FFI::MemoryPointer.new(:pointer)
819
+
820
+ if token_type == TokenUser
821
+ buf = 0.chr * 512
822
+ else
823
+ buf = TOKEN_GROUP.new
824
+ end
825
+
826
+ unless GetTokenInformation(token, token_type, buf, buf.size, rlength)
827
+ raise SystemCallError.new("GetTokenInformation", FFI.errno)
828
+ end
829
+
830
+ if token_type == TokenUser
831
+ tsid = buf[FFI.type_size(:pointer)*2, (rlength.read_ulong - FFI.type_size(:pointer)*2)]
832
+ else
833
+ tsid = buf[:Groups][0][:Sid]
834
+ end
835
+
836
+ ptr = FFI::MemoryPointer.new(:string)
837
+
838
+ unless ConvertSidToStringSid(tsid, ptr)
839
+ raise SystemCallError.new("ConvertSidToStringSid")
840
+ end
841
+
842
+ sid = ptr.read_pointer.read_string
843
+ ensure
844
+ CloseHandle(token)
845
+ end
846
+
847
+ sid
848
+ end
849
+
850
+ # Returns whether or not the current process has given access rights for +path+.
851
+ #
852
+ def access_check(path, access_rights)
853
+ wfile = path.wincode
854
+ check = false
855
+ size_needed_ptr = FFI::MemoryPointer.new(:ulong)
856
+
857
+ flags = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION |
858
+ DACL_SECURITY_INFORMATION
859
+
860
+ # First attempt, get the size needed
861
+ bool = GetFileSecurity(wfile, flags, nil, 0, size_needed_ptr)
862
+
863
+ # If it fails horribly here, assume the answer is no.
864
+ if !bool && FFI.errno != ERROR_INSUFFICIENT_BUFFER
865
+ return false
866
+ end
867
+
868
+ size_needed = size_needed_ptr.read_ulong
869
+ security_ptr = FFI::MemoryPointer.new(size_needed)
870
+
871
+ # Second attempt, now with the needed size
872
+ if GetFileSecurity(wfile, flags, security_ptr, size_needed, size_needed_ptr)
873
+ token = FFI::MemoryPointer.new(get_ptr_type)
874
+
875
+ pflags = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ
876
+
877
+ if OpenProcessToken(GetCurrentProcess(), pflags, token)
878
+ begin
879
+ token = token.read_pointer.to_i
880
+ token2 = FFI::MemoryPointer.new(get_ptr_type)
881
+
882
+ if DuplicateToken(token, SecurityImpersonation, token2)
883
+ begin
884
+ token2 = token2.read_pointer.to_i
885
+ mapping = GENERIC_MAPPING.new
886
+ privileges = PRIVILEGE_SET.new
887
+ privileges[:PrivilegeCount] = 0
888
+ privileges_length = privileges.size
889
+
890
+ mapping[:GenericRead] = FILE_GENERIC_READ
891
+ mapping[:GenericWrite] = FILE_GENERIC_WRITE
892
+ mapping[:GenericExecute] = FILE_GENERIC_EXECUTE
893
+ mapping[:GenericAll] = FILE_ALL_ACCESS
894
+
895
+ rights_ptr = FFI::MemoryPointer.new(:ulong)
896
+ rights_ptr.write_ulong(access_rights)
897
+
898
+ MapGenericMask(rights_ptr, mapping)
899
+ rights = rights_ptr.read_ulong
900
+
901
+ result_ptr = FFI::MemoryPointer.new(:ulong)
902
+ privileges_length_ptr = FFI::MemoryPointer.new(:ulong)
903
+ privileges_length_ptr.write_ulong(privileges_length)
904
+ granted_access_ptr = FFI::MemoryPointer.new(:ulong)
905
+
906
+ bool = AccessCheck(
907
+ security_ptr,
908
+ token2,
909
+ rights,
910
+ mapping,
911
+ privileges,
912
+ privileges_length_ptr,
913
+ granted_access_ptr,
914
+ result_ptr
915
+ )
916
+
917
+ if bool
918
+ check = result_ptr.read_ulong == 1
919
+ else
920
+ raise SystemCallError.new('AccessCheck', FFI.errno)
921
+ end
922
+ ensure
923
+ CloseHandle(token2)
924
+ end
925
+ end
926
+ ensure
927
+ CloseHandle(token)
928
+ end
929
+ end
930
+ end
931
+
932
+ check
933
+ end
934
+
935
+ # Returns whether or not the Everyone has given access rights for +path+.
936
+ #
937
+ def access_check_world(path, access_rights)
938
+ wfile = path.wincode
939
+ check = false
940
+ size_needed_ptr = FFI::MemoryPointer.new(:ulong)
941
+
942
+ flags = DACL_SECURITY_INFORMATION
943
+
944
+ # First attempt, get the size needed
945
+ bool = GetFileSecurity(wfile, flags, nil, 0, size_needed_ptr)
946
+
947
+ # If it fails horribly here, assume the answer is no.
948
+ if !bool && FFI.errno != ERROR_INSUFFICIENT_BUFFER
949
+ return false
950
+ end
951
+
952
+ size_needed = size_needed_ptr.read_ulong
953
+ security_ptr = FFI::MemoryPointer.new(size_needed)
954
+
955
+ # Second attempt, now with the needed size
956
+ if GetFileSecurity(wfile, flags, security_ptr, size_needed, size_needed_ptr)
957
+ present_ptr = FFI::MemoryPointer.new(:ulong)
958
+ pdacl_ptr = FFI::MemoryPointer.new(:pointer)
959
+ defaulted_ptr = FFI::MemoryPointer.new(:ulong)
960
+
961
+ bool = GetSecurityDescriptorDacl(
962
+ security_ptr,
963
+ present_ptr,
964
+ pdacl_ptr,
965
+ defaulted_ptr
966
+ )
967
+
968
+ # If it fails, or the dacl isn't present, return false.
969
+ if !bool || present_ptr.read_ulong == 0
970
+ return false
971
+ end
972
+
973
+ pdacl = pdacl_ptr.read_pointer
974
+ psid_ptr = FFI::MemoryPointer.new(:pointer)
975
+
976
+ # S-1-1-0 is the well known SID for "Everyone".
977
+ ConvertStringSidToSid('S-1-1-0', psid_ptr)
978
+
979
+ psid = psid_ptr.read_pointer
980
+ trustee_ptr = FFI::MemoryPointer.new(TRUSTEE)
981
+
982
+ BuildTrusteeWithSid(trustee_ptr, psid)
983
+
984
+ rights_ptr = FFI::MemoryPointer.new(:ulong)
985
+
986
+ if GetEffectiveRightsFromAcl(pdacl, trustee_ptr, rights_ptr) == NO_ERROR
987
+ rights = rights_ptr.read_ulong
988
+ check = (rights & access_rights) == access_rights
989
+ end
990
+ end
991
+
992
+ check
993
+ end
994
+ end