def _convert_CFUID_to_UID(plist, use_plistlib=False): ''' For converting XML plists to binary, UIDs which are represented as strings 'CF$UID' must be translated to actual UIDs. ''' if isinstance(plist, dict): for k, v in plist.items(): if isinstance(v, dict): num = v.get('CF$UID', None) if (num is None) or (not isinstance(num, int)): _convert_CFUID_to_UID(v, use_plistlib) else: if use_plistlib: plist[k] = plistlib.UID(num) else: plist[k] = biplist.Uid(num) elif isinstance(v, list): _convert_CFUID_to_UID(v, use_plistlib) else: # list for index, v in enumerate(plist): if isinstance(v, dict): num = v.get('CF$UID', None) if (num is None) or (not isinstance(num, int)): _convert_CFUID_to_UID(v, use_plistlib) else: if use_plistlib: plist[index] = plistlib.UID(num) else: plist[index] = biplist.Uid(num) elif isinstance(v, list): _convert_CFUID_to_UID(v, use_plistlib)
def archive(self, obj) -> plistlib.UID: "Add the encoded form of obj to the archive, returning the UID of obj." if obj is None: return NULL_UID # the ref_map allows us to avoid infinite recursion caused by # cycles in the object graph by functioning as a sort of promise ref = self.ref_map.get(id(obj)) if ref: return ref index = plistlib.UID(len(self.objects)) self.ref_map[id(obj)] = index cls = obj.__class__ if cls in Archive.primitive_types: self.objects.append(obj) return index archive_obj: Dict[str, object] = {} self.objects.append(archive_obj) self.encode_top_level(obj, archive_obj) return index
def uid_for_archiver(self, archiver: type) -> plistlib.UID: """ Ensure the class definition for the archiver is included in the arcive. Non-primitive objects are encoded as a dictionary of key-value pairs; there is always a $class key, which has a UID value...the UID is itself a pointer/index which points to the definition of the class (which is also in the archive). This method makes sure that all the metadata is included in the archive exactly once (no duplicates class metadata). """ val = self.class_map.get(archiver) if val: return val val = plistlib.UID(len(self.objects)) self.class_map[archiver] = val # TODO: this is where we might need to include the full class ancestry; # though the open source code from apple does not appear to check self.objects.append({'$classes': [archiver], '$classname': archiver}) return val
def create_color_entry(colors): rgb = rgb_dict_to_rgb_bytes(colors) plist = { "$version": 100000, "$objects": [ "$null", { "NSRGB": rgb, "NSColorSpace": 2, "$class": plistlib.UID(2) }, { "$classname": "NSColor", "$classes": ["NSColor", "NSObject"] } ], "$archiver": "NSKeyedArchiver", "$top": { "root": plistlib.UID(1) } } return plistlib.dumps(plist, fmt=plistlib.FMT_BINARY, sort_keys=False)
def to_bytes(self) -> bytes: "Generate the archive and return it as a bytes blob" # avoid regenerating if len(self.objects) == 1: self.archive(self.input) d = { '$archiver': 'NSKeyedArchiver', '$version': NSKeyedArchiveVersion, '$objects': self.objects, '$top': { 'root': plistlib.UID(1) } } return plistlib.dumps(d, fmt=plistlib.FMT_BINARY) # pylint: disable=no-member
from typing import Mapping, Dict from bpylist.archive_types import timestamp, NSMutableData if sys.version_info < (3, 8, 0): from . import _plistlib as plistlib else: import plistlib # type: ignore # The magic number which Cocoa uses as an implementation version. # I don' think there were 99_999 previous implementations, I think # Apple just likes to store a lot of zeros NSKeyedArchiveVersion = 100_000 # Cached for convenience NULL_UID = plistlib.UID(0) def unarchive(plist: bytes) -> object: "Unpack an NSKeyedArchived byte blob into a more useful object tree." return Unarchive(plist).top_object() def unarchive_file(path: str) -> object: """Loads an archive from a file path.""" with open(path, 'rb') as fd: return unarchive(fd.read()) def archive(obj: object) -> bytes: "Pack an object tree into an NSKeyedArchived blob."
def add_supported_app( app_config: Dict[str, Any], source_app: AppInfo, target_apps: Sequence[AppInfo], ) -> None: def fix_uuids( copy_from: int, copy_to: int, previous_length: int, key: Any, value: Any, ) -> plistlib.UID: if isinstance(value, plistlib.UID): value.data = compute_new_uid(copy_from, copy_to, previous_length, value.data) return None activation_group_cond = app_config["BTTActivationGroupCondition"] # Condition is a base64-encoded binary plist parsed_plist = plistlib.loads( base64.urlsafe_b64decode(activation_group_cond)) # Note to future self: I have no clue how plist's work - just what I gathered # from reading and reversing the existing file # # figure out the right operators try: center = parsed_plist["$objects"].index(source_app.bundle_name) use_bundle = True except: center = parsed_plist["$objects"].index(source_app.app_name) use_bundle = False # start searching back from the located bundle / app name # keep track of any index with a forward ref to our bundle/app name # or a forward ref to another item that has one to it (transitive) idxs_to_search = [center] for i in range(center, 0, -1): obj_at_pos = parsed_plist["$objects"][i] if not isinstance(obj_at_pos, dict): continue for k, v in obj_at_pos.items(): # if the curre if isinstance(v, plistlib.UID): if v.data in idxs_to_search: # Keep track of the new forward ref idxs_to_search.append(i) copy_from = idxs_to_search[-1] # The first item in our predicate will have forward refs to all the required bits # including the predicate # transitively search for it copy_to = max((v.data for k, v in parsed_plist["$objects"][copy_from].items() if isinstance(v, plistlib.UID))) # search for potential forward refs from copy_to onwards while True: if not isinstance(parsed_plist["$objects"][copy_to], dict): break new_max = max((v.data for k, v in parsed_plist["$objects"][copy_to].items() if isinstance(v, plistlib.UID))) if new_max <= copy_to: break copy_to = new_max root_levels_to_add = [] for target_app in target_apps: new_items = copy.deepcopy(parsed_plist["$objects"][copy_from:(copy_to + 1)]) previous_length = len(parsed_plist["$objects"]) print(f"Adding {target_app} - starting at ID {previous_length}") root_levels_to_add.append(previous_length) fix_callable = partial(fix_uuids, copy_from, copy_to, previous_length) for item_pos, item in enumerate(new_items): if isinstance(item, plistlib.UID): fix_callable(None, item) elif isinstance(item, (list, dict)): recursive_modify_collection(item, fix_callable) elif isinstance(item, str): if use_bundle and item == source_app.bundle_name: new_items[item_pos] = target_app.bundle_name elif not use_bundle and item == source_app.app_name: new_items[item_pos] = target_app.app_name parsed_plist["$objects"].extend(new_items) # find top level pointer to all the apps for item_pos, item in enumerate(parsed_plist["$objects"]): if not isinstance(item, dict): continue if "NS.objects" not in item: continue # search for our minimum range - i.e. the first item that denoted the app entry we copied if plistlib.UID(copy_from) in item["NS.objects"]: for new_root in root_levels_to_add: item["NS.objects"].append(plistlib.UID(new_root)) break else: raise ValueError( "Could not append new app - could not locate root level list") print("Added to root tree - dumping plist and we'll be done") app_config["BTTActivationGroupCondition"] = base64.standard_b64encode( plistlib.dumps( parsed_plist, fmt=plistlib.FMT_BINARY, sort_keys=True, )).decode("ascii")