[flutter_tools] tool exit access denied during symlinking (#106213)

diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart
index 2421143..8950364 100644
--- a/packages/flutter_tools/lib/src/flutter_plugins.dart
+++ b/packages/flutter_tools/lib/src/flutter_plugins.dart
@@ -1004,20 +1004,32 @@
 void handleSymlinkException(FileSystemException e, {
   required Platform platform,
   required OperatingSystemUtils os,
+  required String destination,
+  required String source,
 }) {
-  if (platform.isWindows && (e.osError?.errorCode ?? 0) == 1314) {
-    final String? versionString = RegExp(r'[\d.]+').firstMatch(os.name)?.group(0);
-    final Version? version = Version.parse(versionString);
-    // Windows 10 14972 is the oldest version that allows creating symlinks
-    // just by enabling developer mode; before that it requires running the
-    // terminal as Administrator.
-    // https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10/
-    final String instructions = (version != null && version >= Version(10, 0, 14972))
-        ? 'Please enable Developer Mode in your system settings. Run\n'
-          '  start ms-settings:developers\n'
-          'to open settings.'
-        : 'You must build from a terminal run as administrator.';
-    throwToolExit('Building with plugins requires symlink support.\n\n$instructions');
+  if (platform.isWindows) {
+    // ERROR_ACCESS_DENIED
+    if (e.osError?.errorCode == 5) {
+      throwToolExit(
+        'ERROR_ACCESS_DENIED file system exception thrown while trying to '
+        'create a symlink from $source to $destination',
+      );
+    }
+    // ERROR_PRIVILEGE_NOT_HELD, user cannot symlink
+    if (e.osError?.errorCode == 1314) {
+      final String? versionString = RegExp(r'[\d.]+').firstMatch(os.name)?.group(0);
+      final Version? version = Version.parse(versionString);
+      // Windows 10 14972 is the oldest version that allows creating symlinks
+      // just by enabling developer mode; before that it requires running the
+      // terminal as Administrator.
+      // https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10/
+      final String instructions = (version != null && version >= Version(10, 0, 14972))
+          ? 'Please enable Developer Mode in your system settings. Run\n'
+            '  start ms-settings:developers\n'
+            'to open settings.'
+          : 'You must build from a terminal run as administrator.';
+      throwToolExit('Building with plugins requires symlink support.\n\n$instructions');
+    }
   }
 }
 
@@ -1043,7 +1055,13 @@
     try {
       link.createSync(path);
     } on FileSystemException catch (e) {
-      handleSymlinkException(e, platform: globals.platform, os: globals.os);
+      handleSymlinkException(
+        e,
+        platform: globals.platform,
+        os: globals.os,
+        destination: 'dest',
+        source: 'source',
+      );
       rethrow;
     }
   }
diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart
index 71c5fb2..7de3ab4 100644
--- a/packages/flutter_tools/test/general.shard/plugins_test.dart
+++ b/packages/flutter_tools/test/general.shard/plugins_test.dart
@@ -84,6 +84,8 @@
     // using it instead of fs must re-run any necessary setup (e.g.,
     // setUpProject).
     late FileSystem fsWindows;
+    const String pubCachePath = '/path/to/.pub-cache/hosted/pub.dartlang.org/foo-1.2.3';
+    const String ephemeralPackagePath = '/path/to/app/linux/flutter/ephemeral/foo-1.2.3';
 
     // Adds basic properties to the flutterProject and its subprojects.
     void setUpProject(FileSystem fileSystem) {
@@ -1612,8 +1614,36 @@
 
       const FileSystemException e = FileSystemException('', '', OSError('', 1314));
 
-      expect(() => handleSymlinkException(e, platform: platform, os: os),
-        throwsToolExit(message: 'start ms-settings:developers'));
+      expect(
+        () => handleSymlinkException(
+          e,
+          platform: platform,
+          os: os,
+          source: pubCachePath,
+          destination: ephemeralPackagePath,
+        ),
+        throwsToolExit(message: 'start ms-settings:developers'),
+      );
+    });
+
+    testWithoutContext('Symlink ERROR_ACCESS_DENIED failures show developers paths that were used', () async {
+      final Platform platform = FakePlatform(operatingSystem: 'windows');
+      final FakeOperatingSystemUtils os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972.1]');
+
+      const FileSystemException e = FileSystemException('', '', OSError('', 5));
+
+      expect(
+        () => handleSymlinkException(
+          e,
+          platform: platform,
+          os: os,
+          source: pubCachePath,
+          destination: ephemeralPackagePath,
+        ),
+        throwsToolExit(
+          message: 'ERROR_ACCESS_DENIED file system exception thrown while trying to create a symlink from $pubCachePath to $ephemeralPackagePath',
+        ),
+      );
     });
 
     testWithoutContext('Symlink failures instruct developers to run as administrator on older versions of Windows', () async {
@@ -1622,8 +1652,16 @@
 
       const FileSystemException e = FileSystemException('', '', OSError('', 1314));
 
-      expect(() => handleSymlinkException(e, platform: platform, os: os),
-        throwsToolExit(message: 'administrator'));
+      expect(
+        () => handleSymlinkException(
+          e,
+          platform: platform,
+          os: os,
+          source: pubCachePath,
+          destination: ephemeralPackagePath,
+        ),
+        throwsToolExit(message: 'administrator'),
+      );
     });
 
     testWithoutContext('Symlink failures only give instructions for specific errors', () async {
@@ -1632,7 +1670,16 @@
 
       const FileSystemException e = FileSystemException('', '', OSError('', 999));
 
-      expect(() => handleSymlinkException(e, platform: platform, os: os), returnsNormally);
+      expect(
+        () => handleSymlinkException(
+          e,
+          platform: platform,
+          os: os,
+          source: pubCachePath,
+          destination: ephemeralPackagePath,
+        ),
+        returnsNormally,
+      );
     });
   });
 }