plist_parser.dart 4.71 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 28 29 30

  static const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
  static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
  static const String kCFBundleExecutable = 'CFBundleExecutable';

31 32
  /// Returns the content, converted to XML, of the plist file located at
  /// [plistFilePath].
33 34
  ///
  /// If [plistFilePath] points to a non-existent file or a file that's not a
35
  /// valid property list file, this will return null.
36 37
  ///
  /// The [plistFilePath] argument must not be null.
38
  String? plistXmlContent(String plistFilePath) {
39
    const String executable = '/usr/bin/plutil';
40
    if (!_fileSystem.isFileSync(executable)) {
41
      throw const FileNotFoundException(executable);
42
    }
43 44 45
    final List<String> args = <String>[
      executable, '-convert', 'xml1', '-o', '-', plistFilePath,
    ];
46
    try {
47
      final String xmlContent = _processUtils.runSync(
48 49 50
        args,
        throwOnError: true,
      ).stdout.trim();
51
      return xmlContent;
52
    } on ProcessException catch (error) {
53
      _logger.printTrace('$error');
54 55 56 57 58 59 60 61 62 63 64 65 66 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 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
      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);
128
    }
129
    return null;
130 131
  }

132 133
  /// Parses the Plist file located at [plistFilePath] and returns the string
  /// value that's associated with the specified [key] within the property list.
134 135 136 137 138 139 140
  ///
  /// 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.
  ///
  /// The [plistFilePath] and [key] arguments must not be null.
141
  String? getStringValueFromFile(String plistFilePath, String key) {
142
    final Map<String, dynamic> parsed = parseFile(plistFilePath);
143
    return parsed[key] as String?;
144 145
  }
}