def test_uid(self): self.generate_and_parse(uid(1)) self.generate_and_parse(uid(255)) self.generate_and_parse(uid(256)) self.generate_and_parse(uid(32_767)) self.generate_and_parse(uid(65_535)) self.generate_and_parse(uid(4_000_000_000))
def archive(self, obj) -> 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 = 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 = {} self.objects.append(archive_obj) self.encode_top_level(obj, archive_obj) return index
def uid_for_archiver(self, archiver: type) -> 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 = 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 test_circular_ref(self): foo = FooArchive('herp', timestamp(9001), 42, ['strawberries', 'dragonfruit'], {'key': 'value'}, False, None) foo.recursive = foo plist = bplist.parse(archiver.archive(foo)) foo_obj = plist['$objects'][1] self.assertEqual(uid(1), foo_obj['recurse'])
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': uid(1) } } return bplist.generate(d)
from bpylist import bplist from bpylist.archive_types import timestamp, uid from typing import Mapping # 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 = 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: "A convenience for unarchive(plist) which loads an archive from a file for you" with open(path, 'rb') as fd: return unarchive(fd.read()) def archive(obj: object) -> bytes: "Pack an object tree into an NSKeyedArchived blob." return Archive(obj).to_bytes() class ArchiverError(Exception): pass