1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
2 | // for details. All rights reserved. Use of this source code is governed by a |
3 | // BSD-style license that can be found in the LICENSE file. |
4 | |
5 | #include "platform/globals.h" |
6 | #if defined(HOST_OS_WINDOWS) |
7 | |
8 | #include "bin/directory.h" |
9 | |
10 | #include <errno.h> // NOLINT |
11 | #include <sys/stat.h> // NOLINT |
12 | |
13 | #include "bin/crypto.h" |
14 | #include "bin/dartutils.h" |
15 | #include "bin/file.h" |
16 | #include "bin/namespace.h" |
17 | #include "bin/utils.h" |
18 | #include "bin/utils_win.h" |
19 | #include "platform/syslog.h" |
20 | #include "platform/utils.h" |
21 | |
22 | #undef DeleteFile |
23 | |
24 | #define MAX_LONG_PATH 32767 |
25 | |
26 | namespace dart { |
27 | namespace bin { |
28 | |
29 | PathBuffer::PathBuffer() : length_(0) { |
30 | data_ = calloc(MAX_LONG_PATH + 1, sizeof(wchar_t)); // NOLINT |
31 | } |
32 | |
33 | PathBuffer::~PathBuffer() { |
34 | free(data_); |
35 | } |
36 | |
37 | char* PathBuffer::AsString() const { |
38 | UNREACHABLE(); |
39 | return NULL; |
40 | } |
41 | |
42 | wchar_t* PathBuffer::AsStringW() const { |
43 | return reinterpret_cast<wchar_t*>(data_); |
44 | } |
45 | |
46 | const char* PathBuffer::AsScopedString() const { |
47 | return StringUtilsWin::WideToUtf8(AsStringW()); |
48 | } |
49 | |
50 | bool PathBuffer::Add(const char* name) { |
51 | Utf8ToWideScope wide_name(name); |
52 | return AddW(wide_name.wide()); |
53 | } |
54 | |
55 | bool PathBuffer::AddW(const wchar_t* name) { |
56 | wchar_t* data = AsStringW(); |
57 | int written = |
58 | _snwprintf(data + length_, MAX_LONG_PATH - length_, L"%s" , name); |
59 | data[MAX_LONG_PATH] = L'\0'; |
60 | if ((written <= MAX_LONG_PATH - length_) && (written >= 0) && |
61 | (static_cast<size_t>(written) == wcsnlen(name, MAX_LONG_PATH + 1))) { |
62 | length_ += written; |
63 | return true; |
64 | } else { |
65 | SetLastError(ERROR_BUFFER_OVERFLOW); |
66 | return false; |
67 | } |
68 | } |
69 | |
70 | void PathBuffer::Reset(intptr_t new_length) { |
71 | length_ = new_length; |
72 | AsStringW()[length_] = L'\0'; |
73 | } |
74 | |
75 | // If link_name points to a link, IsBrokenLink will return true if link_name |
76 | // points to an invalid target. |
77 | static bool IsBrokenLink(const wchar_t* link_name) { |
78 | HANDLE handle = CreateFileW( |
79 | link_name, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, |
80 | NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); |
81 | if (handle == INVALID_HANDLE_VALUE) { |
82 | return true; |
83 | } else { |
84 | CloseHandle(handle); |
85 | return false; |
86 | } |
87 | } |
88 | |
89 | // A linked list structure holding a link target's unique file system ID. |
90 | // Used to detect loops in the file system when listing recursively. |
91 | struct LinkList { |
92 | DWORD volume; |
93 | DWORD id_low; |
94 | DWORD id_high; |
95 | LinkList* next; |
96 | }; |
97 | |
98 | // Forward declarations. |
99 | static bool DeleteRecursively(PathBuffer* path); |
100 | |
101 | static ListType HandleFindFile(DirectoryListing* listing, |
102 | DirectoryListingEntry* entry, |
103 | const WIN32_FIND_DATAW& find_file_data) { |
104 | if (!listing->path_buffer().AddW(find_file_data.cFileName)) { |
105 | return kListError; |
106 | } |
107 | DWORD attributes = find_file_data.dwFileAttributes; |
108 | if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { |
109 | if (!listing->follow_links()) { |
110 | return kListLink; |
111 | } |
112 | HANDLE handle = |
113 | CreateFileW(listing->path_buffer().AsStringW(), 0, |
114 | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, |
115 | NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); |
116 | if (handle == INVALID_HANDLE_VALUE) { |
117 | // Report as (broken) link. |
118 | return kListLink; |
119 | } |
120 | if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { |
121 | // Check the seen link targets to see if we are in a file system loop. |
122 | LinkList current_link; |
123 | BY_HANDLE_FILE_INFORMATION info; |
124 | // Get info |
125 | if (!GetFileInformationByHandle(handle, &info)) { |
126 | DWORD error = GetLastError(); |
127 | CloseHandle(handle); |
128 | SetLastError(error); |
129 | return kListError; |
130 | } |
131 | CloseHandle(handle); |
132 | current_link.volume = info.dwVolumeSerialNumber; |
133 | current_link.id_low = info.nFileIndexLow; |
134 | current_link.id_high = info.nFileIndexHigh; |
135 | current_link.next = entry->link(); |
136 | LinkList* previous = entry->link(); |
137 | while (previous != NULL) { |
138 | if ((previous->volume == current_link.volume) && |
139 | (previous->id_low == current_link.id_low) && |
140 | (previous->id_high == current_link.id_high)) { |
141 | // Report the looping link as a link, rather than following it. |
142 | return kListLink; |
143 | } |
144 | previous = previous->next; |
145 | } |
146 | // Recurse into the directory, adding current link to the seen links list. |
147 | if ((wcscmp(find_file_data.cFileName, L"." ) == 0) || |
148 | (wcscmp(find_file_data.cFileName, L".." ) == 0)) { |
149 | return entry->Next(listing); |
150 | } |
151 | entry->set_link(new LinkList(current_link)); |
152 | return kListDirectory; |
153 | } |
154 | } |
155 | if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { |
156 | if ((wcscmp(find_file_data.cFileName, L"." ) == 0) || |
157 | (wcscmp(find_file_data.cFileName, L".." ) == 0)) { |
158 | return entry->Next(listing); |
159 | } |
160 | return kListDirectory; |
161 | } else { |
162 | return kListFile; |
163 | } |
164 | } |
165 | |
166 | ListType DirectoryListingEntry::Next(DirectoryListing* listing) { |
167 | if (done_) { |
168 | return kListDone; |
169 | } |
170 | |
171 | WIN32_FIND_DATAW find_file_data; |
172 | |
173 | if (lister_ == 0) { |
174 | const wchar_t* tail = parent_ == NULL ? L"*" : L"\\*" ; |
175 | if (!listing->path_buffer().AddW(tail)) { |
176 | done_ = true; |
177 | return kListError; |
178 | } |
179 | |
180 | path_length_ = listing->path_buffer().length() - 1; |
181 | |
182 | HANDLE find_handle = |
183 | FindFirstFileW(listing->path_buffer().AsStringW(), &find_file_data); |
184 | |
185 | if (find_handle == INVALID_HANDLE_VALUE) { |
186 | done_ = true; |
187 | return kListError; |
188 | } |
189 | |
190 | lister_ = reinterpret_cast<intptr_t>(find_handle); |
191 | |
192 | listing->path_buffer().Reset(path_length_); |
193 | |
194 | return HandleFindFile(listing, this, find_file_data); |
195 | } |
196 | |
197 | // Reset. |
198 | listing->path_buffer().Reset(path_length_); |
199 | ResetLink(); |
200 | |
201 | if (FindNextFileW(reinterpret_cast<HANDLE>(lister_), &find_file_data) != 0) { |
202 | return HandleFindFile(listing, this, find_file_data); |
203 | } |
204 | |
205 | done_ = true; |
206 | |
207 | if (GetLastError() != ERROR_NO_MORE_FILES) { |
208 | return kListError; |
209 | } |
210 | |
211 | return kListDone; |
212 | } |
213 | |
214 | DirectoryListingEntry::~DirectoryListingEntry() { |
215 | ResetLink(); |
216 | if (lister_ != 0) { |
217 | FindClose(reinterpret_cast<HANDLE>(lister_)); |
218 | } |
219 | } |
220 | |
221 | void DirectoryListingEntry::ResetLink() { |
222 | if ((link_ != NULL) && ((parent_ == NULL) || (parent_->link_ != link_))) { |
223 | delete link_; |
224 | link_ = NULL; |
225 | } |
226 | if (parent_ != NULL) { |
227 | link_ = parent_->link_; |
228 | } |
229 | } |
230 | |
231 | static bool DeleteFile(const wchar_t* file_name, PathBuffer* path) { |
232 | if (!path->AddW(file_name)) { |
233 | return false; |
234 | } |
235 | |
236 | if (DeleteFileW(path->AsStringW()) != 0) { |
237 | return true; |
238 | } |
239 | |
240 | // If we failed because the file is read-only, make it writeable and try |
241 | // again. This mirrors Linux/Mac where a directory containing read-only files |
242 | // can still be recursively deleted. |
243 | if (GetLastError() == ERROR_ACCESS_DENIED) { |
244 | DWORD attributes = GetFileAttributesW(path->AsStringW()); |
245 | if (attributes == INVALID_FILE_ATTRIBUTES) { |
246 | return false; |
247 | } |
248 | |
249 | if ((attributes & FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY) { |
250 | attributes &= ~FILE_ATTRIBUTE_READONLY; |
251 | |
252 | if (SetFileAttributesW(path->AsStringW(), attributes) == 0) { |
253 | return false; |
254 | } |
255 | |
256 | return DeleteFileW(path->AsStringW()) != 0; |
257 | } |
258 | } |
259 | |
260 | return false; |
261 | } |
262 | |
263 | static bool DeleteDir(const wchar_t* dir_name, PathBuffer* path) { |
264 | if ((wcscmp(dir_name, L"." ) == 0) || (wcscmp(dir_name, L".." ) == 0)) { |
265 | return true; |
266 | } |
267 | return path->AddW(dir_name) && DeleteRecursively(path); |
268 | } |
269 | |
270 | static bool DeleteEntry(LPWIN32_FIND_DATAW find_file_data, PathBuffer* path) { |
271 | DWORD attributes = find_file_data->dwFileAttributes; |
272 | |
273 | if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { |
274 | return DeleteDir(find_file_data->cFileName, path); |
275 | } else { |
276 | return DeleteFile(find_file_data->cFileName, path); |
277 | } |
278 | } |
279 | |
280 | static bool DeleteRecursively(PathBuffer* path) { |
281 | DWORD attributes = GetFileAttributesW(path->AsStringW()); |
282 | if (attributes == INVALID_FILE_ATTRIBUTES) { |
283 | return false; |
284 | } |
285 | // If the directory is a junction, it's pointing to some other place in the |
286 | // filesystem that we do not want to recurse into. |
287 | if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { |
288 | // Just delete the junction itself. |
289 | return RemoveDirectoryW(path->AsStringW()) != 0; |
290 | } |
291 | // If it's a file, remove it directly. |
292 | if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { |
293 | return DeleteFile(L"" , path); |
294 | } |
295 | |
296 | if (!path->AddW(L"\\*" )) { |
297 | return false; |
298 | } |
299 | |
300 | WIN32_FIND_DATAW find_file_data; |
301 | HANDLE find_handle = FindFirstFileW(path->AsStringW(), &find_file_data); |
302 | |
303 | if (find_handle == INVALID_HANDLE_VALUE) { |
304 | return false; |
305 | } |
306 | |
307 | // Adjust the path by removing the '*' used for the search. |
308 | int path_length = path->length() - 1; |
309 | path->Reset(path_length); |
310 | |
311 | do { |
312 | if (!DeleteEntry(&find_file_data, path)) { |
313 | break; |
314 | } |
315 | path->Reset(path_length); // DeleteEntry adds to the path. |
316 | } while (FindNextFileW(find_handle, &find_file_data) != 0); |
317 | |
318 | DWORD last_error = GetLastError(); |
319 | // Always close handle. |
320 | FindClose(find_handle); |
321 | if (last_error != ERROR_NO_MORE_FILES) { |
322 | // Unexpected error, set and return. |
323 | SetLastError(last_error); |
324 | return false; |
325 | } |
326 | // All content deleted succesfully, try to delete directory. |
327 | path->Reset(path_length - 1); // Drop the "\" from the end of the path. |
328 | return RemoveDirectoryW(path->AsStringW()) != 0; |
329 | } |
330 | |
331 | static Directory::ExistsResult ExistsHelper(const wchar_t* dir_name) { |
332 | DWORD attributes = GetFileAttributesW(dir_name); |
333 | if (attributes == INVALID_FILE_ATTRIBUTES) { |
334 | DWORD last_error = GetLastError(); |
335 | if ((last_error == ERROR_FILE_NOT_FOUND) || |
336 | (last_error == ERROR_PATH_NOT_FOUND)) { |
337 | return Directory::DOES_NOT_EXIST; |
338 | } else { |
339 | // We might not be able to get the file attributes for other |
340 | // reasons such as lack of permissions. In that case we do |
341 | // not know if the directory exists. |
342 | return Directory::UNKNOWN; |
343 | } |
344 | } |
345 | bool exists = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; |
346 | exists = exists && !IsBrokenLink(dir_name); |
347 | return exists ? Directory::EXISTS : Directory::DOES_NOT_EXIST; |
348 | } |
349 | |
350 | Directory::ExistsResult Directory::Exists(Namespace* namespc, |
351 | const char* dir_name) { |
352 | Utf8ToWideScope system_name(dir_name); |
353 | return ExistsHelper(system_name.wide()); |
354 | } |
355 | |
356 | char* Directory::CurrentNoScope() { |
357 | int length = GetCurrentDirectoryW(0, NULL); |
358 | if (length == 0) { |
359 | return NULL; |
360 | } |
361 | wchar_t* current = new wchar_t[length + 1]; |
362 | GetCurrentDirectoryW(length + 1, current); |
363 | int utf8_len = |
364 | WideCharToMultiByte(CP_UTF8, 0, current, -1, NULL, 0, NULL, NULL); |
365 | char* result = reinterpret_cast<char*>(malloc(utf8_len)); |
366 | WideCharToMultiByte(CP_UTF8, 0, current, -1, result, utf8_len, NULL, NULL); |
367 | delete[] current; |
368 | return result; |
369 | } |
370 | |
371 | bool Directory::Create(Namespace* namespc, const char* dir_name) { |
372 | Utf8ToWideScope system_name(dir_name); |
373 | int create_status = CreateDirectoryW(system_name.wide(), NULL); |
374 | // If the directory already existed, treat it as a success. |
375 | if ((create_status == 0) && (GetLastError() == ERROR_ALREADY_EXISTS) && |
376 | (ExistsHelper(system_name.wide()) == EXISTS)) { |
377 | return true; |
378 | } |
379 | return (create_status != 0); |
380 | } |
381 | |
382 | const char* Directory::SystemTemp(Namespace* namespc) { |
383 | PathBuffer path; |
384 | // Remove \ at end. |
385 | path.Reset(GetTempPathW(MAX_LONG_PATH, path.AsStringW()) - 1); |
386 | return path.AsScopedString(); |
387 | } |
388 | |
389 | // Creates a new temporary directory with a UUID as suffix. |
390 | static const char* CreateTempFromUUID(const char* prefix) { |
391 | PathBuffer path; |
392 | Utf8ToWideScope system_prefix(prefix); |
393 | if (!path.AddW(system_prefix.wide())) { |
394 | return NULL; |
395 | } |
396 | |
397 | // Length of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx is 36. |
398 | if (path.length() > MAX_LONG_PATH - 36) { |
399 | return NULL; |
400 | } |
401 | |
402 | UUID uuid; |
403 | RPC_STATUS status = UuidCreateSequential(&uuid); |
404 | if ((status != RPC_S_OK) && (status != RPC_S_UUID_LOCAL_ONLY)) { |
405 | return NULL; |
406 | } |
407 | RPC_WSTR uuid_string; |
408 | status = UuidToStringW(&uuid, &uuid_string); |
409 | if (status != RPC_S_OK) { |
410 | return NULL; |
411 | } |
412 | |
413 | // RPC_WSTR is an unsigned short*, so we cast to wchar_t*. |
414 | if (!path.AddW(reinterpret_cast<wchar_t*>(uuid_string))) { |
415 | return NULL; |
416 | } |
417 | RpcStringFreeW(&uuid_string); |
418 | if (!CreateDirectoryW(path.AsStringW(), NULL)) { |
419 | return NULL; |
420 | } |
421 | return path.AsScopedString(); |
422 | } |
423 | |
424 | // Creates a new, unused directory, adding characters to the end of prefix, and |
425 | // returns the directory's name. |
426 | // |
427 | // Creates this directory, with a default security descriptor inherited from its |
428 | // parent directory. The return value is Dart_ScopeAllocated. |
429 | // |
430 | // First, attempts appending a suffix created from a random uint32_t. If that |
431 | // name is already taken, falls back on using a UUID for the suffix. |
432 | // |
433 | // Note: More attempts at finding an available short suffix would more reliably |
434 | // avoid a uuid suffix. We choose one attempt here because it is simpler, and |
435 | // to have a small bound on the number of calls to CreateDirectoryW(). |
436 | const char* Directory::CreateTemp(Namespace* namespc, const char* prefix) { |
437 | PathBuffer path; |
438 | Utf8ToWideScope system_prefix(prefix); |
439 | if (!path.AddW(system_prefix.wide())) { |
440 | return NULL; |
441 | } |
442 | |
443 | // Adding 8 hex digits. |
444 | if (path.length() > MAX_LONG_PATH - 8) { |
445 | // No fallback, there won't be enough room for the UUID, either. |
446 | return NULL; |
447 | } |
448 | |
449 | // First try a short suffix using the rng, then if that fails fall back on |
450 | // a uuid. |
451 | uint32_t suffix_bytes = 0; |
452 | const int kSuffixSize = sizeof(suffix_bytes); |
453 | if (!Crypto::GetRandomBytes(kSuffixSize, |
454 | reinterpret_cast<uint8_t*>(&suffix_bytes))) { |
455 | // Getting random bytes failed, maybe the UUID will work? |
456 | return CreateTempFromUUID(prefix); |
457 | } |
458 | |
459 | // Two digits per byte plus null. |
460 | char suffix[kSuffixSize * 2 + 1]; |
461 | Utils::SNPrint(suffix, sizeof(suffix), "%x" , suffix_bytes); |
462 | if (!path.Add(suffix)) { |
463 | // Adding to the path failed, maybe because of low-memory. Don't fall back. |
464 | return NULL; |
465 | } |
466 | |
467 | if (!CreateDirectoryW(path.AsStringW(), NULL)) { |
468 | // Creation failed, possibly because an entry with the name already exists. |
469 | // Fall back to using the UUID suffix. |
470 | return CreateTempFromUUID(prefix); |
471 | } |
472 | return path.AsScopedString(); |
473 | } |
474 | |
475 | bool Directory::Delete(Namespace* namespc, |
476 | const char* dir_name, |
477 | bool recursive) { |
478 | bool result = false; |
479 | Utf8ToWideScope system_dir_name(dir_name); |
480 | if (!recursive) { |
481 | if (File::GetType(namespc, dir_name, true) == File::kIsDirectory) { |
482 | result = (RemoveDirectoryW(system_dir_name.wide()) != 0); |
483 | } else { |
484 | SetLastError(ERROR_FILE_NOT_FOUND); |
485 | } |
486 | } else { |
487 | PathBuffer path; |
488 | if (path.AddW(system_dir_name.wide())) { |
489 | result = DeleteRecursively(&path); |
490 | } |
491 | } |
492 | return result; |
493 | } |
494 | |
495 | bool Directory::Rename(Namespace* namespc, |
496 | const char* path, |
497 | const char* new_path) { |
498 | Utf8ToWideScope system_path(path); |
499 | Utf8ToWideScope system_new_path(new_path); |
500 | ExistsResult exists = ExistsHelper(system_path.wide()); |
501 | if (exists != EXISTS) { |
502 | return false; |
503 | } |
504 | ExistsResult new_exists = ExistsHelper(system_new_path.wide()); |
505 | // MoveFile does not allow replacing existing directories. Therefore, |
506 | // if the new_path is currently a directory we need to delete it |
507 | // first. |
508 | if (new_exists == EXISTS) { |
509 | bool success = Delete(namespc, new_path, true); |
510 | if (!success) { |
511 | return false; |
512 | } |
513 | } |
514 | DWORD flags = MOVEFILE_WRITE_THROUGH; |
515 | int move_status = |
516 | MoveFileExW(system_path.wide(), system_new_path.wide(), flags); |
517 | return (move_status != 0); |
518 | } |
519 | |
520 | } // namespace bin |
521 | } // namespace dart |
522 | |
523 | #endif // defined(HOST_OS_WINDOWS) |
524 | |