Skip to content

Commit 47a92db

Browse files
[path_provider] Remove win32 (flutter#7073)
Per https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#dependencies, we generally do not want third-party dependencies. `path_provider` in particular is a key part of the ecosystem (well over 1,000 packages depend directly on it, and transitively it's probably several times that at least), and thus the maintenance and security considerations are particularly acute. This eliminates the dependency on `win32`, a large third-party dependency, in favor of direct FFI code written from scratch using the official Win32 reference documentation from Microsoft as the source. The only behavioral change that should result here is that the exceptions thrown in failure cases have changed, but they were never documented, and were entirely platform-specific, so it's relatively unlikely that people will be broken by that. (As noted in a TODO, the longer term solution is to provide real exceptions for this package, and use those across platforms.) Fixes flutter#130940
1 parent 9627de9 commit 47a92db

File tree

8 files changed

+314
-67
lines changed

8 files changed

+314
-67
lines changed

packages/path_provider/path_provider_windows/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 2.3.0
22

3+
* Replaces `win32` dependency with direct FFI usage.
34
* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2.
45

56
## 2.2.1

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

Lines changed: 61 additions & 54 deletions
Large diffs are not rendered by default.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:ffi';
6+
import 'dart:typed_data';
7+
8+
/// Representation of the Win32 GUID struct.
9+
// For the layout of this struct, see
10+
// https://learn.microsoft.com/windows/win32/api/guiddef/ns-guiddef-guid
11+
@Packed(4)
12+
base class GUID extends Struct {
13+
/// Native Data1 field.
14+
@Uint32()
15+
external int data1;
16+
17+
/// Native Data2 field.
18+
@Uint16()
19+
external int data2;
20+
21+
/// Native Data3 field.
22+
@Uint16()
23+
external int data3;
24+
25+
/// Native Data4 field.
26+
// This should be an eight-element byte array, but there's no such annotation.
27+
@Uint64()
28+
external int data4;
29+
30+
/// Parses a GUID string, with optional enclosing "{}"s and optional "-"s,
31+
/// into data.
32+
void parse(String guid) {
33+
final String hexOnly = guid.replaceAll(RegExp(r'[{}-]'), '');
34+
if (hexOnly.length != 32) {
35+
throw ArgumentError.value(guid, 'guid', 'Invalid GUID string');
36+
}
37+
final ByteData bytes = ByteData(16);
38+
for (int i = 0; i < 16; ++i) {
39+
bytes.setUint8(
40+
i, int.parse(hexOnly.substring(i * 2, i * 2 + 2), radix: 16));
41+
}
42+
data1 = bytes.getInt32(0);
43+
data2 = bytes.getInt16(4);
44+
data3 = bytes.getInt16(6);
45+
// [bytes] is big endian, but the host is little endian, so a default
46+
// big-endian read would reverse the bytes. Since data4 is supposed to be
47+
// a byte array, the order should be preserved, so do a little-endian read.
48+
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding
49+
data4 = bytes.getInt64(8, Endian.little);
50+
}
51+
}

packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import 'dart:io';
77

88
import 'package:ffi/ffi.dart';
99
import 'package:flutter/foundation.dart' show visibleForTesting;
10+
import 'package:flutter/services.dart';
1011
import 'package:path/path.dart' as path;
1112
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
12-
import 'package:win32/win32.dart';
1313

1414
import 'folders.dart';
15+
import 'guid.dart';
16+
import 'win32_wrappers.dart';
1517

1618
/// Constant for en-US language used in VersionInfo keys.
1719
@visibleForTesting
@@ -35,7 +37,7 @@ class VersionInfoQuerier {
3537
/// language and encoding, or null if there is no such entry,
3638
/// or if versionInfo is null.
3739
///
38-
/// See https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
40+
/// See https://docs.microsoft.com/windows/win32/menurc/versioninfo-resource
3941
/// for list of possible language and encoding values.
4042
String? getStringValue(
4143
Pointer<Uint8>? versionInfo,
@@ -49,7 +51,7 @@ class VersionInfoQuerier {
4951
return null;
5052
}
5153
final Pointer<Utf16> keyPath =
52-
TEXT('\\StringFileInfo\\$language$encoding\\$key');
54+
'\\StringFileInfo\\$language$encoding\\$key'.toNativeUtf16();
5355
final Pointer<UINT> length = calloc<UINT>();
5456
final Pointer<Pointer<Utf16>> valueAddress = calloc<Pointer<Utf16>>();
5557
try {
@@ -89,7 +91,7 @@ class PathProviderWindows extends PathProviderPlatform {
8991

9092
if (length == 0) {
9193
final int error = GetLastError();
92-
throw WindowsException(error);
94+
throw _createWin32Exception(error);
9395
} else {
9496
path = buffer.toDartString();
9597

@@ -134,7 +136,7 @@ class PathProviderWindows extends PathProviderPlatform {
134136
/// [WindowsKnownFolder].
135137
Future<String?> getPath(String folderID) {
136138
final Pointer<Pointer<Utf16>> pathPtrPtr = calloc<Pointer<Utf16>>();
137-
final Pointer<GUID> knownFolderID = calloc<GUID>()..ref.setGUID(folderID);
139+
final Pointer<GUID> knownFolderID = calloc<GUID>()..ref.parse(folderID);
138140

139141
try {
140142
final int hr = SHGetKnownFolderPath(
@@ -146,7 +148,7 @@ class PathProviderWindows extends PathProviderPlatform {
146148

147149
if (FAILED(hr)) {
148150
if (hr == E_INVALIDARG || hr == E_FAIL) {
149-
throw WindowsException(hr);
151+
throw _createWin32Exception(hr);
150152
}
151153
return Future<String?>.value();
152154
}
@@ -179,7 +181,8 @@ class PathProviderWindows extends PathProviderPlatform {
179181
String? companyName;
180182
String? productName;
181183

182-
final Pointer<Utf16> moduleNameBuffer = wsalloc(MAX_PATH + 1);
184+
final Pointer<Utf16> moduleNameBuffer =
185+
calloc<WCHAR>(MAX_PATH + 1).cast<Utf16>();
183186
final Pointer<DWORD> unused = calloc<DWORD>();
184187
Pointer<BYTE>? infoBuffer;
185188
try {
@@ -188,7 +191,7 @@ class PathProviderWindows extends PathProviderPlatform {
188191
GetModuleFileName(0, moduleNameBuffer, MAX_PATH);
189192
if (moduleNameLength == 0) {
190193
final int error = GetLastError();
191-
throw WindowsException(error);
194+
throw _createWin32Exception(error);
192195
}
193196

194197
// From that, load the VERSIONINFO resource
@@ -223,7 +226,7 @@ class PathProviderWindows extends PathProviderPlatform {
223226
}
224227

225228
/// Makes [rawString] safe as a directory component. See
226-
/// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
229+
/// https://docs.microsoft.com/windows/win32/fileio/naming-a-file#naming-conventions
227230
///
228231
/// If after sanitizing the string is empty, returns null.
229232
String? _sanitizedDirectoryName(String? rawString) {
@@ -263,3 +266,15 @@ class PathProviderWindows extends PathProviderPlatform {
263266
return directory.path;
264267
}
265268
}
269+
270+
Exception _createWin32Exception(int errorCode) {
271+
return PlatformException(
272+
code: 'Win32 Error',
273+
// TODO(stuartmorgan): Consider getting the system error message via
274+
// FormatMessage if it turns out to be necessary for debugging issues.
275+
// Plugin-client-level usability isn't a major consideration since per
276+
// https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#platform-exception-handling
277+
// any case that comes up in practice should be handled and returned
278+
// via a plugin-specific exception, not this fallback.
279+
message: 'Error code 0x${errorCode.toRadixString(16)}');
280+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// The types and functions here correspond directly to corresponding Windows
6+
// types and functions, so the Windows docs are the definitive source of
7+
// documentation.
8+
// ignore_for_file: public_member_api_docs
9+
10+
import 'dart:ffi';
11+
12+
import 'package:ffi/ffi.dart';
13+
14+
import 'guid.dart';
15+
16+
typedef BOOL = Int32;
17+
typedef BYTE = Uint8;
18+
typedef DWORD = Uint32;
19+
typedef UINT = Uint32;
20+
typedef HANDLE = IntPtr;
21+
typedef HMODULE = HANDLE;
22+
typedef HRESULT = Int32;
23+
typedef LPCVOID = Pointer<NativeType>;
24+
typedef LPCWSTR = Pointer<Utf16>;
25+
typedef LPDWORD = Pointer<DWORD>;
26+
typedef LPWSTR = Pointer<Utf16>;
27+
typedef LPVOID = Pointer<NativeType>;
28+
typedef PUINT = Pointer<UINT>;
29+
typedef PWSTR = Pointer<Pointer<Utf16>>;
30+
typedef WCHAR = Uint16;
31+
32+
const int NULL = 0;
33+
34+
// https://learn.microsoft.com/windows/win32/fileio/maximum-file-path-limitation?tabs=registry
35+
const int MAX_PATH = 260;
36+
37+
// https://learn.microsoft.com/windows/win32/seccrypto/common-hresult-values
38+
// ignore: non_constant_identifier_names
39+
final int E_FAIL = 0x80004005.toSigned(32);
40+
// ignore: non_constant_identifier_names
41+
final int E_INVALIDARG = 0x80070057.toSigned(32);
42+
43+
// https://learn.microsoft.com/windows/win32/api/winerror/nf-winerror-failed#remarks
44+
// ignore: non_constant_identifier_names
45+
bool FAILED(int hr) => hr < 0;
46+
47+
// https://learn.microsoft.com/windows/win32/api/shlobj_core/ne-shlobj_core-known_folder_flag
48+
const int KF_FLAG_DEFAULT = 0x00000000;
49+
50+
final DynamicLibrary _dllKernel32 = DynamicLibrary.open('kernel32.dll');
51+
final DynamicLibrary _dllVersion = DynamicLibrary.open('version.dll');
52+
final DynamicLibrary _dllShell32 = DynamicLibrary.open('shell32.dll');
53+
54+
// https://learn.microsoft.com/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath
55+
typedef _FFITypeSHGetKnownFolderPath = HRESULT Function(
56+
Pointer<GUID>, DWORD, HANDLE, PWSTR);
57+
typedef FFITypeSHGetKnownFolderPathDart = int Function(
58+
Pointer<GUID>, int, int, Pointer<Pointer<Utf16>>);
59+
// ignore: non_constant_identifier_names
60+
final FFITypeSHGetKnownFolderPathDart SHGetKnownFolderPath =
61+
_dllShell32.lookupFunction<_FFITypeSHGetKnownFolderPath,
62+
FFITypeSHGetKnownFolderPathDart>('SHGetKnownFolderPath');
63+
64+
// https://learn.microsoft.com/windows/win32/api/winver/nf-winver-getfileversioninfow
65+
typedef _FFITypeGetFileVersionInfoW = BOOL Function(
66+
LPCWSTR, DWORD, DWORD, LPVOID);
67+
typedef FFITypeGetFileVersionInfoW = int Function(
68+
Pointer<Utf16>, int, int, Pointer<NativeType>);
69+
// ignore: non_constant_identifier_names
70+
final FFITypeGetFileVersionInfoW GetFileVersionInfo = _dllVersion
71+
.lookupFunction<_FFITypeGetFileVersionInfoW, FFITypeGetFileVersionInfoW>(
72+
'GetFileVersionInfoW');
73+
74+
// https://learn.microsoft.com/windows/win32/api/winver/nf-winver-getfileversioninfosizew
75+
typedef _FFITypeGetFileVersionInfoSizeW = DWORD Function(LPCWSTR, LPDWORD);
76+
typedef FFITypeGetFileVersionInfoSizeW = int Function(
77+
Pointer<Utf16>, Pointer<Uint32>);
78+
// ignore: non_constant_identifier_names
79+
final FFITypeGetFileVersionInfoSizeW GetFileVersionInfoSize =
80+
_dllVersion.lookupFunction<_FFITypeGetFileVersionInfoSizeW,
81+
FFITypeGetFileVersionInfoSizeW>('GetFileVersionInfoSizeW');
82+
83+
// https://learn.microsoft.com/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
84+
typedef _FFITypeGetLastError = DWORD Function();
85+
typedef FFITypeGetLastError = int Function();
86+
// ignore: non_constant_identifier_names
87+
final FFITypeGetLastError GetLastError = _dllKernel32
88+
.lookupFunction<_FFITypeGetLastError, FFITypeGetLastError>('GetLastError');
89+
90+
// https://learn.microsoft.com/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamew
91+
typedef _FFITypeGetModuleFileNameW = DWORD Function(HMODULE, LPWSTR, DWORD);
92+
typedef FFITypeGetModuleFileNameW = int Function(int, Pointer<Utf16>, int);
93+
// ignore: non_constant_identifier_names
94+
final FFITypeGetModuleFileNameW GetModuleFileName = _dllKernel32.lookupFunction<
95+
_FFITypeGetModuleFileNameW,
96+
FFITypeGetModuleFileNameW>('GetModuleFileNameW');
97+
98+
// https://learn.microsoft.com/windows/win32/api/winver/nf-winver-verqueryvaluew
99+
typedef _FFITypeVerQueryValueW = BOOL Function(LPCVOID, LPCWSTR, LPVOID, PUINT);
100+
typedef FFITypeVerQueryValueW = int Function(
101+
Pointer<NativeType>, Pointer<Utf16>, Pointer<NativeType>, Pointer<Uint32>);
102+
// ignore: non_constant_identifier_names
103+
final FFITypeVerQueryValueW VerQueryValue =
104+
_dllVersion.lookupFunction<_FFITypeVerQueryValueW, FFITypeVerQueryValueW>(
105+
'VerQueryValueW');
106+
107+
// https://learn.microsoft.com/windows/win32/api/fileapi/nf-fileapi-gettemppathw
108+
typedef _FFITypeGetTempPathW = DWORD Function(DWORD, LPWSTR);
109+
typedef FFITypeGetTempPathW = int Function(int, Pointer<Utf16>);
110+
// ignore: non_constant_identifier_names
111+
final FFITypeGetTempPathW GetTempPath = _dllKernel32
112+
.lookupFunction<_FFITypeGetTempPathW, FFITypeGetTempPathW>('GetTempPathW');

packages/path_provider/path_provider_windows/pubspec.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: path_provider_windows
22
description: Windows implementation of the path_provider plugin
33
repository: https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_windows
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22
5-
version: 2.2.1
5+
version: 2.3.0
66

77
environment:
88
sdk: ^3.2.0
@@ -21,7 +21,6 @@ dependencies:
2121
sdk: flutter
2222
path: ^1.8.0
2323
path_provider_platform_interface: ^2.1.0
24-
win32: ">=2.1.0 <6.0.0"
2524

2625
dev_dependencies:
2726
flutter_test:
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:ffi';
6+
import 'dart:typed_data';
7+
8+
import 'package:ffi/ffi.dart';
9+
import 'package:flutter_test/flutter_test.dart';
10+
import 'package:path_provider_windows/src/guid.dart';
11+
12+
void main() {
13+
test('has correct byte representation', () async {
14+
final Pointer<GUID> guid = calloc<GUID>()
15+
..ref.parse('{00112233-4455-6677-8899-aabbccddeeff}');
16+
final ByteData data = ByteData(16)
17+
..setInt32(0, guid.ref.data1, Endian.little)
18+
..setInt16(4, guid.ref.data2, Endian.little)
19+
..setInt16(6, guid.ref.data3, Endian.little)
20+
..setInt64(8, guid.ref.data4, Endian.little);
21+
expect(data.getUint8(0), 0x33);
22+
expect(data.getUint8(1), 0x22);
23+
expect(data.getUint8(2), 0x11);
24+
expect(data.getUint8(3), 0x00);
25+
expect(data.getUint8(4), 0x55);
26+
expect(data.getUint8(5), 0x44);
27+
expect(data.getUint8(6), 0x77);
28+
expect(data.getUint8(7), 0x66);
29+
expect(data.getUint8(8), 0x88);
30+
expect(data.getUint8(9), 0x99);
31+
expect(data.getUint8(10), 0xAA);
32+
expect(data.getUint8(11), 0xBB);
33+
expect(data.getUint8(12), 0xCC);
34+
expect(data.getUint8(13), 0xDD);
35+
expect(data.getUint8(14), 0xEE);
36+
expect(data.getUint8(15), 0xFF);
37+
38+
calloc.free(guid);
39+
});
40+
41+
test('handles alternate forms', () async {
42+
final Pointer<GUID> guid1 = calloc<GUID>()
43+
..ref.parse('{00112233-4455-6677-8899-aabbccddeeff}');
44+
final Pointer<GUID> guid2 = calloc<GUID>()
45+
..ref.parse('00112233445566778899AABBCCDDEEFF');
46+
47+
expect(guid1.ref.data1, guid2.ref.data1);
48+
expect(guid1.ref.data2, guid2.ref.data2);
49+
expect(guid1.ref.data3, guid2.ref.data3);
50+
expect(guid1.ref.data4, guid2.ref.data4);
51+
52+
calloc.free(guid1);
53+
calloc.free(guid2);
54+
});
55+
56+
test('throws for bad data', () async {
57+
final Pointer<GUID> guid = calloc<GUID>();
58+
59+
expect(() => guid.ref.parse('{00112233-4455-6677-88'), throwsArgumentError);
60+
61+
calloc.free(guid);
62+
});
63+
}

script/configs/allowed_unpinned_deps.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# cautious about adding to this list.
1414
- build_verify
1515
- google_maps
16-
- win32
1716

1817
## Allowed by default
1918

0 commit comments

Comments
 (0)