Skip to content

Commit 42fcc18

Browse files
zichanggcommit-bot@chromium.org
authored andcommitted
Reland "File::Copy avoids direct copying on Windows"
This is a reland of 391d3bc The root cause of the failure is that the destination path uses forward slashes as path separator. This fix checks for forward slashes as a potential path separators. If both forward and back slashes exist, the one closer to the end of the path will be used to get the destination directory. If any step fails, it will fall back to original copy. Original change's description: > File::Copy avoids direct copying on Windows > > There is a race condition for copying file on Windows, where CopyFile() > returns success but data has not been populated into destination file. > E.g process is killed or died in the middle. > > This cl will change File::Copy as > 1. Copy file to a temp file in the same directory of destination file. > 2. Rename the file to the target file. > > Bug: #42119 > Change-Id: I39b6d451f6ace970bc554501148259d33de232c7 > Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/149667 > Commit-Queue: Zichang Guo <[email protected]> > Reviewed-by: Zach Anderson <[email protected]> Bug: #42119 Change-Id: I58c3aa432d3f64bddb1deace4c9a1ceb2f0f5e16 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/151035 Commit-Queue: Zichang Guo <[email protected]> Reviewed-by: Zach Anderson <[email protected]>
1 parent 7ac8f42 commit 42fcc18

File tree

3 files changed

+182
-5
lines changed

3 files changed

+182
-5
lines changed

runtime/bin/file_win.cc

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <sys/utime.h> // NOLINT
1919

2020
#include "bin/builtin.h"
21+
#include "bin/crypto.h"
2122
#include "bin/directory.h"
2223
#include "bin/namespace.h"
2324
#include "bin/utils.h"
@@ -517,6 +518,100 @@ bool File::RenameLink(Namespace* namespc,
517518
return (move_status != 0);
518519
}
519520

