win32-dir 0.4.0 → 0.4.1

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