Skip to content

Commit b457490

Browse files
brianquinlanCommit Queue
authored and
Commit Queue
committed
[io] Fix a bug where relative symlinks to directories did not work on Windows.
Also modified `File::GetType` on Windows to correctly report the type of broken links as notFound. Windows fixes: co19/LibTest/io/Link/createSync_A04_t08 co19/LibTest/io/Link/createSync_A04_t10 co19/LibTest/io/Link/createSync_A04_t12 co19/LibTest/io/Link/createSync_A04_t14 co19/LibTest/io/Link/createSync_A04_t16 co19/LibTest/io/Link/createSync_A06_t01 co19/LibTest/io/Link/createSync_A06_t03 co19/LibTest/io/Link/createSync_A06_t06 co19/LibTest/io/Link/createSync_A06_t08 co19/LibTest/io/Link/create_A04_t08 co19/LibTest/io/Link/create_A04_t10 co19/LibTest/io/Link/create_A04_t12 co19/LibTest/io/Link/create_A04_t14 co19/LibTest/io/Link/create_A04_t16 co19/LibTest/io/Link/create_A06_t01 co19/LibTest/io/Link/create_A06_t03 co19/LibTest/io/Link/create_A06_t06 co19/LibTest/io/Link/create_A06_t08 co19/LibTest/io/Link/resolveSymbolicLinksSync_A01_t01 co19/LibTest/io/Link/resolveSymbolicLinks_A01_t01 Bug:#53848 Bug:#45981 Change-Id: I3d156f38540089d8adb12dbb79d0477330d9eb07 Tested: updated unit tests plus fixes existing tests Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/335940 Commit-Queue: Brian Quinlan <[email protected]> Reviewed-by: Siva Annamalai <[email protected]>
1 parent d67f159 commit b457490

File tree

5 files changed

+150
-38
lines changed

5 files changed

+150
-38
lines changed

