From c29a7a2d22e6a685fc06c6685bb4dc72b021d05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Sharma?= <737941+loic-sharma@users.noreply.github.com> Date: Tue, 24 May 2022 13:30:37 -0700 Subject: [PATCH] Ignore replacement characters from vswhere.exe output (#104284) Flutter uses `vswhere.exe` to find Visual Studio installations and determine if they satisfy Flutter's requirements. However, `vswhere.exe`'s JSON output is known to contain bad UTF-8. This change ignores bad UTF-8 as long as they affect JSON properties that are either unused, or, used only for display purposes by Flutter. Fixes: https://github.com/flutter/flutter/issues/102451 --- packages/flutter_tools/lib/src/convert.dart | 8 +- .../lib/src/windows/visual_studio.dart | 40 ++++++++-- .../windows/visual_studio_test.dart | 79 +++++++++++++++++++ 3 files changed, 119 insertions(+), 8 deletions(-) diff --git a/packages/flutter_tools/lib/src/convert.dart b/packages/flutter_tools/lib/src/convert.dart index 70ac4ce227dd..caec2776aa1f 100644 --- a/packages/flutter_tools/lib/src/convert.dart +++ b/packages/flutter_tools/lib/src/convert.dart @@ -26,10 +26,14 @@ const Encoding utf8ForTesting = cnv.utf8; /// that aren't UTF-8 and we're not quite sure how this is happening. /// This tells people to report a bug when they see this. class Utf8Codec extends Encoding { - const Utf8Codec(); + const Utf8Codec({this.reportErrors = true}); + + final bool reportErrors; @override - Converter, String> get decoder => const Utf8Decoder(); + Converter, String> get decoder => reportErrors + ? const Utf8Decoder() + : const Utf8Decoder(reportErrors: false); @override Converter> get encoder => cnv.utf8.encoder; diff --git a/packages/flutter_tools/lib/src/windows/visual_studio.dart b/packages/flutter_tools/lib/src/windows/visual_studio.dart index 0754b261a5b8..e5bef8425948 100644 --- a/packages/flutter_tools/lib/src/windows/visual_studio.dart +++ b/packages/flutter_tools/lib/src/windows/visual_studio.dart @@ -47,12 +47,14 @@ class VisualStudio { /// The name of the Visual Studio install. /// - /// For instance: "Visual Studio Community 2019". + /// For instance: "Visual Studio Community 2019". This should only be used for + /// display purposes. String? get displayName => _bestVisualStudioDetails?.displayName; /// The user-friendly version number of the Visual Studio install. /// - /// For instance: "15.4.0". + /// For instance: "15.4.0". This should only be used for display purposes. + /// Logic based off the installation's version should use the `fullVersion`. String? get displayVersion => _bestVisualStudioDetails?.catalogDisplayVersion; /// The directory where Visual Studio is installed. @@ -282,12 +284,15 @@ class VisualStudio { '-utf8', '-latest', ]; + // Ignore replacement characters as vswhere.exe is known to output them. + // See: https://github.com/flutter/flutter/issues/102451 + const Encoding encoding = Utf8Codec(reportErrors: false); final RunResult whereResult = _processUtils.runSync([ _vswherePath, ...defaultArguments, ...?additionalArguments, ...requirementArguments, - ], encoding: utf8); + ], encoding: encoding); if (whereResult.exitCode == 0) { final List> installations = (json.decode(whereResult.stdout) as List).cast>(); @@ -416,17 +421,40 @@ class VswhereDetails { return VswhereDetails( meetsRequirements: meetsRequirements, - installationPath: details['installationPath'] as String?, - displayName: details['displayName'] as String?, - fullVersion: details['installationVersion'] as String?, isComplete: details['isComplete'] as bool?, isLaunchable: details['isLaunchable'] as bool?, isRebootRequired: details['isRebootRequired'] as bool?, isPrerelease: details['isPrerelease'] as bool?, + + // Below are strings that must be well-formed without replacement characters. + installationPath: _validateString(details['installationPath'] as String?), + fullVersion: _validateString(details['installationVersion'] as String?), + + // Below are strings that are used only for display purposes and are allowed to + // contain replacement characters. + displayName: details['displayName'] as String?, catalogDisplayVersion: catalog == null ? null : catalog['productDisplayVersion'] as String?, ); } + /// Verify JSON strings from vswhere.exe output are valid. + /// + /// The output of vswhere.exe is known to output replacement characters. + /// Use this to ensure values that must be well-formed are valid. Strings that + /// are only used for display purposes should skip this check. + /// See: https://github.com/flutter/flutter/issues/102451 + static String? _validateString(String? value) { + if (value != null && value.contains('\u{FFFD}')) { + throwToolExit( + 'Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found in string: $value. ' + 'The Flutter team would greatly appreciate if you could file a bug explaining ' + 'exactly what you were doing when this happened:\n' + 'https://github.com/flutter/flutter/issues/new/choose\n'); + } + + return value; + } + /// Whether the installation satisfies the required workloads and minimum version. final bool meetsRequirements; diff --git a/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart index 4eed98c2685f..f63fce5ddb3a 100644 --- a/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart +++ b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart @@ -864,6 +864,85 @@ void main() { }); }); + // The output of vswhere.exe is known to contain bad UTF8. + // See: https://github.com/flutter/flutter/issues/102451 + group('Correctly handles bad UTF-8 from vswhere.exe output', () { + late VisualStudioFixture fixture; + late VisualStudio visualStudio; + + setUp(() { + fixture = setUpVisualStudio(); + visualStudio = fixture.visualStudio; + }); + + testWithoutContext('Ignores unicode replacement char in unused properties', () { + final Map response = Map.of(_defaultResponse) + ..['unused'] = 'Bad UTF8 \u{FFFD}'; + + setMockCompatibleVisualStudioInstallation( + response, + fixture.fileSystem, + fixture.processManager, + ); + + expect(visualStudio.isInstalled, true); + expect(visualStudio.isAtLeastMinimumVersion, true); + expect(visualStudio.hasNecessaryComponents, true); + expect(visualStudio.cmakePath, equals(cmakePath)); + expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019')); + }); + + testWithoutContext('Throws ToolExit on bad UTF-8 in installationPath', () { + final Map response = Map.of(_defaultResponse) + ..['installationPath'] = '\u{FFFD}'; + + setMockCompatibleVisualStudioInstallation(response, fixture.fileSystem, fixture.processManager); + + expect(() => visualStudio.isInstalled, + throwsToolExit(message: 'Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found in string')); + }); + + testWithoutContext('Throws ToolExit on bad UTF-8 in installationVersion', () { + final Map response = Map.of(_defaultResponse) + ..['installationVersion'] = '\u{FFFD}'; + + setMockCompatibleVisualStudioInstallation(response, fixture.fileSystem, fixture.processManager); + + expect(() => visualStudio.isInstalled, + throwsToolExit(message: 'Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found in string')); + }); + + testWithoutContext('Ignores bad UTF-8 in displayName', () { + final Map response = Map.of(_defaultResponse) + ..['displayName'] = '\u{FFFD}'; + + setMockCompatibleVisualStudioInstallation(response, fixture.fileSystem, fixture.processManager); + + expect(visualStudio.isInstalled, true); + expect(visualStudio.isAtLeastMinimumVersion, true); + expect(visualStudio.hasNecessaryComponents, true); + expect(visualStudio.cmakePath, equals(cmakePath)); + expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019')); + expect(visualStudio.displayName, equals('\u{FFFD}')); + }); + + testWithoutContext("Ignores bad UTF-8 in catalog's productDisplayVersion", () { + final Map catalog = Map.of(_defaultResponse['catalog'] as Map) + ..['productDisplayVersion'] = '\u{FFFD}'; + final Map response = Map.of(_defaultResponse) + ..['catalog'] = catalog; + + setMockCompatibleVisualStudioInstallation(response, fixture.fileSystem, fixture.processManager); + + expect(visualStudio.isInstalled, true); + expect(visualStudio.isAtLeastMinimumVersion, true); + expect(visualStudio.hasNecessaryComponents, true); + expect(visualStudio.cmakePath, equals(cmakePath)); + expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019')); + expect(visualStudio.displayVersion, equals('\u{FFFD}')); + }); + }); + group(VswhereDetails, () { test('Accepts empty JSON', () { const bool meetsRequirements = true;