[io/doc/test] Modify the Windows symlink resolution behavior so that resolving a link that points to a non-existent file results in a type of `notFound`, which is consistent with all other platforms.

Bug:https://github.com/dart-lang/sdk/issues/53684
Change-Id: I1b594e1a85906d1f510358eec71792ea15ab801b
CoreLibraryReviewExempt: VM- and doc-only
Tested: unit tests
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/331841
Reviewed-by: Lasse Nielsen <lrn@google.com>
Commit-Queue: Brian Quinlan <bquinlan@google.com>
diff --git a/runtime/bin/file_win.cc b/runtime/bin/file_win.cc
index bbfc4fe..7ffd66a 100644
--- a/runtime/bin/file_win.cc
+++ b/runtime/bin/file_win.cc
@@ -1130,7 +1130,12 @@
           FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
           OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
       if (target_handle == INVALID_HANDLE_VALUE) {
-        result = File::kIsLink;
+        DWORD last_error = GetLastError();
+        if ((last_error == ERROR_FILE_NOT_FOUND) ||
+            (last_error == ERROR_PATH_NOT_FOUND)) {
+          return kDoesNotExist;
+        }
+        result = kIsLink;
       } else {
         BY_HANDLE_FILE_INFORMATION info;
         if (!GetFileInformationByHandle(target_handle, &info)) {
@@ -1139,8 +1144,8 @@
         }
         CloseHandle(target_handle);
         return ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
-                   ? File::kIsDirectory
-                   : File::kIsFile;
+                   ? kIsDirectory
+                   : kIsFile;
       }
     } else {
       result = kIsLink;
diff --git a/sdk/lib/io/file_system_entity.dart b/sdk/lib/io/file_system_entity.dart
index 07d176f..d9f16c7 100644
--- a/sdk/lib/io/file_system_entity.dart
+++ b/sdk/lib/io/file_system_entity.dart
@@ -308,6 +308,10 @@
   /// ```
   /// since `Uri.resolve` removes `..` segments. This will result in the Windows
   /// behavior.
+  ///
+  /// On Windows, attempting to resolve a symbolic link where the link type
+  /// does not match the type of the resolved file system object will cause the
+  /// Future to complete with a [PathAccessException] error.
   Future<String> resolveSymbolicLinks() {
     return _File._dispatchWithNamespace(
         _IOService.fileResolveSymbolicLinks, [null, _rawPath]).then((response) {
@@ -343,6 +347,11 @@
   /// ```
   /// since `Uri.resolve` removes `..` segments. This will result in the Windows
   /// behavior.
+  ///
+  /// On Windows, a symbolic link is created as either a file link or a
+  /// directory link. Attempting to resolve such a symbolic link requires the
+  /// link type to match the type of the file system object that it points to,
+  /// otherwise it throws a [PathAccessException].
   String resolveSymbolicLinksSync() {
     var result = _resolveSymbolicLinks(_Namespace._namespace, _rawPath);
     _throwIfError(result, "Cannot resolve symbolic links", path);
@@ -685,11 +694,15 @@
 
   /// Synchronously finds the type of file system object that a path points to.
   ///
-  /// Returns [FileSystemEntityType.link] only if [followLinks] is `false` and if
-  /// [path] points to a link.
-  ///
   /// Returns [FileSystemEntityType.notFound] if [path] does not point to a file
   /// system object or if any other error occurs in looking up the path.
+  ///
+  /// If [path] points to a link and [followLinks] is `true` then the result
+  /// will be for the file system object that the link points to. If that
+  /// object does not exist then the result will be
+  /// [FileSystemEntityType.notFound]. If [path] points to a link and
+  /// [followLinks] is `false` then the result will be
+  /// [FileSystemEntityType.link].
   static FileSystemEntityType typeSync(String path, {bool followLinks = true}) {
     return _getTypeSync(_toUtf8Array(path), followLinks);
   }
diff --git a/sdk/lib/io/link.dart b/sdk/lib/io/link.dart
index 7ae352c..97f001c 100644
--- a/sdk/lib/io/link.dart
+++ b/sdk/lib/io/link.dart
@@ -48,10 +48,20 @@
   /// not affected, unless they are also in [path].
   ///
   /// On the Windows platform, this call will create a true symbolic link
-  /// instead of a junction. The link represents a file or directory and
-  /// does not change its type after creation. If [target] exists then
-  /// the type of the link will match the type [target], otherwise a file
-  /// symlink is created.
+  /// instead of a junction. Windows treats links to files and links to
+  /// directories as different and non-interchangable kinds of links.
+  /// Each link is either a file-link or a directory-link, and the type is
+  /// chosen when the link is created, and the link then counts as either a
+  /// file or directory for most purposes. Different Win32 API calls are
+  /// used to manipulate each.  For example, the `DeleteFile` function is
+  /// used to delete links to files, and `RemoveDirectory` must be used to
+  /// delete links to directories.
+  ///
+  /// The created Windows symbolic link will match the type of the [target],
+  /// if [target] exists, otherwise a file-link is created. The type of the
+  /// created link will not change if [target] is later replaced by something
+  /// of a different type and the link will not be resolvable by
+  /// [resolveSymbolicLinks].
   ///
   /// In order to create a symbolic link on Windows, Dart must be run in
   /// Administrator mode or the system must have Developer Mode enabled,
@@ -76,10 +86,20 @@
   /// the path of [target] are not affected, unless they are also in [path].
   ///
   /// On the Windows platform, this call will create a true symbolic link
-  /// instead of a junction. The link represents a file or directory and
-  /// does not change its type after creation. If [target] exists then
-  /// the type of the link will match the type [target], otherwise a file
-  /// symlink is created.
+  /// instead of a junction. Windows treats links to files and links to
+  /// directories as different and non-interchangable kinds of links.
+  /// Each link is either a file-link or a directory-link, and the type is
+  /// chosen when the link is created, and the link then counts as either a
+  /// file or directory for most purposes. Different Win32 API calls are
+  /// used to manipulate each.  For example, the `DeleteFile` function is
+  /// used to delete links to files, and `RemoveDirectory` must be used to
+  /// delete links to directories.
+  ///
+  /// The created Windows symbolic link will match the type of the [target],
+  /// if [target] exists, otherwise a file-link is created. The type of the
+  /// created link will not change if [target] is later replaced by something
+  /// of a different type and the link will not be resolvable by
+  /// [resolveSymbolicLinks].
   ///
   /// In order to create a symbolic link on Windows, Dart must be run in
   /// Administrator mode or the system must have Developer Mode enabled,
@@ -91,16 +111,22 @@
   /// it will be interpreted relative to the directory containing the link.
   void createSync(String target, {bool recursive = false});
 
-  /// Synchronously updates the link.
+  /// Synchronously updates an existing link.
   ///
-  /// Calling [updateSync] on a non-existing link will throw an exception.
+  /// Deletes the existing link at [path] and uses [createSync] to create a new
+  /// link to [target]. Throws [PathNotFoundException] if the original link
+  /// does not exist or with any [FileSystemException] that [deleteSync] or
+  /// [createSync] can throw.
   void updateSync(String target);
 
-  /// Updates the link.
+  /// Updates an existing link.
   ///
-  /// Returns a `Future<Link>` that completes with the
-  /// link when it has been updated. Calling [update] on a non-existing link
-  /// will complete its returned future with an exception.
+  /// Deletes the existing link at [path] and creates a new link to [target],
+  /// using [create].
+  ///
+  /// Returns a future which completes with this `Link` if successful,
+  /// and with a [PathNotFoundException] if there is no existing link at [path],
+  /// or with any [FileSystemException] that [delete] or [create] can throw.
   Future<Link> update(String target);
 
   Future<String> resolveSymbolicLinks();
diff --git a/tests/standalone/io/link_async_test.dart b/tests/standalone/io/link_async_test.dart
index 0b33f61..e23e812 100644
--- a/tests/standalone/io/link_async_test.dart
+++ b/tests/standalone/io/link_async_test.dart
@@ -280,11 +280,24 @@
   });
 }
 
+Future testBrokenLinkTypeSync(_) async {
+  String base = (await Directory.systemTemp.createTemp('dart_link')).path;
+  String link = join(base, 'link');
+  await Link(link).create('does not exist');
+
+  Expect.equals(FileSystemEntityType.link,
+      await FileSystemEntity.type(link, followLinks: false));
+
+  Expect.equals(FileSystemEntityType.notFound,
+      await FileSystemEntity.type(link, followLinks: true));
+}
+
 main() {
   asyncStart();
   testCreate()
       .then(testCreateLoopingLink)
       .then(testRename)
       .then(testRelativeLinks)
+      .then(testBrokenLinkTypeSync)
       .then((_) => asyncEnd());
 }
diff --git a/tests/standalone/io/link_test.dart b/tests/standalone/io/link_test.dart
index 144c073..cb0700b 100644
--- a/tests/standalone/io/link_test.dart
+++ b/tests/standalone/io/link_test.dart
@@ -314,6 +314,18 @@
   sandbox.deleteSync(recursive: true);
 }
 
+testBrokenLinkTypeSync() {
+  String base = Directory.systemTemp.createTempSync('dart_link').path;
+  String link = join(base, 'link');
+  Link(link).createSync('does not exist');
+
+  Expect.equals(FileSystemEntityType.link,
+      FileSystemEntity.typeSync(link, followLinks: false));
+
+  Expect.equals(FileSystemEntityType.notFound,
+      FileSystemEntity.typeSync(link, followLinks: true));
+}
+
 main() {
   testCreateSync();
   testCreateLoopingLink();
@@ -321,4 +333,5 @@
   testLinkErrorSync();
   testRelativeLinksSync();
   testIsDir();
+  testBrokenLinkTypeSync();
 }
diff --git a/tests/standalone/io/resolve_symbolic_links_test.dart b/tests/standalone/io/resolve_symbolic_links_test.dart
index 9ff0e9e..01df6ac 100644
--- a/tests/standalone/io/resolve_symbolic_links_test.dart
+++ b/tests/standalone/io/resolve_symbolic_links_test.dart
@@ -95,6 +95,8 @@
           tempDir.delete(recursive: true);
         });
       }));
