Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 2595703

Browse files
[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 flutter/flutter#52267 is fixed, since only Windows builds will ever need to have code-level dependency on path_provider_windows.
1 parent e80865a commit 2595703

File tree

6 files changed

+262
-219
lines changed

6 files changed

+262
-219
lines changed

packages/path_provider/path_provider_windows/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.0.4
2+
3+
* Move the actual implementation behind a conditional import, exporting
4+
a stub for platforms that don't support FFI. Fixes web builds in
5+
projects with transitive dependencies on path_provider.
6+
17
## 0.0.3
28

39
* Add missing `pluginClass: none` for compatibilty with stable channel.

packages/path_provider/path_provider_windows/lib/path_provider_windows.dart

Lines changed: 5 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -2,221 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:async';
6-
import 'dart:io';
7-
import 'dart:ffi';
8-
9-
import 'package:ffi/ffi.dart';
10-
import 'package:meta/meta.dart';
11-
import 'package:path/path.dart' as path;
12-
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
13-
import 'package:path_provider_windows/folders.dart';
14-
import 'package:win32/win32.dart';
15-
16-
/// Wraps the Win32 VerQueryValue API call.
17-
///
18-
/// This class exists to allow injecting alternate metadata in tests without
19-
/// building multiple custom test binaries.
20-
@visibleForTesting
21-
class VersionInfoQuerier {
22-
/// Returns the value for [key] in [versionInfo]s English strings section, or
23-
/// null if there is no such entry, or if versionInfo is null.
24-
getStringValue(Pointer<Uint8> versionInfo, key) {
25-
if (versionInfo == null) {
26-
return null;
27-
}
28-
const kEnUsLanguageCode = '040904e4';
29-
final keyPath = TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key');
30-
final length = allocate<Uint32>();
31-
final valueAddress = allocate<IntPtr>();
32-
try {
33-
if (VerQueryValue(versionInfo, keyPath, valueAddress, length) == 0) {
34-
return null;
35-
}
36-
return Pointer<Utf16>.fromAddress(valueAddress.value)
37-
.unpackString(length.value);
38-
} finally {
39-
free(keyPath);
40-
free(length);
41-
free(valueAddress);
42-
}
43-
}
44-
}
45-
46-
/// The Windows implementation of [PathProviderPlatform]
47-
///
48-
/// This class implements the `package:path_provider` functionality for Windows.
49-
class PathProviderWindows extends PathProviderPlatform {
50-
/// The object to use for performing VerQueryValue calls.
51-
@visibleForTesting
52-
VersionInfoQuerier versionInfoQuerier = VersionInfoQuerier();
53-
54-
/// This is typically the same as the TMP environment variable.
55-
@override
56-
Future<String> getTemporaryPath() async {
57-
final buffer = allocate<Uint16>(count: MAX_PATH + 1).cast<Utf16>();
58-
String path;
59-
60-
try {
61-
final length = GetTempPath(MAX_PATH, buffer);
62-
63-
if (length == 0) {
64-
final error = GetLastError();
65-
throw WindowsException(error);
66-
} else {
67-
path = buffer.unpackString(length);
68-
69-
// GetTempPath adds a trailing backslash, but SHGetKnownFolderPath does
70-
// not. Strip off trailing backslash for consistency with other methods
71-
// here.
72-
if (path.endsWith('\\')) {
73-
path = path.substring(0, path.length - 1);
74-
}
75-
}
76-
77-
// Ensure that the directory exists, since GetTempPath doesn't.
78-
final directory = Directory(path);
79-
if (!directory.existsSync()) {
80-
await directory.create(recursive: true);
81-
}
82-
83-
return Future.value(path);
84-
} finally {
85-
free(buffer);
86-
}
87-
}
88-
89-
@override
90-
Future<String> getApplicationSupportPath() async {
91-
final appDataRoot = await getPath(WindowsKnownFolder.RoamingAppData);
92-
final directory = Directory(
93-
path.join(appDataRoot, _getApplicationSpecificSubdirectory()));
94-
// Ensure that the directory exists if possible, since it will on other
95-
// platforms. If the name is longer than MAXPATH, creating will fail, so
96-
// skip that step; it's up to the client to decide what to do with the path
97-
// in that case (e.g., using a short path).
98-
if (directory.path.length <= MAX_PATH) {
99-
if (!directory.existsSync()) {
100-
await directory.create(recursive: true);
101-
}
102-
}
103-
return directory.path;
104-
}
105-
106-
@override
107-
Future<String> getApplicationDocumentsPath() =>
108-
getPath(WindowsKnownFolder.Documents);
109-
110-
@override
111-
Future<String> getDownloadsPath() => getPath(WindowsKnownFolder.Downloads);
112-
113-
/// Retrieve any known folder from Windows.
114-
///
115-
/// folderID is a GUID that represents a specific known folder ID, drawn from
116-
/// [WindowsKnownFolder].
117-
Future<String> getPath(String folderID) {
118-
final pathPtrPtr = allocate<IntPtr>();
119-
Pointer<Utf16> pathPtr;
120-
121-
try {
122-
GUID knownFolderID = GUID.fromString(folderID);
123-
124-
final hr = SHGetKnownFolderPath(
125-
knownFolderID.addressOf, KF_FLAG_DEFAULT, NULL, pathPtrPtr);
126-
127-
if (FAILED(hr)) {
128-
if (hr == E_INVALIDARG || hr == E_FAIL) {
129-
throw WindowsException(hr);
130-
}
131-
}
132-
133-
pathPtr = Pointer<Utf16>.fromAddress(pathPtrPtr.value);
134-
final path = pathPtr.unpackString(MAX_PATH);
135-
return Future.value(path);
136-
} finally {
137-
CoTaskMemFree(pathPtr.cast());
138-
free(pathPtrPtr);
139-
}
140-
}
141-
142-
/// Returns the relative path string to append to the root directory returned
143-
/// by Win32 APIs for application storage (such as RoamingAppDir) to get a
144-
/// directory that is unique to the application.
145-
///
146-
/// The convention is to use company-name\product-name\. This will use that if
147-
/// possible, using the data in the VERSIONINFO resource, with the following
148-
/// fallbacks:
149-
/// - If the company name isn't there, that component will be dropped.
150-
/// - If the product name isn't there, it will use the exe's filename (without
151-
/// extension).
152-
String _getApplicationSpecificSubdirectory() {
153-
String companyName;
154-
String productName;
155-
156-
final Pointer<Utf16> moduleNameBuffer =
157-
allocate<Uint16>(count: MAX_PATH + 1).cast<Utf16>();
158-
final Pointer<Uint32> unused = allocate<Uint32>();
159-
Pointer<Uint8> infoBuffer;
160-
try {
161-
// Get the module name.
162-
final moduleNameLength = GetModuleFileName(0, moduleNameBuffer, MAX_PATH);
163-
if (moduleNameLength == 0) {
164-
final error = GetLastError();
165-
throw WindowsException(error);
166-
}
167-
168-
// From that, load the VERSIONINFO resource
169-
int infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused);
170-
if (infoSize != 0) {
171-
infoBuffer = allocate<Uint8>(count: infoSize);
172-
if (GetFileVersionInfo(moduleNameBuffer, 0, infoSize, infoBuffer) ==
173-
0) {
174-
free(infoBuffer);
175-
infoBuffer = null;
176-
}
177-
}
178-
companyName = _sanitizedDirectoryName(
179-
versionInfoQuerier.getStringValue(infoBuffer, 'CompanyName'));
180-
productName = _sanitizedDirectoryName(
181-
versionInfoQuerier.getStringValue(infoBuffer, 'ProductName'));
182-
183-
// If there was no product name, use the executable name.
184-
if (productName == null) {
185-
productName = path.basenameWithoutExtension(
186-
moduleNameBuffer.unpackString(moduleNameLength));
187-
}
188-
189-
return companyName != null
190-
? path.join(companyName, productName)
191-
: productName;
192-
} finally {
193-
free(moduleNameBuffer);
194-
free(unused);
195-
if (infoBuffer != null) {
196-
free(infoBuffer);
197-
}
198-
}
199-
}
200-
201-
/// Makes [rawString] safe as a directory component. See
202-
/// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
203-
///
204-
/// If after sanitizing the string is empty, returns null.
205-
String _sanitizedDirectoryName(String rawString) {
206-
if (rawString == null) {
207-
return null;
208-
}
209-
String sanitized = rawString
210-
// Replace banned characters.
211-
.replaceAll(RegExp(r'[<>:"/\\|?*]'), '_')
212-
// Remove trailing whitespace.
213-
.trimRight()
214-
// Ensure that it does not end with a '.'.
215-
.replaceAll(RegExp(r'[.]+$'), '');
216-
const kMaxComponentLength = 255;
217-
if (sanitized.length > kMaxComponentLength) {
218-
sanitized = sanitized.substring(0, kMaxComponentLength);
219-
}
220-
return sanitized.isEmpty ? null : sanitized;
221-
}
222-
}
5+
// path_provider_windows is implemented using FFI; export a stub for platforms
6+
// that don't support FFI (e.g., web) to avoid having transitive dependencies
7+
// break web compilation.
8+
export 'src/path_provider_windows_stub.dart'
9+
if (dart.library.ffi) 'src/path_provider_windows_real.dart';

packages/path_provider/path_provider_windows/lib/folders.dart renamed to packages/path_provider/path_provider_windows/lib/src/folders.dart

File renamed without changes.

0 commit comments

Comments
 (0)