Skip to content

Commit

Permalink
Ignore replacement characters from vswhere.exe output (flutter#104284)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
loic-sharma authored May 24, 2022
1 parent ac29c11 commit c29a7a2
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 8 deletions.
8 changes: 6 additions & 2 deletions packages/flutter_tools/lib/src/convert.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<int>, String> get decoder => const Utf8Decoder();
Converter<List<int>, String> get decoder => reportErrors
? const Utf8Decoder()
: const Utf8Decoder(reportErrors: false);

@override
Converter<String, List<int>> get encoder => cnv.utf8.encoder;
Expand Down
40 changes: 34 additions & 6 deletions packages/flutter_tools/lib/src/windows/visual_studio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(<String>[
_vswherePath,
...defaultArguments,
...?additionalArguments,
...requirementArguments,
], encoding: utf8);
], encoding: encoding);
if (whereResult.exitCode == 0) {
final List<Map<String, dynamic>> installations =
(json.decode(whereResult.stdout) as List<dynamic>).cast<Map<String, dynamic>>();
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic> response = Map<String, dynamic>.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<String, dynamic> response = Map<String, dynamic>.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<String, dynamic> response = Map<String, dynamic>.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<String, dynamic> response = Map<String, dynamic>.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<String, dynamic> catalog = Map<String, dynamic>.of(_defaultResponse['catalog'] as Map<String, dynamic>)
..['productDisplayVersion'] = '\u{FFFD}';
final Map<String, dynamic> response = Map<String, dynamic>.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;
Expand Down

0 comments on commit c29a7a2

Please sign in to comment.