Skip to content

Commit c29a7a2

Browse files
authored
Ignore replacement characters from vswhere.exe output (flutter#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: flutter#102451
1 parent ac29c11 commit c29a7a2

File tree

3 files changed

+119
-8
lines changed

3 files changed

+119
-8
lines changed

packages/flutter_tools/lib/src/convert.dart

+6-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ const Encoding utf8ForTesting = cnv.utf8;
2626
/// that aren't UTF-8 and we're not quite sure how this is happening.
2727
/// This tells people to report a bug when they see this.
2828
class Utf8Codec extends Encoding {
29-
const Utf8Codec();
29+
const Utf8Codec({this.reportErrors = true});
30+
31+
final bool reportErrors;
3032

3133
@override
32-
Converter<List<int>, String> get decoder => const Utf8Decoder();
34+
Converter<List<int>, String> get decoder => reportErrors
35+
? const Utf8Decoder()
36+
: const Utf8Decoder(reportErrors: false);
3337

3438
@override
3539
Converter<String, List<int>> get encoder => cnv.utf8.encoder;

packages/flutter_tools/lib/src/windows/visual_studio.dart

+34-6
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ class VisualStudio {
4747

4848
/// The name of the Visual Studio install.
4949
///
50-
/// For instance: "Visual Studio Community 2019".
50+
/// For instance: "Visual Studio Community 2019". This should only be used for
51+
/// display purposes.
5152
String? get displayName => _bestVisualStudioDetails?.displayName;
5253

5354
/// The user-friendly version number of the Visual Studio install.
5455
///
55-
/// For instance: "15.4.0".
56+
/// For instance: "15.4.0". This should only be used for display purposes.
57+
/// Logic based off the installation's version should use the `fullVersion`.
5658
String? get displayVersion => _bestVisualStudioDetails?.catalogDisplayVersion;
5759

5860
/// The directory where Visual Studio is installed.
@@ -282,12 +284,15 @@ class VisualStudio {
282284
'-utf8',
283285
'-latest',
284286
];
287+
// Ignore replacement characters as vswhere.exe is known to output them.
288+
// See: https://github.com/flutter/flutter/issues/102451
289+
const Encoding encoding = Utf8Codec(reportErrors: false);
285290
final RunResult whereResult = _processUtils.runSync(<String>[
286291
_vswherePath,
287292
...defaultArguments,
288293
...?additionalArguments,
289294
...requirementArguments,
290-
], encoding: utf8);
295+
], encoding: encoding);
291296
if (whereResult.exitCode == 0) {
292297
final List<Map<String, dynamic>> installations =
293298
(json.decode(whereResult.stdout) as List<dynamic>).cast<Map<String, dynamic>>();
@@ -416,17 +421,40 @@ class VswhereDetails {
416421

417422
return VswhereDetails(
418423
meetsRequirements: meetsRequirements,
419-
installationPath: details['installationPath'] as String?,
420-
displayName: details['displayName'] as String?,
421-
fullVersion: details['installationVersion'] as String?,
422424
isComplete: details['isComplete'] as bool?,
423425
isLaunchable: details['isLaunchable'] as bool?,
424426
isRebootRequired: details['isRebootRequired'] as bool?,
425427
isPrerelease: details['isPrerelease'] as bool?,
428+
429+
// Below are strings that must be well-formed without replacement characters.
430+
installationPath: _validateString(details['installationPath'] as String?),
431+
fullVersion: _validateString(details['installationVersion'] as String?),
432+
433+
// Below are strings that are used only for display purposes and are allowed to
434+
// contain replacement characters.
435+
displayName: details['displayName'] as String?,
426436
catalogDisplayVersion: catalog == null ? null : catalog['productDisplayVersion'] as String?,
427437
);
428438
}
429439

440+
/// Verify JSON strings from vswhere.exe output are valid.
441+
///
442+
/// The output of vswhere.exe is known to output replacement characters.
443+
/// Use this to ensure values that must be well-formed are valid. Strings that
444+
/// are only used for display purposes should skip this check.
445+
/// See: https://github.com/flutter/flutter/issues/102451
446+
static String? _validateString(String? value) {
447+
if (value != null && value.contains('\u{FFFD}')) {
448+
throwToolExit(
449+
'Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found in string: $value. '
450+
'The Flutter team would greatly appreciate if you could file a bug explaining '
451+
'exactly what you were doing when this happened:\n'
452+
'https://github.com/flutter/flutter/issues/new/choose\n');
453+
}
454+
455+
return value;
456+
}
457+
430458
/// Whether the installation satisfies the required workloads and minimum version.
431459
final bool meetsRequirements;
432460

packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart

+79
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,85 @@ void main() {
864864
});
865865
});
866866

867+
// The output of vswhere.exe is known to contain bad UTF8.
868+
// See: https://github.com/flutter/flutter/issues/102451
869+
group('Correctly handles bad UTF-8 from vswhere.exe output', () {
870+
late VisualStudioFixture fixture;
871+
late VisualStudio visualStudio;
872+
873+
setUp(() {
874+
fixture = setUpVisualStudio();
875+
visualStudio = fixture.visualStudio;
876+
});
877+
878+
testWithoutContext('Ignores unicode replacement char in unused properties', () {
879+
final Map<String, dynamic> response = Map<String, dynamic>.of(_defaultResponse)
880+
..['unused'] = 'Bad UTF8 \u{FFFD}';
881+
882+
setMockCompatibleVisualStudioInstallation(
883+
response,
884+
fixture.fileSystem,
885+
fixture.processManager,
886+
);
887+
888+
expect(visualStudio.isInstalled, true);
889+
expect(visualStudio.isAtLeastMinimumVersion, true);
890+
expect(visualStudio.hasNecessaryComponents, true);
891+
expect(visualStudio.cmakePath, equals(cmakePath));
892+
expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019'));
893+
});
894+
895+
testWithoutContext('Throws ToolExit on bad UTF-8 in installationPath', () {
896+
final Map<String, dynamic> response = Map<String, dynamic>.of(_defaultResponse)
897+
..['installationPath'] = '\u{FFFD}';
898+
899+
setMockCompatibleVisualStudioInstallation(response, fixture.fileSystem, fixture.processManager);
900+
901+
expect(() => visualStudio.isInstalled,
902+
throwsToolExit(message: 'Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found in string'));
903+
});
904+
905+
testWithoutContext('Throws ToolExit on bad UTF-8 in installationVersion', () {
906+
final Map<String, dynamic> response = Map<String, dynamic>.of(_defaultResponse)
907+
..['installationVersion'] = '\u{FFFD}';
908+
909+
setMockCompatibleVisualStudioInstallation(response, fixture.fileSystem, fixture.processManager);
910+
911+
expect(() => visualStudio.isInstalled,
912+
throwsToolExit(message: 'Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found in string'));
913+
});
914+
915+
testWithoutContext('Ignores bad UTF-8 in displayName', () {
916+
final Map<String, dynamic> response = Map<String, dynamic>.of(_defaultResponse)
917+
..['displayName'] = '\u{FFFD}';
918+
919+
setMockCompatibleVisualStudioInstallation(response, fixture.fileSystem, fixture.processManager);
920+
921+
expect(visualStudio.isInstalled, true);
922+
expect(visualStudio.isAtLeastMinimumVersion, true);
923+
expect(visualStudio.hasNecessaryComponents, true);
924+
expect(visualStudio.cmakePath, equals(cmakePath));
925+
expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019'));
926+
expect(visualStudio.displayName, equals('\u{FFFD}'));
927+
});
928+
929+
testWithoutContext("Ignores bad UTF-8 in catalog's productDisplayVersion", () {
930+
final Map<String, dynamic> catalog = Map<String, dynamic>.of(_defaultResponse['catalog'] as Map<String, dynamic>)
931+
..['productDisplayVersion'] = '\u{FFFD}';
932+
final Map<String, dynamic> response = Map<String, dynamic>.of(_defaultResponse)
933+
..['catalog'] = catalog;
934+
935+
setMockCompatibleVisualStudioInstallation(response, fixture.fileSystem, fixture.processManager);
936+
937+
expect(visualStudio.isInstalled, true);
938+
expect(visualStudio.isAtLeastMinimumVersion, true);
939+
expect(visualStudio.hasNecessaryComponents, true);
940+
expect(visualStudio.cmakePath, equals(cmakePath));
941+
expect(visualStudio.cmakeGenerator, equals('Visual Studio 16 2019'));
942+
expect(visualStudio.displayVersion, equals('\u{FFFD}'));
943+
});
944+
});
945+
867946
group(VswhereDetails, () {
868947
test('Accepts empty JSON', () {
869948
const bool meetsRequirements = true;

0 commit comments

Comments
 (0)