521+
static wchar_t* CopyToDartScopeString(wchar_t* string) {
522+
wchar_t* wide_path = reinterpret_cast<wchar_t*>(
523+
Dart_ScopeAllocate(MAX_PATH * sizeof(wchar_t) + 1));
524+
wcscpy(wide_path, string);
525+
return wide_path;
526+
}
527+
528+
static wchar_t* CopyIntoTempFile(const char* src, const char* dest) {
529+
// This function will copy the file to a temp file in the destination
530+
// directory and return the path of temp file.
531+
// Creating temp file name has the same logic as Directory::CreateTemp(),
532+
// which tries with the rng and falls back to a uuid if it failed.
533+
const char* last_back_slash = strrchr(dest, '\\');
534+
// It is possible the path uses forwardslash as path separator.
535+
const char* last_forward_slash = strrchr(dest, '/');
536+
const char* last_path_separator = NULL;
537+
if (last_back_slash == NULL && last_forward_slash == NULL) {
538+
return NULL;
539+
} else if (last_forward_slash != NULL && last_forward_slash != NULL) {
540+
// If both types occur in the path, use the one closer to the end.
541+
if (last_back_slash - dest > last_forward_slash - dest) {
542+
last_path_separator = last_back_slash;
543+
} else {
544+
last_path_separator = last_forward_slash;
545+
}
546+
} else {
547+
last_path_separator =
548+
(last_forward_slash == NULL) ? last_back_slash : last_forward_slash;
549+
}
550+
int length_of_parent_dir = last_path_separator - dest + 1;
551+
if (length_of_parent_dir + 8 > MAX_PATH) {
552+
return NULL;
553+
}
554+
uint32_t suffix_bytes = 0;
555+
const int kSuffixSize = sizeof(suffix_bytes);
556+
if (Crypto::GetRandomBytes(kSuffixSize,
557+
reinterpret_cast<uint8_t*>(&suffix_bytes))) {
558+
PathBuffer buffer;
559+
char* dir = reinterpret_cast<char*>(
560+
Dart_ScopeAllocate(1 + sizeof(char) * length_of_parent_dir));
561+
memmove(dir, dest, length_of_parent_dir);
562+
dir[length_of_parent_dir] = '\0';
563+
if (!buffer.Add(dir)) {
564+
return NULL;
565+
}
566+
567+
char suffix[8 + 1];
568+
Utils::SNPrint(suffix, sizeof(suffix), "%x", suffix_bytes);
569+
Utf8ToWideScope source_path(src);
570+
if (!buffer.Add(suffix)) {
571+
return NULL;
572+
}
573+
if (CopyFileExW(source_path.wide(), buffer.AsStringW(), NULL, NULL, NULL,
574+
0) != 0) {
575+
return CopyToDartScopeString(buffer.AsStringW());
576+
}
577+
// If CopyFileExW() fails to copy to a temp file with random hex, fall
578+
// back to copy to a uuid temp file.
579+
}
580+
// UUID has a total of 36 characters in the form of
581+
// xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx.
582+
if (length_of_parent_dir + 36 > MAX_PATH) {
583+
return NULL;
584+
}
585+
UUID uuid;
586+
RPC_STATUS status = UuidCreateSequential(&uuid);
587+
if ((status != RPC_S_OK) && (status != RPC_S_UUID_LOCAL_ONLY)) {
588+
return NULL;
589+
}
590+
RPC_WSTR uuid_string;
591+
status = UuidToStringW(&uuid, &uuid_string);
592+
if (status != RPC_S_OK) {
593+
return NULL;
594+
}
595+
PathBuffer buffer;
596+
char* dir = reinterpret_cast<char*>(
597+
Dart_ScopeAllocate(1 + sizeof(char) * length_of_parent_dir));
598+
memmove(dir, dest, length_of_parent_dir);
599+
dir[length_of_parent_dir] = '\0';
600+
Utf8ToWideScope dest_path(dir);
601+
if (!buffer.AddW(dest_path.wide()) ||
602+
!buffer.AddW(reinterpret_cast<wchar_t*>(uuid_string))) {
603+
return NULL;
604+
}
605+
606+
RpcStringFreeW(&uuid_string);
607+
Utf8ToWideScope source_path(src);
608+
if (CopyFileExW(source_path.wide(), buffer.AsStringW(), NULL, NULL, NULL,
609+
0) != 0) {
610+
return CopyToDartScopeString(buffer.AsStringW());
611+
}
612+
return NULL;
613+
}
614+
520615
bool File::Copy(Namespace* namespc,
521616
const char* old_path,
522617
const char* new_path) {
@@ -525,11 +620,29 @@ bool File::Copy(Namespace* namespc,
525620
SetLastError(ERROR_FILE_NOT_FOUND);
526621
return false;
527622
}
528-
Utf8ToWideScope system_old_path(old_path);
529-
Utf8ToWideScope system_new_path(new_path);
530-
bool success = CopyFileExW(system_old_path.wide(), system_new_path.wide(),
531-
NULL, NULL, NULL, 0) != 0;
532-
return success;
623+
624+
wchar_t* temp_file = CopyIntoTempFile(old_path, new_path);
625+
if (temp_file == NULL) {
626+
// If temp file creation fails, fall back on doing a direct copy.
627+
Utf8ToWideScope system_old_path(old_path);
628+
Utf8ToWideScope system_new_path(new_path);
629+
return CopyFileExW(system_old_path.wide(), system_new_path.wide(), NULL,
630+
NULL, NULL, 0) != 0;
631+
}
632+
Utf8ToWideScope system_new_dest(new_path);
633+
634+
// Remove the existing file. Otherwise, renaming will fail.
635+
if (Exists(namespc, new_path)) {
636+
DeleteFileW(system_new_dest.wide());
637+
}
638+
639+
if (!MoveFileW(temp_file, system_new_dest.wide())) {
640+
DWORD error = GetLastError();
641+
DeleteFileW(temp_file);
642+
SetLastError(error);
643+
return false;
644+
}
645+
return true;
533646
}
534647

