plist_parser.dart 4.87 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

  static const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
  static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
29 30 31 32
  static const String kCFBundleExecutableKey = 'CFBundleExecutable';
  static const String kCFBundleVersionKey = 'CFBundleVersion';
  static const String kCFBundleDisplayNameKey = 'CFBundleDisplayName';
  static const String kMinimumOSVersionKey = 'MinimumOSVersion';
33

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

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