plist_parser.dart 5.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:process/process.dart';
6
import 'package:xml/xml.dart';
7

8
import '../base/file_system.dart';
9
import '../base/io.dart';
10
import '../base/logger.dart';
11 12 13 14
import '../base/process.dart';
import '../convert.dart';

class PlistParser {
15
  PlistParser({
16 17 18
    required FileSystem fileSystem,
    required Logger logger,
    required ProcessManager processManager,
19 20 21 22 23 24 25
  }) : _fileSystem = fileSystem,
       _logger = logger,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager);

  final FileSystem _fileSystem;
  final Logger _logger;
  final ProcessUtils _processUtils;
26

27
  // info.pList keys
28 29
  static const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
  static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
30 31 32
  static const String kCFBundleExecutableKey = 'CFBundleExecutable';
  static const String kCFBundleVersionKey = 'CFBundleVersion';
  static const String kCFBundleDisplayNameKey = 'CFBundleDisplayName';
33
  static const String kCFBundleNameKey = 'CFBundleName';
34
  static const String kFLTEnableImpellerKey = 'FLTEnableImpeller';
35
  static const String kMinimumOSVersionKey = 'MinimumOSVersion';
36 37
  static const String kNSPrincipalClassKey = 'NSPrincipalClass';

38 39 40
  // entitlement file keys
  static const String kAssociatedDomainsKey = 'com.apple.developer.associated-domains';

41
  static const String _plutilExecutable = '/usr/bin/plutil';
42

43 44
  /// Returns the content, converted to XML, of the plist file located at
  /// [plistFilePath].
45 46
  ///
  /// If [plistFilePath] points to a non-existent file or a file that's not a
47 48
  /// valid property list file, this will return null.
  String? plistXmlContent(String plistFilePath) {
49 50
    if (!_fileSystem.isFileSync(_plutilExecutable)) {
      throw const FileNotFoundException(_plutilExecutable);
51
    }
52
    final List<String> args = <String>[
53
      _plutilExecutable, '-convert', 'xml1', '-o', '-', plistFilePath,
54
    ];
55
    try {
56
      final String xmlContent = _processUtils.runSync(
57 58 59
        args,
        throwOnError: true,
      ).stdout.trim();
60
      return xmlContent;
61
    } on ProcessException catch (error) {
62
      _logger.printError('$error');
63 64 65 66
      return null;
    }
  }

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
  /// Replaces the string key in the given plist file with the given value.
  ///
  /// If the value is null, then the key will be removed.
  ///
  /// Returns true if successful.
  bool replaceKey(String plistFilePath, {required String key, String? value }) {
    if (!_fileSystem.isFileSync(_plutilExecutable)) {
      throw const FileNotFoundException(_plutilExecutable);
    }
    final List<String> args;
    if (value == null) {
      args = <String>[
        _plutilExecutable, '-remove', key, plistFilePath,
      ];
    } else {
      args = <String>[
        _plutilExecutable, '-replace', key, '-string', value, plistFilePath,
      ];
    }
    try {
      _processUtils.runSync(
        args,
        throwOnError: true,
      );
    } on ProcessException catch (error) {
      _logger.printError('$error');
      return false;
    }
    return true;
  }

98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
  /// 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.
  Map<String, Object> parseFile(String plistFilePath) {
    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') {
131
          lastKey = child.innerText;
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
        } 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':
148
        return node.innerText;
149
      case 'real':
150
        return double.parse(node.innerText);
151
      case 'integer':
152
        return int.parse(node.innerText);
153 154 155 156 157
      case 'true':
        return true;
      case 'false':
        return false;
      case 'date':
158
        return DateTime.parse(node.innerText);
159
      case 'data':
160
        return base64.decode(node.innerText.replaceAll(_nonBase64Pattern, ''));
161 162 163 164
      case 'array':
        return node.children.whereType<XmlElement>().map<Object?>(_parseXmlNode).whereType<Object>().toList();
      case 'dict':
        return _parseXmlDict(node);
165
    }
166
    return null;
167 168
  }

169 170
  /// Parses the Plist file located at [plistFilePath] and returns the value
  /// that's associated with the specified [key] within the property list.
171 172 173 174 175
  ///
  /// If [plistFilePath] points to a non-existent file or a file that's not a
  /// valid property list file, this will return null.
  ///
  /// If [key] is not found in the property list, this will return null.
176
  T? getValueFromFile<T>(String plistFilePath, String key) {
177
    final Map<String, dynamic> parsed = parseFile(plistFilePath);
178
    return parsed[key] as T?;
179 180
  }
}