535648
int64_t File::LengthFromPath(Namespace* namespc, const char* name) {

tests/standalone/io/file_copy_test.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ void testCopySync() {
3030
Expect.equals(FILE_CONTENT2, file1.readAsStringSync());
3131
Expect.equals(FILE_CONTENT2, file2.readAsStringSync());
3232

33+
// Check there is no temporary files existing.
34+
var list = tmp.listSync();
35+
Expect.equals(2, list.length);
36+
for (var file in list) {
37+
final fileName = file.path.toString();
38+
Expect.isTrue(fileName.contains("file1") || fileName.contains("file2"));
39+
}
40+
3341
// Fail when coping to directory.
3442
var dir = new Directory('${tmp.path}/dir')..createSync();
3543
Expect.throws(() => file1.copySync(dir.path));
@@ -38,6 +46,28 @@ void testCopySync() {
3846
tmp.deleteSync(recursive: true);
3947
}
4048

49+
void testWithForwardSlashes() {
50+
if (Platform.isWindows) {
51+
final tmp = Directory.systemTemp.createTempSync('dart-file-copy');
52+
53+
final file1 = File('${tmp.path}/file1');
54+
file1.writeAsStringSync(FILE_CONTENT1);
55+
Expect.equals(FILE_CONTENT1, file1.readAsStringSync());
56+
57+
// Test with a path contains only forward slashes.
58+
final dest = tmp.path.toString().replaceAll("\\", "/");
59+
final file2 = file1.copySync('${dest}/file2');
60+
Expect.equals(FILE_CONTENT1, file2.readAsStringSync());
61+
62+
// Test with a path mixing both forward and backward slashes.
63+
final file3 = file1.copySync('${dest}\\file3');
64+
Expect.equals(FILE_CONTENT1, file3.readAsStringSync());
65+
66+
// Clean up the directory
67+
tmp.deleteSync(recursive: true);
68+
}
69+
}
70+
4171
void testCopy() {
4272
asyncStart();
4373
var tmp = Directory.systemTemp.createTempSync('dart-file-copy');
@@ -76,4 +106,6 @@ void testCopy() {
76106
main() {
77107
testCopySync();
78108
testCopy();
109+
// This is Windows only test.
110+
testWithForwardSlashes();
79111
}

tests/standalone_2/io/file_copy_test.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ void testCopySync() {
3030
Expect.equals(FILE_CONTENT2, file1.readAsStringSync());
3131
Expect.equals(FILE_CONTENT2, file2.readAsStringSync());
3232

33+
// Check there is no temporary files existing.
34+
var list = tmp.listSync();
35+
Expect.equals(2, list.length);
36+
for (var file in list) {
37+
final fileName = file.path.toString();
38+
Expect.isTrue(fileName.contains("file1") || fileName.contains("file2"));
39+
}
40+
3341
// Fail when coping to directory.
3442
var dir = new Directory('${tmp.path}/dir')..createSync();
3543
Expect.throws(() => file1.copySync(dir.path));
@@ -38,6 +46,28 @@ void testCopySync() {
3846
tmp.deleteSync(recursive: true);
3947
}
4048

49+
void testWithForwardSlashes() {
50+
if (Platform.isWindows) {
51+
final tmp = Directory.systemTemp.createTempSync('dart-file-copy');
52+
53+
final file1 = File('${tmp.path}/file1');
54+
file1.writeAsStringSync(FILE_CONTENT1);
55+
Expect.equals(FILE_CONTENT1, file1.readAsStringSync());
56+
57+
// Test with a path contains only forward slashes.
58+
final dest = tmp.path.toString().replaceAll("\\", "/");
59+
final file2 = file1.copySync('${dest}/file2');
60+
Expect.equals(FILE_CONTENT1, file2.readAsStringSync());
61+
62+
// Test with a path mixing both forward and backward slashes.
63+
final file3 = file1.copySync('${dest}\\file3');
64+
Expect.equals(FILE_CONTENT1, file3.readAsStringSync());
65+
66+
// Clean up the directory
67+
tmp.deleteSync(recursive: true);
68+
}
69+
}
70+
4171
void testCopy() {
4272
asyncStart();
4373
var tmp = Directory.systemTemp.createTempSync('dart-file-copy');
@@ -76,4 +106,6 @@ void testCopy() {
76106
main() {
77107
testCopySync();
78108
testCopy();
109+
// This is Windows only test.
110+
testWithForwardSlashes();
79111
}

0 commit comments

Comments
 (0)