1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:process/process.dart';
6import 'package:xml/xml.dart';
7
8import '../base/file_system.dart';
9import '../base/io.dart';
10import '../base/logger.dart';
11import '../base/process.dart';
12import '../convert.dart';
13
14class PlistParser {
15 PlistParser({
16 required FileSystem fileSystem,
17 required Logger logger,
18 required ProcessManager processManager,
19 }) : _fileSystem = fileSystem,
20 _logger = logger,
21 _processUtils = ProcessUtils(logger: logger, processManager: processManager);
22
23 final FileSystem _fileSystem;
24 final Logger _logger;
25 final ProcessUtils _processUtils;
26
27 // info.pList keys
28 static const kCFBundleIdentifierKey = 'CFBundleIdentifier';
29 static const kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
30 static const kCFBundleExecutableKey = 'CFBundleExecutable';
31 static const kCFBundleVersionKey = 'CFBundleVersion';
32 static const kCFBundleDisplayNameKey = 'CFBundleDisplayName';
33 static const kCFBundleNameKey = 'CFBundleName';
34 static const kFLTEnableImpellerKey = 'FLTEnableImpeller';
35 static const kFLTEnableFlutterGpuKey = 'FLTEnableFlutterGpu';
36 static const kMinimumOSVersionKey = 'MinimumOSVersion';
37 static const kNSPrincipalClassKey = 'NSPrincipalClass';
38
39 // entitlement file keys
40 static const kAssociatedDomainsKey = 'com.apple.developer.associated-domains';
41
42 static const _plutilExecutable = '/usr/bin/plutil';
43
44 /// Returns the content, converted to XML, of the plist file located at
45 /// [plistFilePath].
46 ///
47 /// If [plistFilePath] points to a non-existent file or a file that's not a
48 /// valid property list file, this will return null.
49 String? plistXmlContent(String plistFilePath) {
50 if (!_fileSystem.isFileSync(_plutilExecutable)) {
51 throw const FileNotFoundException(_plutilExecutable);
52 }
53 final args = <String>[_plutilExecutable, '-convert', 'xml1', '-o', '-', plistFilePath];
54 try {
55 final String xmlContent = _processUtils.runSync(args, throwOnError: true).stdout.trim();
56 return xmlContent;
57 } on ProcessException catch (error) {
58 _logger.printError('$error');
59 return null;
60 }
61 }
62
63 /// Returns the content, converted to JSON, of the plist file located at
64 /// [filePath].
65 ///
66 /// If [filePath] points to a non-existent file or a file that's not a
67 /// valid property list file, this will return null.
68 String? plistJsonContent(String filePath) {
69 if (!_fileSystem.isFileSync(_plutilExecutable)) {
70 throw const FileNotFoundException(_plutilExecutable);
71 }
72 final args = <String>[_plutilExecutable, '-convert', 'json', '-o', '-', filePath];
73 try {
74 final String jsonContent = _processUtils.runSync(args, throwOnError: true).stdout.trim();
75 return jsonContent;
76 } on ProcessException catch (error) {
77 _logger.printError('$error');
78 return null;
79 }
80 }
81
82 /// Replaces the string key in the given plist file with the given value.
83 ///
84 /// If the value is null, then the key will be removed.
85 ///
86 /// Returns true if successful.
87 bool replaceKey(String plistFilePath, {required String key, String? value}) {
88 if (!_fileSystem.isFileSync(_plutilExecutable)) {
89 throw const FileNotFoundException(_plutilExecutable);
90 }
91 final List<String> args;
92 if (value == null) {
93 args = <String>[_plutilExecutable, '-remove', key, plistFilePath];
94 } else {
95 args = <String>[_plutilExecutable, '-replace', key, '-string', value, plistFilePath];
96 }
97 try {
98 _processUtils.runSync(args, throwOnError: true);
99 } on ProcessException catch (error) {
100 _logger.printError('$error');
101 return false;
102 }
103 return true;
104 }
105
106 /// Parses the plist file located at [plistFilePath] and returns the
107 /// associated map of key/value property list pairs.
108 ///
109 /// If [plistFilePath] points to a non-existent file or a file that's not a
110 /// valid property list file, this will return an empty map.
111 Map<String, Object> parseFile(String plistFilePath) {
112 if (!_fileSystem.isFileSync(plistFilePath)) {
113 return const <String, Object>{};
114 }
115
116 final String normalizedPlistPath = _fileSystem.path.absolute(plistFilePath);
117
118 final String? xmlContent = plistXmlContent(normalizedPlistPath);
119 if (xmlContent == null) {
120 return const <String, Object>{};
121 }
122
123 return _parseXml(xmlContent);
124 }
125
126 Map<String, Object> _parseXml(String xmlContent) {
127 final document = XmlDocument.parse(xmlContent);
128 // First element child is . The first element child of plist is .
129 final XmlElement dictObject = document.firstElementChild!.firstElementChild!;
130 return _parseXmlDict(dictObject);
131 }
132
133 Map<String, Object> _parseXmlDict(XmlElement node) {
134 String? lastKey;
135 final result = <String, Object>{};
136 for (final XmlNode child in node.children) {
137 if (child is XmlElement) {
138 if (child.name.local == 'key') {
139 lastKey = child.innerText;
140 } else {
141 assert(lastKey != null);
142 result[lastKey!] = _parseXmlNode(child)!;
143 lastKey = null;
144 }
145 }
146 }
147
148 return result;
149 }
150
151 static final _nonBase64Pattern = RegExp('[^a-zA-Z0-9+/=]+');
152
153 Object? _parseXmlNode(XmlElement node) {
154 return switch (node.name.local) {
155 'string' => node.innerText,
156 'real' => double.parse(node.innerText),
157 'integer' => int.parse(node.innerText),
158 'true' => true,
159 'false' => false,
160 'date' => DateTime.parse(node.innerText),
161 'data' => base64.decode(node.innerText.replaceAll(_nonBase64Pattern, '')),
162 'array' =>
163 node.children
164 .whereType<XmlElement>()
165 .map<Object?>(_parseXmlNode)
166 .whereType<Object>()
167 .toList(),
168 'dict' => _parseXmlDict(node),
169 _ => null,
170 };
171 }
172
173 /// Parses the Plist file located at [plistFilePath] and returns the value
174 /// that's associated with the specified [key] within the property list.
175 ///
176 /// If [plistFilePath] points to a non-existent file or a file that's not a
177 /// valid property list file, this will return null.
178 ///
179 /// If [key] is not found in the property list, this will return null.
180 T? getValueFromFile<T>(String plistFilePath, String key) {
181 final Map<String, dynamic> parsed = parseFile(plistFilePath);
182 return parsed[key] as T?;
183 }
184}
185