win32-dir 0.5.1 → 0.7.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.
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