runtime/bin/BUILD.gn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ template("build_gen_snapshot") {
228228
libs = [
229229
"iphlpapi.lib",
230230
"ws2_32.lib",
231+
"Pathcch.lib",
231232
"Rpcrt4.lib",
232233
"shlwapi.lib",
233234
"winmm.lib",
@@ -814,6 +815,7 @@ template("dart_executable") {
814815
# CoTaskMemAlloc up with `DynamicLibrary.process()`.
815816
"ole32.lib",
816817
"iphlpapi.lib",
818+
"Pathcch.lib",
817819
"psapi.lib",
818820
"ws2_32.lib",
819821
"Rpcrt4.lib",
@@ -1032,6 +1034,7 @@ executable("run_vm_tests") {
10321034
# CoTaskMemAlloc up with `DynamicLibrary.process()`.
10331035
"ole32.lib",
10341036
"iphlpapi.lib",
1037+
"Pathcch.lib",
10351038
"psapi.lib",
10361039
"ws2_32.lib",
10371040
"Rpcrt4.lib",

runtime/bin/file_win.cc

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <Shlwapi.h> // NOLINT
1313
#include <fcntl.h> // NOLINT
1414
#include <io.h> // NOLINT
15+
#include <pathcch.h> // NOLINT
1516
#include <winioctl.h> // NOLINT
1617
#undef StrDup // defined in Shlwapi.h as StrDupW
1718
#include <stdio.h> // NOLINT
@@ -589,22 +590,69 @@ typedef struct _REPARSE_DATA_BUFFER {
589590
};
590591
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
591592

592-
static constexpr int kReparseDataHeaderSize =
593-
sizeof(ULONG) + 2 * sizeof(USHORT);
594-
static constexpr int kMountPointHeaderSize = 4 * sizeof(USHORT);
595-
596593
bool File::CreateLink(Namespace* namespc,
597594
const char* utf8_name,
598595
const char* utf8_target) {
599596
Utf8ToWideScope name(PrefixLongFilePath(utf8_name));
600597
Utf8ToWideScope target(PrefixLongFilePath(utf8_target));
601598
DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
602599

603-
File::Type type = File::GetType(namespc, utf8_target, true);
600+
File::Type type;
601+
if (File::IsAbsolutePath(utf8_target)) {
602+
type = File::GetType(namespc, utf8_target, true);
603+
} else {
604+
// The path of `target` is relative to `name`.
605+
//
606+
// To determine if `target` is a file or directory, we need to calculate
607+
// either its absolute path or its path relative to the current working
608+
// directory.
609+
//
610+
// For example:
611+
//
612+
// name= C:\A\B\Link ..\..\Link ..\..\Link
613+
// target= MyFile MyFile ..\Dir\MyFile
614+
// --------------------------------------------------------------------
615+
// target_path= C:\A\B\MyFile ..\..\MyFile ..\..\..\Dir\MyFile
616+
//
617+
// The transformation steps are:
618+
// 1. target_path := name ..\..\Link
619+
// 2. target_path := remove_file(target_path) ..\..\
620+
// 3. target_path := combine(target_path, target) ..\..\..\Dir\MyFile
621+
622+
// 1. target_path := name
623+
intptr_t target_path_max_length = name.length() + target.length();
624+
Wchart target_path(target_path_max_length);
625+
wcscpy_s(target_path.buf(), target_path_max_length, name.wide());
626+
627+
// 2. target_path := remove_file(target_path)
628+
HRESULT remove_result =
629+
PathCchRemoveFileSpec(target_path.buf(), target_path_max_length);
630+
if (remove_result == S_FALSE) {
631+
// If the file component could not be removed, then `name` is
632+
// top-level, like "C:\" or "/". Attempts to create files at those paths
633+
// will fail with ERROR_ACCESS_DENIED.
634+
SetLastError(ERROR_ACCESS_DENIED);
635+
return false;
636+
} else if (remove_result != S_OK) {
637+
SetLastError(remove_result);
638+
return false;
639+
}
640+
641+
// 3. target_path := combine(target_path, target)
642+
HRESULT combine_result = PathCchCombineEx(
643+
target_path.buf(), target_path_max_length, target_path.buf(),
644+
target.wide(), PATHCCH_ALLOW_LONG_PATHS);
645+
if (combine_result != S_OK) {
646+
SetLastError(combine_result);
647+
return false;
648+
}
649+
650+
type = PathIsDirectoryW(target_path.buf()) ? kIsDirectory : kIsFile;
651+
}
652+
604653
if (type == kIsDirectory) {
605654
flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
606655
}
607-
608656
int create_status = CreateSymbolicLinkW(name.wide(), target.wide(), flags);
609657

610658
// If running on a Windows 10 build older than 14972, an invalid parameter
@@ -1120,40 +1168,34 @@ File::Type File::GetType(Namespace* namespc,
11201168
// Convert to wchar_t string.
11211169
Utf8ToWideScope name(prefixed_path);
11221170
DWORD attributes = GetFileAttributesW(name.wide());
1123-
File::Type result = kIsFile;
11241171
if (attributes == INVALID_FILE_ATTRIBUTES) {
1125-
result = kDoesNotExist;
1172+
return kDoesNotExist;
11261173
} else if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
11271174
if (follow_links) {
11281175
HANDLE target_handle = CreateFileW(
11291176
name.wide(), 0,
11301177
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
11311178
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
11321179
if (target_handle == INVALID_HANDLE_VALUE) {
1133-
DWORD last_error = GetLastError();
1134-
if ((last_error == ERROR_FILE_NOT_FOUND) ||
1135-
(last_error == ERROR_PATH_NOT_FOUND)) {
1136-
return kDoesNotExist;
1137-
}
1138-
result = kIsLink;
1180+
return kDoesNotExist;
11391181
} else {
11401182
BY_HANDLE_FILE_INFORMATION info;
11411183
if (!GetFileInformationByHandle(target_handle, &info)) {
11421184
CloseHandle(target_handle);
1143-
return File::kIsLink;
1185+
return File::kDoesNotExist;
11441186
}
11451187
CloseHandle(target_handle);
11461188
return ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
11471189
? kIsDirectory
11481190
: kIsFile;
11491191
}
11501192
} else {
1151-
result = kIsLink;
1193+
return kIsLink;
11521194
}
11531195
} else if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
1154-
result = kIsDirectory;
1196+
return kIsDirectory;
11551197
}
1156-
return result;
1198+
return kIsFile;
11571199
}
11581200

11591201
File::Identical File::AreIdentical(Namespace* namespc_1,

tests/standalone/io/file_long_path_test.dart

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -109,31 +109,44 @@ void testFileStat(String dir) {
109109
}
110110

111111
void testCreateLinkToDir(String dir) {
112-
final path = '${dir}\\a_long_path_linkname';
113-
Expect.isTrue(path.length > maxPath);
114-
var target = '$dir\\a_long_path_target';
115-
final link = Link(path)..createSync(target);
112+
final linkPath = '$dir\\a_long_path_linkname';
113+
final renamedPath = '$dir\\a_long_renamed_path_linkname';
116114

117-
final dest = Directory(target)..createSync();
118-
Expect.isTrue(dest.existsSync());
115+
Expect.isTrue(linkPath.length > maxPath);
116+
Expect.isTrue(renamedPath.length > maxPath);
117+
118+
var targetDirectory1 = '$dir\\a_long_directory_target1';
119+
var targetDirectory2 = '$dir\\a_long_directory_target2';
119120

121+
Directory(targetDirectory1).createSync();
122+
Directory(targetDirectory2).createSync();
123+
124+
// Create link
125+
final link = Link(linkPath)..createSync(targetDirectory1);
120126
Expect.isTrue(link.existsSync());
121-
Expect.isTrue(link.targetSync().contains('a_long_path_target'));
127+
final resolvedCreatePath = link.resolveSymbolicLinksSync();
128+
Expect.isTrue(
129+
FileSystemEntity.identicalSync(targetDirectory1, resolvedCreatePath),
130+
'${link.path} should resolve to $targetDirectory1 but resolved to $resolvedCreatePath');
122131

123132
// Rename link
124-
var renamedLink = link.renameSync('${dir}\\a_renamed_long_path_link');
133+
var renamedLink = link.renameSync(renamedPath);
125134
Expect.isTrue(renamedLink.existsSync());
126135
Expect.isFalse(link.existsSync());
127-
Expect.isTrue(renamedLink.targetSync().contains('a_long_path_target'));
136+
final resolvedRenamePath = renamedLink.resolveSymbolicLinksSync();
137+
Expect.isTrue(
138+
FileSystemEntity.identicalSync(targetDirectory1, resolvedRenamePath),
139+
'${link.path} should resolve to $targetDirectory1 but resolved to $resolvedRenamePath');
128140

129141
// Update link target
130-
target = '$dir\\an_updated_target';
131-
final renamedDest = Directory(target)..createSync();
132-
renamedLink.updateSync(target);
133-
Expect.isTrue(renamedLink.targetSync().contains('an_updated_target'));
134-
135-
dest.deleteSync();
136-
renamedDest.deleteSync();
142+
renamedLink.updateSync(targetDirectory2);
143+
final resolvedUpdatedPath = renamedLink.resolveSymbolicLinksSync();
144+
Expect.isTrue(
145+
FileSystemEntity.identicalSync(targetDirectory2, resolvedUpdatedPath),
146+
'${link.path} should resolve to $targetDirectory2 but resolved to $resolvedRenamePath');
147+
148+
Directory(targetDirectory1).deleteSync();
149+
Directory(targetDirectory2).deleteSync();
137150
renamedLink.deleteSync();
138151
}
139152

@@ -147,8 +160,9 @@ void testCreateLinkToFile(String dir) {
147160
Expect.isTrue(dest.existsSync());
148161

149162
Expect.isTrue(link.existsSync());
150-
Expect.isTrue(link.targetSync().contains('a_long_path_target'));
151-
Expect.isTrue(link.resolveSymbolicLinksSync().contains('a_long_path_target'));
163+
final resolvedPath = link.resolveSymbolicLinksSync();
164+
Expect.isTrue(FileSystemEntity.identicalSync(target, resolvedPath),
165+
'${link.path} should resolve to $target but resolved to $resolvedPath');
152166

153167
// Rename link
154168
var renamedLink = link.renameSync('${dir}\\a_renamed_long_path_link');

tests/standalone/io/link_async_test.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,22 @@ Future testRelativeLinks(_) {
280280
});
281281
}
282282

283-
Future testBrokenLinkTypeSync(_) async {
283+
Future testRelativeLinkToDirectoryNotRelativeToCurrentWorkingDirectory(
284+
_) async {
285+
final tempDirectory = await Directory.systemTemp.createTemp('dart_link');
286+
final dir2 = await Directory(join(tempDirectory.path, 'dir1', 'dir2'))
287+
.create(recursive: true);
288+
289+
final link =
290+
await Link(join(tempDirectory.path, 'link')).create(join('dir1', 'dir2'));
291+
292+
String resolvedDir2Path = await link.resolveSymbolicLinks();
293+
Expect.isTrue(await FileSystemEntity.identical(dir2.path, resolvedDir2Path));
294+
295+
await tempDirectory.delete(recursive: true);
296+
}
297+
298+
Future testBrokenLinkType(_) async {
284299
String base = (await Directory.systemTemp.createTemp('dart_link')).path;
285300
String link = join(base, 'link');
286301
await Link(link).create('does not exist');
@@ -292,12 +307,24 @@ Future testBrokenLinkTypeSync(_) async {
292307
await FileSystemEntity.type(link, followLinks: true));
293308
}
294309

310+
Future testTopLevelLink(_) async {
311+
if (!Platform.isWindows) return;
312+
try {
313+
await Link(r"C:\").create('the target does not matter');
314+
Expect.fail("expected FileSystemException");
315+
} on FileSystemException catch (e) {
316+
Expect.equals(5, e.osError!.errorCode); // ERROR_ACCESS_DENIED
317+
}
318+
}
319+
295320
main() {
296321
asyncStart();
297322
testCreate()
298323
.then(testCreateLoopingLink)
299324
.then(testRename)
300325
.then(testRelativeLinks)
301-
.then(testBrokenLinkTypeSync)
326+
.then(testRelativeLinkToDirectoryNotRelativeToCurrentWorkingDirectory)
327+
.then(testBrokenLinkType)
328+
.then(testTopLevelLink)
302329
.then((_) => asyncEnd());
303330
}

tests/standalone/io/link_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,20 @@ testRelativeLinksSync() {
275275
tempDirectory.deleteSync(recursive: true);
276276
}
277277

278+
void testRelativeLinkToDirectoryNotRelativeToCurrentWorkingDirectory() {
279+
final tempDirectory = Directory.systemTemp.createTempSync('dart_link');
280+
final dir2 = Directory(join(tempDirectory.path, 'dir1', 'dir2'))
281+
..createSync(recursive: true);
282+
283+
final link = Link(join(tempDirectory.path, 'link'))
284+
..createSync(join('dir1', 'dir2'));
285+
286+
String resolvedDir2Path = link.resolveSymbolicLinksSync();
287+
Expect.isTrue(FileSystemEntity.identicalSync(dir2.path, resolvedDir2Path));
288+
289+
tempDirectory.deleteSync(recursive: true);
290+
}
291+
278292
testIsDir() async {
279293
// Only run on Platforms that supports file watcher
280294
if (!Platform.isWindows && !Platform.isLinux && !Platform.isMacOS) return;
@@ -326,12 +340,24 @@ testBrokenLinkTypeSync() {
326340
FileSystemEntity.typeSync(link, followLinks: true));
327341
}
328342

343+
void testTopLevelLinkSync() {
344+
if (!Platform.isWindows) return;
345+
try {
346+
Link(r"C:\").createSync('the target does not matter');
347+
Expect.fail("expected FileSystemException");
348+
} on FileSystemException catch (e) {
349+
Expect.equals(5, e.osError!.errorCode); // ERROR_ACCESS_DENIED
350+
}
351+
}
352+
329353
main() {
330354
testCreateSync();
331355
testCreateLoopingLink();
332356
testRenameSync();
333357
testLinkErrorSync();
334358
testRelativeLinksSync();
359+
testRelativeLinkToDirectoryNotRelativeToCurrentWorkingDirectory();
335360
testIsDir();
336361
testBrokenLinkTypeSync();
362+
testTopLevelLinkSync();
337363
}

0 commit comments

Comments
 (0)