win32-dir 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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