[path_provider] Move Windows FFI behind a conditional import (#3056)

Moves the real implementation of path_provider_windows behind a
conditional export, instead exporting a stub on platforms that don't
support dart:ffi. This avoids build breakage in web projects that have
transitive dependencies on path_provider (and thus path_provider_windows
due to manual endorsement).

This will no longer be necessary once
https://github.com/flutter/flutter/issues/52267 is fixed, since only
Windows builds will ever need to have code-level dependency on
path_provider_windows.
diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md
index 0b73286..a7cfe1d 100644
--- a/packages/path_provider/path_provider_windows/CHANGELOG.md
+++ b/packages/path_provider/path_provider_windows/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.0.4
+
+* Move the actual implementation behind a conditional import, exporting
+  a stub for platforms that don't support FFI. Fixes web builds in
+  projects with transitive dependencies on path_provider.
+
 ## 0.0.3
 
 * Add missing `pluginClass: none` for compatibilty with stable channel.
diff --git a/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart b/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart
index e29b70a..ed96698 100644
--- a/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart
+++ b/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart
@@ -2,221 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
-import 'dart:io';
-import 'dart:ffi';
-
-import 'package:ffi/ffi.dart';
-import 'package:meta/meta.dart';
-import 'package:path/path.dart' as path;
-import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
-import 'package:path_provider_windows/folders.dart';
-import 'package:win32/win32.dart';
-
-/// Wraps the Win32 VerQueryValue API call.
-///
-/// This class exists to allow injecting alternate metadata in tests without
-/// building multiple custom test binaries.
-@visibleForTesting
-class VersionInfoQuerier {
-  /// Returns the value for [key] in [versionInfo]s English strings section, or
-  /// null if there is no such entry, or if versionInfo is null.
-  getStringValue(Pointer<Uint8> versionInfo, key) {
-    if (versionInfo == null) {
-      return null;
-    }
-    const kEnUsLanguageCode = '040904e4';
-    final keyPath = TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key');
-    final length = allocate<Uint32>();
-    final valueAddress = allocate<IntPtr>();
-    try {
-      if (VerQueryValue(versionInfo, keyPath, valueAddress, length) == 0) {
-        return null;
-      }
-      return Pointer<Utf16>.fromAddress(valueAddress.value)
-          .unpackString(length.value);
-    } finally {
-      free(keyPath);
-      free(length);
-      free(valueAddress);
-    }
-  }
-}
-
-/// The Windows implementation of [PathProviderPlatform]
-///
-/// This class implements the `package:path_provider` functionality for Windows.
-class PathProviderWindows extends PathProviderPlatform {
-  /// The object to use for performing VerQueryValue calls.
-  @visibleForTesting
-  VersionInfoQuerier versionInfoQuerier = VersionInfoQuerier();
-
-  /// This is typically the same as the TMP environment variable.
-  @override
-  Future<String> getTemporaryPath() async {
-    final buffer = allocate<Uint16>(count: MAX_PATH + 1).cast<Utf16>();
-    String path;
-
-    try {
-      final length = GetTempPath(MAX_PATH, buffer);
-
-      if (length == 0) {
-        final error = GetLastError();
-        throw WindowsException(error);
-      } else {
-        path = buffer.unpackString(length);
-
-        // GetTempPath adds a trailing backslash, but SHGetKnownFolderPath does
-        // not. Strip off trailing backslash for consistency with other methods
-        // here.
-        if (path.endsWith('\\')) {
-          path = path.substring(0, path.length - 1);
-        }
-      }
-
-      // Ensure that the directory exists, since GetTempPath doesn't.
-      final directory = Directory(path);
-      if (!directory.existsSync()) {
-        await directory.create(recursive: true);
-      }
-
-      return Future.value(path);
-    } finally {
-      free(buffer);
-    }
-  }
-
-  @override
-  Future<String> getApplicationSupportPath() async {
-    final appDataRoot = await getPath(WindowsKnownFolder.RoamingAppData);
-    final directory = Directory(
-        path.join(appDataRoot, _getApplicationSpecificSubdirectory()));
-    // Ensure that the directory exists if possible, since it will on other
-    // platforms. If the name is longer than MAXPATH, creating will fail, so
-    // skip that step; it's up to the client to decide what to do with the path
-    // in that case (e.g., using a short path).
-    if (directory.path.length <= MAX_PATH) {
-      if (!directory.existsSync()) {
-        await directory.create(recursive: true);
-      }
-    }
-    return directory.path;
-  }
-
-  @override
-  Future<String> getApplicationDocumentsPath() =>
-      getPath(WindowsKnownFolder.Documents);
-
-  @override
-  Future<String> getDownloadsPath() => getPath(WindowsKnownFolder.Downloads);
-
-  /// Retrieve any known folder from Windows.
-  ///
-  /// folderID is a GUID that represents a specific known folder ID, drawn from
-  /// [WindowsKnownFolder].
-  Future<String> getPath(String folderID) {
-    final pathPtrPtr = allocate<IntPtr>();
-    Pointer<Utf16> pathPtr;
-
-    try {
-      GUID knownFolderID = GUID.fromString(folderID);
-
-      final hr = SHGetKnownFolderPath(
-          knownFolderID.addressOf, KF_FLAG_DEFAULT, NULL, pathPtrPtr);
-
-      if (FAILED(hr)) {
-        if (hr == E_INVALIDARG || hr == E_FAIL) {
-          throw WindowsException(hr);
-        }
-      }
-
-      pathPtr = Pointer<Utf16>.fromAddress(pathPtrPtr.value);
-      final path = pathPtr.unpackString(MAX_PATH);
-      return Future.value(path);
-    } finally {
-      CoTaskMemFree(pathPtr.cast());
-      free(pathPtrPtr);
-    }
-  }
-
-  /// Returns the relative path string to append to the root directory returned
-  /// by Win32 APIs for application storage (such as RoamingAppDir) to get a
-  /// directory that is unique to the application.
-  ///
-  /// The convention is to use company-name\product-name\. This will use that if
-  /// possible, using the data in the VERSIONINFO resource, with the following
-  /// fallbacks:
-  /// - If the company name isn't there, that component will be dropped.
-  /// - If the product name isn't there, it will use the exe's filename (without
-  ///   extension).
-  String _getApplicationSpecificSubdirectory() {
-    String companyName;
-    String productName;
-
-    final Pointer<Utf16> moduleNameBuffer =
-        allocate<Uint16>(count: MAX_PATH + 1).cast<Utf16>();
-    final Pointer<Uint32> unused = allocate<Uint32>();
-    Pointer<Uint8> infoBuffer;
-    try {
-      // Get the module name.
-      final moduleNameLength = GetModuleFileName(0, moduleNameBuffer, MAX_PATH);
-      if (moduleNameLength == 0) {
-        final error = GetLastError();
-        throw WindowsException(error);
-      }
-
-      // From that, load the VERSIONINFO resource
-      int infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused);
-      if (infoSize != 0) {
-        infoBuffer = allocate<Uint8>(count: infoSize);
-        if (GetFileVersionInfo(moduleNameBuffer, 0, infoSize, infoBuffer) ==
-            0) {
-          free(infoBuffer);
-          infoBuffer = null;
-        }
-      }
-      companyName = _sanitizedDirectoryName(
-          versionInfoQuerier.getStringValue(infoBuffer, 'CompanyName'));
-      productName = _sanitizedDirectoryName(
-          versionInfoQuerier.getStringValue(infoBuffer, 'ProductName'));
-
-      // If there was no product name, use the executable name.
-      if (productName == null) {
-        productName = path.basenameWithoutExtension(
-            moduleNameBuffer.unpackString(moduleNameLength));
-      }
-
-      return companyName != null
-          ? path.join(companyName, productName)
-          : productName;
-    } finally {
-      free(moduleNameBuffer);
-      free(unused);
-      if (infoBuffer != null) {
-        free(infoBuffer);
-      }
-    }
-  }
-
-  /// Makes [rawString] safe as a directory component. See
-  /// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
-  ///
-  /// If after sanitizing the string is empty, returns null.
-  String _sanitizedDirectoryName(String rawString) {
-    if (rawString == null) {
-      return null;
-    }
-    String sanitized = rawString
-        // Replace banned characters.
-        .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_')
-        // Remove trailing whitespace.
-        .trimRight()
-        // Ensure that it does not end with a '.'.
-        .replaceAll(RegExp(r'[.]+$'), '');
-    const kMaxComponentLength = 255;
-    if (sanitized.length > kMaxComponentLength) {
-      sanitized = sanitized.substring(0, kMaxComponentLength);
-    }
-    return sanitized.isEmpty ? null : sanitized;
-  }
-}
+// path_provider_windows is implemented using FFI; export a stub for platforms
+// that don't support FFI (e.g., web) to avoid having transitive dependencies
+// break web compilation.
+export 'src/path_provider_windows_stub.dart'
+    if (dart.library.ffi) 'src/path_provider_windows_real.dart';
diff --git a/packages/path_provider/path_provider_windows/lib/folders.dart b/packages/path_provider/path_provider_windows/lib/src/folders.dart
similarity index 100%
rename from packages/path_provider/path_provider_windows/lib/folders.dart
rename to packages/path_provider/path_provider_windows/lib/src/folders.dart
diff --git a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart
new file mode 100644
index 0000000..7ff448a
--- /dev/null
+++ b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart
@@ -0,0 +1,223 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+import 'dart:ffi';
+
+import 'package:ffi/ffi.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
+import 'package:win32/win32.dart';
+
+import 'folders.dart';
+
+/// Wraps the Win32 VerQueryValue API call.
+///
+/// This class exists to allow injecting alternate metadata in tests without
+/// building multiple custom test binaries.
+@visibleForTesting
+class VersionInfoQuerier {
+  /// Returns the value for [key] in [versionInfo]s English strings section, or
+  /// null if there is no such entry, or if versionInfo is null.
+  getStringValue(Pointer<Uint8> versionInfo, key) {
+    if (versionInfo == null) {
+      return null;
+    }
+    const kEnUsLanguageCode = '040904e4';
+    final keyPath = TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key');
+    final length = allocate<Uint32>();
+    final valueAddress = allocate<IntPtr>();
+    try {
+      if (VerQueryValue(versionInfo, keyPath, valueAddress, length) == 0) {
+        return null;
+      }
+      return Pointer<Utf16>.fromAddress(valueAddress.value)
+          .unpackString(length.value);
+    } finally {
+      free(keyPath);
+      free(length);
+      free(valueAddress);
+    }
+  }
+}
+
+/// The Windows implementation of [PathProviderPlatform]
+///
+/// This class implements the `package:path_provider` functionality for Windows.
+class PathProviderWindows extends PathProviderPlatform {
+  /// The object to use for performing VerQueryValue calls.
+  @visibleForTesting
+  VersionInfoQuerier versionInfoQuerier = VersionInfoQuerier();
+
+  /// This is typically the same as the TMP environment variable.
+  @override
+  Future<String> getTemporaryPath() async {
+    final buffer = allocate<Uint16>(count: MAX_PATH + 1).cast<Utf16>();
+    String path;
+
+    try {
+      final length = GetTempPath(MAX_PATH, buffer);
+
+      if (length == 0) {
+        final error = GetLastError();
+        throw WindowsException(error);
+      } else {
+        path = buffer.unpackString(length);
+
+        // GetTempPath adds a trailing backslash, but SHGetKnownFolderPath does
+        // not. Strip off trailing backslash for consistency with other methods
+        // here.
+        if (path.endsWith('\\')) {
+          path = path.substring(0, path.length - 1);
+        }
+      }
+
+      // Ensure that the directory exists, since GetTempPath doesn't.
+      final directory = Directory(path);
+      if (!directory.existsSync()) {
+        await directory.create(recursive: true);
+      }
+
+      return Future.value(path);
+    } finally {
+      free(buffer);
+    }
+  }
+
+  @override
+  Future<String> getApplicationSupportPath() async {
+    final appDataRoot = await getPath(WindowsKnownFolder.RoamingAppData);
+    final directory = Directory(
+        path.join(appDataRoot, _getApplicationSpecificSubdirectory()));
+    // Ensure that the directory exists if possible, since it will on other
+    // platforms. If the name is longer than MAXPATH, creating will fail, so
+    // skip that step; it's up to the client to decide what to do with the path
+    // in that case (e.g., using a short path).
+    if (directory.path.length <= MAX_PATH) {
+      if (!directory.existsSync()) {
+        await directory.create(recursive: true);
+      }
+    }
+    return directory.path;
+  }
+
+  @override
+  Future<String> getApplicationDocumentsPath() =>
+      getPath(WindowsKnownFolder.Documents);
+
+  @override
+  Future<String> getDownloadsPath() => getPath(WindowsKnownFolder.Downloads);
+
+  /// Retrieve any known folder from Windows.
+  ///
+  /// folderID is a GUID that represents a specific known folder ID, drawn from
+  /// [WindowsKnownFolder].
+  Future<String> getPath(String folderID) {
+    final pathPtrPtr = allocate<IntPtr>();
+    Pointer<Utf16> pathPtr;
+
+    try {
+      GUID knownFolderID = GUID.fromString(folderID);
+
+      final hr = SHGetKnownFolderPath(
+          knownFolderID.addressOf, KF_FLAG_DEFAULT, NULL, pathPtrPtr);
+
+      if (FAILED(hr)) {
+        if (hr == E_INVALIDARG || hr == E_FAIL) {
+          throw WindowsException(hr);
+        }
+      }
+
+      pathPtr = Pointer<Utf16>.fromAddress(pathPtrPtr.value);
+      final path = pathPtr.unpackString(MAX_PATH);
+      return Future.value(path);
+    } finally {
+      CoTaskMemFree(pathPtr.cast());
+      free(pathPtrPtr);
+    }
+  }
+
+  /// Returns the relative path string to append to the root directory returned
+  /// by Win32 APIs for application storage (such as RoamingAppDir) to get a
+  /// directory that is unique to the application.
+  ///
+  /// The convention is to use company-name\product-name\. This will use that if
+  /// possible, using the data in the VERSIONINFO resource, with the following
+  /// fallbacks:
+  /// - If the company name isn't there, that component will be dropped.
+  /// - If the product name isn't there, it will use the exe's filename (without
+  ///   extension).
+  String _getApplicationSpecificSubdirectory() {
+    String companyName;
+    String productName;
+
+    final Pointer<Utf16> moduleNameBuffer =
+        allocate<Uint16>(count: MAX_PATH + 1).cast<Utf16>();
+    final Pointer<Uint32> unused = allocate<Uint32>();
+    Pointer<Uint8> infoBuffer;
+    try {
+      // Get the module name.
+      final moduleNameLength = GetModuleFileName(0, moduleNameBuffer, MAX_PATH);
+      if (moduleNameLength == 0) {
+        final error = GetLastError();
+        throw WindowsException(error);
+      }
+
+      // From that, load the VERSIONINFO resource
+      int infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused);
+      if (infoSize != 0) {
+        infoBuffer = allocate<Uint8>(count: infoSize);
+        if (GetFileVersionInfo(moduleNameBuffer, 0, infoSize, infoBuffer) ==
+            0) {
+          free(infoBuffer);
+          infoBuffer = null;
+        }
+      }
+      companyName = _sanitizedDirectoryName(
+          versionInfoQuerier.getStringValue(infoBuffer, 'CompanyName'));
+      productName = _sanitizedDirectoryName(
+          versionInfoQuerier.getStringValue(infoBuffer, 'ProductName'));
+
+      // If there was no product name, use the executable name.
+      if (productName == null) {
+        productName = path.basenameWithoutExtension(
+            moduleNameBuffer.unpackString(moduleNameLength));
+      }
+
+      return companyName != null
+          ? path.join(companyName, productName)
+          : productName;
+    } finally {
+      free(moduleNameBuffer);
+      free(unused);
+      if (infoBuffer != null) {
+        free(infoBuffer);
+      }
+    }
+  }
+
+  /// Makes [rawString] safe as a directory component. See
+  /// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
+  ///
+  /// If after sanitizing the string is empty, returns null.
+  String _sanitizedDirectoryName(String rawString) {
+    if (rawString == null) {
+      return null;
+    }
+    String sanitized = rawString
+        // Replace banned characters.
+        .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_')
+        // Remove trailing whitespace.
+        .trimRight()
+        // Ensure that it does not end with a '.'.
+        .replaceAll(RegExp(r'[.]+$'), '');
+    const kMaxComponentLength = 255;
+    if (sanitized.length > kMaxComponentLength) {
+      sanitized = sanitized.substring(0, kMaxComponentLength);
+    }
+    return sanitized.isEmpty ? null : sanitized;
+  }
+}
diff --git a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart
new file mode 100644
index 0000000..fedb537
--- /dev/null
+++ b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart
@@ -0,0 +1,27 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
+
+/// A stub implementation to satisfy compilation of multi-platform packages that
+/// depend on path_provider_windows. This should never actually be created.
+///
+/// Notably, because path_provider needs to manually register
+/// path_provider_windows, anything with a transitive dependency on
+/// path_provider will also depend on path_provider_windows, not just at the
+/// pubspec level but the code level.
+class PathProviderWindows extends PathProviderPlatform {
+  /// Errors on attempted instantiation of the stub. It exists only to satisfy
+  /// compile-time dependencies, and should never actually be created.
+  PathProviderWindows() {
+    assert(false);
+  }
+
+  /// Stub; see comment on VersionInfoQuerier.
+  VersionInfoQuerier versionInfoQuerier;
+}
+
+/// Stub to satisfy the analyzer, which doesn't seem to handle conditional
+/// exports correctly.
+class VersionInfoQuerier {}
diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml
index 998f4e2..38f6316 100644
--- a/packages/path_provider/path_provider_windows/pubspec.yaml
+++ b/packages/path_provider/path_provider_windows/pubspec.yaml
@@ -1,7 +1,7 @@
 name: path_provider_windows
 description: Windows implementation of the path_provider plugin
 homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows
-version: 0.0.3
+version: 0.0.4
 
 flutter:
   plugin: