Unverified Commit 8186fb74 authored by Lau Ching Jun's avatar Lau Ching Jun Committed by GitHub

Use XML format in PlistParser (#94863)

* Switch to use xml in PlistParser

* Review feedbacks: Change dynamic to Object, remove getValueFromFile
parent 56916711
......@@ -500,7 +500,7 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
@override
String get version {
return _version ??= _plistParser.getValueFromFile(
return _version ??= _plistParser.getStringValueFromFile(
plistFile,
PlistParser.kCFBundleShortVersionStringKey,
) ?? 'unknown';
......@@ -514,7 +514,7 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
}
final String? altLocation = _plistParser
.getValueFromFile(plistFile, 'JetBrainsToolboxApp');
.getStringValueFromFile(plistFile, 'JetBrainsToolboxApp');
if (altLocation != null) {
_pluginsPath = '$altLocation.plugins';
......
......@@ -56,7 +56,7 @@ abstract class IOSApp extends ApplicationPackage {
globals.printError('Invalid prebuilt iOS app. Does not contain Info.plist.');
return null;
}
final String? id = globals.plistParser.getValueFromFile(
final String? id = globals.plistParser.getStringValueFromFile(
plistPath,
PlistParser.kCFBundleIdentifierKey,
);
......
......@@ -27,7 +27,7 @@ Future<void> validateBitcode(BuildMode buildMode, TargetPlatform targetPlatform,
final String? clangVersion = clangResult?.stdout.split('\n').first;
final String? engineClangVersion = flutterFrameworkPath == null
? null
: globals.plistParser.getValueFromFile(
: globals.plistParser.getStringValueFromFile(
globals.fs.path.join(flutterFrameworkPath, 'Info.plist'),
'ClangVersion',
);
......
......@@ -3,12 +3,12 @@
// found in the LICENSE file.
import 'package:process/process.dart';
import 'package:xml/xml.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../convert.dart';
class PlistParser {
......@@ -28,42 +28,109 @@ class PlistParser {
static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
static const String kCFBundleExecutable = 'CFBundleExecutable';
/// Parses the plist file located at [plistFilePath] and returns the
/// associated map of key/value property list pairs.
/// Returns the content, converted to XML, of the plist file located at
/// [plistFilePath].
///
/// If [plistFilePath] points to a non-existent file or a file that's not a
/// valid property list file, this will return an empty map.
/// valid property list file, this will return null.
///
/// The [plistFilePath] argument must not be null.
Map<String, dynamic> parseFile(String plistFilePath) {
assert(plistFilePath != null);
String? plistXmlContent(String plistFilePath) {
const String executable = '/usr/bin/plutil';
if (!_fileSystem.isFileSync(executable)) {
throw const FileNotFoundException(executable);
}
if (!_fileSystem.isFileSync(plistFilePath)) {
return const <String, dynamic>{};
}
final String normalizedPlistPath = _fileSystem.path.absolute(plistFilePath);
final List<String> args = <String>[
executable, '-convert', 'xml1', '-o', '-', plistFilePath,
];
try {
final List<String> args = <String>[
executable, '-convert', 'json', '-o', '-', normalizedPlistPath,
];
final String jsonContent = _processUtils.runSync(
final String xmlContent = _processUtils.runSync(
args,
throwOnError: true,
).stdout.trim();
return castStringKeyedMap(json.decode(jsonContent)) ?? const <String, dynamic>{};
return xmlContent;
} on ProcessException catch (error) {
_logger.printTrace('$error');
return const <String, dynamic>{};
return null;
}
}
/// Parses the plist file located at [plistFilePath] and returns the
/// associated map of key/value property list pairs.
///
/// If [plistFilePath] points to a non-existent file or a file that's not a
/// valid property list file, this will return an empty map.
///
/// The [plistFilePath] argument must not be null.
Map<String, Object> parseFile(String plistFilePath) {
assert(plistFilePath != null);
if (!_fileSystem.isFileSync(plistFilePath)) {
return const <String, Object>{};
}
final String normalizedPlistPath = _fileSystem.path.absolute(plistFilePath);
final String? xmlContent = plistXmlContent(normalizedPlistPath);
if (xmlContent == null) {
return const <String, Object>{};
}
return _parseXml(xmlContent);
}
Map<String, Object> _parseXml(String xmlContent) {
final XmlDocument document = XmlDocument.parse(xmlContent);
// First element child is <plist>. The first element child of plist is <dict>.
final XmlElement dictObject = document.firstElementChild!.firstElementChild!;
return _parseXmlDict(dictObject);
}
Map<String, Object> _parseXmlDict(XmlElement node) {
String? lastKey;
final Map<String, Object> result = <String, Object>{};
for (final XmlNode child in node.children) {
if (child is XmlElement) {
if (child.name.local == 'key') {
lastKey = child.text;
} else {
assert(lastKey != null);
result[lastKey!] = _parseXmlNode(child)!;
lastKey = null;
}
}
}
return result;
}
static final RegExp _nonBase64Pattern = RegExp('[^a-zA-Z0-9+/=]+');
Object? _parseXmlNode(XmlElement node) {
switch (node.name.local){
case 'string':
return node.text;
case 'real':
return double.parse(node.text);
case 'integer':
return int.parse(node.text);
case 'true':
return true;
case 'false':
return false;
case 'date':
return DateTime.parse(node.text);
case 'data':
return base64.decode(node.text.replaceAll(_nonBase64Pattern, ''));
case 'array':
return node.children.whereType<XmlElement>().map<Object?>(_parseXmlNode).whereType<Object>().toList();
case 'dict':
return _parseXmlDict(node);
}
return null;
}
/// Parses the Plist file located at [plistFilePath] and returns the value
/// that's associated with the specified [key] within the property list.
/// Parses the Plist file located at [plistFilePath] and returns the string
/// value that's associated with the specified [key] within the property list.
///
/// If [plistFilePath] points to a non-existent file or a file that's not a
/// valid property list file, this will return null.
......@@ -71,9 +138,8 @@ class PlistParser {
/// If [key] is not found in the property list, this will return null.
///
/// The [plistFilePath] and [key] arguments must not be null.
String? getValueFromFile(String plistFilePath, String key) {
assert(key != null);
String? getStringValueFromFile(String plistFilePath, String key) {
final Map<String, dynamic> parsed = parseFile(plistFilePath);
return parsed[key] as String;
return parsed[key] as String?;
}
}
......@@ -495,7 +495,7 @@ class IOSSimulator extends Device {
// parsing the xcodeproj or configuration files.
// See https://github.com/flutter/flutter/issues/31037 for more information.
final String plistPath = globals.fs.path.join(package.simulatorBundlePath, 'Info.plist');
final String? bundleIdentifier = globals.plistParser.getValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
final String? bundleIdentifier = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
if (bundleIdentifier == null) {
globals.printError('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier');
return LaunchResult.failed();
......
......@@ -180,7 +180,7 @@ class IosProject extends XcodeBasedProject {
// Try parsing the default, first.
if (defaultInfoPlist.existsSync()) {
try {
fromPlist = globals.plistParser.getValueFromFile(
fromPlist = globals.plistParser.getStringValueFromFile(
defaultHostInfoPlist.path,
PlistParser.kCFBundleIdentifierKey,
);
......@@ -332,7 +332,7 @@ class IosProject extends XcodeBasedProject {
// The Info.plist file of a target contains the key WKCompanionAppBundleIdentifier,
// if it is a watchOS companion app.
if (infoFile.existsSync()) {
final String? fromPlist = globals.plistParser.getValueFromFile(infoFile.path, 'WKCompanionAppBundleIdentifier');
final String? fromPlist = globals.plistParser.getStringValueFromFile(infoFile.path, 'WKCompanionAppBundleIdentifier');
if (bundleIdentifier == fromPlist) {
return true;
}
......
......@@ -11,10 +11,10 @@ import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/doctor_validator.dart';
import 'package:flutter_tools/src/intellij/intellij_validator.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart';
final Platform macPlatform = FakePlatform(
operatingSystem: 'macos',
......@@ -373,17 +373,6 @@ void main() {
});
}
class FakePlistParser extends Fake implements PlistParser {
FakePlistParser(this.values);
final Map<String, String> values;
@override
String? getValueFromFile(String plistFilePath, String key) {
return values[key];
}
}
class IntelliJValidatorTestTarget extends IntelliJValidator {
IntelliJValidatorTestTarget(String title, String installPath, FileSystem fileSystem)
: super(title, installPath, fileSystem: fileSystem, userMessages: UserMessages());
......
......@@ -33,6 +33,20 @@ const String base64PlistJson =
'eyJDRkJ1bmRsZUV4ZWN1dGFibGUiOiJBcHAiLCJDRkJ1bmRsZUlkZW50aWZpZXIiOiJpby5mb'
'HV0dGVyLmZsdXR0ZXIuYXBwIn0=';
const String base64PlistXmlWithComplexDatatypes =
'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0I'
'FBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS'
'5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo'
'8ZGljdD4KICA8a2V5PkNGQnVuZGxlRXhlY3V0YWJsZTwva2V5PgogIDxzdHJpbmc+QXBwPC9z'
'dHJpbmc+CiAgPGtleT5DRkJ1bmRsZUlkZW50aWZpZXI8L2tleT4KICA8c3RyaW5nPmlvLmZsd'
'XR0ZXIuZmx1dHRlci5hcHA8L3N0cmluZz4KICA8a2V5PmludFZhbHVlPC9rZXk+CiAgPGludG'
'VnZXI+MjwvaW50ZWdlcj4KICA8a2V5PmRvdWJsZVZhbHVlPC9rZXk+CiAgPHJlYWw+MS41PC9'
'yZWFsPgogIDxrZXk+YmluYXJ5VmFsdWU8L2tleT4KICA8ZGF0YT5ZV0pqWkE9PTwvZGF0YT4K'
'ICA8a2V5PmFycmF5VmFsdWU8L2tleT4KICA8YXJyYXk+CiAgICA8dHJ1ZSAvPgogICAgPGZhb'
'HNlIC8+CiAgICA8aW50ZWdlcj4zPC9pbnRlZ2VyPgogIDwvYXJyYXk+CiAgPGtleT5kYXRlVm'
'FsdWU8L2tleT4KICA8ZGF0ZT4yMDIxLTEyLTAxVDEyOjM0OjU2WjwvZGF0ZT4KPC9kaWN0Pgo'
'8L3BsaXN0Pg==';
void main() {
// The tests herein explicitly don't use `MemoryFileSystem` or a mocked
// `ProcessManager` because doing so wouldn't actually test what we want to
......@@ -62,62 +76,79 @@ void main() {
file.deleteSync();
});
testWithoutContext('PlistParser.getValueFromFile works with xml file', () {
testWithoutContext('PlistParser.getStringValueFromFile works with xml file', () {
file.writeAsBytesSync(base64.decode(base64PlistXml));
expect(parser.getValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getValueFromFile works with binary file', () {
testWithoutContext('PlistParser.getStringValueFromFile works with binary file', () {
file.writeAsBytesSync(base64.decode(base64PlistBinary));
expect(parser.getValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getValueFromFile works with json file', () {
testWithoutContext('PlistParser.getStringValueFromFile works with json file', () {
file.writeAsBytesSync(base64.decode(base64PlistJson));
expect(parser.getValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getValueFromFile returns null for non-existent plist file', () {
expect(parser.getValueFromFile('missing.plist', 'CFBundleIdentifier'), null);
testWithoutContext('PlistParser.getStringValueFromFile returns null for non-existent plist file', () {
expect(parser.getStringValueFromFile('missing.plist', 'CFBundleIdentifier'), null);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getValueFromFile returns null for non-existent key within plist', () {
testWithoutContext('PlistParser.getStringValueFromFile returns null for non-existent key within plist', () {
file.writeAsBytesSync(base64.decode(base64PlistXml));
expect(parser.getValueFromFile(file.path, 'BadKey'), null);
expect(parser.getValueFromFile(file.absolute.path, 'BadKey'), null);
expect(parser.getStringValueFromFile(file.path, 'BadKey'), null);
expect(parser.getStringValueFromFile(file.absolute.path, 'BadKey'), null);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getValueFromFile returns null for malformed plist file', () {
testWithoutContext('PlistParser.getStringValueFromFile returns null for malformed plist file', () {
file.writeAsBytesSync(const <int>[1, 2, 3, 4, 5, 6]);
expect(parser.getValueFromFile(file.path, 'CFBundleIdentifier'), null);
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), null);
expect(logger.statusText, isNotEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getValueFromFile throws when /usr/bin/plutil is not found', () async {
testWithoutContext('PlistParser.getStringValueFromFile throws when /usr/bin/plutil is not found', () async {
file.writeAsBytesSync(base64.decode(base64PlistXml));
expect(
() => parser.getValueFromFile('irrelevant.plist', 'ununsed'),
() => parser.getStringValueFromFile(file.path, 'unused'),
throwsA(isA<FileNotFoundException>()),
);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.parseFile can handle different datatypes', () async {
file.writeAsBytesSync(base64.decode(base64PlistXmlWithComplexDatatypes));
final Map<String, Object> values = parser.parseFile(file.path);
expect(values['CFBundleIdentifier'], 'io.flutter.flutter.app');
expect(values['CFBundleIdentifier'], 'io.flutter.flutter.app');
expect(values['intValue'], 2);
expect(values['doubleValue'], 1.5);
expect(values['binaryValue'], base64.decode('YWJjZA=='));
expect(values['arrayValue'], <dynamic>[true, false, 3]);
expect(values['dateValue'], DateTime.utc(2021, 12, 1, 12, 34, 56));
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
}
......@@ -289,20 +289,26 @@ class FakeStdio extends Stdio {
}
class FakePlistParser implements PlistParser {
final Map<String, dynamic> _underlyingValues = <String, String>{};
FakePlistParser([Map<String, Object>? underlyingValues]):
_underlyingValues = underlyingValues ?? <String, Object>{};
void setProperty(String key, dynamic value) {
final Map<String, Object> _underlyingValues;
void setProperty(String key, Object value) {
_underlyingValues[key] = value;
}
@override
Map<String, dynamic> parseFile(String plistFilePath) {
String? plistXmlContent(String plistFilePath) => throw UnimplementedError();
@override
Map<String, Object> parseFile(String plistFilePath) {
return _underlyingValues;
}
@override
String getValueFromFile(String plistFilePath, String key) {
return _underlyingValues[key] as String;
String? getStringValueFromFile(String plistFilePath, String key) {
return _underlyingValues[key] as String?;
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment