PK!k*protool/.pylintrc[MASTER] # Pickle collected data for later comparisons. persistent=yes [REPORTS] # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio).You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=parseable [FORMAT] # Maximum number of characters on a single line. max-line-length=120 [DESIGN] # Maximum number of arguments for function / method max-args=6 # Minimum number of public methods for a class (see R0903). min-public-methods=0 PK!Uؼ!!protool/__init__.py#!/usr/bin/env python3 """A utility for dealing with provisioning profiles""" from enum import Enum import copy import os import plistlib import shutil import subprocess import sys import tempfile class ProvisioningType(Enum): """Enum representing the type of provisioning profile.""" IOS_DEVELOPMENT = 1 APP_STORE_DISTRIBUTION = 3 AD_HOC_DISTRIBUTION = 5 ENTERPRISE_DISTRIBUTION = 7 class ProvisioningProfile(object): """Represents a provisioning profile.""" def __init__(self, file_path): self.file_path = os.path.abspath(file_path) self.file_name = os.path.basename(self.file_path) self.xml = None self._contents = None self._load_xml() self._load_contents_dict() self._parse_contents() def contents(self): """Return a copy of the content dict.""" return copy.deepcopy(self._contents) @property def profile_type(self): if self.provisions_all_devices: return ProvisioningType.ENTERPRISE_DISTRIBUTION elif not self.entitlements.get("get-task-allow") and self.provisioned_devices: return ProvisioningType.AD_HOC_DISTRIBUTION elif not self.entitlements.get("get-task-allow") and not self.provisioned_devices: return ProvisioningType.APP_STORE_DISTRIBUTION elif self.entitlements.get("get-task-allow") and self.provisioned_devices: return ProvisioningType.IOS_DEVELOPMENT def _parse_contents(self): """Parse the contents of the profile.""" self.app_id_name = self._contents.get("AppIDName") self.application_identifier_prefix = self._contents.get("ApplicationIdentifierPrefix") self.creation_date = self._contents.get("CreationDate") self.platform = self._contents.get("Platform") self.developer_certificates = self._contents.get("DeveloperCertificates") self.entitlements = self._contents.get("Entitlements") self.expiration_date = self._contents.get("ExpirationDate") self.name = self._contents.get("Name") self.team_identifier = self._contents.get("TeamIdentifier") self.team_name = self._contents.get("TeamName") self.time_to_live = self._contents.get("TimeToLive") self.uuid = self._contents.get("UUID") self.version = self._contents.get("Version") self.provisioned_devices = self._contents.get("ProvisionedDevices") self.provisions_all_devices = True if self._contents.get("ProvisionsAllDevices") else False def _load_xml(self): """Load the XML contents of a provisioning profile.""" if not os.path.exists(self.file_path): raise Exception(f"File does not exist: {self.file_path}") security_cmd = f'security cms -D -i "{self.file_path}" 2> /dev/null' self.xml = subprocess.check_output( security_cmd, universal_newlines=True, shell=True ).strip() def _load_contents_dict(self): """Return the contents of a provisioning profile.""" self._contents = plistlib.loads(self.xml.encode()) def profiles(profiles_dir = None): """Returns a list of all currently installed provisioning profiles.""" if profiles_dir: dir_path = os.path.expanduser(profiles_dir) else: user_path = os.path.expanduser('~') dir_path = os.path.join(user_path, "Library", "MobileDevice", "Provisioning Profiles") profiles = [] for profile in os.listdir(dir_path): full_path = os.path.join(dir_path, profile) _, ext = os.path.splitext(full_path) if ext == ".mobileprovision": provisioning_profile = ProvisioningProfile(full_path) profiles.append(provisioning_profile) return profiles def diff(a_path, b_path, ignore_keys=None, tool_override=None): """Diff two provisioning profiles.""" if tool_override is None: diff_tool = "opendiff" else: diff_tool = tool_override profile_a = ProvisioningProfile(a_path) profile_b = ProvisioningProfile(b_path) if ignore_keys is None: a_xml = profile_a.xml b_xml = profile_b.xml else: a_dict = profile_a.contents() b_dict = profile_b.contents() for key in ignore_keys: try: del a_dict[key] except: pass try: del b_dict[key] except: pass a_xml = plistlib.dumps(a_dict) b_xml = plistlib.dumps(b_dict) temp_dir = tempfile.mkdtemp() a_temp_path = os.path.join(temp_dir, profile_a.file_name) b_temp_path = os.path.join(temp_dir, profile_b.file_name) with open(a_temp_path, 'w') as temp_profile: temp_profile.write(a_xml) with open(b_temp_path, 'w') as temp_profile: temp_profile.write(b_xml) # We deliberately don't wrap the tool so that arguments work as well diff_command = '%s "%s" "%s"' % (diff_tool, a_temp_path, b_temp_path) try: diff_contents = subprocess.check_output( diff_command, universal_newlines=True, shell=True ).strip() except subprocess.CalledProcessError as ex: # Diff tools usually return a non-0 exit code if there are differences, # so we just swallow this error diff_contents = ex.output # Cleanup shutil.rmtree(temp_dir) return diff_contents def value_for_key(profile_path, key): """Return the value for a given key""" profile = ProvisioningProfile(profile_path) try: value = profile.contents()[key] return value except KeyError: return None def decode(profile_path, xml=True): """Decode a profile, returning as a dictionary if xml is set to False.""" profile = ProvisioningProfile(profile_path) if xml: return profile.xml else: return profile.contents() if __name__ == "__main__": print("This should only be used as a module.") sys.exit(1) PK!TTprotool/command_line.py#!/usr/bin/env python3 import argparse import json import os import sys import protool def _handle_diff(args): """Handle the diff sub command.""" if len(args.profiles) != 2: print("Expected 2 profiles for diff command") sys.exit(1) try: print(protool.diff(args.profiles[0], args.profiles[1], ignore_keys=args.ignore, tool_override=args.tool)) except Exception as ex: print(f"Could not diff: {ex}", file=sys.stderr) def _handle_git_diff(args): """Handle the gitdiff sub command.""" try: print(protool.diff(args.git_args[1], args.git_args[4], ignore_keys=args.ignore, tool_override=args.tool)) except Exception as ex: print(f"Could not diff: {ex}", file=sys.stderr) def _handle_read(args): """Handle the read sub command.""" try: value = protool.value_for_key(args.profile, args.key) except Exception as ex: print(f"Could not read file: {ex}", file=sys.stderr) regular_types = [str, int, float] found_supported_type = False for regular_type in regular_types: if isinstance(value, regular_type): found_supported_type = True print(value) break if not found_supported_type: try: result = json.dumps(value) except: print("Unable to serialize values. Please use the XML format instead.", file=sys.stderr) sys.exit(1) print(result) def _handle_decode(args): """Handle the decode sub command.""" try: print(protool.decode(args.profile)) except Exception as ex: print(f"Could not decode: {ex}", file=sys.stderr) def _handle_arguments(): """Handle command line arguments and call the correct method.""" print(sys.argv) parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() diff_parser = subparsers.add_parser('diff', help="Perform a diff between two profiles") diff_parser.add_argument("-i", "--ignore", dest="ignore", action="store", nargs='+', default=None, help='A list of keys to ignore. e.g. --ignore TimeToLive UUID') diff_parser.add_argument("-t", "--tool", dest="tool", action="store", default=None, help='Specify a diff command to use. It should take two file paths as the final two arguments. Defaults to opendiff') diff_parser.add_argument("-p", "--profiles", dest="profiles", action="store", nargs=2, required=True, help='The two profiles to diff') diff_parser.set_defaults(subcommand="diff") diff_parser = subparsers.add_parser('gitdiff', help="Perform a diff between two profiles with the git diff parameters") diff_parser.add_argument("-i", "--ignore", dest="ignore", action="store", nargs='+', default=None, help='A list of keys to ignore. e.g. --ignore TimeToLive UUID') diff_parser.add_argument("-t", "--tool", dest="tool", action="store", default=None, help='Specify a diff command to use. It should take two file paths as the final two arguments. Defaults to opendiff') diff_parser.add_argument("-g", "--git-args", dest="git_args", action="store", nargs=7, required=True, help='The arguments from git') diff_parser.set_defaults(subcommand="gitdiff") read_parser = subparsers.add_parser('read', help="Read the value from a profile using the key specified command") read_parser.add_argument("-p", "--profile", dest="profile", action="store", required=True, help='The profile to read the value from') read_parser.add_argument("-k", "--key", dest="key", action="store", required=True, help='The key to read the value for') read_parser.set_defaults(subcommand="read") decode_parser = subparsers.add_parser('decode', help="Decode a provisioning profile and display in a readable format") decode_parser.add_argument("-p", "--profile", dest="profile", action="store", required=True, help='The profile to read the value from') decode_parser.set_defaults(subcommand="decode") args = parser.parse_args() try: _ = args.subcommand except: parser.print_help() sys.exit(1) if args.subcommand == "diff": _handle_diff(args) elif args.subcommand == "gitdiff": _handle_git_diff(args) elif args.subcommand == "read": _handle_read(args) elif args.subcommand == "decode": _handle_decode(args) def run(): _handle_arguments() if __name__ == "__main__": _handle_arguments() PK!Hb04(protool-0.7.0.dist-info/entry_points.txtN+I/N.,()*(/ϱVy)9yzEy\\PK!/`GGprotool-0.7.0.dist-info/LICENSEMIT License Copyright (c) Microsoft Corporation. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HnHTUprotool-0.7.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!Ho protool-0.7.0.dist-info/METADATAV[o6~8K^A^mFvƋCPDI\)Q%);;Own 3x0V:sW"hbճu-۪K`t 6 d]F֒$?reJ /Pq\c8.ݮTW\F[xz!SQ[DϯhdGy0(x(HmTg" G,&S; h{K@Uk5AN+Fp! U‰\ *B㮵$B;سz-=A'Z s^.vD6NnPp[" q*Y$j[-r哖dPR 7U7qP: f t^ kuX"C}9{p,=?8yQ[y*؁f,:c_OsZ;rEZOHAZYO!?MwZq8iguhRZN"<#_ĻmBX̵u{ ӯbtKA`6đ0]99Mu IIעgI%O] h-qgav\r6jk7 <-Ķ } @o]Wyx1,wW!-)N5̫aQ w}ċ+>#6QJR7`C sxQ|fL_~xuGF '"0>Z\ے$Fhjh&L7Nv=p KC'TDX:xAO, jyi(6%.}=vǓK<Ũr 8Wׯud>n|h3$vPK!H Kjprotool-0.7.0.dist-info/RECORDuIv@ydPJ) ESPBnz኶1 +_7_(ID FF0MQ&8J8kȂQ27-w;^υ/xW_+&eV z=KxhLv` h ҧ}mTL8. smFDeXnθY;*#+xD5yw3Ӏg:Zڕ&v)$=@.y|8Ms\aٝc.*KtBoq\gJn`BTg,B&bAsll[rZKAC(;Gzi=0L۩L-!=SV'px`DN\80PK!k*protool/.pylintrcPK!Uؼ!!@protool/__init__.pyPK!TT큒protool/command_line.pyPK!Hb04(,protool-0.7.0.dist-info/entry_points.txtPK!/`GG,protool-0.7.0.dist-info/LICENSEPK!HnHTU1protool-0.7.0.dist-info/WHEELPK!Ho 1protool-0.7.0.dist-info/METADATAPK!H Kj6protool-0.7.0.dist-info/RECORDPKM8