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
26namespace dart {
27namespace bin {
28
29PathBuffer::PathBuffer() : length_(0) {
30 data_ = calloc(MAX_LONG_PATH + 1, sizeof(wchar_t)); // NOLINT
31}
32
33PathBuffer::~PathBuffer() {
34 free(data_);
35}
36
37char* PathBuffer::AsString() const {
38 UNREACHABLE();
39 return NULL;
40}
41
42wchar_t* PathBuffer::AsStringW() const {
43 return reinterpret_cast<wchar_t*>(data_);
44}
45
46const char* PathBuffer::AsScopedString() const {
47 return StringUtilsWin::WideToUtf8(AsStringW());
48}
49
50bool PathBuffer::Add(const char* name) {
51 Utf8ToWideScope wide_name(name);
52 return AddW(wide_name.wide());
53}
54
55bool 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
70void 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.
77static 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.
91struct LinkList {
92 DWORD volume;
93 DWORD id_low;
94 DWORD id_high;
95 LinkList* next;
96};
97
98// Forward declarations.
99static bool DeleteRecursively(PathBuffer* path);
100
101static 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
166ListType 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
214DirectoryListingEntry::~DirectoryListingEntry() {
215 ResetLink();
216 if (lister_ != 0) {
217 FindClose(reinterpret_cast<HANDLE>(lister_));
218 }
219}
220
221void 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
231static 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
263static 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
270static 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
280static 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
331static 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
350Directory::ExistsResult Directory::Exists(Namespace* namespc,
351 const char* dir_name) {
352 Utf8ToWideScope system_name(dir_name);
353 return ExistsHelper(system_name.wide());
354}
355
356char* 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
371bool 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
382const 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.
390static 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().
436const 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
475bool 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
495bool 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