diff options
Diffstat (limited to 'src/3rdparty/python/lib/python3.9/site-packages/mac_alias/alias.py')
-rw-r--r-- | src/3rdparty/python/lib/python3.9/site-packages/mac_alias/alias.py | 772 |
1 files changed, 772 insertions, 0 deletions
diff --git a/src/3rdparty/python/lib/python3.9/site-packages/mac_alias/alias.py b/src/3rdparty/python/lib/python3.9/site-packages/mac_alias/alias.py new file mode 100644 index 000000000..bf34fb01b --- /dev/null +++ b/src/3rdparty/python/lib/python3.9/site-packages/mac_alias/alias.py @@ -0,0 +1,772 @@ +import datetime +import io +import os +import os.path +import struct +import sys +from unicodedata import normalize + +if sys.platform == "darwin": + from . import osx + +from .utils import mac_epoch + +ALIAS_KIND_FILE = 0 +ALIAS_KIND_FOLDER = 1 + +ALIAS_HFS_VOLUME_SIGNATURE = b"H+" + +ALIAS_FILESYSTEM_UDF = "UDF (CD/DVD)" +ALIAS_FILESYSTEM_FAT32 = "FAT32" +ALIAS_FILESYSTEM_EXFAT = "exFAT" +ALIAS_FILESYSTEM_HFSX = "HFSX" +ALIAS_FILESYSTEM_HFSPLUS = "HFS+" +ALIAS_FILESYSTEM_FTP = "FTP" +ALIAS_FILESYSTEM_NTFS = "NTFS" +ALIAS_FILESYSTEM_UNKNOWN = "unknown" + +ALIAS_FIXED_DISK = 0 +ALIAS_NETWORK_DISK = 1 +ALIAS_400KB_FLOPPY_DISK = 2 +ALIAS_800KB_FLOPPY_DISK = 3 +ALIAS_1_44MB_FLOPPY_DISK = 4 +ALIAS_EJECTABLE_DISK = 5 + +ALIAS_NO_CNID = 0xFFFFFFFF + +ALIAS_FSTYPE_MAP = { + # Version 2 aliases + b"HX": ALIAS_FILESYSTEM_HFSX, + b"H+": ALIAS_FILESYSTEM_HFSPLUS, + # Version 3 aliases + b"BDcu": ALIAS_FILESYSTEM_UDF, + b"BDIS": ALIAS_FILESYSTEM_FAT32, + b"BDxF": ALIAS_FILESYSTEM_EXFAT, + b"HX\0\0": ALIAS_FILESYSTEM_HFSX, + b"H+\0\0": ALIAS_FILESYSTEM_HFSPLUS, + b"KG\0\0": ALIAS_FILESYSTEM_FTP, + b"NTcu": ALIAS_FILESYSTEM_NTFS, +} + + +def encode_utf8(s): + if isinstance(s, bytes): + return s + return s.encode("utf-8") + + +def decode_utf8(s): + if isinstance(s, bytes): + return s.decode("utf-8") + return s + + +class AppleShareInfo: + def __init__(self, zone=None, server=None, user=None): + #: The AppleShare zone + self.zone = zone + #: The AFP server + self.server = server + #: The username + self.user = user + + def __repr__(self): + return "AppleShareInfo({!r},{!r},{!r})".format( + self.zone, self.server, self.user + ) + + +class VolumeInfo: + def __init__( + self, + name, + creation_date, + fs_type, + disk_type, + attribute_flags, + fs_id, + appleshare_info=None, + driver_name=None, + posix_path=None, + disk_image_alias=None, + dialup_info=None, + network_mount_info=None, + ): + #: The name of the volume on which the target resides + self.name = name + + #: The creation date of the target's volume + self.creation_date = creation_date + + #: The filesystem type + #: (for v2 aliases, this is a 2-character code; for v3 aliases, a + #: 4-character code). + self.fs_type = fs_type + + #: The type of disk; should be one of + #: + #: * ALIAS_FIXED_DISK + #: * ALIAS_NETWORK_DISK + #: * ALIAS_400KB_FLOPPY_DISK + #: * ALIAS_800KB_FLOPPY_DISK + #: * ALIAS_1_44MB_FLOPPY_DISK + #: * ALIAS_EJECTABLE_DISK + self.disk_type = disk_type + + #: Filesystem attribute flags (from HFS volume header) + self.attribute_flags = attribute_flags + + #: Filesystem identifier + self.fs_id = fs_id + + #: AppleShare information (for automatic remounting of network shares) + #: *(optional)* + self.appleshare_info = appleshare_info + + #: Driver name (*probably* contains a disk driver name on older Macs) + #: *(optional)* + self.driver_name = driver_name + + #: POSIX path of the mount point of the target's volume + #: *(optional)* + self.posix_path = posix_path + + #: :class:`Alias` object pointing at the disk image on which the + #: target's volume resides *(optional)* + self.disk_image_alias = disk_image_alias + + #: Dialup information (for automatic establishment of dialup connections) + self.dialup_info = dialup_info + + #: Network mount information (for automatic remounting) + self.network_mount_info = network_mount_info + + @property + def filesystem_type(self): + return ALIAS_FSTYPE_MAP.get(self.fs_type, ALIAS_FILESYSTEM_UNKNOWN) + + def __repr__(self): + args = [ + "name", + "creation_date", + "fs_type", + "disk_type", + "attribute_flags", + "fs_id", + ] + values = [] + for a in args: + v = getattr(self, a) + values.append(repr(v)) + + kwargs = [ + "appleshare_info", + "driver_name", + "posix_path", + "disk_image_alias", + "dialup_info", + "network_mount_info", + ] + for a in kwargs: + v = getattr(self, a) + if v is not None: + values.append(f"{a}={v!r}") + return "VolumeInfo(%s)" % ",".join(values) + + +class TargetInfo: + def __init__( + self, + kind, + filename, + folder_cnid, + cnid, + creation_date, + creator_code, + type_code, + levels_from=-1, + levels_to=-1, + folder_name=None, + cnid_path=None, + carbon_path=None, + posix_path=None, + user_home_prefix_len=None, + ): + #: Either ALIAS_KIND_FILE or ALIAS_KIND_FOLDER + self.kind = kind + + #: The filename of the target + self.filename = filename + + #: The CNID (Catalog Node ID) of the target's containing folder; + #: CNIDs are similar to but different than traditional UNIX inode + #: numbers + self.folder_cnid = folder_cnid + + #: The CNID (Catalog Node ID) of the target + self.cnid = cnid + + #: The target's *creation* date. + self.creation_date = creation_date + + #: The target's Mac creator code (a four-character binary string) + self.creator_code = creator_code + + #: The target's Mac type code (a four-character binary string) + self.type_code = type_code + + #: The depth of the alias? Always seems to be -1 on OS X. + self.levels_from = levels_from + + #: The depth of the target? Always seems to be -1 on OS X. + self.levels_to = levels_to + + #: The (POSIX) name of the target's containing folder. *(optional)* + self.folder_name = folder_name + + #: The path from the volume root as a sequence of CNIDs. *(optional)* + self.cnid_path = cnid_path + + #: The Carbon path of the target *(optional)* + self.carbon_path = carbon_path + + #: The POSIX path of the target relative to the volume root. Note + #: that this may or may not have a leading '/' character, but it is + #: always relative to the containing volume. *(optional)* + self.posix_path = posix_path + + #: If the path points into a user's home folder, the number of folders + #: deep that we go before we get to that home folder. *(optional)* + self.user_home_prefix_len = user_home_prefix_len + + def __repr__(self): + args = [ + "kind", + "filename", + "folder_cnid", + "cnid", + "creation_date", + "creator_code", + "type_code", + ] + values = [] + for a in args: + v = getattr(self, a) + values.append(repr(v)) + + if self.levels_from != -1: + values.append("levels_from=%r" % self.levels_from) + if self.levels_to != -1: + values.append("levels_to=%r" % self.levels_to) + + kwargs = [ + "folder_name", + "cnid_path", + "carbon_path", + "posix_path", + "user_home_prefix_len", + ] + for a in kwargs: + v = getattr(self, a) + values.append(f"{a}={v!r}") + + return "TargetInfo(%s)" % ",".join(values) + + +TAG_CARBON_FOLDER_NAME = 0 +TAG_CNID_PATH = 1 +TAG_CARBON_PATH = 2 +TAG_APPLESHARE_ZONE = 3 +TAG_APPLESHARE_SERVER_NAME = 4 +TAG_APPLESHARE_USERNAME = 5 +TAG_DRIVER_NAME = 6 +TAG_NETWORK_MOUNT_INFO = 9 +TAG_DIALUP_INFO = 10 +TAG_UNICODE_FILENAME = 14 +TAG_UNICODE_VOLUME_NAME = 15 +TAG_HIGH_RES_VOLUME_CREATION_DATE = 16 +TAG_HIGH_RES_CREATION_DATE = 17 +TAG_POSIX_PATH = 18 +TAG_POSIX_PATH_TO_MOUNTPOINT = 19 +TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE = 20 +TAG_USER_HOME_LENGTH_PREFIX = 21 + + +class Alias: + def __init__( + self, + appinfo=b"\0\0\0\0", + version=2, + volume=None, + target=None, + extra=[], + ): + """Construct a new :class:`Alias` object with the specified + contents.""" + + #: Application specific information (four byte byte-string) + self.appinfo = appinfo + + #: Version (we support versions 2 and 3) + self.version = version + + #: A :class:`VolumeInfo` object describing the target's volume + self.volume = volume + + #: A :class:`TargetInfo` object describing the target + self.target = target + + #: A list of extra `(tag, value)` pairs + self.extra = list(extra) + + @classmethod + def _from_fd(cls, b): + appinfo, recsize, version = struct.unpack(b">4shh", b.read(8)) + + if recsize < 150: + raise ValueError("Incorrect alias length") + + if version not in (2, 3): + raise ValueError("Unsupported alias version %u" % version) + + if version == 2: + ( + kind, # h + volname, # 28p + voldate, # I + fstype, # 2s + disktype, # h + folder_cnid, # I + filename, # 64p + cnid, # I + crdate, # I + creator_code, # 4s + type_code, # 4s + levels_from, # h + levels_to, # h + volattrs, # I + volfsid, # 2s + reserved, # 10s + ) = struct.unpack(b">h28pI2shI64pII4s4shhI2s10s", b.read(142)) + else: + ( + kind, # h + voldate_hr, # Q + fstype, # 4s + disktype, # h + folder_cnid, # I + cnid, # I + crdate_hr, # Q + volattrs, # I + reserved, # 14s + ) = struct.unpack(b">hQ4shIIQI14s", b.read(46)) + + volname = b"" + filename = b"" + creator_code = None + type_code = None + voldate = voldate_hr / 65536.0 + crdate = crdate_hr / 65536.0 + + voldate = mac_epoch + datetime.timedelta(seconds=voldate) + crdate = mac_epoch + datetime.timedelta(seconds=crdate) + + alias = Alias() + alias.appinfo = appinfo + + alias.volume = VolumeInfo( + volname.decode().replace("/", ":"), + voldate, + fstype, + disktype, + volattrs, + volfsid, + ) + alias.target = TargetInfo( + kind, + filename.decode().replace("/", ":"), + folder_cnid, + cnid, + crdate, + creator_code, + type_code, + ) + alias.target.levels_from = levels_from + alias.target.levels_to = levels_to + + tag = struct.unpack(b">h", b.read(2))[0] + + while tag != -1: + length = struct.unpack(b">h", b.read(2))[0] + value = b.read(length) + if length & 1: + b.read(1) + + if tag == TAG_CARBON_FOLDER_NAME: + alias.target.folder_name = value.decode().replace("/", ":") + elif tag == TAG_CNID_PATH: + alias.target.cnid_path = struct.unpack(">%uI" % (length // 4), value) + elif tag == TAG_CARBON_PATH: + alias.target.carbon_path = value + elif tag == TAG_APPLESHARE_ZONE: + if alias.volume.appleshare_info is None: + alias.volume.appleshare_info = AppleShareInfo() + alias.volume.appleshare_info.zone = value + elif tag == TAG_APPLESHARE_SERVER_NAME: + if alias.volume.appleshare_info is None: + alias.volume.appleshare_info = AppleShareInfo() + alias.volume.appleshare_info.server = value + elif tag == TAG_APPLESHARE_USERNAME: + if alias.volume.appleshare_info is None: + alias.volume.appleshare_info = AppleShareInfo() + alias.volume.appleshare_info.user = value + elif tag == TAG_DRIVER_NAME: + alias.volume.driver_name = value + elif tag == TAG_NETWORK_MOUNT_INFO: + alias.volume.network_mount_info = value + elif tag == TAG_DIALUP_INFO: + alias.volume.dialup_info = value + elif tag == TAG_UNICODE_FILENAME: + alias.target.filename = value[2:].decode("utf-16be") + elif tag == TAG_UNICODE_VOLUME_NAME: + alias.volume.name = value[2:].decode("utf-16be") + elif tag == TAG_HIGH_RES_VOLUME_CREATION_DATE: + seconds = struct.unpack(b">Q", value)[0] / 65536.0 + alias.volume.creation_date = mac_epoch + datetime.timedelta( + seconds=seconds + ) + elif tag == TAG_HIGH_RES_CREATION_DATE: + seconds = struct.unpack(b">Q", value)[0] / 65536.0 + alias.target.creation_date = mac_epoch + datetime.timedelta( + seconds=seconds + ) + elif tag == TAG_POSIX_PATH: + alias.target.posix_path = value.decode() + elif tag == TAG_POSIX_PATH_TO_MOUNTPOINT: + alias.volume.posix_path = value.decode() + elif tag == TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE: + alias.volume.disk_image_alias = Alias.from_bytes(value) + elif tag == TAG_USER_HOME_LENGTH_PREFIX: + alias.target.user_home_prefix_len = struct.unpack(b">h", value)[0] + else: + alias.extra.append((tag, value)) + + tag = struct.unpack(b">h", b.read(2))[0] + + return alias + + @classmethod + def from_bytes(cls, bytes): + """Construct an :class:`Alias` object given binary Alias data.""" + with io.BytesIO(bytes) as b: + return cls._from_fd(b) + + @classmethod + def for_file(cls, path): + """Create an :class:`Alias` that points at the specified file.""" + if sys.platform != "darwin": + raise Exception("Not implemented (requires special support)") + + path = encode_utf8(path) + + a = Alias() + + # Find the filesystem + st = osx.statfs(path) + vol_path = st.f_mntonname + + # File and folder names in HFS+ are normalized to a form similar to NFD. + # Must be normalized (NFD->NFC) before use to avoid unicode string comparison issues. + vol_path = normalize("NFC", vol_path.decode("utf-8")).encode("utf-8") + + # Grab its attributes + attrs = [osx.ATTR_CMN_CRTIME, osx.ATTR_VOL_NAME, 0, 0, 0] + volinfo = osx.getattrlist(vol_path, attrs, 0) + + vol_crtime = volinfo[0] + vol_name = encode_utf8(volinfo[1]) + + # Also grab various attributes of the file + attrs = [ + osx.ATTR_CMN_OBJTYPE + | osx.ATTR_CMN_CRTIME + | osx.ATTR_CMN_FNDRINFO + | osx.ATTR_CMN_FILEID + | osx.ATTR_CMN_PARENTID, + 0, + 0, + 0, + 0, + ] + info = osx.getattrlist(path, attrs, osx.FSOPT_NOFOLLOW) + + if info[0] == osx.VDIR: + kind = ALIAS_KIND_FOLDER + else: + kind = ALIAS_KIND_FILE + + cnid = info[3] + folder_cnid = info[4] + + dirname, filename = os.path.split(path) + + if dirname == b"" or dirname == b".": + dirname = os.getcwd() + + foldername = os.path.basename(dirname) + + creation_date = info[1] + + if kind == ALIAS_KIND_FILE: + creator_code = struct.pack(b"I", info[2].fileInfo.fileCreator) + type_code = struct.pack(b"I", info[2].fileInfo.fileType) + else: + creator_code = b"\0\0\0\0" + type_code = b"\0\0\0\0" + + a.target = TargetInfo( + kind, filename, folder_cnid, cnid, creation_date, creator_code, type_code + ) + a.volume = VolumeInfo(vol_name, vol_crtime, b"H+", ALIAS_FIXED_DISK, 0, b"\0\0") + + a.target.folder_name = foldername + a.volume.posix_path = vol_path + + rel_path = os.path.relpath(path, vol_path) + + # Leave off the initial '/' if vol_path is '/' (no idea why) + if vol_path == b"/": + a.target.posix_path = rel_path + else: + a.target.posix_path = b"/" + rel_path + + # Construct the Carbon and CNID paths + carbon_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.append(info[0]) + carbon_tail = tail.replace(b":", b"/") + carbon_path.insert(0, carbon_tail) + head, tail = os.path.split(head) + + carbon_path = vol_name + b":" + b":\0".join(carbon_path) + + a.target.carbon_path = carbon_path + a.target.cnid_path = cnid_path + + return a + + def _to_fd(self, b): + # We'll come back and fix the length when we're done + pos = b.tell() + b.write(struct.pack(b">4shh", self.appinfo, 0, self.version)) + + carbon_volname = encode_utf8(self.volume.name).replace(b":", b"/") + carbon_filename = encode_utf8(self.target.filename).replace(b":", b"/") + voldate = (self.volume.creation_date - mac_epoch).total_seconds() + crdate = (self.target.creation_date - mac_epoch).total_seconds() + + if self.version == 2: + # NOTE: crdate should be in local time, but that's system dependent + # (so doing so is ridiculous, and nothing could rely on it). + b.write( + struct.pack( + b">h28pI2shI64pII4s4shhI2s10s", + self.target.kind, # h + carbon_volname, # 28p + int(voldate), # I + self.volume.fs_type, # 2s + self.volume.disk_type, # h + self.target.folder_cnid, # I + carbon_filename, # 64p + self.target.cnid, # I + int(crdate), # I + self.target.creator_code, # 4s + self.target.type_code, # 4s + self.target.levels_from, # h + self.target.levels_to, # h + self.volume.attribute_flags, # I + self.volume.fs_id, # 2s + b"\0" * 10, # 10s + ) + ) + else: + b.write( + struct.pack( + b">hQ4shIIQI14s", + self.target.kind, # h + int(voldate * 65536), # Q + self.volume.fs_type, # 4s + self.volume.disk_type, # h + self.target.folder_cnid, # I + self.target.cnid, # I + int(crdate * 65536), # Q + self.volume.attribute_flags, # I + b"\0" * 14, # 14s + ) + ) + + # Excuse the odd order; we're copying Finder + if self.target.folder_name: + carbon_foldername = encode_utf8(self.target.folder_name).replace(b":", b"/") + b.write(struct.pack(b">hh", TAG_CARBON_FOLDER_NAME, len(carbon_foldername))) + b.write(carbon_foldername) + if len(carbon_foldername) & 1: + b.write(b"\0") + + b.write( + struct.pack( + b">hhQhhQ", + TAG_HIGH_RES_VOLUME_CREATION_DATE, + 8, + int(voldate * 65536), + TAG_HIGH_RES_CREATION_DATE, + 8, + int(crdate * 65536), + ) + ) + + if self.target.cnid_path: + cnid_path = struct.pack( + ">%uI" % len(self.target.cnid_path), *self.target.cnid_path + ) + b.write(struct.pack(b">hh", TAG_CNID_PATH, len(cnid_path))) + b.write(cnid_path) + + if self.target.carbon_path: + carbon_path = encode_utf8(self.target.carbon_path) + b.write(struct.pack(b">hh", TAG_CARBON_PATH, len(carbon_path))) + b.write(carbon_path) + if len(carbon_path) & 1: + b.write(b"\0") + + if self.volume.appleshare_info: + ai = self.volume.appleshare_info + if ai.zone: + b.write(struct.pack(b">hh", TAG_APPLESHARE_ZONE, len(ai.zone))) + b.write(ai.zone) + if len(ai.zone) & 1: + b.write(b"\0") + if ai.server: + b.write(struct.pack(b">hh", TAG_APPLESHARE_SERVER_NAME, len(ai.server))) + b.write(ai.server) + if len(ai.server) & 1: + b.write(b"\0") + if ai.username: + b.write(struct.pack(b">hh", TAG_APPLESHARE_USERNAME, len(ai.username))) + b.write(ai.username) + if len(ai.username) & 1: + b.write(b"\0") + + if self.volume.driver_name: + driver_name = encode_utf8(self.volume.driver_name) + b.write(struct.pack(b">hh", TAG_DRIVER_NAME, len(driver_name))) + b.write(driver_name) + if len(driver_name) & 1: + b.write(b"\0") + + if self.volume.network_mount_info: + b.write( + struct.pack( + b">hh", TAG_NETWORK_MOUNT_INFO, len(self.volume.network_mount_info) + ) + ) + b.write(self.volume.network_mount_info) + if len(self.volume.network_mount_info) & 1: + b.write(b"\0") + + if self.volume.dialup_info: + b.write( + struct.pack( + b">hh", TAG_DIALUP_INFO, len(self.volume.network_mount_info) + ) + ) + b.write(self.volume.network_mount_info) + if len(self.volume.network_mount_info) & 1: + b.write(b"\0") + + utf16 = decode_utf8(self.target.filename).replace(":", "/").encode("utf-16-be") + b.write( + struct.pack(b">hhh", TAG_UNICODE_FILENAME, len(utf16) + 2, len(utf16) // 2) + ) + b.write(utf16) + + utf16 = decode_utf8(self.volume.name).replace(":", "/").encode("utf-16-be") + b.write( + struct.pack( + b">hhh", TAG_UNICODE_VOLUME_NAME, len(utf16) + 2, len(utf16) // 2 + ) + ) + b.write(utf16) + + if self.target.posix_path: + posix_path = encode_utf8(self.target.posix_path) + b.write(struct.pack(b">hh", TAG_POSIX_PATH, len(posix_path))) + b.write(posix_path) + if len(posix_path) & 1: + b.write(b"\0") + + if self.volume.posix_path: + posix_path = encode_utf8(self.volume.posix_path) + b.write(struct.pack(b">hh", TAG_POSIX_PATH_TO_MOUNTPOINT, len(posix_path))) + b.write(posix_path) + if len(posix_path) & 1: + b.write(b"\0") + + if self.volume.disk_image_alias: + d = self.volume.disk_image_alias.to_bytes() + b.write(struct.pack(b">hh", TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE, len(d))) + b.write(d) + if len(d) & 1: + b.write(b"\0") + + if self.target.user_home_prefix_len is not None: + b.write( + struct.pack( + b">hhh", + TAG_USER_HOME_LENGTH_PREFIX, + 2, + self.target.user_home_prefix_len, + ) + ) + + for t, v in self.extra: + b.write(struct.pack(b">hh", t, len(v))) + b.write(v) + if len(v) & 1: + b.write(b"\0") + + b.write(struct.pack(b">hh", -1, 0)) + + blen = b.tell() - pos + b.seek(pos + 4, os.SEEK_SET) + b.write(struct.pack(b">h", blen)) + + def to_bytes(self): + """Returns the binary representation for this :class:`Alias`.""" + with io.BytesIO() as b: + self._to_fd(b) + return b.getvalue() + + def __str__(self): + return "<Alias target=%s>" % self.target.filename + + def __repr__(self): + values = [] + if self.appinfo != b"\0\0\0\0": + values.append("appinfo=%r" % self.appinfo) + if self.version != 2: + values.append("version=%r" % self.version) + if self.volume is not None: + values.append("volume=%r" % self.volume) + if self.target is not None: + values.append("target=%r" % self.target) + if self.extra: + values.append("extra=%r" % self.extra) + return "Alias(%s)" % ",".join(values) |