+
+  asyncTest(testLinkTargetTypeChangedAfterCreation);
 }
 
 Future makeEntities(String temp) {
@@ -154,3 +156,32 @@
         .then((identical) => Expect.isTrue(identical));
   });
 }
+
+Future testLinkTargetTypeChangedAfterCreation() async {
+  // Test the following scenario:
+  // 1. create a file
+  // 2. create a link to that file
+  // 3. replace the file with a directory
+  // 4. attempt to resolve the link
+  final tmp =
+      await Directory.systemTemp.createTemp('dart_resolve_symbolic_links');
+  final tmpPath = tmp.absolute.path;
+  final filePath = join(tmpPath, "file");
+  final linkPath = join(tmpPath, "link");
+  await File(filePath).create();
+  await Link(linkPath).create(filePath);
+
+  Expect.isTrue(FileSystemEntity.identicalSync(
+      filePath, await Directory(linkPath).resolveSymbolicLinks()));
+
+  await File(filePath).delete();
+  await Directory(filePath).create();
+
+  if (Platform.isWindows) {
+    await asyncExpectThrows<PathAccessException>(
+        Directory(linkPath).resolveSymbolicLinks());
+  } else {
+    Expect.isTrue(await FileSystemEntity.identical(
+        filePath, await Directory(linkPath).resolveSymbolicLinks()));
+  }
+}
diff --git a/tests/standalone/io/windows_file_system_async_links_test.dart b/tests/standalone/io/windows_file_system_async_links_test.dart
index 207d9a5..4ab13c2 100644
--- a/tests/standalone/io/windows_file_system_async_links_test.dart
+++ b/tests/standalone/io/windows_file_system_async_links_test.dart
@@ -57,7 +57,7 @@
         .then((_) => FutureExpect.isFalse(FileSystemEntity.isDirectory(y)))
         .then((_) => FutureExpect.isFalse(FileSystemEntity.isDirectory(x)))
         .then((_) => FutureExpect.equals(
-            FileSystemEntityType.link, FileSystemEntity.type(y)))
+            FileSystemEntityType.notFound, FileSystemEntity.type(y)))
         .then((_) => FutureExpect.equals(
             FileSystemEntityType.notFound, FileSystemEntity.type(x)))
         .then((_) => FutureExpect.equals(FileSystemEntityType.link,
@@ -86,8 +86,8 @@
             FileSystemEntityType.notFound, FileSystemEntity.type(y)))
         .then((_) => FutureExpect.equals(FileSystemEntityType.notFound,
             FileSystemEntity.type(y, followLinks: false)))
