Skip to content

Commit

Permalink
[path_provider] Remove win32 (#7073)
Browse files Browse the repository at this point in the history
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/flutter#130940
  • Loading branch information
stuartmorgan committed Jul 9, 2024
1 parent 9627de9 commit 47a92db
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 67 deletions.
3 changes: 2 additions & 1 deletion packages/path_provider/path_provider_windows/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 2.3.0

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

## 2.2.1
Expand Down
115 changes: 61 additions & 54 deletions packages/path_provider/path_provider_windows/lib/src/folders.dart

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions packages/path_provider/path_provider_windows/lib/src/guid.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2013 The Flutter 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:ffi';
import 'dart:typed_data';

/// Representation of the Win32 GUID struct.
// For the layout of this struct, see
// https://learn.microsoft.com/windows/win32/api/guiddef/ns-guiddef-guid
@Packed(4)
base class GUID extends Struct {
/// Native Data1 field.
@Uint32()
external int data1;

/// Native Data2 field.
@Uint16()
external int data2;

/// Native Data3 field.
@Uint16()
external int data3;

/// Native Data4 field.
// This should be an eight-element byte array, but there's no such annotation.
@Uint64()
external int data4;

/// Parses a GUID string, with optional enclosing "{}"s and optional "-"s,
/// into data.
void parse(String guid) {
final String hexOnly = guid.replaceAll(RegExp(r'[{}-]'), '');
if (hexOnly.length != 32) {
throw ArgumentError.value(guid, 'guid', 'Invalid GUID string');
}
final ByteData bytes = ByteData(16);
for (int i = 0; i < 16; ++i) {
bytes.setUint8(
i, int.parse(hexOnly.substring(i * 2, i * 2 + 2), radix: 16));
}
data1 = bytes.getInt32(0);
data2 = bytes.getInt16(4);
data3 = bytes.getInt16(6);
// [bytes] is big endian, but the host is little endian, so a default
// big-endian read would reverse the bytes. Since data4 is supposed to be
// a byte array, the order should be preserved, so do a little-endian read.
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding
data4 = bytes.getInt64(8, Endian.little);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import 'dart:io';

import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:flutter/services.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';
import 'guid.dart';
import 'win32_wrappers.dart';

/// Constant for en-US language used in VersionInfo keys.
@visibleForTesting
Expand All @@ -35,7 +37,7 @@ class VersionInfoQuerier {
/// language and encoding, or null if there is no such entry,
/// or if versionInfo is null.
///
/// See https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
/// See https://docs.microsoft.com/windows/win32/menurc/versioninfo-resource
/// for list of possible language and encoding values.
String? getStringValue(
Pointer<Uint8>? versionInfo,
Expand All @@ -49,7 +51,7 @@ class VersionInfoQuerier {
return null;
}
final Pointer<Utf16> keyPath =
TEXT('\\StringFileInfo\\$language$encoding\\$key');
'\\StringFileInfo\\$language$encoding\\$key'.toNativeUtf16();
final Pointer<UINT> length = calloc<UINT>();
final Pointer<Pointer<Utf16>> valueAddress = calloc<Pointer<Utf16>>();
try {
Expand Down Expand Up @@ -89,7 +91,7 @@ class PathProviderWindows extends PathProviderPlatform {

if (length == 0) {
final int error = GetLastError();
throw WindowsException(error);
throw _createWin32Exception(error);
} else {
path = buffer.toDartString();

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

try {
final int hr = SHGetKnownFolderPath(
Expand All @@ -146,7 +148,7 @@ class PathProviderWindows extends PathProviderPlatform {

if (FAILED(hr)) {
if (hr == E_INVALIDARG || hr == E_FAIL) {
throw WindowsException(hr);
throw _createWin32Exception(hr);
}
return Future<String?>.value();
}
Expand Down Expand Up @@ -179,7 +181,8 @@ class PathProviderWindows extends PathProviderPlatform {
String? companyName;
String? productName;

final Pointer<Utf16> moduleNameBuffer = wsalloc(MAX_PATH + 1);
final Pointer<Utf16> moduleNameBuffer =
calloc<WCHAR>(MAX_PATH + 1).cast<Utf16>();
final Pointer<DWORD> unused = calloc<DWORD>();
Pointer<BYTE>? infoBuffer;
try {
Expand All @@ -188,7 +191,7 @@ class PathProviderWindows extends PathProviderPlatform {
GetModuleFileName(0, moduleNameBuffer, MAX_PATH);
if (moduleNameLength == 0) {
final int error = GetLastError();
throw WindowsException(error);
throw _createWin32Exception(error);
}

// From that, load the VERSIONINFO resource
Expand Down Expand Up @@ -223,7 +226,7 @@ class PathProviderWindows extends PathProviderPlatform {
}

/// Makes [rawString] safe as a directory component. See
/// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
/// https://docs.microsoft.com/windows/win32/fileio/naming-a-file#naming-conventions
///
/// If after sanitizing the string is empty, returns null.
String? _sanitizedDirectoryName(String? rawString) {
Expand Down Expand Up @@ -263,3 +266,15 @@ class PathProviderWindows extends PathProviderPlatform {
return directory.path;
}
}

Exception _createWin32Exception(int errorCode) {
return PlatformException(
code: 'Win32 Error',
// TODO(stuartmorgan): Consider getting the system error message via
// FormatMessage if it turns out to be necessary for debugging issues.
// Plugin-client-level usability isn't a major consideration since per
// https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#platform-exception-handling
// any case that comes up in practice should be handled and returned
// via a plugin-specific exception, not this fallback.
message: 'Error code 0x${errorCode.toRadixString(16)}');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// The types and functions here correspond directly to corresponding Windows
// types and functions, so the Windows docs are the definitive source of
// documentation.
// ignore_for_file: public_member_api_docs

import 'dart:ffi';

import 'package:ffi/ffi.dart';

import 'guid.dart';

typedef BOOL = Int32;
typedef BYTE = Uint8;
typedef DWORD = Uint32;
typedef UINT = Uint32;
typedef HANDLE = IntPtr;
typedef HMODULE = HANDLE;
typedef HRESULT = Int32;
typedef LPCVOID = Pointer<NativeType>;
typedef LPCWSTR = Pointer<Utf16>;
typedef LPDWORD = Pointer<DWORD>;
typedef LPWSTR = Pointer<Utf16>;
typedef LPVOID = Pointer<NativeType>;
typedef PUINT = Pointer<UINT>;
typedef PWSTR = Pointer<Pointer<Utf16>>;
typedef WCHAR = Uint16;

const int NULL = 0;

// https://learn.microsoft.com/windows/win32/fileio/maximum-file-path-limitation?tabs=registry
const int MAX_PATH = 260;

// https://learn.microsoft.com/windows/win32/seccrypto/common-hresult-values
// ignore: non_constant_identifier_names
final int E_FAIL = 0x80004005.toSigned(32);
// ignore: non_constant_identifier_names
final int E_INVALIDARG = 0x80070057.toSigned(32);

// https://learn.microsoft.com/windows/win32/api/winerror/nf-winerror-failed#remarks
// ignore: non_constant_identifier_names
bool FAILED(int hr) => hr < 0;

// https://learn.microsoft.com/windows/win32/api/shlobj_core/ne-shlobj_core-known_folder_flag
const int KF_FLAG_DEFAULT = 0x00000000;

final DynamicLibrary _dllKernel32 = DynamicLibrary.open('kernel32.dll');
final DynamicLibrary _dllVersion = DynamicLibrary.open('version.dll');
final DynamicLibrary _dllShell32 = DynamicLibrary.open('shell32.dll');

// https://learn.microsoft.com/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath
typedef _FFITypeSHGetKnownFolderPath = HRESULT Function(
Pointer<GUID>, DWORD, HANDLE, PWSTR);
typedef FFITypeSHGetKnownFolderPathDart = int Function(
Pointer<GUID>, int, int, Pointer<Pointer<Utf16>>);
// ignore: non_constant_identifier_names
final FFITypeSHGetKnownFolderPathDart SHGetKnownFolderPath =
_dllShell32.lookupFunction<_FFITypeSHGetKnownFolderPath,
FFITypeSHGetKnownFolderPathDart>('SHGetKnownFolderPath');

// https://learn.microsoft.com/windows/win32/api/winver/nf-winver-getfileversioninfow
typedef _FFITypeGetFileVersionInfoW = BOOL Function(
LPCWSTR, DWORD, DWORD, LPVOID);
typedef FFITypeGetFileVersionInfoW = int Function(
Pointer<Utf16>, int, int, Pointer<NativeType>);
// ignore: non_constant_identifier_names
final FFITypeGetFileVersionInfoW GetFileVersionInfo = _dllVersion
.lookupFunction<_FFITypeGetFileVersionInfoW, FFITypeGetFileVersionInfoW>(
'GetFileVersionInfoW');

// https://learn.microsoft.com/windows/win32/api/winver/nf-winver-getfileversioninfosizew
typedef _FFITypeGetFileVersionInfoSizeW = DWORD Function(LPCWSTR, LPDWORD);
typedef FFITypeGetFileVersionInfoSizeW = int Function(
Pointer<Utf16>, Pointer<Uint32>);
// ignore: non_constant_identifier_names
final FFITypeGetFileVersionInfoSizeW GetFileVersionInfoSize =
_dllVersion.lookupFunction<_FFITypeGetFileVersionInfoSizeW,
FFITypeGetFileVersionInfoSizeW>('GetFileVersionInfoSizeW');

// https://learn.microsoft.com/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
typedef _FFITypeGetLastError = DWORD Function();
typedef FFITypeGetLastError = int Function();
// ignore: non_constant_identifier_names
final FFITypeGetLastError GetLastError = _dllKernel32
.lookupFunction<_FFITypeGetLastError, FFITypeGetLastError>('GetLastError');

// https://learn.microsoft.com/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamew
typedef _FFITypeGetModuleFileNameW = DWORD Function(HMODULE, LPWSTR, DWORD);
typedef FFITypeGetModuleFileNameW = int Function(int, Pointer<Utf16>, int);
// ignore: non_constant_identifier_names
final FFITypeGetModuleFileNameW GetModuleFileName = _dllKernel32.lookupFunction<
_FFITypeGetModuleFileNameW,
FFITypeGetModuleFileNameW>('GetModuleFileNameW');

// https://learn.microsoft.com/windows/win32/api/winver/nf-winver-verqueryvaluew
typedef _FFITypeVerQueryValueW = BOOL Function(LPCVOID, LPCWSTR, LPVOID, PUINT);
typedef FFITypeVerQueryValueW = int Function(
Pointer<NativeType>, Pointer<Utf16>, Pointer<NativeType>, Pointer<Uint32>);
// ignore: non_constant_identifier_names
final FFITypeVerQueryValueW VerQueryValue =
_dllVersion.lookupFunction<_FFITypeVerQueryValueW, FFITypeVerQueryValueW>(
'VerQueryValueW');

// https://learn.microsoft.com/windows/win32/api/fileapi/nf-fileapi-gettemppathw
typedef _FFITypeGetTempPathW = DWORD Function(DWORD, LPWSTR);
typedef FFITypeGetTempPathW = int Function(int, Pointer<Utf16>);
// ignore: non_constant_identifier_names
final FFITypeGetTempPathW GetTempPath = _dllKernel32
.lookupFunction<_FFITypeGetTempPathW, FFITypeGetTempPathW>('GetTempPathW');
3 changes: 1 addition & 2 deletions packages/path_provider/path_provider_windows/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: path_provider_windows
description: Windows implementation of the path_provider plugin
repository: https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_windows
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22
version: 2.2.1
version: 2.3.0

environment:
sdk: ^3.2.0
Expand All @@ -21,7 +21,6 @@ dependencies:
sdk: flutter
path: ^1.8.0
path_provider_platform_interface: ^2.1.0
win32: ">=2.1.0 <6.0.0"

dev_dependencies:
flutter_test:
Expand Down
63 changes: 63 additions & 0 deletions packages/path_provider/path_provider_windows/test/guid_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2013 The Flutter 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:ffi';
import 'dart:typed_data';

import 'package:ffi/ffi.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_windows/src/guid.dart';

void main() {
test('has correct byte representation', () async {
final Pointer<GUID> guid = calloc<GUID>()
..ref.parse('{00112233-4455-6677-8899-aabbccddeeff}');
final ByteData data = ByteData(16)
..setInt32(0, guid.ref.data1, Endian.little)
..setInt16(4, guid.ref.data2, Endian.little)
..setInt16(6, guid.ref.data3, Endian.little)
..setInt64(8, guid.ref.data4, Endian.little);
expect(data.getUint8(0), 0x33);
expect(data.getUint8(1), 0x22);
expect(data.getUint8(2), 0x11);
expect(data.getUint8(3), 0x00);
expect(data.getUint8(4), 0x55);
expect(data.getUint8(5), 0x44);
expect(data.getUint8(6), 0x77);
expect(data.getUint8(7), 0x66);
expect(data.getUint8(8), 0x88);
expect(data.getUint8(9), 0x99);
expect(data.getUint8(10), 0xAA);
expect(data.getUint8(11), 0xBB);
expect(data.getUint8(12), 0xCC);
expect(data.getUint8(13), 0xDD);
expect(data.getUint8(14), 0xEE);
expect(data.getUint8(15), 0xFF);

calloc.free(guid);
});

test('handles alternate forms', () async {
final Pointer<GUID> guid1 = calloc<GUID>()
..ref.parse('{00112233-4455-6677-8899-aabbccddeeff}');
final Pointer<GUID> guid2 = calloc<GUID>()
..ref.parse('00112233445566778899AABBCCDDEEFF');

expect(guid1.ref.data1, guid2.ref.data1);
expect(guid1.ref.data2, guid2.ref.data2);
expect(guid1.ref.data3, guid2.ref.data3);
expect(guid1.ref.data4, guid2.ref.data4);

calloc.free(guid1);
calloc.free(guid2);
});

test('throws for bad data', () async {
final Pointer<GUID> guid = calloc<GUID>();

expect(() => guid.ref.parse('{00112233-4455-6677-88'), throwsArgumentError);

calloc.free(guid);
});
}
1 change: 0 additions & 1 deletion script/configs/allowed_unpinned_deps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# cautious about adding to this list.
- build_verify
- google_maps
- win32

## Allowed by default

Expand Down

0 comments on commit 47a92db

Please sign in to comment.