diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index d933b0d51da6..f48093bdbcc1 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.7 +* Added support for unicode encoded VERSIONINFO * Minor fixes for new analysis options. ## 2.0.6 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 index bfffa848981a..9a8b0cf43bbc 100644 --- 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 @@ -13,21 +13,43 @@ import 'package:win32/win32.dart'; import 'folders.dart'; +/// Constant for en-US language used in VersionInfo keys. +@visibleForTesting +const String languageEn = '0409'; + +/// Constant for CP1252 encoding used in VersionInfo keys +@visibleForTesting +const String encodingCP1252 = '04e4'; + +/// Constant for Unicode encoding used in VersionInfo keys +@visibleForTesting +const String encodingUnicode = '04b0'; + /// 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. - String? getStringValue(Pointer? versionInfo, String key) { + /// Returns the value for [key] in [versionInfo]s in section with given + /// 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 + /// for list of possible language and encoding values. + String? getStringValue( + Pointer? versionInfo, + String key, { + required String language, + required String encoding, + }) { + assert(language.isNotEmpty); + assert(encoding.isNotEmpty); if (versionInfo == null) { return null; } - const String kEnUsLanguageCode = '040904e4'; final Pointer keyPath = - TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key'); + TEXT('\\StringFileInfo\\$language$encoding\\$key'); final Pointer length = calloc(); final Pointer> valueAddress = calloc>(); try { @@ -150,6 +172,12 @@ class PathProviderWindows extends PathProviderPlatform { } } + String? _getStringValue(Pointer? infoBuffer, String key) => + versionInfoQuerier.getStringValue(infoBuffer, key, + language: languageEn, encoding: encodingCP1252) ?? + versionInfoQuerier.getStringValue(infoBuffer, key, + language: languageEn, encoding: encodingUnicode); + /// 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. @@ -187,10 +215,10 @@ class PathProviderWindows extends PathProviderPlatform { infoBuffer = null; } } - companyName = _sanitizedDirectoryName( - versionInfoQuerier.getStringValue(infoBuffer, 'CompanyName')); - productName = _sanitizedDirectoryName( - versionInfoQuerier.getStringValue(infoBuffer, 'ProductName')); + companyName = + _sanitizedDirectoryName(_getStringValue(infoBuffer, 'CompanyName')); + productName = + _sanitizedDirectoryName(_getStringValue(infoBuffer, 'ProductName')); // If there was no product name, use the executable name. productName ??= diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml index f75dd058b36b..a995b9f81791 100644 --- a/packages/path_provider/path_provider_windows/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_windows description: Windows implementation of the path_provider plugin repository: https://github.com/flutter/plugins/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.0.6 +version: 2.0.7 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart index 7e4118c1ccc6..571c31473a0b 100644 --- a/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart +++ b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart @@ -7,15 +7,33 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:path_provider_windows/path_provider_windows.dart'; +import 'package:path_provider_windows/src/path_provider_windows_real.dart' + show languageEn, encodingCP1252, encodingUnicode; // A fake VersionInfoQuerier that just returns preset responses. class FakeVersionInfoQuerier implements VersionInfoQuerier { - FakeVersionInfoQuerier(this.responses); + FakeVersionInfoQuerier( + this.responses, { + this.language = languageEn, + this.encoding = encodingUnicode, + }); + final String language; + final String encoding; final Map responses; - String? getStringValue(Pointer? versionInfo, String key) => - responses[key]; + String? getStringValue( + Pointer? versionInfo, + String key, { + required String language, + required String encoding, + }) { + if (language == this.language && encoding == this.encoding) { + return responses[key]; + } else { + return null; + } + } } void main() { @@ -40,12 +58,26 @@ void main() { expect(path, endsWith(r'flutter_tester')); }, skip: !Platform.isWindows); - test('getApplicationSupportPath with full version info', () async { + test('getApplicationSupportPath with full version info in CP1252', () async { final PathProviderWindows pathProvider = PathProviderWindows(); pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ 'CompanyName': 'A Company', 'ProductName': 'Amazing App', - }); + }, language: languageEn, encoding: encodingCP1252); + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, isNotNull); + if (path != null) { + expect(path, endsWith(r'AppData\Roaming\A Company\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + } + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with full version info in Unicode', () async { + final PathProviderWindows pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': 'A Company', + 'ProductName': 'Amazing App', + }, language: languageEn, encoding: encodingUnicode); final String? path = await pathProvider.getApplicationSupportPath(); expect(path, isNotNull); if (path != null) { @@ -54,6 +86,21 @@ void main() { } }, skip: !Platform.isWindows); + test( + 'getApplicationSupportPath with full version info in Unsupported Encoding', + () async { + final PathProviderWindows pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': 'A Company', + 'ProductName': 'Amazing App', + }, language: '0000', encoding: '0000'); + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, contains(r'C:\')); + expect(path, contains(r'AppData')); + // The last path component should be the executable name. + expect(path, endsWith(r'flutter_tester')); + }, skip: !Platform.isWindows); + test('getApplicationSupportPath with missing company', () async { final PathProviderWindows pathProvider = PathProviderWindows(); pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({