diff options
Diffstat (limited to 'src/3rdparty/python/lib/python3.9/site-packages/mac_alias/bookmark.py')
-rw-r--r-- | src/3rdparty/python/lib/python3.9/site-packages/mac_alias/bookmark.py | 677 |
1 files changed, 677 insertions, 0 deletions
diff --git a/src/3rdparty/python/lib/python3.9/site-packages/mac_alias/bookmark.py b/src/3rdparty/python/lib/python3.9/site-packages/mac_alias/bookmark.py new file mode 100644 index 000000000..00e16feb1 --- /dev/null +++ b/src/3rdparty/python/lib/python3.9/site-packages/mac_alias/bookmark.py @@ -0,0 +1,677 @@ +# This file implements the Apple "bookmark" format, which is the replacement +# for the old-fashioned alias format. The details of this format were +# reverse engineered; some things are still not entirely clear. +# +import datetime +import os +import struct +import sys +import uuid +from urllib.parse import urljoin + +if sys.platform == "darwin": + from . import osx + +from .utils import osx_epoch + +BMK_DATA_TYPE_MASK = 0xFFFFFF00 +BMK_DATA_SUBTYPE_MASK = 0x000000FF + +BMK_STRING = 0x0100 +BMK_DATA = 0x0200 +BMK_NUMBER = 0x0300 +BMK_DATE = 0x0400 +BMK_BOOLEAN = 0x0500 +BMK_ARRAY = 0x0600 +BMK_DICT = 0x0700 +BMK_UUID = 0x0800 +BMK_URL = 0x0900 +BMK_NULL = 0x0A00 + +BMK_ST_ZERO = 0x0000 +BMK_ST_ONE = 0x0001 + +BMK_BOOLEAN_ST_FALSE = 0x0000 +BMK_BOOLEAN_ST_TRUE = 0x0001 + +# Subtypes for BMK_NUMBER are really CFNumberType values +kCFNumberSInt8Type = 1 +kCFNumberSInt16Type = 2 +kCFNumberSInt32Type = 3 +kCFNumberSInt64Type = 4 +kCFNumberFloat32Type = 5 +kCFNumberFloat64Type = 6 +kCFNumberCharType = 7 +kCFNumberShortType = 8 +kCFNumberIntType = 9 +kCFNumberLongType = 10 +kCFNumberLongLongType = 11 +kCFNumberFloatType = 12 +kCFNumberDoubleType = 13 +kCFNumberCFIndexType = 14 +kCFNumberNSIntegerType = 15 +kCFNumberCGFloatType = 16 + +# Resource property flags (from CFURLPriv.h) +kCFURLResourceIsRegularFile = 0x00000001 +kCFURLResourceIsDirectory = 0x00000002 +kCFURLResourceIsSymbolicLink = 0x00000004 +kCFURLResourceIsVolume = 0x00000008 +kCFURLResourceIsPackage = 0x00000010 +kCFURLResourceIsSystemImmutable = 0x00000020 +kCFURLResourceIsUserImmutable = 0x00000040 +kCFURLResourceIsHidden = 0x00000080 +kCFURLResourceHasHiddenExtension = 0x00000100 +kCFURLResourceIsApplication = 0x00000200 +kCFURLResourceIsCompressed = 0x00000400 +kCFURLResourceIsSystemCompressed = 0x00000400 +kCFURLCanSetHiddenExtension = 0x00000800 +kCFURLResourceIsReadable = 0x00001000 +kCFURLResourceIsWriteable = 0x00002000 +kCFURLResourceIsExecutable = 0x00004000 +kCFURLIsAliasFile = 0x00008000 +kCFURLIsMountTrigger = 0x00010000 + +# Volume property flags (from CFURLPriv.h) +kCFURLVolumeIsLocal = 0x1 +kCFURLVolumeIsAutomount = 0x2 +kCFURLVolumeDontBrowse = 0x4 +kCFURLVolumeIsReadOnly = 0x8 +kCFURLVolumeIsQuarantined = 0x10 +kCFURLVolumeIsEjectable = 0x20 +kCFURLVolumeIsRemovable = 0x40 +kCFURLVolumeIsInternal = 0x80 +kCFURLVolumeIsExternal = 0x100 +kCFURLVolumeIsDiskImage = 0x200 +kCFURLVolumeIsFileVault = 0x400 +kCFURLVolumeIsLocaliDiskMirror = 0x800 +kCFURLVolumeIsiPod = 0x1000 +kCFURLVolumeIsiDisk = 0x2000 +kCFURLVolumeIsCD = 0x4000 +kCFURLVolumeIsDVD = 0x8000 +kCFURLVolumeIsDeviceFileSystem = 0x10000 +kCFURLVolumeSupportsPersistentIDs = 0x100000000 +kCFURLVolumeSupportsSearchFS = 0x200000000 +kCFURLVolumeSupportsExchange = 0x400000000 +# reserved 0x800000000 +kCFURLVolumeSupportsSymbolicLinks = 0x1000000000 +kCFURLVolumeSupportsDenyModes = 0x2000000000 +kCFURLVolumeSupportsCopyFile = 0x4000000000 +kCFURLVolumeSupportsReadDirAttr = 0x8000000000 +kCFURLVolumeSupportsJournaling = 0x10000000000 +kCFURLVolumeSupportsRename = 0x20000000000 +kCFURLVolumeSupportsFastStatFS = 0x40000000000 +kCFURLVolumeSupportsCaseSensitiveNames = 0x80000000000 +kCFURLVolumeSupportsCasePreservedNames = 0x100000000000 +kCFURLVolumeSupportsFLock = 0x200000000000 +kCFURLVolumeHasNoRootDirectoryTimes = 0x400000000000 +kCFURLVolumeSupportsExtendedSecurity = 0x800000000000 +kCFURLVolumeSupports2TBFileSize = 0x1000000000000 +kCFURLVolumeSupportsHardLinks = 0x2000000000000 +kCFURLVolumeSupportsMandatoryByteRangeLocks = 0x4000000000000 +kCFURLVolumeSupportsPathFromID = 0x8000000000000 +# reserved 0x10000000000000 +kCFURLVolumeIsJournaling = 0x20000000000000 +kCFURLVolumeSupportsSparseFiles = 0x40000000000000 +kCFURLVolumeSupportsZeroRuns = 0x80000000000000 +kCFURLVolumeSupportsVolumeSizes = 0x100000000000000 +kCFURLVolumeSupportsRemoteEvents = 0x200000000000000 +kCFURLVolumeSupportsHiddenFiles = 0x400000000000000 +kCFURLVolumeSupportsDecmpFSCompression = 0x800000000000000 +kCFURLVolumeHas64BitObjectIDs = 0x1000000000000000 +kCFURLVolumePropertyFlagsAll = 0xFFFFFFFFFFFFFFFF + +BMK_URL_ST_ABSOLUTE = 0x0001 +BMK_URL_ST_RELATIVE = 0x0002 + +# Bookmark keys +kBookmarkURL = 0x1003 # A URL +kBookmarkPath = 0x1004 # Array of path components +kBookmarkCNIDPath = 0x1005 # Array of CNIDs +kBookmarkFileProperties = ( + 0x1010 # (CFURL rp flags, CFURL rp flags asked for, 8 bytes NULL) +) +kBookmarkFileName = 0x1020 +kBookmarkFileID = 0x1030 +kBookmarkFileCreationDate = 0x1040 +# = 0x1054 # ? +# = 0x1055 # ? +# = 0x1056 # ? +# = 0x1101 # ? +# = 0x1102 # ? +kBookmarkTOCPath = 0x2000 # A list of (TOC id, ?) pairs +kBookmarkVolumePath = 0x2002 +kBookmarkVolumeURL = 0x2005 +kBookmarkVolumeName = 0x2010 +kBookmarkVolumeUUID = 0x2011 # Stored (perversely) as a string +kBookmarkVolumeSize = 0x2012 +kBookmarkVolumeCreationDate = 0x2013 +kBookmarkVolumeProperties = ( + 0x2020 # (CFURL vp flags, CFURL vp flags asked for, 8 bytes NULL) +) +kBookmarkVolumeIsRoot = 0x2030 # True if volume is FS root +kBookmarkVolumeBookmark = 0x2040 # Embedded bookmark for disk image (TOC id) +kBookmarkVolumeMountPoint = 0x2050 # A URL +# = 0x2070 +kBookmarkContainingFolder = 0xC001 # Index of containing folder in path +kBookmarkUserName = 0xC011 # User that created bookmark +kBookmarkUID = 0xC012 # UID that created bookmark +kBookmarkWasFileReference = 0xD001 # True if the URL was a file reference +kBookmarkCreationOptions = 0xD010 +kBookmarkURLLengths = 0xE003 # See below +kBookmarkDisplayName = 0xF017 +kBookmarkIconData = 0xF020 +kBookmarkIconRef = 0xF021 +kBookmarkTypeBindingData = 0xF022 +kBookmarkCreationTime = 0xF030 +kBookmarkSandboxRwExtension = 0xF080 +kBookmarkSandboxRoExtension = 0xF081 +kBookmarkAliasData = 0xFE00 + +# Alias for backwards compatibility +kBookmarkSecurityExtension = kBookmarkSandboxRwExtension + +# kBookmarkURLLengths is an array that is set if the URL encoded by the +# bookmark had a base URL; in that case, each entry is the length of the +# base URL in question. Thus a URL +# +# file:///foo/bar/baz blam/blat.html +# +# will result in [3, 2], while the URL +# +# file:///foo bar/baz blam blat.html +# +# would result in [1, 2, 1, 1] + + +class Data: + def __init__(self, bytedata=None): + #: The bytes, stored as a byte string + self.bytes = bytes(bytedata) + + def __repr__(self): + return "Data(%r)" % self.bytes + + +class URL: + def __init__(self, base, rel=None): + if rel is not None: + #: The base URL, if any (a :class:`URL`) + self.base = base + #: The rest of the URL (a string) + self.relative = rel + else: + self.base = None + self.relative = base + + @property + def absolute(self): + """Return an absolute URL.""" + if self.base is None: + return self.relative + else: + return urljoin(self.base.absolute, self.relative) + + def __repr__(self): + return "URL(%r)" % self.absolute + + +class Bookmark: + def __init__(self, tocs=None): + if tocs is None: + #: The TOCs for this Bookmark + self.tocs = [] + else: + self.tocs = tocs + + @classmethod + def _get_item(cls, data, hdrsize, offset): + offset += hdrsize + if offset > len(data) - 8: + raise ValueError("Offset out of range") + + length, typecode = struct.unpack(b"<II", data[offset : offset + 8]) + + if len(data) - offset < 8 + length: + raise ValueError("Data item truncated") + + databytes = data[offset + 8 : offset + 8 + length] + + dsubtype = typecode & BMK_DATA_SUBTYPE_MASK + dtype = typecode & BMK_DATA_TYPE_MASK + + if dtype == BMK_STRING: + return databytes.decode("utf-8") + elif dtype == BMK_DATA: + return Data(databytes) + elif dtype == BMK_NUMBER: + if dsubtype == kCFNumberSInt8Type: + return ord(databytes[0]) + elif dsubtype == kCFNumberSInt16Type: + return struct.unpack(b"<h", databytes)[0] + elif dsubtype == kCFNumberSInt32Type: + return struct.unpack(b"<i", databytes)[0] + elif dsubtype == kCFNumberSInt64Type: + return struct.unpack(b"<q", databytes)[0] + elif dsubtype == kCFNumberFloat32Type: + return struct.unpack(b"<f", databytes)[0] + elif dsubtype == kCFNumberFloat64Type: + return struct.unpack(b"<d", databytes)[0] + elif dtype == BMK_DATE: + # Yes, dates really are stored as *BIG-endian* doubles; everything + # else is little-endian + secs = datetime.timedelta(seconds=struct.unpack(b">d", databytes)[0]) + return osx_epoch + secs + elif dtype == BMK_BOOLEAN: + if dsubtype == BMK_BOOLEAN_ST_TRUE: + return True + elif dsubtype == BMK_BOOLEAN_ST_FALSE: + return False + elif dtype == BMK_UUID: + return uuid.UUID(bytes=databytes) + elif dtype == BMK_URL: + if dsubtype == BMK_URL_ST_ABSOLUTE: + return URL(databytes.decode("utf-8")) + elif dsubtype == BMK_URL_ST_RELATIVE: + baseoff, reloff = struct.unpack(b"<II", databytes) + base = cls._get_item(data, hdrsize, baseoff) + rel = cls._get_item(data, hdrsize, reloff) + return URL(base, rel) + elif dtype == BMK_ARRAY: + result = [] + for aoff in range(offset + 8, offset + 8 + length, 4): + (eltoff,) = struct.unpack(b"<I", data[aoff : aoff + 4]) + result.append(cls._get_item(data, hdrsize, eltoff)) + return result + elif dtype == BMK_DICT: + result = {} + for eoff in range(offset + 8, offset + 8 + length, 8): + keyoff, valoff = struct.unpack(b"<II", data[eoff : eoff + 8]) + key = cls._get_item(data, hdrsize, keyoff) + val = cls._get_item(data, hdrsize, valoff) + result[key] = val + return result + elif dtype == BMK_NULL: + return None + + print("Unknown data type %08x" % typecode) + return (typecode, databytes) + + @classmethod + def from_bytes(cls, data): + """Create a :class:`Bookmark` given byte data.""" + + if len(data) < 16: + raise ValueError("Not a bookmark file (too short)") + + if isinstance(data, bytearray): + data = bytes(data) + + magic, size, dummy, hdrsize = struct.unpack(b"<4sIII", data[0:16]) + + if magic not in (b"book", b"alis"): + raise ValueError("Not a bookmark file (bad magic) %r" % magic) + + if hdrsize < 16: + raise ValueError("Not a bookmark file (header size too short)") + + if hdrsize > size: + raise ValueError("Not a bookmark file (header size too large)") + + if size != len(data): + raise ValueError("Not a bookmark file (truncated)") + + (tocoffset,) = struct.unpack(b"<I", data[hdrsize : hdrsize + 4]) + + tocs = [] + + while tocoffset != 0: + tocbase = hdrsize + tocoffset + if (tocoffset > size - hdrsize) or (size - tocbase < 20): + raise ValueError("TOC offset out of range") + + (tocsize, tocmagic, tocid, nexttoc, toccount) = struct.unpack( + b"<IIIII", data[tocbase : tocbase + 20] + ) + + if tocmagic != 0xFFFFFFFE: + break + + tocsize += 8 + + if size - tocbase < tocsize: + raise ValueError("TOC truncated") + + if tocsize < 12 * toccount: + raise ValueError("TOC entries overrun TOC size") + + toc = {} + for n in range(0, toccount): + ebase = tocbase + 20 + 12 * n + eid, eoffset, edummy = struct.unpack(b"<III", data[ebase : ebase + 12]) + + if eid & 0x80000000: + eid = cls._get_item(data, hdrsize, eid & 0x7FFFFFFF) + + toc[eid] = cls._get_item(data, hdrsize, eoffset) + + tocs.append((tocid, toc)) + + tocoffset = nexttoc + + return cls(tocs) + + def __getitem__(self, key): + for tid, toc in self.tocs: + if key in toc: + return toc[key] + raise KeyError("Key not found") + + def __setitem__(self, key, value): + if len(self.tocs) == 0: + self.tocs = [(1, {})] + self.tocs[0][1][key] = value + + def get(self, key, default=None): + """Lookup the value for a given key, returning a default if not + present.""" + for tid, toc in self.tocs: + if key in toc: + return toc[key] + return default + + @classmethod + def _encode_item(cls, item, offset): + if item is True: + result = struct.pack(b"<II", 0, BMK_BOOLEAN | BMK_BOOLEAN_ST_TRUE) + elif item is False: + result = struct.pack(b"<II", 0, BMK_BOOLEAN | BMK_BOOLEAN_ST_FALSE) + elif isinstance(item, str): + encoded = item.encode("utf-8") + result = ( + struct.pack(b"<II", len(encoded), BMK_STRING | BMK_ST_ONE) + encoded + ) + elif isinstance(item, bytes): + result = struct.pack(b"<II", len(item), BMK_STRING | BMK_ST_ONE) + item + elif isinstance(item, Data): + result = struct.pack( + b"<II", len(item.bytes), BMK_DATA | BMK_ST_ONE + ) + bytes(item.bytes) + elif isinstance(item, bytearray): + result = struct.pack(b"<II", len(item), BMK_DATA | BMK_ST_ONE) + bytes(item) + elif isinstance(item, int): + if item > -0x80000000 and item < 0x7FFFFFFF: + result = struct.pack(b"<IIi", 4, BMK_NUMBER | kCFNumberSInt32Type, item) + else: + result = struct.pack(b"<IIq", 8, BMK_NUMBER | kCFNumberSInt64Type, item) + elif isinstance(item, float): + result = struct.pack(b"<IId", 8, BMK_NUMBER | kCFNumberFloat64Type, item) + elif isinstance(item, datetime.datetime): + secs = item - osx_epoch + result = struct.pack(b"<II", 8, BMK_DATE | BMK_ST_ZERO) + struct.pack( + b">d", float(secs.total_seconds()) + ) + + elif isinstance(item, uuid.UUID): + result = struct.pack(b"<II", 16, BMK_UUID | BMK_ST_ONE) + item.bytes + elif isinstance(item, URL): + if item.base: + baseoff = offset + 16 + reloff, baseenc = cls._encode_item(item.base, baseoff) + xoffset, relenc = cls._encode_item(item.relative, reloff) + result = b"".join( + [ + struct.pack( + b"<IIII", 8, BMK_URL | BMK_URL_ST_RELATIVE, baseoff, reloff + ), + baseenc, + relenc, + ] + ) + else: + encoded = item.relative.encode("utf-8") + result = ( + struct.pack(b"<II", len(encoded), BMK_URL | BMK_URL_ST_ABSOLUTE) + + encoded + ) + elif isinstance(item, list): + ioffset = offset + 8 + len(item) * 4 + result = [struct.pack(b"<II", len(item) * 4, BMK_ARRAY | BMK_ST_ONE)] + enc = [] + for elt in item: + result.append(struct.pack(b"<I", ioffset)) + ioffset, ienc = cls._encode_item(elt, ioffset) + enc.append(ienc) + result = b"".join(result + enc) + elif isinstance(item, dict): + ioffset = offset + 8 + len(item) * 8 + result = [struct.pack(b"<II", len(item) * 8, BMK_DICT | BMK_ST_ONE)] + enc = [] + for k, v in item.items(): + result.append(struct.pack(b"<I", ioffset)) + ioffset, ienc = cls._encode_item(k, ioffset) + enc.append(ienc) + result.append(struct.pack(b"<I", ioffset)) + ioffset, ienc = cls._encode_item(v, ioffset) + enc.append(ienc) + result = b"".join(result + enc) + elif item is None: + result = struct.pack(b"<II", 0, BMK_NULL | BMK_ST_ONE) + else: + raise ValueError("Unknown item type when encoding: %s" % item) + + offset += len(result) + + # Pad to a multiple of 4 bytes + if offset & 3: + extra = 4 - (offset & 3) + result += b"\0" * extra + offset += extra + + return (offset, result) + + def to_bytes(self): + """Convert this :class:`Bookmark` to a byte representation.""" + + result = [] + tocs = [] + offset = 4 # For the offset to the first TOC + + # Generate the data and build the TOCs + for tid, toc in self.tocs: + entries = [] + + for k, v in toc.items(): + if isinstance(k, str): + noffset = offset + voffset, enc = self._encode_item(k, offset) + result.append(enc) + offset, enc = self._encode_item(v, voffset) + result.append(enc) + entries.append((noffset | 0x80000000, voffset)) + else: + entries.append((k, offset)) + offset, enc = self._encode_item(v, offset) + result.append(enc) + + # TOC entries must be sorted - CoreServicesInternal does a + # binary search to find data + entries.sort() + + tocs.append( + (tid, b"".join([struct.pack(b"<III", k, o, 0) for k, o in entries])) + ) + + first_toc_offset = offset + + # Now generate the TOC headers + for ndx, toc in enumerate(tocs): + tid, data = toc + if ndx == len(tocs) - 1: + next_offset = 0 + else: + next_offset = offset + 20 + len(data) + + result.append( + struct.pack( + b"<IIIII", + len(data) - 8, + 0xFFFFFFFE, + tid, + next_offset, + len(data) // 12, + ) + ) + result.append(data) + + offset += 20 + len(data) + + # Finally, add the header (and the first TOC offset, which isn't part + # of the header, but goes just after it) + header = struct.pack( + b"<4sIIIQQQQI", + b"book", + offset + 48, + 0x10040000, + 48, + 0, + 0, + 0, + 0, + first_toc_offset, + ) + + result.insert(0, header) + + return b"".join(result) + + @classmethod + def for_file(cls, path): + """Construct a :class:`Bookmark` for a given file.""" + + # Find the filesystem + st = osx.statfs(path) + vol_path = st.f_mntonname.decode("utf-8") + + # Grab its attributes + attrs = [ + osx.ATTR_CMN_CRTIME, + osx.ATTR_VOL_SIZE | osx.ATTR_VOL_NAME | osx.ATTR_VOL_UUID, + 0, + 0, + 0, + ] + volinfo = osx.getattrlist(vol_path, attrs, 0) + + vol_crtime = volinfo[0] + vol_size = volinfo[1] + vol_name = volinfo[2] + vol_uuid = volinfo[3] + + # Also grab various attributes of the file + attrs = [ + (osx.ATTR_CMN_OBJTYPE | osx.ATTR_CMN_CRTIME | osx.ATTR_CMN_FILEID), + 0, + 0, + 0, + 0, + ] + info = osx.getattrlist(path, attrs, osx.FSOPT_NOFOLLOW) + + cnid = info[2] + crtime = info[1] + + if info[0] == osx.VREG: + flags = kCFURLResourceIsRegularFile + elif info[0] == osx.VDIR: + flags = kCFURLResourceIsDirectory + elif info[0] == osx.VLNK: + flags = kCFURLResourceIsSymbolicLink + else: + flags = kCFURLResourceIsRegularFile + + dirname, filename = os.path.split(path) + + relcount = 0 + if not os.path.isabs(dirname): + curdir = os.getcwd() + head, tail = os.path.split(curdir) + relcount = 0 + while head and tail: + relcount += 1 + head, tail = os.path.split(head) + dirname = os.path.join(curdir, dirname) + + # ?? foldername = os.path.basename(dirname) + + rel_path = os.path.relpath(path, vol_path) + + # Build the path arrays + name_path = [] + cnid_path = [] + head, tail = os.path.split(rel_path) + if not tail: + head, tail = os.path.split(head) + while head or tail: + if head: + attrs = [osx.ATTR_CMN_FILEID, 0, 0, 0, 0] + info = osx.getattrlist(os.path.join(vol_path, head), attrs, 0) + cnid_path.insert(0, info[0]) + head, tail = os.path.split(head) + name_path.insert(0, tail) + else: + head, tail = os.path.split(head) + name_path.append(filename) + cnid_path.append(cnid) + + url_lengths = [relcount, len(name_path) - relcount] + + fileprops = Data(struct.pack(b"<QQQ", flags, 0x0F, 0)) + volprops = Data( + struct.pack( + b"<QQQ", + 0x81 | kCFURLVolumeSupportsPersistentIDs, + 0x13EF | kCFURLVolumeSupportsPersistentIDs, + 0, + ) + ) + + toc = { + kBookmarkPath: name_path, + kBookmarkCNIDPath: cnid_path, + kBookmarkFileCreationDate: crtime, + kBookmarkFileProperties: fileprops, + kBookmarkContainingFolder: len(name_path) - 2, + kBookmarkVolumePath: vol_path, + kBookmarkVolumeIsRoot: vol_path == "/", + kBookmarkVolumeURL: URL("file://" + vol_path), + kBookmarkVolumeName: vol_name, + kBookmarkVolumeSize: vol_size, + kBookmarkVolumeCreationDate: vol_crtime, + kBookmarkVolumeUUID: str(vol_uuid).upper(), + kBookmarkVolumeProperties: volprops, + kBookmarkCreationOptions: 512, + kBookmarkWasFileReference: True, + kBookmarkUserName: "unknown", + kBookmarkUID: 99, + } + + if relcount: + toc[kBookmarkURLLengths] = url_lengths + + return Bookmark([(1, toc)]) + + def __repr__(self): + result = ["Bookmark(["] + for tid, toc in self.tocs: + result.append("(0x%x, {\n" % tid) + for k, v in toc.items(): + if isinstance(k, str): + kf = repr(k) + else: + kf = "0x%04x" % k + result.append(f" {kf}: {v!r}\n") + result.append("}),\n") + result.append("])") + + return "".join(result) |