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
This commit is contained in:
parent
ac29c11aec
commit
c29a7a2d22
@ -26,10 +26,14 @@ const Encoding utf8ForTesting = cnv.utf8;
|
|||||||
/// that aren't UTF-8 and we're not quite sure how this is happening.
|
/// 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.
|
/// This tells people to report a bug when they see this.
|
||||||
class Utf8Codec extends Encoding {
|
class Utf8Codec extends Encoding {
|
||||||
const Utf8Codec();
|
const Utf8Codec({this.reportErrors = true});
|
||||||
|
|
||||||
|
final bool reportErrors;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Converter<List<int>, String> get decoder => const Utf8Decoder();
|
Converter<List<int>, String> get decoder => reportErrors
|
||||||
|
? const Utf8Decoder()
|
||||||
|
: const Utf8Decoder(reportErrors: false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Converter<String, List<int>> get encoder => cnv.utf8.encoder;
|
Converter<String, List<int>> get encoder => cnv.utf8.encoder;
|
||||||
|
@ -47,12 +47,14 @@ class VisualStudio {
|
|||||||
|
|
||||||
/// The name of the Visual Studio install.
|
/// 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;
|
String? get displayName => _bestVisualStudioDetails?.displayName;
|
||||||
|
|
||||||
/// The user-friendly version number of the Visual Studio install.
|
/// 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;
|
String? get displayVersion => _bestVisualStudioDetails?.catalogDisplayVersion;
|
||||||
|
|
||||||
/// The directory where Visual Studio is installed.
|
/// The directory where Visual Studio is installed.
|
||||||
@ -282,12 +284,15 @@ class VisualStudio {
|
|||||||
'-utf8',
|
'-utf8',
|
||||||
'-latest',
|
'-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>[
|
final RunResult whereResult = _processUtils.runSync(<String>[
|
||||||
_vswherePath,
|
_vswherePath,
|
||||||
...defaultArguments,
|
...defaultArguments,
|
||||||
...?additionalArguments,
|
...?additionalArguments,
|
||||||
...requirementArguments,
|
...requirementArguments,
|
||||||
], encoding: utf8);
|
], encoding: encoding);
|
||||||
if (whereResult.exitCode == 0) {
|
if (whereResult.exitCode == 0) {
|
||||||
final List<Map<String, dynamic>> installations =
|
final List<Map<String, dynamic>> installations =
|
||||||
(json.decode(whereResult.stdout) as List<dynamic>).cast<Map<String, dynamic>>();
|
(json.decode(whereResult.stdout) as List<dynamic>).cast<Map<String, dynamic>>();
|
||||||
@ -416,17 +421,40 @@ class VswhereDetails {
|
|||||||
|
|
||||||
return VswhereDetails(
|
return VswhereDetails(
|
||||||
meetsRequirements: meetsRequirements,
|
meetsRequirements: meetsRequirements,
|
||||||
installationPath: details['installationPath'] as String?,
|
|
||||||
displayName: details['displayName'] as String?,
|
|
||||||
fullVersion: details['installationVersion'] as String?,
|
|
||||||
isComplete: details['isComplete'] as bool?,
|
isComplete: details['isComplete'] as bool?,
|
||||||
isLaunchable: details['isLaunchable'] as bool?,
|
isLaunchable: details['isLaunchable'] as bool?,
|
||||||
isRebootRequired: details['isRebootRequired'] as bool?,
|
isRebootRequired: details['isRebootRequired'] as bool?,
|
||||||
isPrerelease: details['isPrerelease'] 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?,
|
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.
|
/// Whether the installation satisfies the required workloads and minimum version.
|
||||||
final bool meetsRequirements;
|
final bool meetsRequirements;
|
||||||
|
|
||||||
|
@ -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, () {
|
group(VswhereDetails, () {
|
||||||
test('Accepts empty JSON', () {
|
test('Accepts empty JSON', () {
|
||||||
const bool meetsRequirements = true;
|
const bool meetsRequirements = true;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user