win32-file-stat 1.5.1 → 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
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