Source code for plistrc.decoders

"""Property list decoders."""

import plistlib
import uuid

from dfdatetime import cocoa_time as dfdatetime_cocoa_time


[docs] class NSKeyedArchiverDecoder: """Decoder for NSKeyedArchiver encoded plists. Also see: https://developer.apple.com/documentation/foundation/nskeyedarchiver """ _CALLBACKS = { "NSArray": "_DecodeNSArray", "NSData": "_DecodeNSData", "NSDate": "_DecodeNSDate", "NSDictionary": "_DecodeNSDictionary", "NSHashTable": "_DecodeNSHashTable", "NSNull": "_DecodeNSNull", "NSObject": "_DecodeCompositeObject", "NSSet": "_DecodeNSArray", "NSString": "_DecodeNSString", "NSURL": "_DecodeNSURL", "NSUUID": "_DecodeNSUUID", } def _DecodeCompositeObject(self, plist_property, objects_array, parent_objects): """Decodes a composite object. Args: plist_property (object): property containing the encoded composite object. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: object: decoded composite object. Raises: RuntimeError: if the composite object cannot be decoded. """ composite_object = {} for key in plist_property: if key == "$class": continue value_plist_property = plist_property.get(key) value_plist_uid = self._GetPlistUID(value_plist_property) if value_plist_uid is None: composite_object[key] = self._DecodeObject( value_plist_property, objects_array, parent_objects ) continue if value_plist_uid in parent_objects: continue parent_objects.append(value_plist_uid) composite_object[key] = self._DecodeObject( objects_array[value_plist_uid], objects_array, parent_objects ) parent_objects.pop(-1) return composite_object def _DecodeNSArray(self, plist_property, objects_array, parent_objects): """Decodes a NSArray or NSSet. Args: plist_property (object): property containing the encoded NSArray or NSSet. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: list[object]: decoded NSArray or NSSet. Raises: RuntimeError: if the NSArray or NSSet cannot be decoded. """ class_name = self._GetClassName(plist_property, objects_array) if "NS.objects" not in plist_property: raise RuntimeError(f"Missing NS.objects in {class_name:s}") ns_objects_property = plist_property["NS.objects"] ns_array = [] for index, ns_object_property in enumerate(ns_objects_property): ns_object_plist_uid = self._GetPlistUID(ns_object_property) if ns_object_plist_uid is None: raise RuntimeError( f"Missing UID in NS.objects[{index:d}] property of {class_name:s}." ) if ns_object_plist_uid in parent_objects: continue ns_object_referenced_property = objects_array[ns_object_plist_uid] parent_objects.append(ns_object_plist_uid) ns_array_element = self._DecodeObject( ns_object_referenced_property, objects_array, parent_objects ) parent_objects.pop(-1) ns_array.append(ns_array_element) return ns_array # pylint: disable=unused-argument def _DecodeNSData(self, plist_property, objects_array, parent_objects): """Decodes a NSData. Args: plist_property (object): property containing the encoded NSData. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: bytes: decoded NSData. Raises: RuntimeError: if the NSData cannot be decoded. """ class_name = self._GetClassName(plist_property, objects_array) if "NS.data" not in plist_property: raise RuntimeError(f"Missing NS.data in {class_name:s}") ns_data = plist_property["NS.data"] if not isinstance(ns_data, bytes): type_string = type(ns_data) raise RuntimeError( f"Unsupported type: {type_string!s} in {class_name:s}.NS.data." ) return ns_data def _DecodeNSDate(self, plist_property, objects_array, parent_objects): """Decodes a NSDate. Args: plist_property (object): property containing the encoded NSDate. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: dfdatetime.CocoaTime: decoded NSDate. Raises: RuntimeError: if the NSDate cannot be decoded. """ class_name = self._GetClassName(plist_property, objects_array) if "NS.time" not in plist_property: raise RuntimeError(f"Missing NS.time in {class_name:s}") ns_time = plist_property["NS.time"] if not isinstance(ns_time, float): type_string = type(ns_time) raise RuntimeError( f"Unsupported type: {type_string!s} in {class_name:s}.NS.time." ) return dfdatetime_cocoa_time.CocoaTime(timestamp=ns_time) # pylint: enable=unused-argument def _DecodeNSDictionary(self, plist_property, objects_array, parent_objects): """Decodes a NSDictionary. Args: plist_property (object): property containing the encoded NSDictionary. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: dict[str, object]: decoded NSDictionary. Raises: RuntimeError: if the NSDictionary cannot be decoded. """ class_name = self._GetClassName(plist_property, objects_array) if "NS.keys" not in plist_property or "NS.objects" not in plist_property: raise RuntimeError(f"Missing NS.keys or NS.objects in {class_name:s}") ns_keys_property = plist_property["NS.keys"] ns_objects_property = plist_property["NS.objects"] if len(ns_keys_property) != len(ns_objects_property): raise RuntimeError( ( f"Mismatch between number of NS.keys and NS.objects in " f"{class_name:s}" ) ) ns_dictionary = {} for index, ns_object_property in enumerate(ns_objects_property): ns_key_property = ns_keys_property[index] ns_key_plist_uid = self._GetPlistUID(ns_key_property) if ns_key_plist_uid is None: raise RuntimeError( f"Missing UID in NS.keys[{index:d}] property of {class_name:s}." ) ns_key_referenced_property = objects_array[ns_key_plist_uid] parent_objects.append(ns_key_plist_uid) ns_key = self._DecodeObject( ns_key_referenced_property, objects_array, parent_objects ) parent_objects.pop(-1) if not ns_key: raise RuntimeError( f"Missing {class_name:s}.NS.keys[{index:d}] with UID: " f"{ns_key_plist_uid:d}." ) if not isinstance(ns_key, str): type_string = type(ns_key) raise RuntimeError( f"Unsupported type: {type_string!s} in {class_name:s}.NS.keys" f"[{index:d}] with UID: {ns_key_plist_uid:d}." ) ns_object_plist_uid = self._GetPlistUID(ns_object_property) if ns_object_plist_uid is None: raise RuntimeError( f"Missing UID in NS.objects[{index:d}] property of {class_name:s}" f".{ns_key:s}." ) if ns_object_plist_uid in parent_objects: continue ns_object_referenced_property = objects_array[ns_object_plist_uid] parent_objects.append(ns_object_plist_uid) ns_dictionary[ns_key] = self._DecodeObject( ns_object_referenced_property, objects_array, parent_objects ) parent_objects.pop(-1) return ns_dictionary def _DecodeNSHashTable(self, plist_property, objects_array, parent_objects): """Decodes a NSHashTable. Args: plist_property (object): property containing the encoded NSHashTable. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: object: decoded NSHashTable. Raises: RuntimeError: if the NSHashTable cannot be decoded. """ class_name = self._GetClassName(plist_property, objects_array) if "$1" not in plist_property: raise RuntimeError(f"Missing $1 in {class_name:s}") value_property = plist_property["$1"] value_plist_uid = self._GetPlistUID(value_property) if value_plist_uid is None: type_string = type(value_property) raise RuntimeError(f"Unsupported {class_name:s}.$1 type: {type_string!s}") if value_plist_uid in parent_objects: raise RuntimeError( (f"{class_name:s}.$1 wth UID: {value_plist_uid:d} in parent objects") ) referenced_property = objects_array[value_plist_uid] if not referenced_property: raise RuntimeError( f"Missing {class_name:s}.$1 with UID: {value_plist_uid:d}." ) # TODO: what about value $0? It seems to indicate the number of elements # in the hash table. # TODO: what about value $2? parent_objects.append(value_plist_uid) ns_hash_table = self._DecodeCompositeObject( referenced_property, objects_array, parent_objects ) parent_objects.pop(-1) return ns_hash_table # pylint: disable=unused-argument def _DecodeNSNull(self, plist_property, objects_array, parent_objects): """Decodes a NSNull. Args: plist_property (object): property containing the encoded NSNull. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: None: decoded NSNull. """ return None # pylint: enable=unused-argument def _DecodeNSObject(self, plist_property, objects_array, parent_objects): """Decodes a NSObject. Args: plist_property (object): property containing the encoded NSObject. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: object: decoded NSObject. Raises: RuntimeError: if the NSObject cannot be decoded. """ class_property = plist_property.get("$class") if not class_property: raise RuntimeError("$class property missing in NSObject.") class_plist_uid = self._GetPlistUID(class_property) if class_plist_uid is None: raise RuntimeError("Missing UID in $class property of NSObject.") referenced_property = objects_array[class_plist_uid] if not referenced_property: raise RuntimeError( f"Missing NSObject.$class with UID: {class_plist_uid:d}." ) class_name = referenced_property.get("$classname") if not class_name: raise RuntimeError( f"$classname property missing in NSObject.$class with UID: " f"{class_plist_uid:d}." ) classes = referenced_property.get("$classes") if not classes: raise RuntimeError( f"$classes property missing in NSObject.$class with UID: " f"{class_plist_uid:d}." ) for name in classes: callback_method = self._CALLBACKS.get(name) if callback_method: break if not callback_method: raise RuntimeError(f"Missing callback for class: {class_name:s}") callback = getattr(self, callback_method, None) return callback(plist_property, objects_array, parent_objects) # pylint: disable=unused-argument def _DecodeNSString(self, plist_property, objects_array, parent_objects): """Decodes a NSString. Args: plist_property (object): property containing the encoded NSString. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: str: decoded NSString. Raises: RuntimeError: if the NSString cannot be decoded. """ class_name = self._GetClassName(plist_property, objects_array) if "NS.string" not in plist_property: raise RuntimeError(f"Missing NS.string in {class_name:s}") ns_string = plist_property["NS.string"] if not isinstance(ns_string, str): type_string = type(ns_string) raise RuntimeError( f"Unsupported type: {type_string!s} in {class_name:s}.NS.string." ) return ns_string def _DecodeNSURL(self, plist_property, objects_array, parent_objects): """Decodes a NSURL. Args: plist_property (object): property containing the encoded NSURL. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: str: decoded NSURL. Raises: RuntimeError: if the NSURL cannot be decoded. """ if "NS.base" not in plist_property or "NS.relative" not in plist_property: raise RuntimeError("Missing NS.base or NS.relative in NSURL") ns_base_plist_uid = self._GetPlistUID(plist_property["NS.base"]) if ns_base_plist_uid is None: raise RuntimeError("Missing UID in NS.base property of NSURL.") ns_base = objects_array[ns_base_plist_uid] if not ns_base: raise RuntimeError( f"Missing NSURL.NS.base with UID: {ns_base_plist_uid:d}." ) if not isinstance(ns_base, str): type_string = type(ns_base) raise RuntimeError( f"Unsupported type: {type_string!s} in NSURL.NS.base with UID: " f"{ns_base_plist_uid:d}." ) ns_relative_plist_uid = self._GetPlistUID(plist_property["NS.relative"]) if ns_relative_plist_uid is None: raise RuntimeError("Missing UID in NS.relative property of NSURL.") ns_relative = objects_array[ns_relative_plist_uid] # Note that ns_relative can be an empty string. if not isinstance(ns_relative, str): type_string = type(ns_relative) raise RuntimeError( f"Unsupported type: {type_string!s} in NSURL.NS.relative with UID: " f"{ns_relative_plist_uid:d}." ) if ns_base == "$null": ns_url = ns_relative else: ns_url = "/".join([ns_base, ns_relative]) return ns_url def _DecodeNSUUID(self, plist_property, objects_array, parent_objects): """Decodes a NSUUID. Args: plist_property (object): property containing the encoded NSUUID. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: str: decoded NSUUID. Raises: RuntimeError: if the NSUUID cannot be decoded. """ if "NS.uuidbytes" not in plist_property: raise RuntimeError("Missing NS.uuidbytes") ns_uuidbytes = plist_property["NS.uuidbytes"] if len(ns_uuidbytes) != 16: raise RuntimeError("Unsupported NS.uuidbytes size") return str(uuid.UUID(bytes=ns_uuidbytes)) # pylint: enable=unused-argument def _DecodeObject(self, plist_property, objects_array, parent_objects): """Decodes an object. Args: plist_property (object): property containing the encoded NSUUID. objects_array (list[object]): $objects array. parent_objects (list[int]): parent object UIDs. Returns: object: decoded object. Raises: RuntimeError: if the object cannot be decoded. """ if isinstance(plist_property, dict) and "$class" in plist_property: return self._DecodeNSObject(plist_property, objects_array, parent_objects) if isinstance(plist_property, str) and plist_property == "$null": return None return plist_property def _GetClassName(self, plist_property, objects_array): """Retrieves a class name. Args: plist_property (object): property containing the $class property. objects_array (list[object]): $objects array. Returns: str: class name or None if not available. Raises: RuntimeError: if the class name cannot be retrieved. """ class_property = plist_property.get("$class") if not class_property: raise RuntimeError("Missing $class property.") class_plist_uid = self._GetPlistUID(class_property) if class_plist_uid is None: raise RuntimeError("Missing UID in $class property.") referenced_property = objects_array[class_plist_uid] if not referenced_property: raise RuntimeError(f"Missing class with UID: {class_plist_uid:d}.") class_name = referenced_property.get("$classname") if not class_name: raise RuntimeError( f"$classname property missing in class with UID: " f"{class_plist_uid:d}." ) return class_name def _GetPlistUID(self, plist_property): """Retrieves a plist UID. Args: plist_property (object): property containing the plist UID. Returns: int: plist UID or None if not available. """ if isinstance(plist_property, plistlib.UID): return plist_property.data if ( isinstance(plist_property, dict) and "CF$UID" in plist_property and len(plist_property) == 1 ): return plist_property["CF$UID"] return None
[docs] def Decode(self, root_item): """Decodes a NSKeyedArchiver encoded plist. Args: root_item (object): root object of the NSKeyedArchiver encoded plist. Returns: dict[str, object]: root object of the decoded plist. Raises: RuntimeError: if the plist cannot be decoded. """ if not isinstance(root_item, dict): type_string = type(root_item) raise RuntimeError( f"Unsupported plist: unsupported root item type: {type_string!s}." ) archiver = root_item.get("$archiver") version = root_item.get("$version") if archiver != "NSKeyedArchiver" or version != 100000: raise RuntimeError(f"Unsupported plist: {archiver!s} {version!s}") decoded_object = {} objects_array = root_item.get("$objects") or [] top_property = root_item.get("$top") or {} for name, value_property in top_property.items(): value_plist_uid = self._GetPlistUID(value_property) if value_plist_uid is None: decoded_object[name] = value_property continue value_referenced_property = objects_array[value_plist_uid] if not value_referenced_property: raise RuntimeError( f'Missing $top["{name:s}"] with UID: {value_plist_uid:d}.' ) decoded_object[name] = self._DecodeObject( value_referenced_property, objects_array, [value_plist_uid] ) # The root $top appears to be internal only to the NSKeyedArchiver encoded # plist. if len(decoded_object) == 1: decoded_object = decoded_object.get("root", decoded_object) return decoded_object
[docs] def IsEncoded(self, root_item): """Determines if a plist is NSKeyedArchiver encoded. Args: root_item (object): root object of the NSKeyedArchiver encoded plist. Returns: bool: True if NSKeyedArchiver encoded, False otherwise. """ if not isinstance(root_item, dict): return False archiver = root_item.get("$archiver") version = root_item.get("$version") return archiver == "NSKeyedArchiver" and version == 100000