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

[path_provider] Support unicode encoded version info values #4986

Merged
merged 6 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.0.7

* Added support for unicode encoded VERSIONINFO
* Minor fixes for new analysis options.

## 2.0.6
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8>? 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<Uint8>? versionInfo,
String key, {
required String language,
required String encoding,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optionally, if you wanted to be really paranoid, assert(language.isNotEmpty) and same for encoding at the top of the method.

}) {
assert(language.isNotEmpty);
assert(encoding.isNotEmpty);
if (versionInfo == null) {
return null;
}
const String kEnUsLanguageCode = '040904e4';
final Pointer<Utf16> keyPath =
TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key');
TEXT('\\StringFileInfo\\$language$encoding\\$key');
final Pointer<Uint32> length = calloc<Uint32>();
final Pointer<Pointer<Utf16>> valueAddress = calloc<Pointer<Utf16>>();
try {
Expand Down Expand Up @@ -150,6 +172,12 @@ class PathProviderWindows extends PathProviderPlatform {
}
}

String? _getStringValue(Pointer<Uint8>? 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.
Expand Down Expand Up @@ -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 ??=
Expand Down
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/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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> responses;

String? getStringValue(Pointer<Uint8>? versionInfo, String key) =>
responses[key];
String? getStringValue(
Pointer<Uint8>? versionInfo,
String key, {
required String language,
required String encoding,
}) {
if (language == this.language && encoding == this.encoding) {
return responses[key];
} else {
return null;
}
}
}

void main() {
Expand All @@ -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(<String, String>{
'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(<String, String>{
'CompanyName': 'A Company',
'ProductName': 'Amazing App',
}, language: languageEn, encoding: encodingUnicode);
final String? path = await pathProvider.getApplicationSupportPath();
expect(path, isNotNull);
if (path != null) {
Expand All @@ -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(<String, String>{
'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(<String, String>{
Expand Down