-        .then((_) =>
-            FutureExpect.equals(FileSystemEntityType.directory, FileSystemEntity.type(x)))
+        .then(
+            (_) => FutureExpect.equals(FileSystemEntityType.directory, FileSystemEntity.type(x)))
         .then((_) => FutureExpect.equals(FileSystemEntityType.directory, FileSystemEntity.type(x, followLinks: false)))
         .then((_) => FutureExpect.throws(new Link(y).target()))
         .then((_) => temp.delete(recursive: true));
diff --git a/tests/standalone/io/windows_file_system_links_test.dart b/tests/standalone/io/windows_file_system_links_test.dart
index dcbc571c..6160e37 100644
--- a/tests/standalone/io/windows_file_system_links_test.dart
+++ b/tests/standalone/io/windows_file_system_links_test.dart
@@ -36,7 +36,7 @@
     Expect.isFalse(FileSystemEntity.isLinkSync(x));
     Expect.isFalse(FileSystemEntity.isDirectorySync(y));
     Expect.isFalse(FileSystemEntity.isDirectorySync(x));
-    Expect.equals(FileSystemEntityType.link, FileSystemEntity.typeSync(y));
+    Expect.equals(FileSystemEntityType.notFound, FileSystemEntity.typeSync(y));
     Expect.equals(FileSystemEntityType.notFound, FileSystemEntity.typeSync(x));
     Expect.equals(FileSystemEntityType.link,
         FileSystemEntity.typeSync(y, followLinks: false));