win32-dir 0.5.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0fa2671864fff6d5baf15a94c38d5d49066c2a2f
4
- data.tar.gz: c036f3097b86f3f178da31c4105d71f44a7b22b6
2
+ SHA256:
3
+ metadata.gz: 2a8115d4e340523af631d3637a12c7e69836ebf25557b68d7ff6cbe57c04af7b
4
+ data.tar.gz: 68d05c5b0298b4deaeba910386066a2f92fbff050606b3b3af8e27c6a09d786c
5
5
  SHA512:
6
- metadata.gz: b55fc21bbba08fae66acddec7c5b635c717b3c38f8aec3726f20f68491b9c4e502ad904405f938ccc174caa0a3eff43b664c0aab2ce0aa63299c569ceb63f350
7
- data.tar.gz: d029bbc35525ab75ea5d6afaf02e5a02233e3feb0cf56aa4b42bed668c50d856e8d6c2bbe00e5b31aa401bc2eb5a3c1ef225714e612a8090548466a95551290b
6
+ metadata.gz: 1a61c6fc6ce9259d40d1922a095b760578da70a31e526aee950e91eef74db83b7ee0c3c1716b50574fd251f462c2fa36aa2dd1233d1bc0e5234972a230e07381
7
+ data.tar.gz: 6007e11c210cc84bed879c0d025f4c94238da5dc96bcf28dadd1a3cae908a48e8eb01823a2943e15bd701e793a9e48402bd5546239b24dab381f147e8eca7fe2
@@ -1 +1 @@
1
- require_relative 'win32/dir'
1
+ require_relative "win32/dir"
@@ -1,417 +1,418 @@
1
- require_relative 'dir/constants'
2
- require_relative 'dir/functions'
3
- require_relative 'dir/structs'
4
-
5
- class Dir
6
- include Dir::Structs
7
- include Dir::Constants
8
- extend Dir::Functions
9
-
10
- # The version of the win32-dir library.
11
- VERSION = '0.5.1'
12
-
13
- # CSIDL constants
14
- csidl = Hash[
15
- 'DESKTOP', 0x0000,
16
- 'INTERNET', 0x0001,
17
- 'PROGRAMS', 0x0002,
18
- 'CONTROLS', 0x0003,
19
- 'PRINTERS', 0x0004,
20
- 'PERSONAL', 0x0005,
21
- 'FAVORITES', 0x0006,
22
- 'STARTUP', 0x0007,
23
- 'RECENT', 0x0008,
24
- 'SENDTO', 0x0009,
25
- 'BITBUCKET', 0x000a,
26
- 'STARTMENU', 0x000b,
27
- 'MYDOCUMENTS', 0x000c,
28
- 'MYMUSIC', 0x000d,
29
- 'MYVIDEO', 0x000e,
30
- 'DESKTOPDIRECTORY', 0x0010,
31
- 'DRIVES', 0x0011,
32
- 'NETWORK', 0x0012,
33
- 'NETHOOD', 0x0013,
34
- 'FONTS', 0x0014,
35
- 'TEMPLATES', 0x0015,
36
- 'COMMON_STARTMENU', 0x0016,
37
- 'COMMON_PROGRAMS', 0X0017,
38
- 'COMMON_STARTUP', 0x0018,
39
- 'COMMON_FAVORITES', 0x001f,
40
- 'COMMON_DESKTOPDIRECTORY', 0x0019,
41
- 'APPDATA', 0x001a,
42
- 'PRINTHOOD', 0x001b,
43
- 'LOCAL_APPDATA', 0x001c,
44
- 'ALTSTARTUP', 0x001d,
45
- 'COMMON_ALTSTARTUP', 0x001e,
46
- 'INTERNET_CACHE', 0x0020,
47
- 'COOKIES', 0x0021,
48
- 'HISTORY', 0x0022,
49
- 'COMMON_APPDATA', 0x0023,
50
- 'WINDOWS', 0x0024,
51
- 'SYSTEM', 0x0025,
52
- 'PROGRAM_FILES', 0x0026,
53
- 'MYPICTURES', 0x0027,
54
- 'PROFILE', 0x0028,
55
- 'SYSTEMX86', 0x0029,
56
- 'PROGRAM_FILESX86', 0x002a,
57
- 'PROGRAM_FILES_COMMON', 0x002b,
58
- 'PROGRAM_FILES_COMMONX86', 0x002c,
59
- 'COMMON_TEMPLATES', 0x002d,
60
- 'COMMON_DOCUMENTS', 0x002e,
61
- 'CONNECTIONS', 0x0031,
62
- 'COMMON_MUSIC', 0x0035,
63
- 'COMMON_PICTURES', 0x0036,
64
- 'COMMON_VIDEO', 0x0037,
65
- 'RESOURCES', 0x0038,
66
- 'RESOURCES_LOCALIZED', 0x0039,
67
- 'COMMON_OEM_LINKS', 0x003a,
68
- 'CDBURN_AREA', 0x003b,
69
- 'COMMON_ADMINTOOLS', 0x002f,
70
- 'ADMINTOOLS', 0x0030
71
- ]
72
-
73
- # Dynamically set each of the CSIDL constants
74
- csidl.each{ |key, value|
75
- buf = 0.chr * 1024
76
- path = nil
77
- buf.encode!('UTF-16LE')
78
-
79
- if SHGetFolderPathW(0, value, 0, 0, buf) == 0 # Current path
80
- path = buf.strip
81
- elsif SHGetFolderPathW(0, value, 0, 1, buf) == 0 # Default path
82
- path = buf.strip
83
- else
84
- ptr = FFI::MemoryPointer.new(:long)
85
- info = SHFILEINFO.new
86
- flags = SHGFI_DISPLAYNAME | SHGFI_PIDL
87
-
88
- if SHGetFolderLocation(0, value, 0, 0, ptr) == 0
89
- if SHGetFileInfo(ptr.read_long, 0, info, info.size, flags) != 0
90
- path = info[:szDisplayName].to_s
91
- path.force_encoding(Encoding.default_external)
92
- end
93
- end
94
- end
95
-
96
- begin
97
- Dir.const_set(key, path.encode(Encoding.default_external)) if path
98
- rescue Encoding::UndefinedConversionError
99
- Dir.const_set(key, path.encode('UTF-8')) if path
100
- end
101
- }
102
-
103
- # Set Dir::MYDOCUMENTS to the same as Dir::PERSONAL if undefined
104
- unless defined? MYDOCUMENTS
105
- MYDOCUMENTS = PERSONAL
106
- end
107
-
108
- class << self
109
- alias old_glob glob
110
-
111
- # Same as the standard MRI Dir.glob method except that it handles
112
- # backslashes in path names.
113
- #
114
- def glob(glob_pattern, flags = 0, &block)
115
- if glob_pattern.is_a?(Array)
116
- temp = glob_pattern.map!{ |pattern| string_check(pattern).tr("\\", "/") }
117
- else
118
- temp = string_check(glob_pattern).tr("\\", "/")
119
- end
120
-
121
- old_glob(temp, flags, &block)
122
- end
123
-
124
- alias old_ref []
125
-
126
- # Same as the standard MRI Dir[] method except that it handles
127
- # backslashes in path names.
128
- #
129
- def [](*glob_patterns)
130
- temp = glob_patterns.map!{ |pattern| "#{pattern}".tr("\\", "/") }
131
- old_ref(*temp)
132
- end
133
-
134
- # JRuby normalizes the path by default.
135
- unless RUBY_PLATFORM == 'java'
136
- alias oldgetwd getwd
137
- alias oldpwd pwd
138
-
139
- # Returns the present working directory. Unlike MRI, this method always
140
- # normalizes the path.
141
- #
142
- # Examples:
143
- #
144
- # Dir.chdir("C:/Progra~1")
145
- # Dir.getwd # => C:\Program Files
146
- #
147
- # Dir.chdir("C:/PROGRAM FILES")
148
- # Dir.getwd # => C:\Program Files
149
- #
150
- def getwd
151
- path1 = FFI::Buffer.new(:wint_t, 1024, true)
152
- path2 = FFI::Buffer.new(:wint_t, 1024, true)
153
- path3 = FFI::Buffer.new(:wint_t, 1024, true)
154
-
155
- length = GetCurrentDirectoryW(path1.size, path1)
156
-
157
- if length == 0 || length > path1.size
158
- raise SystemCallError.new("GetCurrentDirectoryW", FFI.errno)
159
- end
160
-
161
- length = GetShortPathNameW(path1, path2, path2.size)
162
-
163
- if length == 0 || length > path2.size
164
- raise SystemCallError.new("GetShortPathNameW", FFI.errno)
165
- end
166
-
167
- length = GetLongPathNameW(path2, path3, path3.size)
168
-
169
- if length == 0 || length > path3.size
170
- raise SystemCallError.new("GetLongPathNameW", FFI.errno)
171
- end
172
-
173
- path = path3.read_bytes(length * 2).wstrip
174
-
175
- begin
176
- path.encode(Encoding.default_external)
177
- rescue Encoding::UndefinedConversionError
178
- path.encode('UTF-8')
179
- end
180
- end
181
-
182
- alias :pwd :getwd
183
- end
184
- end
185
-
186
- # Creates the symlink +to+, linked to the existing directory +from+. If the
187
- # +to+ directory already exists, it must be empty or an error is raised.
188
- #
189
- # Example:
190
- #
191
- # Dir.mkdir('C:/from')
192
- # Dir.create_junction('C:/to', 'C:/from')
193
- #
194
- def self.create_junction(to, from)
195
- to = string_check(to).wincode
196
- from = string_check(from).wincode
197
-
198
- from_path = (0.chr * 1024).encode('UTF-16LE')
199
-
200
- length = GetFullPathNameW(from, from_path.size, from_path, nil)
201
-
202
- if length == 0 || length > from_path.size
203
- raise SystemCallError.new("GetFullPathNameW", FFI.errno)
204
- else
205
- from_path.strip!
206
- end
207
-
208
- to_path = (0.chr * 1024).encode('UTF-16LE')
209
-
210
- length = GetFullPathNameW(to, to_path.size, to_path, nil)
211
-
212
- if length == 0 || length > to_path.size
213
- raise SystemCallError.new("GetFullPathNameW", FFI.errno)
214
- else
215
- to_path.strip!
216
- end
217
-
218
- # You can create a junction to a directory that already exists, so
219
- # long as it's empty.
220
- unless CreateDirectoryW(to_path, nil)
221
- if FFI.errno != ERROR_ALREADY_EXISTS
222
- raise SystemCallError.new("CreateDirectoryW", FFI.errno)
223
- end
224
- end
225
-
226
- begin
227
- # Generic read & write + open existing + reparse point & backup semantics
228
- handle = CreateFileW(
229
- to_path,
230
- GENERIC_READ | GENERIC_WRITE,
231
- 0,
232
- nil,
233
- OPEN_EXISTING,
234
- FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
235
- 0
236
- )
237
-
238
- if handle == INVALID_HANDLE_VALUE
239
- raise SystemCallError.new("CreateFileW", FFI.errno)
240
- end
241
-
242
- target = "\\??\\".encode('UTF-16LE') + from_path
243
-
244
- rdb = REPARSE_JDATA_BUFFER.new
245
- rdb[:ReparseTag] = 2684354563 # IO_REPARSE_TAG_MOUNT_POINT
246
- rdb[:ReparseDataLength] = target.bytesize + 12
247
- rdb[:Reserved] = 0
248
- rdb[:SubstituteNameOffset] = 0
249
- rdb[:SubstituteNameLength] = target.bytesize
250
- rdb[:PrintNameOffset] = target.bytesize + 2
251
- rdb[:PrintNameLength] = 0
252
- rdb[:PathBuffer] = target
253
-
254
- bytes = FFI::MemoryPointer.new(:ulong)
255
-
256
- begin
257
- bool = DeviceIoControl(
258
- handle,
259
- CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 41, METHOD_BUFFERED, 0),
260
- rdb,
261
- rdb[:ReparseDataLength] + rdb.header_size,
262
- nil,
263
- 0,
264
- bytes,
265
- nil
266
- )
267
-
268
- error = FFI.errno
269
-
270
- unless bool
271
- RemoveDirectoryW(to_path)
272
- raise SystemCallError.new("DeviceIoControl", error)
273
- end
274
- ensure
275
- CloseHandle(handle)
276
- end
277
- end
278
-
279
- self
280
- end
281
-
282
- # Returns the path that a given junction points to. Raises an
283
- # Errno::ENOENT error if the given path does not exist. Returns false
284
- # if it is not a junction.
285
- #
286
- # Example:
287
- #
288
- # Dir.mkdir('C:/from')
289
- # Dir.create_junction('C:/to', 'C:/from')
290
- # Dir.read_junction("c:/to") # => "c:/from"
291
- #
292
- def self.read_junction(junction)
293
- return false unless Dir.junction?(junction)
294
-
295
- junction = string_check(junction).wincode
296
-
297
- junction_path = (0.chr * 1024).encode('UTF-16LE')
298
-
299
- length = GetFullPathNameW(junction, junction_path.size, junction_path, nil)
300
-
301
- if length == 0 || length > junction_path.size
302
- raise SystemCallError.new("GetFullPathNameW", FFI.errno)
303
- else
304
- junction_path.strip!
305
- end
306
-
307
- begin
308
- # Generic read & write + open existing + reparse point & backup semantics
309
- handle = CreateFileW(
310
- junction_path,
311
- GENERIC_READ | GENERIC_WRITE,
312
- 0,
313
- nil,
314
- OPEN_EXISTING,
315
- FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
316
- 0
317
- )
318
-
319
- if handle == INVALID_HANDLE_VALUE
320
- raise SystemCallError.new("CreateFileW", FFI.errno)
321
- end
322
-
323
- rdb = REPARSE_JDATA_BUFFER.new
324
- rdb[:ReparseTag] = 0
325
- rdb[:ReparseDataLength] = 0
326
- rdb[:Reserved] = 0
327
- rdb[:SubstituteNameOffset] = 0
328
- rdb[:SubstituteNameLength] = 0
329
- rdb[:PrintNameOffset] = 0
330
- rdb[:PrintNameLength] = 0
331
- rdb[:PathBuffer] = ''
332
-
333
- bytes = FFI::MemoryPointer.new(:ulong)
334
-
335
- begin
336
- bool = DeviceIoControl(
337
- handle,
338
- CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 42, METHOD_BUFFERED, 0),
339
- nil,
340
- 0,
341
- rdb,
342
- 1024,
343
- bytes,
344
- nil
345
- )
346
-
347
- error = FFI.errno
348
-
349
- unless bool
350
- raise SystemCallError.new("DeviceIoControl", error)
351
- end
352
- ensure
353
- CloseHandle(handle)
354
- end
355
- end
356
-
357
- # MSDN says print and substitute names can be in any order
358
- jname = (rdb[:PathBuffer].to_ptr + rdb[:SubstituteNameOffset]).read_string(rdb[:SubstituteNameLength])
359
- jname = jname.bytes.to_a.pack('C*')
360
- jname = jname.force_encoding("UTF-16LE")
361
- raise "Invalid junction name: #{jname.encode('UTF-8')}" unless jname[0..3] == "\\??\\".encode("UTF-16LE")
362
- begin
363
- File.expand_path(jname[4..-1].encode(Encoding.default_external))
364
- rescue Encoding::UndefinedConversionError
365
- File.expand_path(jname[4..-1].encode('UTF-8'))
366
- end
367
- end
368
-
369
- # Returns whether or not +path+ is empty. Returns false if +path+ is not
370
- # a directory, or contains any files other than '.' or '..'.
371
- #
372
- def self.empty?(path)
373
- PathIsDirectoryEmptyW("#{path}".wincode)
374
- end
375
-
376
- # Returns whether or not +path+ is a junction.
377
- #
378
- def self.junction?(path)
379
- string_check(path)
380
- bool = true
381
-
382
- attrib = GetFileAttributesW("#{path}".wincode)
383
-
384
- # Only directories with a reparse point attribute can be junctions
385
- if attrib == INVALID_FILE_ATTRIBUTES ||
386
- attrib & FILE_ATTRIBUTE_DIRECTORY == 0 ||
387
- attrib & FILE_ATTRIBUTE_REPARSE_POINT == 0
388
- then
389
- bool = false
390
- end
391
-
392
- bool
393
- end
394
-
395
- # Class level aliases
396
- #
397
- class << self
398
- alias reparse_dir? junction?
399
- end
400
-
401
- private
402
-
403
- class << self
404
- # Simulate MRI's contortions for a stringiness check.
405
- def string_check(arg)
406
- return arg if arg.is_a?(String)
407
- return arg.send(:to_str) if arg.respond_to?(:to_str, true) # MRI honors it, even if private
408
- return arg.to_path if arg.respond_to?(:to_path)
409
- raise TypeError
410
- end
411
- end
412
-
413
- # Macro from Windows header file, used by the create_junction method.
414
- def self.CTL_CODE(device, function, method, access)
415
- ((device) << 16) | ((access) << 14) | ((function) << 2) | (method)
416
- end
417
- end
1
+ require_relative "dir/constants"
2
+ require_relative "dir/functions"
3
+ require_relative "dir/structs"
4
+
5
+ class Dir
6
+ include Dir::Structs
7
+ include Dir::Constants
8
+ extend Dir::Functions
9
+
10
+ # CSIDL constants
11
+ csidl = Hash[
12
+ "DESKTOP", 0x0000,
13
+ "INTERNET", 0x0001,
14
+ "PROGRAMS", 0x0002,
15
+ "CONTROLS", 0x0003,
16
+ "PRINTERS", 0x0004,
17
+ "PERSONAL", 0x0005,
18
+ "FAVORITES", 0x0006,
19
+ "STARTUP", 0x0007,
20
+ "RECENT", 0x0008,
21
+ "SENDTO", 0x0009,
22
+ "BITBUCKET", 0x000a,
23
+ "STARTMENU", 0x000b,
24
+ "MYDOCUMENTS", 0x000c,
25
+ "MYMUSIC", 0x000d,
26
+ "MYVIDEO", 0x000e,
27
+ "DESKTOPDIRECTORY", 0x0010,
28
+ "DRIVES", 0x0011,
29
+ "NETWORK", 0x0012,
30
+ "NETHOOD", 0x0013,
31
+ "FONTS", 0x0014,
32
+ "TEMPLATES", 0x0015,
33
+ "COMMON_STARTMENU", 0x0016,
34
+ "COMMON_PROGRAMS", 0X0017,
35
+ "COMMON_STARTUP", 0x0018,
36
+ "COMMON_FAVORITES", 0x001f,
37
+ "COMMON_DESKTOPDIRECTORY", 0x0019,
38
+ "APPDATA", 0x001a,
39
+ "PRINTHOOD", 0x001b,
40
+ "LOCAL_APPDATA", 0x001c,
41
+ "ALTSTARTUP", 0x001d,
42
+ "COMMON_ALTSTARTUP", 0x001e,
43
+ "INTERNET_CACHE", 0x0020,
44
+ "COOKIES", 0x0021,
45
+ "HISTORY", 0x0022,
46
+ "COMMON_APPDATA", 0x0023,
47
+ "WINDOWS", 0x0024,
48
+ "SYSTEM", 0x0025,
49
+ "PROGRAM_FILES", 0x0026,
50
+ "MYPICTURES", 0x0027,
51
+ "PROFILE", 0x0028,
52
+ "SYSTEMX86", 0x0029,
53
+ "PROGRAM_FILESX86", 0x002a,
54
+ "PROGRAM_FILES_COMMON", 0x002b,
55
+ "PROGRAM_FILES_COMMONX86", 0x002c,
56
+ "COMMON_TEMPLATES", 0x002d,
57
+ "COMMON_DOCUMENTS", 0x002e,
58
+ "CONNECTIONS", 0x0031,
59
+ "COMMON_MUSIC", 0x0035,
60
+ "COMMON_PICTURES", 0x0036,
61
+ "COMMON_VIDEO", 0x0037,
62
+ "RESOURCES", 0x0038,
63
+ "RESOURCES_LOCALIZED", 0x0039,
64
+ "COMMON_OEM_LINKS", 0x003a,
65
+ "CDBURN_AREA", 0x003b,
66
+ "COMMON_ADMINTOOLS", 0x002f,
67
+ "ADMINTOOLS", 0x0030
68
+ ]
69
+
70
+ # Dynamically set each of the CSIDL constants
71
+ csidl.each do |key, value|
72
+ buf = 0.chr * 1024
73
+ path = nil
74
+ buf.encode!("UTF-16LE")
75
+
76
+ if SHGetFolderPathW(0, value, 0, 0, buf) == 0 # Current path
77
+ path = buf.strip
78
+ elsif SHGetFolderPathW(0, value, 0, 1, buf) == 0 # Default path
79
+ path = buf.strip
80
+ else
81
+ ptr = FFI::MemoryPointer.new(:uint64)
82
+ info = SHFILEINFO.new
83
+ flags = SHGFI_DISPLAYNAME | SHGFI_PIDL
84
+
85
+ if SHGetFolderLocation(0, value, 0, 0, ptr) == 0
86
+ # Use read_array_of_uint64 for compatibility with JRuby if necessary.
87
+ if ptr.respond_to?(:read_uint64)
88
+ res = SHGetFileInfo(ptr.read_uint64, 0, info, info.size, flags)
89
+ else
90
+ res = SHGetFileInfo(ptr.read_array_of_uint64(1).first, 0, info, info.size, flags)
91
+ end
92
+
93
+ if res != 0
94
+ path = info[:szDisplayName].to_s
95
+ path.force_encoding(Encoding.default_external)
96
+ end
97
+ end
98
+ end
99
+
100
+ begin
101
+ Dir.const_set(key, path.encode(Encoding.default_external)) if path
102
+ rescue Encoding::UndefinedConversionError
103
+ Dir.const_set(key, path.encode("UTF-8")) if path
104
+ end
105
+ end
106
+
107
+ # Set Dir::MYDOCUMENTS to the same as Dir::PERSONAL if undefined
108
+ unless defined? MYDOCUMENTS
109
+ MYDOCUMENTS = PERSONAL
110
+ end
111
+
112
+ class << self
113
+ alias old_glob glob
114
+
115
+ # Same as the standard MRI Dir.glob method except that it handles
116
+ # backslashes in path names.
117
+ #
118
+ def glob(glob_pattern, flags = 0, &block)
119
+ if glob_pattern.is_a?(Array)
120
+ temp = glob_pattern.map! { |pattern| string_check(pattern).tr("\\", "/") }
121
+ else
122
+ temp = string_check(glob_pattern).tr("\\", "/")
123
+ end
124
+
125
+ old_glob(temp, flags, &block)
126
+ end
127
+
128
+ alias old_ref []
129
+
130
+ # Same as the standard MRI Dir[] method except that it handles
131
+ # backslashes in path names.
132
+ #
133
+ def [](*glob_patterns)
134
+ temp = glob_patterns.map! { |pattern| "#{pattern}".tr("\\", "/") }
135
+ old_ref(*temp)
136
+ end
137
+
138
+ # JRuby normalizes the path by default.
139
+ unless RUBY_PLATFORM == "java"
140
+ alias oldgetwd getwd
141
+ alias oldpwd pwd
142
+
143
+ # Returns the present working directory. Unlike MRI, this method always
144
+ # normalizes the path.
145
+ #
146
+ # Examples:
147
+ #
148
+ # Dir.chdir("C:/Progra~1")
149
+ # Dir.getwd # => C:\Program Files
150
+ #
151
+ # Dir.chdir("C:/PROGRAM FILES")
152
+ # Dir.getwd # => C:\Program Files
153
+ #
154
+ def getwd
155
+ path1 = FFI::Buffer.new(:wint_t, 1024, true)
156
+ path2 = FFI::Buffer.new(:wint_t, 1024, true)
157
+ path3 = FFI::Buffer.new(:wint_t, 1024, true)
158
+
159
+ length = GetCurrentDirectoryW(path1.size, path1)
160
+
161
+ if length == 0 || length > path1.size
162
+ raise SystemCallError.new("GetCurrentDirectoryW", FFI.errno)
163
+ end
164
+
165
+ length = GetShortPathNameW(path1, path2, path2.size)
166
+
167
+ if length == 0 || length > path2.size
168
+ raise SystemCallError.new("GetShortPathNameW", FFI.errno)
169
+ end
170
+
171
+ length = GetLongPathNameW(path2, path3, path3.size)
172
+
173
+ if length == 0 || length > path3.size
174
+ raise SystemCallError.new("GetLongPathNameW", FFI.errno)
175
+ end
176
+
177
+ path = path3.read_bytes(length * 2).wstrip
178
+
179
+ begin
180
+ path.encode(Encoding.default_external)
181
+ rescue Encoding::UndefinedConversionError
182
+ path.encode("UTF-8")
183
+ end
184
+ end
185
+
186
+ alias :pwd :getwd
187
+ end
188
+ end
189
+
190
+ # Creates the symlink +to+, linked to the existing directory +from+. If the
191
+ # +to+ directory already exists, it must be empty or an error is raised.
192
+ #
193
+ # Example:
194
+ #
195
+ # Dir.mkdir('C:/from')
196
+ # Dir.create_junction('C:/to', 'C:/from')
197
+ #
198
+ def self.create_junction(to, from)
199
+ to = string_check(to).wincode
200
+ from = string_check(from).wincode
201
+
202
+ from_path = (0.chr * 1024).encode("UTF-16LE")
203
+
204
+ length = GetFullPathNameW(from, from_path.size, from_path, nil)
205
+
206
+ if length == 0 || length > from_path.size
207
+ raise SystemCallError.new("GetFullPathNameW", FFI.errno)
208
+ else
209
+ from_path.strip!
210
+ end
211
+
212
+ to_path = (0.chr * 1024).encode("UTF-16LE")
213
+
214
+ length = GetFullPathNameW(to, to_path.size, to_path, nil)
215
+
216
+ if length == 0 || length > to_path.size
217
+ raise SystemCallError.new("GetFullPathNameW", FFI.errno)
218
+ else
219
+ to_path.strip!
220
+ end
221
+
222
+ # You can create a junction to a directory that already exists, so
223
+ # long as it's empty.
224
+ unless CreateDirectoryW(to_path, nil)
225
+ if FFI.errno != ERROR_ALREADY_EXISTS
226
+ raise SystemCallError.new("CreateDirectoryW", FFI.errno)
227
+ end
228
+ end
229
+
230
+ # Generic read & write + open existing + reparse point & backup semantics
231
+ handle = CreateFileW(
232
+ to_path,
233
+ GENERIC_READ | GENERIC_WRITE,
234
+ 0,
235
+ nil,
236
+ OPEN_EXISTING,
237
+ FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
238
+ 0
239
+ )
240
+
241
+ if handle == INVALID_HANDLE_VALUE
242
+ raise SystemCallError.new("CreateFileW", FFI.errno)
243
+ end
244
+
245
+ target = "\\??\\".encode("UTF-16LE") + from_path
246
+
247
+ rdb = REPARSE_JDATA_BUFFER.new
248
+ rdb[:ReparseTag] = 2684354563 # IO_REPARSE_TAG_MOUNT_POINT
249
+ rdb[:ReparseDataLength] = target.bytesize + 12
250
+ rdb[:Reserved] = 0
251
+ rdb[:SubstituteNameOffset] = 0
252
+ rdb[:SubstituteNameLength] = target.bytesize
253
+ rdb[:PrintNameOffset] = target.bytesize + 2
254
+ rdb[:PrintNameLength] = 0
255
+ rdb[:PathBuffer] = target
256
+
257
+ bytes = FFI::MemoryPointer.new(:ulong)
258
+
259
+ begin
260
+ bool = DeviceIoControl(
261
+ handle,
262
+ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 41, METHOD_BUFFERED, 0),
263
+ rdb,
264
+ rdb[:ReparseDataLength] + rdb.header_size,
265
+ nil,
266
+ 0,
267
+ bytes,
268
+ nil
269
+ )
270
+
271
+ error = FFI.errno
272
+
273
+ unless bool
274
+ RemoveDirectoryW(to_path)
275
+ raise SystemCallError.new("DeviceIoControl", error)
276
+ end
277
+ ensure
278
+ CloseHandle(handle)
279
+ end
280
+
281
+ self
282
+ end
283
+
284
+ # Returns the path that a given junction points to. Raises an
285
+ # Errno::ENOENT error if the given path does not exist. Returns false
286
+ # if it is not a junction.
287
+ #
288
+ # Example:
289
+ #
290
+ # Dir.mkdir('C:/from')
291
+ # Dir.create_junction('C:/to', 'C:/from')
292
+ # Dir.read_junction("c:/to") # => "c:/from"
293
+ #
294
+ def self.read_junction(junction)
295
+ return false unless Dir.junction?(junction)
296
+
297
+ junction = string_check(junction).wincode
298
+
299
+ junction_path = (0.chr * 1024).encode("UTF-16LE")
300
+
301
+ length = GetFullPathNameW(junction, junction_path.size, junction_path, nil)
302
+
303
+ if length == 0 || length > junction_path.size
304
+ raise SystemCallError.new("GetFullPathNameW", FFI.errno)
305
+ else
306
+ junction_path.strip!
307
+ end
308
+
309
+ # Generic read & write + open existing + reparse point & backup semantics
310
+ handle = CreateFileW(
311
+ junction_path,
312
+ GENERIC_READ | GENERIC_WRITE,
313
+ 0,
314
+ nil,
315
+ OPEN_EXISTING,
316
+ FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
317
+ 0
318
+ )
319
+
320
+ if handle == INVALID_HANDLE_VALUE
321
+ raise SystemCallError.new("CreateFileW", FFI.errno)
322
+ end
323
+
324
+ rdb = REPARSE_JDATA_BUFFER.new
325
+ rdb[:ReparseTag] = 0
326
+ rdb[:ReparseDataLength] = 0
327
+ rdb[:Reserved] = 0
328
+ rdb[:SubstituteNameOffset] = 0
329
+ rdb[:SubstituteNameLength] = 0
330
+ rdb[:PrintNameOffset] = 0
331
+ rdb[:PrintNameLength] = 0
332
+ rdb[:PathBuffer] = ""
333
+
334
+ bytes = FFI::MemoryPointer.new(:ulong)
335
+
336
+ begin
337
+ bool = DeviceIoControl(
338
+ handle,
339
+ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 42, METHOD_BUFFERED, 0),
340
+ nil,
341
+ 0,
342
+ rdb,
343
+ 1024,
344
+ bytes,
345
+ nil
346
+ )
347
+
348
+ error = FFI.errno
349
+
350
+ unless bool
351
+ raise SystemCallError.new("DeviceIoControl", error)
352
+ end
353
+ ensure
354
+ CloseHandle(handle)
355
+ end
356
+
357
+ # MSDN says print and substitute names can be in any order
358
+ jname = (rdb[:PathBuffer].to_ptr + rdb[:SubstituteNameOffset]).read_string(rdb[:SubstituteNameLength])
359
+ jname = jname.bytes.to_a.pack("C*")
360
+ jname = jname.force_encoding("UTF-16LE")
361
+ raise "Invalid junction name: #{jname.encode("UTF-8")}" unless jname[0..3] == "\\??\\".encode("UTF-16LE")
362
+
363
+ begin
364
+ File.expand_path(jname[4..-1].encode(Encoding.default_external))
365
+ rescue Encoding::UndefinedConversionError
366
+ File.expand_path(jname[4..-1].encode("UTF-8"))
367
+ end
368
+ end
369
+
370
+ # Returns whether or not +path+ is empty. Returns false if +path+ is not
371
+ # a directory, or contains any files other than '.' or '..'.
372
+ #
373
+ def self.empty?(path)
374
+ PathIsDirectoryEmptyW("#{path}".wincode)
375
+ end
376
+
377
+ # Returns whether or not +path+ is a junction.
378
+ #
379
+ def self.junction?(path)
380
+ string_check(path)
381
+ bool = true
382
+
383
+ attrib = GetFileAttributesW("#{path}".wincode)
384
+
385
+ # Only directories with a reparse point attribute can be junctions
386
+ if attrib == INVALID_FILE_ATTRIBUTES ||
387
+ attrib & FILE_ATTRIBUTE_DIRECTORY == 0 ||
388
+ attrib & FILE_ATTRIBUTE_REPARSE_POINT == 0
389
+ bool = false
390
+ end
391
+
392
+ bool
393
+ end
394
+
395
+ # Class level aliases
396
+ #
397
+ class << self
398
+ alias reparse_dir? junction?
399
+ end
400
+
401
+ private
402
+
403
+ class << self
404
+ # Simulate MRI's contortions for a stringiness check.
405
+ def string_check(arg)
406
+ return arg if arg.is_a?(String)
407
+ return arg.send(:to_str) if arg.respond_to?(:to_str, true) # MRI honors it, even if private
408
+ return arg.to_path if arg.respond_to?(:to_path)
409
+
410
+ raise TypeError
411
+ end
412
+ end
413
+
414
+ # Macro from Windows header file, used by the create_junction method.
415
+ def self.CTL_CODE(device, function, method, access)
416
+ ((device) << 16) | ((access) << 14) | ((function) << 2) | (method)
417
+ end
418
+ end