diff --git a/Docs/Changelog.md b/Docs/Changelog.md index 062cd42..8d6572e 100644 --- a/Docs/Changelog.md +++ b/Docs/Changelog.md @@ -1,5 +1,18 @@ OpenCore Changelog ================== +#### v0.8.6 +- Updated NVRAM save script for compatibilty with earlier macOS (Snow Leopard+ tested) +- Updated NVRAM save script to automatically install as launch daemon (Yosemite+) or logout hook (older macOS) +- Fixed maximum click duration and double click speed for non-standard poll frequencies +- Added support for pointer dwell-clicking +- Fixed recursive loop crash at first non-early log line on some systems +- Fixed early log preservation when using unsafe fast file logging +- Updated builtin firmware versions for SMBIOS and the rest +- Resolved wake-from-sleep failure on EFI 1.1 systems (including earlier Macs) with standalone emulated NVRAM driver +- Updated macrecovery commands with macOS 12 and 13, thx @Core-i99 +- Updates SSDT-BRG0 with macOS-specific STA to avoid compatibility issues on Windows, thx @Lorys89 +- Fixed memory issues in OpenLinuxBoot causing crashes on 32-bit UEFI firmware + #### v0.8.5 - Updated builtin firmware versions for SMBIOS and the rest - Moved CPU objects that exist only in Windows Server 2022 into `SSDT-HV-DEV-WS2022.dsl` diff --git a/Docs/Configuration.pdf b/Docs/Configuration.pdf index b3ab196..6a6cfd0 100644 Binary files a/Docs/Configuration.pdf and b/Docs/Configuration.pdf differ diff --git a/Docs/Sample.plist b/Docs/Sample.plist index ced5e3f..bc0c1f8 100644 --- a/Docs/Sample.plist +++ b/Docs/Sample.plist @@ -1379,6 +1379,12 @@ 50 KeySubsequentDelay 5 + PointerDwellClickTimeout + 0 + PointerDwellDoubleClickTimeout + 0 + PointerDwellRadius + 0 PointerPollMask -1 PointerPollMax diff --git a/EFI-Debug/BOOT/BOOTx64.efi b/EFI-Debug/BOOT/BOOTx64.efi index 0e6f3eb..c328780 100644 Binary files a/EFI-Debug/BOOT/BOOTx64.efi and b/EFI-Debug/BOOT/BOOTx64.efi differ diff --git a/EFI-Debug/OC/Drivers/AudioDxe.efi b/EFI-Debug/OC/Drivers/AudioDxe.efi index 106021a..12fe04f 100644 Binary files a/EFI-Debug/OC/Drivers/AudioDxe.efi and b/EFI-Debug/OC/Drivers/AudioDxe.efi differ diff --git a/EFI-Debug/OC/Drivers/OpenRuntime.efi b/EFI-Debug/OC/Drivers/OpenRuntime.efi index c01e272..461ff78 100644 Binary files a/EFI-Debug/OC/Drivers/OpenRuntime.efi and b/EFI-Debug/OC/Drivers/OpenRuntime.efi differ diff --git a/EFI-Debug/OC/Drivers/ResetNvramEntry.efi b/EFI-Debug/OC/Drivers/ResetNvramEntry.efi index 7216407..1c59070 100644 Binary files a/EFI-Debug/OC/Drivers/ResetNvramEntry.efi and b/EFI-Debug/OC/Drivers/ResetNvramEntry.efi differ diff --git a/EFI-Debug/OC/OpenCore.efi b/EFI-Debug/OC/OpenCore.efi index 7c794de..8dfac83 100644 Binary files a/EFI-Debug/OC/OpenCore.efi and b/EFI-Debug/OC/OpenCore.efi differ diff --git a/EFI-Debug/OC/config.plist b/EFI-Debug/OC/config.plist index 933d9f8..bbedb8c 100644 --- a/EFI-Debug/OC/config.plist +++ b/EFI-Debug/OC/config.plist @@ -2,7 +2,7 @@ - # BASE EFI INTEL 11th ALDER LAKE (doest not support iGPU) - DEBUG Version + # BASE EFI INTEL 12h ALDER LAKE (doest not support iGPU) - DEBUG Version # DISCORD: https://discord.universohackintosh.com @@ -555,6 +555,12 @@ 50 KeySubsequentDelay 5 + PointerDwellClickTimeout + 0 + PointerDwellDoubleClickTimeout + 0 + PointerDwellRadius + 0 PointerPollMask -1 PointerPollMax diff --git a/EFI-Release/BOOT/BOOTx64.efi b/EFI-Release/BOOT/BOOTx64.efi index 7be4f13..ceeee84 100644 Binary files a/EFI-Release/BOOT/BOOTx64.efi and b/EFI-Release/BOOT/BOOTx64.efi differ diff --git a/EFI-Release/OC/Drivers/AudioDxe.efi b/EFI-Release/OC/Drivers/AudioDxe.efi index 454f740..f2374fa 100644 Binary files a/EFI-Release/OC/Drivers/AudioDxe.efi and b/EFI-Release/OC/Drivers/AudioDxe.efi differ diff --git a/EFI-Release/OC/Drivers/OpenCanopy.efi b/EFI-Release/OC/Drivers/OpenCanopy.efi index cc889ec..2124962 100644 Binary files a/EFI-Release/OC/Drivers/OpenCanopy.efi and b/EFI-Release/OC/Drivers/OpenCanopy.efi differ diff --git a/EFI-Release/OC/Drivers/ResetNvramEntry.efi b/EFI-Release/OC/Drivers/ResetNvramEntry.efi index 824797b..0cccc6b 100644 Binary files a/EFI-Release/OC/Drivers/ResetNvramEntry.efi and b/EFI-Release/OC/Drivers/ResetNvramEntry.efi differ diff --git a/EFI-Release/OC/OpenCore.efi b/EFI-Release/OC/OpenCore.efi index 67dec35..ef96a86 100644 Binary files a/EFI-Release/OC/OpenCore.efi and b/EFI-Release/OC/OpenCore.efi differ diff --git a/EFI-Release/OC/config.plist b/EFI-Release/OC/config.plist index a1414ab..cea2beb 100644 --- a/EFI-Release/OC/config.plist +++ b/EFI-Release/OC/config.plist @@ -555,6 +555,12 @@ 50 KeySubsequentDelay 5 + PointerDwellClickTimeout + 0 + PointerDwellDoubleClickTimeout + 0 + PointerDwellRadius + 0 PointerPollMask -1 PointerPollMax diff --git a/README.md b/README.md index 1eada91..7b7101b 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Note|Description :----|:---- Initial macOS Support|macOS 10.15, Catalina. -- Opencore version: 0.8.5 -- Release date: 04/10/2022 (late 1 day) +- Opencore version: 0.8.6 +- Release date: 07/11/2022 # Basic Steps diff --git a/Utils/macrecovery/boards.json b/Utils/macrecovery/boards.json index 6bccf2b..95837aa 100644 --- a/Utils/macrecovery/boards.json +++ b/Utils/macrecovery/boards.json @@ -1,31 +1,31 @@ { "Mac-EE2EBD4B90B839A8": "latest", - "Mac-BE0E8AC46FE800CC": "11.7", - "Mac-9AE82516C7C6B903": "12.6", + "Mac-BE0E8AC46FE800CC": "11.7.1", + "Mac-9AE82516C7C6B903": "12.6.1", "Mac-942452F5819B1C1B": "10.13.6", "Mac-942C5DF58193131B": "10.13.6", "Mac-C08A6BB70A942AC2": "10.13.6", "Mac-742912EFDBEE19B3": "10.13.6", "Mac-66F35F19FE2A0D05": "10.15.7", "Mac-2E6FAB96566FE58C": "10.15.7", - "Mac-35C1E88140C3E6CF": "11.7", - "Mac-7DF21CB3ED6977E5": "11.7", - "Mac-9F18E312C5C2BF0B": "12.6", - "Mac-937CB26E2E02BB01": "12.6", + "Mac-35C1E88140C3E6CF": "11.7.1", + "Mac-7DF21CB3ED6977E5": "11.7.1", + "Mac-9F18E312C5C2BF0B": "12.6.1", + "Mac-937CB26E2E02BB01": "12.6.1", "Mac-827FAC58A8FDFA22": "latest", "Mac-226CB3C6A851A671": "latest", "Mac-0CFF9C7C2B63DF8D": "latest", "Mac-C3EC7CD22292981F": "10.15.7", "Mac-AFD8A9D944EA4843": "10.15.7", - "Mac-189A3D4F975D5FFC": "11.7", - "Mac-3CBD00234E554E41": "11.7", - "Mac-2BD1B31983FE1663": "11.7", - "Mac-06F11FD93F0323C5": "12.6", - "Mac-06F11F11946D27C5": "12.6", - "Mac-E43C1C25D4880AD6": "12.6", - "Mac-473D31EABEB93F9B": "12.6", - "Mac-66E35819EE2D0D05": "12.6", - "Mac-A5C67F76ED83108C": "12.6", + "Mac-189A3D4F975D5FFC": "11.7.1", + "Mac-3CBD00234E554E41": "11.7.1", + "Mac-2BD1B31983FE1663": "11.7.1", + "Mac-06F11FD93F0323C5": "12.6.1", + "Mac-06F11F11946D27C5": "12.6.1", + "Mac-E43C1C25D4880AD6": "12.6.1", + "Mac-473D31EABEB93F9B": "12.6.1", + "Mac-66E35819EE2D0D05": "12.6.1", + "Mac-A5C67F76ED83108C": "12.6.1", "Mac-B4831CEBD52A0C4C": "latest", "Mac-CAD6701F7CEA0921": "latest", "Mac-551B86E5744E2388": "latest", @@ -43,7 +43,7 @@ "Mac-942459F5819B171B": "10.13.6", "Mac-4B7AC7E43945597E": "10.15.7", "Mac-6F01561E16C75D06": "10.15.7", - "Mac-F60DEB81FF30ACF6": "12.6", + "Mac-F60DEB81FF30ACF6": "12.6.1", "Mac-27AD2F918AE68F61": "latest", "Mac-F2208EC8": "10.13.6", "Mac-8ED6AF5B48C039E1": "10.13.6", @@ -51,7 +51,7 @@ "Mac-7BA5B2794B2CDB12": "10.13.6", "Mac-031AEE4D24BFF0B1": "10.15.7", "Mac-F65AE981FFA204ED": "10.15.7", - "Mac-35C5E08120C7EEAF": "12.6", + "Mac-35C5E08120C7EEAF": "12.6.1", "Mac-7BA5B2DFE22DDD8C": "latest", "Mac-942B5BF58194151B": "10.13.6", "Mac-942B59F58194171B": "10.13.6", @@ -61,13 +61,13 @@ "Mac-031B6874CF7F642A": "10.15.7", "Mac-27ADBB7B4CEE8E61": "10.15.7", "Mac-77EB7D7DAF985301": "10.15.7", - "Mac-81E3E92DD6088272": "11.7", - "Mac-42FD25EABCABB274": "11.7", - "Mac-A369DDC4E67F1C45": "12.6", - "Mac-FFE5EF870D7BA81A": "12.6", - "Mac-DB15BD556843C820": "12.6", - "Mac-65CE76090165799A": "12.6", - "Mac-B809C3757DA9BB8D": "12.6", + "Mac-81E3E92DD6088272": "11.7.1", + "Mac-42FD25EABCABB274": "11.7.1", + "Mac-A369DDC4E67F1C45": "12.6.1", + "Mac-FFE5EF870D7BA81A": "12.6.1", + "Mac-DB15BD556843C820": "12.6.1", + "Mac-65CE76090165799A": "12.6.1", + "Mac-B809C3757DA9BB8D": "12.6.1", "Mac-4B682C642B45593E": "latest", "Mac-77F17D7DA9285301": "latest", "Mac-BE088AF8C5EB4FA2": "latest", diff --git a/Utils/macrecovery/recovery_urls.txt b/Utils/macrecovery/recovery_urls.txt index 61afe5a..0aea577 100644 --- a/Utils/macrecovery/recovery_urls.txt +++ b/Utils/macrecovery/recovery_urls.txt @@ -30,6 +30,9 @@ python macrecovery.py -b Mac-00BE6ED71E35EB86 -m 00000000000000000 download # Big Sur (11) python macrecovery.py -b Mac-42FD25EABCABB274 -m 00000000000000000 download +# Monterey (12) +python macrecovery.py -b Mac-E43C1C25D4880AD6 -m 00000000000000000 download + # Latest version -# ie. Monterey (12) -python ./macrecovery.py -b Mac-E43C1C25D4880AD6 -m 00000000000000000 download +# ie. Ventura (13) +python ./macrecovery.py -b Mac-B4831CEBD52A0C4C -m 00000000000000000 download diff --git a/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/disk.py b/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/disk.py new file mode 100755 index 0000000..3a8d621 --- /dev/null +++ b/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/disk.py @@ -0,0 +1,411 @@ +import os, sys +sys.path.append(os.path.abspath(os.path.dirname(os.path.realpath(__file__)))) +import run, plist + +class Disk: + def __init__(self): + self.r = run.Run() + self.diskdump = self.check_diskdump() + self.full_os_version = self.r.run({"args":["sw_vers", "-productVersion"]})[0] + if len(self.full_os_version.split(".")) < 3: + # Ensure the format is XX.YY.ZZ + self.full_os_version += ".0" + self.os_version = ".".join(self.full_os_version.split(".")[:2]) + self.sudo_mount_version = "10.13.6" + self.efi_guids = ["C12A7328-F81F-11D2-BA4B-00A0C93EC93B"] + self.disks = self.get_disks() + + def check_diskdump(self): + ddpath = os.path.join(os.path.dirname(os.path.realpath(__file__)),"diskdump") + if not os.path.exists(ddpath): + raise FileNotFoundError("Could not locate diskdump") + if "com.apple.quarantine" in self.r.run({"args":["xattr",ddpath]})[0]: + self.r.run({"args":["xattr","-d","com.apple.quarantine",ddpath]}) + return ddpath + + def update(self): + # Refresh our disk list + self.disks = self.get_disks() + return self.disks + + def get_disks(self): + # Check for our binary - and ensure it's setup to run + ddpath = os.path.join(os.path.dirname(os.path.realpath(__file__)),"diskdump") + if not os.path.exists(ddpath): return {} + # Get our "diskutil list" and diskdump info. Run diskutil list first + # as it takes longer - but will stall while waiting for disks to appear, + # meaning our diskdump output will be better reflected. + diskutil_list = self.r.run({"args":["diskutil","list"]})[0] + diskstring = self.r.run({"args":[ddpath]})[0] + if not diskstring: return {} + diskdump = plist.loads(diskstring) + last_disk = None + for line in diskutil_list.split("\n"): + if line.startswith("/dev/disk"): + last_disk = line.split()[0].split("/")[-1] + elif not last_disk: + continue + elif line.strip().startswith("Logical Volume on"): + # Core Storage + ps = line.split("Logical Volume on")[1].strip().split(", ") + disk = self.get_disk(last_disk,disk_dict=diskdump) + if disk: # Update parent disk + disk["container"] = True + disk["core_storage"] = True + disk["physical_stores"] = ps + # Save a reference to the physical stores + for s in ps: + store = self.get_disk(s,disk_dict=diskdump) + if store: + store["container_for"] = last_disk + store["core_storage_container_for"] = last_disk + elif line.strip().startswith("Physical Store"): + # APFS + ps = line.split("Physical Store")[1].strip().split(", ") + disk = self.get_disk(last_disk,disk_dict=diskdump) + if disk: # Update parent disk + disk["container"] = True + disk["apfs"] = True + disk["physical_stores"] = ps + # Save a reference to the physical stores + for s in ps: + store = self.get_disk(s,disk_dict=diskdump) + if store: + store["container_for"] = last_disk + store["apfs_container_for"] = last_disk + return diskdump + + def get_identifier(self, disk = None, disk_dict = None): + # Should be able to take a mount point, disk name, or disk identifier, + # and return the disk's identifier + if isinstance(disk,dict): disk = disk.get("DAMediaBSDName") + if not disk: return + disk_dict = disk_dict or self.disks # Normalize the dict + disk = disk[6:] if disk.lower().startswith("/dev/rdisk") else disk[5:] if disk.lower().startswith("/dev/disk") else disk + if disk.lower() in disk_dict.get("AllDisks",[]): return disk + for d in disk_dict.get("AllDisksAndPartitions", []): + # Check the parent disk + if any((disk.lower()==d.get(x,"").lower() for x in ("DAMediaBSDName","DAVolumeName","DAVolumeUUID","DAMediaUUID","DAVolumePath"))): + return d.get("DAMediaBSDName") + # Check the partitions + for p in d.get("Partitions", []): + if any((disk.lower()==p.get(x,"").lower() for x in ("DAMediaBSDName","DAVolumeName","DAVolumeUUID","DAMediaUUID","DAVolumePath"))): + return p.get("DAMediaBSDName") + # At this point, we didn't find it + return None + + def get_parent(self, disk = None, disk_dict = None): + # For backward compatibility with the old disk.py approach + return self.get_physical_parent_identifiers(disk,disk_dict=disk_dict) + + def get_parent_identifier(self, disk = None, disk_dict = None): + # Resolves the passed disk value and returns the parent disk/container. + # i.e. Passing disk5s2s1 would return disk5 + disk = self.get_identifier(disk,disk_dict=disk_dict) + if not disk: return + return "disk"+disk.lower().split("disk")[1].split("s")[0] + + def get_physical_parent_identifier(self, disk = None, disk_dict = None): + # Returns the first hit from get_physical_parent_identifiers() + return next(iter(self.get_physical_parent_identifiers(disk, disk_dict=disk_dict) or []), None) + + def get_physical_parent_identifiers(self, disk = None, disk_dict = None): + # Resolves the passed disk to the physical parent disk identifiers. Useful for APFS + # and Core Storage volumes which are logical - and can span multiple disks. + # If you have an APFS container on disk4 and its Physical Store lists + # disk2s2, disk3s2 - this would return [disk2, disk3] + return [self.get_identifier(x,disk_dict=disk_dict) for x in self.get_physical_parent_disks(disk,disk_dict=disk_dict)] + + def get_physical_parent_disks(self, disk = None, disk_dict = None): + # Resolves the passed disk to the physical parent disk dicts. Useful for APFS + # and Core Storage volumes which are logcial and can span multiple physical + # disks. If you have an APFS container on disk4 and its Physical Store is on + # disk2s2 and disk3s2 - this would return the disk dicts for disk2 and disk3. + parent = self.get_parent_disk(disk, disk_dict=disk_dict) + if not parent: return [] + if not "physical_stores" in parent: return [parent] + return [self.get_parent_disk(x,disk_dict=disk_dict) for x in parent.get("physical_stores",[])] + + def get_parent_disk(self, disk = None, disk_dict = None): + # Returns the dict info for the parent of the passed mount point, name, identifier, etc + return self.get_disk(self.get_parent_identifier(disk,disk_dict=disk_dict),disk_dict=disk_dict) + + def get_disk(self, disk = None, disk_dict = None): + # Returns the dict info for the passed mount point, name, identifier, etc + disk = self.get_identifier(disk,disk_dict=disk_dict) + if not disk: return + parent = self.get_parent_identifier(disk,disk_dict=disk_dict) + # Walk AllDisksAndPartitions, and return the first hit + for d in (disk_dict or self.disks).get("AllDisksAndPartitions",[]): + d_ident = d.get("DAMediaBSDName") + if d_ident == disk: + return d # Got the disk + elif d_ident == parent: + # Got the parent - iterate the partitions + return next((p for p in d.get("Partitions",[]) if p.get("DAMediaBSDName")==disk),None) + return None # Didn't find it + + def get_efis(self, disk = None, disk_dict = None): + # Returns the identifiers for any EFI partitions attached to the + # parent disk(s) of the passed disk + efis = [] + for parent in self.get_physical_parent_identifiers(disk,disk_dict=disk_dict): + parent_dict = self.get_disk(parent,disk_dict=disk_dict) + if not parent_dict: continue + for part in parent_dict.get("Partitions",[]): + # Use the GUID instead of media name - as that can vary + if part.get("DAMediaContent","").upper() in self.efi_guids: + efis.append(part["DAMediaBSDName"]) + # Normalize case for the DAMediaName; + # macOS disks: "EFI System Partition", Windows disks: "EFI system partition" + # Maybe use this approach as a fallback at some point - but for now, just use the GUID + # if part.get("DAMediaName").lower() == "efi system partition": + # efis.append(part["DAMediaBSDName"]) + return efis + + def get_efi(self, disk = None, disk_dict = None): + # Returns the identifier for the first EFI partition found for + # the passed disk + return next(iter(self.get_efis(disk,disk_dict=disk_dict) or []), None) + + def get_mounted_volumes(self, disk_dict = None): + # Returns a list of mounted volumes + return (disk_dict or self.disks).get("MountPointsFromDisks",[]) + + def get_mounted_volume_dicts(self, disk_dict = None): + # Returns a list of dicts of name, identifier, mount point dicts + vol_list = [] + for v in (disk_dict or self.disks).get("MountPointsFromDisks"): + i = self.get_disk(v,disk_dict=disk_dict) + if not i: continue # Skip - as it didn't resolve + mount_point = self.get_mount_point(i,disk_dict=disk_dict) + # Check if we're either not mounted - or not mounted in /Volumes/ + if not v or not (v == "/" or v.lower().startswith("/volumes/")): + continue + vol = { + "name": self.get_volume_name(i,disk_dict=disk_dict), + "identifier": self.get_identifier(i,disk_dict=disk_dict), + "mount_point": v, + "disk_uuid": self.get_disk_uuid(i,disk_dict=disk_dict), + "volume_uuid": self.get_volume_uuid(i,disk_dict=disk_dict) + } + if "container_for" in i: vol["container_for"] = i["container_for"] + vol_list.append(vol) + return sorted(vol_list,key=lambda x:x["identifier"]) + + def get_disks_and_partitions_dict(self, disk_dict = None): + # Returns a list of dictionaries like so: + # { "disk0" : { + # "container": true/false, + # "physical_stores": [ + # "diskAsB", + # "diskXsY" + # ], + # "scheme": "Guid_partition_scheme", + # "partitions" : [ + # { + # "identifier" : "disk0s1", + # "name" : "EFI", + # "mount_point" : "/Volumes/EFI", + # "container_for": "diskCsD" + # } + # ] } } + disks = {} + for d in sorted((disk_dict or self.disks).get("AllDisksAndPartitions"),key=lambda x:x.get("DAMediaBSDName")): + if not "DAMediaBSDName" in d: continue # Malformed + parent = d["DAMediaBSDName"] + disks[parent] = {"partitions":[]} + # Save if the disk is logical - and a l)ist of its physical stores + for x in ("container","physical_stores"): + if x in d: disks[parent][x] = d[x] + disks[parent]["scheme"] = self.get_readable_partition_scheme(d,disk_dict=disk_dict) + # Check if this disk is also a volume - i.e. also a leaf, and insert it in the partitions list + partitions = d.get("Partitions",[]) + if d.get("DAMediaLeaf"): + partitions.insert(0,d) + for p in d.get("Partitions",[]): + part = { + "name": self.get_volume_name(p,disk_dict=disk_dict), + "identifier": self.get_identifier(p,disk_dict=disk_dict), + "mount_point": self.get_mount_point(p,disk_dict=disk_dict), + "disk_uuid": self.get_disk_uuid(p,disk_dict=disk_dict), + "volume_uuid": self.get_volume_uuid(p,disk_dict=disk_dict) + } + if "container_for" in p: part["container_for"] = p["container_for"] + disks[parent]["partitions"].append(part) + disks[parent]["partitions"].sort(key=lambda x:x["identifier"]) + return disks + + def _get_value(self, disk = None, value = None, disk_dict = None): + if not disk or not value: return # Missing info + if isinstance(disk,dict): return disk.get(value) + try: return self.get_disk(disk,disk_dict=disk_dict).get(value) + except: return + + def _is_uuid(self, value): + # Helper to return whether a passed value is a UUID + # 7C3CFDDF-920A-4924-AED6-7CD4AF6E4512 + if not isinstance(value,str): return False # Wrong type + value = value.lower() + # Check that all chars are hex or the separator + if not all((x in "-0123456789abcdef" for x in value)): return False + len_list = (8,4,4,4,12) + chunks = value.split("-") + # Make sure we have the right number of chunks - and + # each chunk is the right length. + if not len(chunks)==len(len_list): return False + for i,chunk in enumerate(chunks): + if not len(chunk)==len_list[i]: return False + # Passed all the checks + return True + + def get_partition_scheme(self, disk, allow_logical = True, disk_dict = None): + # let's resolve the disk to its physical parents + comm = self.get_parent_disk if allow_logical else self.get_physical_parent_disks + p = comm(disk,disk_dict=disk_dict) + if p: + if isinstance(p,(list,tuple)): p = p[0] # Extract the first parent if need be + if p.get("apfs"): return "APFS_container_scheme" + elif p.get("core_storage"): return "Core_Storage_container_scheme" + content = self.get_content(p,disk_dict=disk_dict) + if content.lower().endswith("scheme"): + return content + + def get_readable_partition_scheme(self, disk, allow_logical = True, disk_dict = None): + s = self.get_partition_scheme(disk,disk_dict=disk_dict) + if not s: return + # We want to convert GUID_partition_scheme to GUID + # We also want to translate FDisk to MBR + joined = " ".join(["MBR" if x.lower() == "fdisk" else x.capitalize() if x!=x.upper() else x for x in s.replace("_"," ").split() if x]) + return joined + + def get_content(self, disk, disk_dict = None): + return self._get_value(disk,"DAMediaContent",disk_dict=disk_dict) + + def get_volume_name(self, disk, disk_dict = None): + return self._get_value(disk,"DAVolumeName",disk_dict=disk_dict) + + def get_volume_uuid(self, disk, disk_dict = None): + return self._get_value(disk,"DAVolumeUUID",disk_dict=disk_dict) + + def get_disk_uuid(self, disk, disk_dict = None): + return self._get_value(disk,"DAMediaUUID",disk_dict=disk_dict) + + def get_mount_point(self, disk, disk_dict = None): + return self._get_value(disk,"DAVolumePath",disk_dict=disk_dict) + + def open_mount_point(self, disk, new_window = False, disk_dict = None): + disk = self.get_identifier(disk,disk_dict=disk_dict) + if not disk: return + mount = self.get_mount_point(disk) + if not mount: return + return self.r.run({"args":["open", mount]})[2] == 0 + + def compare_version(self, v1, v2): + # Splits the version numbers by periods and compare each value + # Allows 0.0.10 > 0.0.9 where normal string comparison would return false + # Also strips out any non-numeric values from each segment to avoid conflicts + # + # Returns True if v1 > v2, None if v1 == v2, and False if v1 < v2 + if not all((isinstance(x,str) for x in (v1,v2))): + # Wrong types + return False + v1_seg = v1.split(".") + v2_seg = v2.split(".") + # Pad with 0s to ensure common length + v1_seg += ["0"]*(len(v2_seg)-len(v1_seg)) + v2_seg += ["0"]*(len(v1_seg)-len(v2_seg)) + # Compare each segment - stripping non-numbers as needed + for i in range(len(v1_seg)): + a,b = v1_seg[i],v2_seg[i] + try: a = int("".join([x for x in a if x.isdigit()])) + except: a = 0 + try: b = int("".join([x for x in b if x.isdigit()])) + except: b = 0 + if a > b: return True + if a < b: return False + # If we're here, both versions are the same + return None + + def needs_sudo(self, disk = None, disk_dict = None): + # Default to EFI if we didn't pass a disk + if not disk: return self.compare_version(self.full_os_version,self.sudo_mount_version) in (True,None) + return self.compare_version(self.full_os_version,self.sudo_mount_version) in (True,None) and self.get_content(disk,disk_dict=disk_dict).upper() in self.efi_guids + + def mount_partition(self, disk, disk_dict = None): + disk = self.get_identifier(disk,disk_dict=disk_dict) + if not disk: return + sudo = self.needs_sudo(disk,disk_dict=disk_dict) + out = self.r.run({"args":["diskutil","mount",disk],"sudo":sudo}) + self.update() + return out + + def unmount_partition(self, disk, disk_dict = None): + disk = self.get_identifier(disk,disk_dict=disk_dict) + if not disk: return + out = self.r.run({"args":["diskutil","unmount",disk]}) + self.update() + return out + + def is_mounted(self, disk, disk_dict = None): + disk = self.get_identifier(disk,disk_dict=disk_dict) + if not disk: return + m = self.get_mount_point(disk,disk_dict=disk_dict) + return (m != None and len(m)) + + def get_volumes(self, disk_dict = None): + # Returns a list object with all volumes from disks + return sorted((disk_dict or self.disks).get("VolumesFromDisks",[])) + +if __name__ == '__main__': + d = Disk() + # Gather the args + errors = [] + args = [] + for x in sys.argv[1:]: + if x == "/": + args.append(x) + continue + if x.endswith("/"): + x = x[:-1] + if not x.lower().startswith("/volumes/") or len(x.split("/")) > 3: + errors.append("'{}' is not a volume.".format(x)) + continue + if not os.path.exists(x): + # Doesn't exist, skip it + errors.append("'{}' does not exist.".format(x)) + continue + args.append(x) + mount_list = [] + needs_sudo = d.needs_sudo() + for x in args: + name = d.get_volume_name(x) + if not name: name = "Untitled" + name = name.replace('"','\\"') # Escape double quotes in names + efi = d.get_efi(x) + if efi: mount_list.append((efi,name,d.is_mounted(efi),"diskutil mount {}".format(efi))) + else: errors.append("'{}' has no ESP.".format(name)) + if mount_list: + # We have something to mount + efis = [x[-1] for x in mount_list if not x[2]] # Only mount those that aren't mounted + names = [x[1] for x in mount_list if not x[2]] + if efis: # We have something to mount here + command = "do shell script \"{}\" with prompt \"MountEFI would like to mount the ESP{} on {}\"{}".format( + "; ".join(efis), + "s" if len(names) > 1 else "", + ", ".join(names), + " with administrator privileges" if needs_sudo else "") + o,e,r = d.r.run({"args":["osascript","-e",command]}) + if r > 0 and len(e.strip()) and e.strip().lower().endswith("(-128)"): exit() # User canceled, bail + # Update the disks + d.update() + # Walk the mounts and find out which aren't mounted + for efi,name,mounted,comm in mount_list: + mounted_at = d.get_mount_point(efi) + if mounted_at: d.open_mount_point(mounted_at) + else: errors.append("ESP for '{}' failed to mount.".format(name)) + else: + errors.append("No disks with ESPs selected.") + if errors: + # Display our errors before we leave + d.r.run({"args":["osascript","-e","display dialog \"{}\" buttons {{\"OK\"}} default button \"OK\" with icon caution".format("\n".join(errors))]}) \ No newline at end of file diff --git a/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/diskdump b/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/diskdump new file mode 100755 index 0000000..b81958f Binary files /dev/null and b/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/diskdump differ diff --git a/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/plist.py b/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/plist.py new file mode 100755 index 0000000..e47ddd4 --- /dev/null +++ b/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/plist.py @@ -0,0 +1,612 @@ +### ### +# Imports # +### ### + +import datetime, os, plistlib, struct, sys, itertools +from io import BytesIO + +if sys.version_info < (3,0): + # Force use of StringIO instead of cStringIO as the latter + # has issues with Unicode strings + from StringIO import StringIO + +try: + basestring # Python 2 + unicode +except NameError: + basestring = str # Python 3 + unicode = str + +try: + FMT_XML = plistlib.FMT_XML + FMT_BINARY = plistlib.FMT_BINARY +except AttributeError: + FMT_XML = "FMT_XML" + FMT_BINARY = "FMT_BINARY" + +### ### +# Helper Methods # +### ### + +def wrap_data(value): + if not _check_py3(): return plistlib.Data(value) + return value + +def extract_data(value): + if not _check_py3() and isinstance(value,plistlib.Data): return value.data + return value + +def _check_py3(): + return sys.version_info >= (3, 0) + +def _is_binary(fp): + if isinstance(fp, basestring): + return fp.startswith(b"bplist00") + header = fp.read(32) + fp.seek(0) + return header[:8] == b'bplist00' + +### ### +# Deprecated Functions - Remapped # +### ### + +def readPlist(pathOrFile): + if not isinstance(pathOrFile, basestring): + return load(pathOrFile) + with open(pathOrFile, "rb") as f: + return load(f) + +def writePlist(value, pathOrFile): + if not isinstance(pathOrFile, basestring): + return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False) + with open(pathOrFile, "wb") as f: + return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False) + +### ### +# Remapped Functions # +### ### + +def load(fp, fmt=None, use_builtin_types=None, dict_type=dict): + if _check_py3(): + use_builtin_types = True if use_builtin_types == None else use_builtin_types + # We need to monkey patch this to allow for hex integers - code taken/modified from + # https://github.com/python/cpython/blob/3.8/Lib/plistlib.py + if fmt is None: + header = fp.read(32) + fp.seek(0) + for info in plistlib._FORMATS.values(): + if info['detect'](header): + P = info['parser'] + break + else: + raise plistlib.InvalidFileException() + else: + P = plistlib._FORMATS[fmt]['parser'] + try: + p = P(use_builtin_types=use_builtin_types, dict_type=dict_type) + except: + # Python 3.9 removed use_builtin_types + p = P(dict_type=dict_type) + if isinstance(p,plistlib._PlistParser): + # Monkey patch! + def end_integer(): + d = p.get_data() + value = int(d,16) if d.lower().startswith("0x") else int(d) + if -1 << 63 <= value < 1 << 63: + p.add_object(value) + else: + raise OverflowError("Integer overflow at line {}".format(p.parser.CurrentLineNumber)) + def end_data(): + try: + p.add_object(plistlib._decode_base64(p.get_data())) + except Exception as e: + raise Exception("Data error at line {}: {}".format(p.parser.CurrentLineNumber,e)) + p.end_integer = end_integer + p.end_data = end_data + return p.parse(fp) + elif not _is_binary(fp): + # Is not binary - assume a string - and try to load + # We avoid using readPlistFromString() as that uses + # cStringIO and fails when Unicode strings are detected + # Don't subclass - keep the parser local + from xml.parsers.expat import ParserCreate + # Create a new PlistParser object - then we need to set up + # the values and parse. + p = plistlib.PlistParser() + parser = ParserCreate() + parser.StartElementHandler = p.handleBeginElement + parser.EndElementHandler = p.handleEndElement + parser.CharacterDataHandler = p.handleData + # We also need to monkey patch this to allow for other dict_types, hex int support + # proper line output for data errors, and for unicode string decoding + def begin_dict(attrs): + d = dict_type() + p.addObject(d) + p.stack.append(d) + def end_integer(): + d = p.getData() + value = int(d,16) if d.lower().startswith("0x") else int(d) + if -1 << 63 <= value < 1 << 63: + p.addObject(value) + else: + raise OverflowError("Integer overflow at line {}".format(parser.CurrentLineNumber)) + def end_data(): + try: + p.addObject(plistlib.Data.fromBase64(p.getData())) + except Exception as e: + raise Exception("Data error at line {}: {}".format(parser.CurrentLineNumber,e)) + def end_string(): + d = p.getData() + if isinstance(d,unicode): + d = d.encode("utf-8") + p.addObject(d) + p.begin_dict = begin_dict + p.end_integer = end_integer + p.end_data = end_data + p.end_string = end_string + if isinstance(fp, unicode): + # Encode unicode -> string; use utf-8 for safety + fp = fp.encode("utf-8") + if isinstance(fp, basestring): + # It's a string - let's wrap it up + fp = StringIO(fp) + # Parse it + parser.ParseFile(fp) + return p.root + else: + use_builtin_types = False if use_builtin_types == None else use_builtin_types + try: + p = _BinaryPlistParser(use_builtin_types=use_builtin_types, dict_type=dict_type) + except: + # Python 3.9 removed use_builtin_types + p = _BinaryPlistParser(dict_type=dict_type) + return p.parse(fp) + +def loads(value, fmt=None, use_builtin_types=None, dict_type=dict): + if _check_py3() and isinstance(value, basestring): + # If it's a string - encode it + value = value.encode() + try: + return load(BytesIO(value),fmt=fmt,use_builtin_types=use_builtin_types,dict_type=dict_type) + except: + # Python 3.9 removed use_builtin_types + return load(BytesIO(value),fmt=fmt,dict_type=dict_type) +def dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False): + if _check_py3(): + plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) + else: + if fmt == FMT_XML: + # We need to monkey patch a bunch here too in order to avoid auto-sorting + # of keys + writer = plistlib.PlistWriter(fp) + def writeDict(d): + if d: + writer.beginElement("dict") + items = sorted(d.items()) if sort_keys else d.items() + for key, value in items: + if not isinstance(key, basestring): + if skipkeys: + continue + raise TypeError("keys must be strings") + writer.simpleElement("key", key) + writer.writeValue(value) + writer.endElement("dict") + else: + writer.simpleElement("dict") + writer.writeDict = writeDict + writer.writeln("") + writer.writeValue(value) + writer.writeln("") + elif fmt == FMT_BINARY: + # Assume binary at this point + writer = _BinaryPlistWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys) + writer.write(value) + else: + # Not a proper format + raise ValueError("Unsupported format: {}".format(fmt)) + +def dumps(value, fmt=FMT_XML, skipkeys=False, sort_keys=True): + if _check_py3(): + return plistlib.dumps(value, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys).decode("utf-8") + else: + # We avoid using writePlistToString() as that uses + # cStringIO and fails when Unicode strings are detected + f = StringIO() + dump(value, f, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys) + return f.getvalue() + +### ### +# Binary Plist Stuff For Py2 # +### ### + +# From the python 3 plistlib.py source: https://github.com/python/cpython/blob/3.7/Lib/plistlib.py +# Tweaked to function on Python 2 + +class InvalidFileException (ValueError): + def __init__(self, message="Invalid file"): + ValueError.__init__(self, message) + +_BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'} + +_undefined = object() + +class _BinaryPlistParser: + """ + Read or write a binary plist file, following the description of the binary + format. Raise InvalidFileException in case of error, otherwise return the + root object. + see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c + """ + def __init__(self, use_builtin_types, dict_type): + self._use_builtin_types = use_builtin_types + self._dict_type = dict_type + + def parse(self, fp): + try: + # The basic file format: + # HEADER + # object... + # refid->offset... + # TRAILER + self._fp = fp + self._fp.seek(-32, os.SEEK_END) + trailer = self._fp.read(32) + if len(trailer) != 32: + raise InvalidFileException() + ( + offset_size, self._ref_size, num_objects, top_object, + offset_table_offset + ) = struct.unpack('>6xBBQQQ', trailer) + self._fp.seek(offset_table_offset) + self._object_offsets = self._read_ints(num_objects, offset_size) + self._objects = [_undefined] * num_objects + return self._read_object(top_object) + + except (OSError, IndexError, struct.error, OverflowError, + UnicodeDecodeError): + raise InvalidFileException() + + def _get_size(self, tokenL): + """ return the size of the next object.""" + if tokenL == 0xF: + m = ord(self._fp.read(1)[0]) & 0x3 + s = 1 << m + f = '>' + _BINARY_FORMAT[s] + return struct.unpack(f, self._fp.read(s))[0] + + return tokenL + + def _read_ints(self, n, size): + data = self._fp.read(size * n) + if size in _BINARY_FORMAT: + return struct.unpack('>' + _BINARY_FORMAT[size] * n, data) + else: + if not size or len(data) != size * n: + raise InvalidFileException() + return tuple(int.from_bytes(data[i: i + size], 'big') + for i in range(0, size * n, size)) + + def _read_refs(self, n): + return self._read_ints(n, self._ref_size) + + def _read_object(self, ref): + """ + read the object by reference. + May recursively read sub-objects (content of an array/dict/set) + """ + result = self._objects[ref] + if result is not _undefined: + return result + + offset = self._object_offsets[ref] + self._fp.seek(offset) + token = ord(self._fp.read(1)[0]) + tokenH, tokenL = token & 0xF0, token & 0x0F + + if token == 0: # \x00 or 0x00 + result = None + + elif token == 8: # \x08 or 0x08 + result = False + + elif token == 9: # \x09 or 0x09 + result = True + + # The referenced source code also mentions URL (0x0c, 0x0d) and + # UUID (0x0e), but neither can be generated using the Cocoa libraries. + + elif token == 15: # \x0f or 0x0f + result = b'' + + elif tokenH == 0x10: # int + result = 0 + for k in range((2 << tokenL) - 1): + result = (result << 8) + ord(self._fp.read(1)) + # result = int.from_bytes(self._fp.read(1 << tokenL), + # 'big', signed=tokenL >= 3) + + elif token == 0x22: # real + result = struct.unpack('>f', self._fp.read(4))[0] + + elif token == 0x23: # real + result = struct.unpack('>d', self._fp.read(8))[0] + + elif token == 0x33: # date + f = struct.unpack('>d', self._fp.read(8))[0] + # timestamp 0 of binary plists corresponds to 1/1/2001 + # (year of Mac OS X 10.0), instead of 1/1/1970. + result = (datetime.datetime(2001, 1, 1) + + datetime.timedelta(seconds=f)) + + elif tokenH == 0x40: # data + s = self._get_size(tokenL) + if self._use_builtin_types: + result = self._fp.read(s) + else: + result = plistlib.Data(self._fp.read(s)) + + elif tokenH == 0x50: # ascii string + s = self._get_size(tokenL) + result = self._fp.read(s).decode('ascii') + result = result + + elif tokenH == 0x60: # unicode string + s = self._get_size(tokenL) + result = self._fp.read(s * 2).decode('utf-16be') + + # tokenH == 0x80 is documented as 'UID' and appears to be used for + # keyed-archiving, not in plists. + + elif tokenH == 0xA0: # array + s = self._get_size(tokenL) + obj_refs = self._read_refs(s) + result = [] + self._objects[ref] = result + result.extend(self._read_object(x) for x in obj_refs) + + # tokenH == 0xB0 is documented as 'ordset', but is not actually + # implemented in the Apple reference code. + + # tokenH == 0xC0 is documented as 'set', but sets cannot be used in + # plists. + + elif tokenH == 0xD0: # dict + s = self._get_size(tokenL) + key_refs = self._read_refs(s) + obj_refs = self._read_refs(s) + result = self._dict_type() + self._objects[ref] = result + for k, o in zip(key_refs, obj_refs): + key = self._read_object(k) + if isinstance(key, plistlib.Data): + key = key.data + result[key] = self._read_object(o) + + else: + raise InvalidFileException() + + self._objects[ref] = result + return result + +def _count_to_size(count): + if count < 1 << 8: + return 1 + + elif count < 1 << 16: + return 2 + + elif count << 1 << 32: + return 4 + + else: + return 8 + +_scalars = (str, int, float, datetime.datetime, bytes) + +class _BinaryPlistWriter (object): + def __init__(self, fp, sort_keys, skipkeys): + self._fp = fp + self._sort_keys = sort_keys + self._skipkeys = skipkeys + + def write(self, value): + + # Flattened object list: + self._objlist = [] + + # Mappings from object->objectid + # First dict has (type(object), object) as the key, + # second dict is used when object is not hashable and + # has id(object) as the key. + self._objtable = {} + self._objidtable = {} + + # Create list of all objects in the plist + self._flatten(value) + + # Size of object references in serialized containers + # depends on the number of objects in the plist. + num_objects = len(self._objlist) + self._object_offsets = [0]*num_objects + self._ref_size = _count_to_size(num_objects) + + self._ref_format = _BINARY_FORMAT[self._ref_size] + + # Write file header + self._fp.write(b'bplist00') + + # Write object list + for obj in self._objlist: + self._write_object(obj) + + # Write refnum->object offset table + top_object = self._getrefnum(value) + offset_table_offset = self._fp.tell() + offset_size = _count_to_size(offset_table_offset) + offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects + self._fp.write(struct.pack(offset_format, *self._object_offsets)) + + # Write trailer + sort_version = 0 + trailer = ( + sort_version, offset_size, self._ref_size, num_objects, + top_object, offset_table_offset + ) + self._fp.write(struct.pack('>5xBBBQQQ', *trailer)) + + def _flatten(self, value): + # First check if the object is in the object table, not used for + # containers to ensure that two subcontainers with the same contents + # will be serialized as distinct values. + if isinstance(value, _scalars): + if (type(value), value) in self._objtable: + return + + elif isinstance(value, plistlib.Data): + if (type(value.data), value.data) in self._objtable: + return + + elif id(value) in self._objidtable: + return + + # Add to objectreference map + refnum = len(self._objlist) + self._objlist.append(value) + if isinstance(value, _scalars): + self._objtable[(type(value), value)] = refnum + elif isinstance(value, plistlib.Data): + self._objtable[(type(value.data), value.data)] = refnum + else: + self._objidtable[id(value)] = refnum + + # And finally recurse into containers + if isinstance(value, dict): + keys = [] + values = [] + items = value.items() + if self._sort_keys: + items = sorted(items) + + for k, v in items: + if not isinstance(k, basestring): + if self._skipkeys: + continue + raise TypeError("keys must be strings") + keys.append(k) + values.append(v) + + for o in itertools.chain(keys, values): + self._flatten(o) + + elif isinstance(value, (list, tuple)): + for o in value: + self._flatten(o) + + def _getrefnum(self, value): + if isinstance(value, _scalars): + return self._objtable[(type(value), value)] + elif isinstance(value, plistlib.Data): + return self._objtable[(type(value.data), value.data)] + else: + return self._objidtable[id(value)] + + def _write_size(self, token, size): + if size < 15: + self._fp.write(struct.pack('>B', token | size)) + + elif size < 1 << 8: + self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size)) + + elif size < 1 << 16: + self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size)) + + elif size < 1 << 32: + self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size)) + + else: + self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size)) + + def _write_object(self, value): + ref = self._getrefnum(value) + self._object_offsets[ref] = self._fp.tell() + if value is None: + self._fp.write(b'\x00') + + elif value is False: + self._fp.write(b'\x08') + + elif value is True: + self._fp.write(b'\x09') + + elif isinstance(value, int): + if value < 0: + try: + self._fp.write(struct.pack('>Bq', 0x13, value)) + except struct.error: + raise OverflowError(value) # from None + elif value < 1 << 8: + self._fp.write(struct.pack('>BB', 0x10, value)) + elif value < 1 << 16: + self._fp.write(struct.pack('>BH', 0x11, value)) + elif value < 1 << 32: + self._fp.write(struct.pack('>BL', 0x12, value)) + elif value < 1 << 63: + self._fp.write(struct.pack('>BQ', 0x13, value)) + elif value < 1 << 64: + self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True)) + else: + raise OverflowError(value) + + elif isinstance(value, float): + self._fp.write(struct.pack('>Bd', 0x23, value)) + + elif isinstance(value, datetime.datetime): + f = (value - datetime.datetime(2001, 1, 1)).total_seconds() + self._fp.write(struct.pack('>Bd', 0x33, f)) + + elif isinstance(value, plistlib.Data): + self._write_size(0x40, len(value.data)) + self._fp.write(value.data) + + elif isinstance(value, basestring): + try: + t = value.encode('ascii') + self._write_size(0x50, len(value)) + except UnicodeEncodeError: + t = value.encode('utf-16be') + self._write_size(0x60, len(t) // 2) + self._fp.write(t) + + elif isinstance(value, (bytes, bytearray)): + self._write_size(0x40, len(value)) + self._fp.write(value) + + elif isinstance(value, (list, tuple)): + refs = [self._getrefnum(o) for o in value] + s = len(refs) + self._write_size(0xA0, s) + self._fp.write(struct.pack('>' + self._ref_format * s, *refs)) + + elif isinstance(value, dict): + keyRefs, valRefs = [], [] + + if self._sort_keys: + rootItems = sorted(value.items()) + else: + rootItems = value.items() + + for k, v in rootItems: + if not isinstance(k, basestring): + if self._skipkeys: + continue + raise TypeError("keys must be strings") + keyRefs.append(self._getrefnum(k)) + valRefs.append(self._getrefnum(v)) + + s = len(keyRefs) + self._write_size(0xD0, s) + self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs)) + self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs)) + + else: + raise TypeError(value) diff --git a/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/run.py b/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/run.py new file mode 100755 index 0000000..b586adc --- /dev/null +++ b/Utils/mountefi/Mount EFI.workflow/Contents/Scripts/run.py @@ -0,0 +1,151 @@ +import sys, subprocess, time, threading, shlex +try: + from Queue import Queue, Empty +except: + from queue import Queue, Empty + +ON_POSIX = 'posix' in sys.builtin_module_names + +class Run: + + def __init__(self): + return + + def _read_output(self, pipe, q): + try: + for line in iter(lambda: pipe.read(1), b''): + q.put(line) + except ValueError: + pass + pipe.close() + + def _create_thread(self, output): + # Creates a new queue and thread object to watch based on the output pipe sent + q = Queue() + t = threading.Thread(target=self._read_output, args=(output, q)) + t.daemon = True + return (q,t) + + def _stream_output(self, comm, shell = False): + output = error = "" + p = None + try: + if shell and type(comm) is list: + comm = " ".join(shlex.quote(x) for x in comm) + if not shell and type(comm) is str: + comm = shlex.split(comm) + p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=True, close_fds=ON_POSIX) + # Setup the stdout thread/queue + q,t = self._create_thread(p.stdout) + qe,te = self._create_thread(p.stderr) + # Start both threads + t.start() + te.start() + + while True: + c = z = "" + try: c = q.get_nowait() + except Empty: pass + else: + sys.stdout.write(c) + output += c + sys.stdout.flush() + try: z = qe.get_nowait() + except Empty: pass + else: + sys.stderr.write(z) + error += z + sys.stderr.flush() + if not c==z=="": continue # Keep going until empty + # No output - see if still running + p.poll() + if p.returncode != None: + # Subprocess ended + break + # No output, but subprocess still running - stall for 20ms + time.sleep(0.02) + + o, e = p.communicate() + return (output+o, error+e, p.returncode) + except: + if p: + try: o, e = p.communicate() + except: o = e = "" + return (output+o, error+e, p.returncode) + return ("", "Command not found!", 1) + + def _decode(self, value, encoding="utf-8", errors="ignore"): + # Helper method to only decode if bytes type + if sys.version_info >= (3,0) and isinstance(value, bytes): + return value.decode(encoding,errors) + return value + + def _run_command(self, comm, shell = False): + c = None + try: + if shell and type(comm) is list: + comm = " ".join(shlex.quote(x) for x in comm) + if not shell and type(comm) is str: + comm = shlex.split(comm) + p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + c = p.communicate() + except: + if c == None: + return ("", "Command not found!", 1) + return (self._decode(c[0]), self._decode(c[1]), p.returncode) + + def run(self, command_list, leave_on_fail = False): + # Command list should be an array of dicts + if type(command_list) is dict: + # We only have one command + command_list = [command_list] + output_list = [] + for comm in command_list: + args = comm.get("args", []) + shell = comm.get("shell", False) + stream = comm.get("stream", False) + sudo = comm.get("sudo", False) + stdout = comm.get("stdout", False) + stderr = comm.get("stderr", False) + mess = comm.get("message", None) + show = comm.get("show", False) + + if not mess == None: + print(mess) + + if not len(args): + # nothing to process + continue + if sudo: + # Check if we have sudo + out = self._run_command(["which", "sudo"]) + if "sudo" in out[0]: + # Can sudo + if type(args) is list: + args.insert(0, out[0].replace("\n", "")) # add to start of list + elif type(args) is str: + args = out[0].replace("\n", "") + " " + args # add to start of string + + if show: + print(" ".join(args)) + + if stream: + # Stream it! + out = self._stream_output(args, shell) + else: + # Just run and gather output + out = self._run_command(args, shell) + if stdout and len(out[0]): + print(out[0]) + if stderr and len(out[1]): + print(out[1]) + # Append output + output_list.append(out) + # Check for errors + if leave_on_fail and out[2] != 0: + # Got an error - leave + break + if len(output_list) == 1: + # We only ran one command - just return that output + return output_list[0] + return output_list diff --git a/Utils/mountefi/Mount EFI.workflow/Contents/document.wflow b/Utils/mountefi/Mount EFI.workflow/Contents/document.wflow index e96d9f2..6a5b8a5 100644 --- a/Utils/mountefi/Mount EFI.workflow/Contents/document.wflow +++ b/Utils/mountefi/Mount EFI.workflow/Contents/document.wflow @@ -3,7 +3,7 @@ AMApplicationBuild - 509 + 512 AMApplicationVersion 2.10 AMDocumentVersion @@ -51,7 +51,7 @@ ActionParameters source - on run {input, parameters} set the_list to {} repeat with the_path in input set the_text to POSIX path of (the_path as text) set end of the_list to the_text end repeat return the_list end run + on run {input, parameters} # Hacky nonsense to export paths that aren't normally available set export to "export PATH=/usr/local/bin:/usr/local/sbin/:$PATH; " # Try to get the system python versions - starting with py 3 set python to "" repeat with py in {"python3", "python"} try set pypath to do shell script export & "which " & py if pypath is "/usr/bin/python3" then # Check if we're running 10.15 or newer - and verify "xcode-select -p" # runs properly before allowing the stub if (do shell script "sw_vers -productVersion") ≥ "10.15" then do shell script "xcode-select -p" end if end if do shell script export & "/usr/bin/env " & py & " -V" set python to py exit repeat on error errorMsg # Didn't resolve - onto the next end try end repeat if python is "" then my print_error("Could not locate python3 or python via /usr/bin/env!", "Please download and install the latest from python.org") return end if # If we got here - we have a python version to run. # Organize the input paths set passed_paths to "" repeat with the_path in input set the_text to quoted form of POSIX path of (the_path as text) as string set passed_paths to passed_paths & " " & the_text end repeat # Let's make sure we have disk.py file set target to "disk.py" # Get the contents of the ~/Library/Services folder, and look for disk.py to run set services to POSIX path of (path to home folder) & "Library/Services/" set quick_action_list to do shell script "ls -1 " & quoted form of services set quick_actions to paragraphs of quick_action_list repeat with qa in quick_actions set test_path to services & qa & "/Contents/Scripts/" & target if my check_exists(test_path) is "0" then # We got a valid path - let's run it do shell script export & "/usr/bin/env " & python & " " & quoted form of test_path & passed_paths return end if end repeat # If we got here - then we didn't find the quick action my print_error("Required files were not found!", "Please reinstall the latest Mount EFI quick action from https://github.com/corpnewt/MountEFI") return end run on check_exists(path) # Quick one-liner to abuse bash and see if a file/folder exists return do shell script "if [[ -e " & quoted form of path & " ]]; then echo 0; else echo 1; fi" end check_exists on print_error(header, footer) beep display alert header & " " & footer end print_error BundleIdentifier com.apple.Automator.RunScript @@ -67,18 +67,16 @@ Class Name RunScriptAction - IgnoresInput - InputUUID - 75C1496C-F97C-463E-8C60-9352C5A4B9C5 + 34AED1F9-B31F-4927-A2F3-43392E378CAC Keywords Run OutputUUID - E8D5BFA7-19E8-444A-A035-17C400DBCEFA + EAB751F1-47E9-4256-9F92-ED4CD9CF95F5 UUID - D6C847AF-3800-48CD-B4BA-DBFB7C59F37A + FDE567E1-0DA9-43F5-B7DC-9D30FD0B8553 UnlocalizedApplications Automator @@ -105,529 +103,18 @@ end run isViewVisible - + 1 location - 784.000000:368.000000 + 301.500000:558.000000 nibPath /System/Library/Automator/Run AppleScript.action/Contents/Resources/Base.lproj/main.nib isViewVisible - - - - action - - AMAccepts - - Container - List - Optional - - Types - - com.apple.cocoa.string - - - AMActionVersion - 2.0.3 - AMApplication - - Automator - - AMParameterProperties - - COMMAND_STRING - - CheckedForUserDefaultShell - - inputMethod - - shell - - source - - - AMProvides - - Container - List - Types - - com.apple.cocoa.string - - - ActionBundlePath - /System/Library/Automator/Run Shell Script.action - ActionName - Run Shell Script - ActionParameters - - COMMAND_STRING - import subprocess, plistlib, sys, os, shlex - -class Disk: - - def __init__(self): - self.diskutil = "diskutil" # self.get_diskutil() - self.os_version = self.run(["sw_vers", "-productVersion"])[0].strip() - self.sudo_mount_version = "10.13.6" - self.sudo_mount_types = ["efi"] - self.apfs = {} - self._update_disks() - - def run(self, comm, shell = False): - c = None - try: - if shell and type(comm) is list: comm = " ".join(shlex.quote(x) for x in comm) - if not shell and type(comm) is str: comm = shlex.split(comm) - p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - c = p.communicate() - except: - if c == None: return ("", "Command not found!", 1) - return (self._get_str(c[0]), self._get_str(c[1]), p.returncode) - - def _get_str(self, val): - # Helper method to return a string value based on input type - if sys.version_info >= (3,0) and isinstance(val, bytes): return val.encode("utf-8") - return val - - def _get_plist(self, s): - p = {} - try: - if sys.version_info >= (3, 0): - p = plistlib.loads(s.encode("utf-8")) - else: - # We avoid using readPlistFromString() as that uses - # cStringIO and fails when Unicode strings are detected - # Don't subclass - keep the parser local - from xml.parsers.expat import ParserCreate - # Create a new PlistParser object - then we need to set up - # the values and parse. - pa = plistlib.PlistParser() - # We also monkey patch this to encode unicode as utf-8 - def end_string(): - d = pa.getData() - if isinstance(d,unicode): - d = d.encode("utf-8") - pa.addObject(d) - pa.end_string = end_string - parser = ParserCreate() - parser.StartElementHandler = pa.handleBeginElement - parser.EndElementHandler = pa.handleEndElement - parser.CharacterDataHandler = pa.handleData - if isinstance(s, unicode): - # Encode unicode -> string; use utf-8 for safety - s = s.encode("utf-8") - # Parse the string - parser.Parse(s, 1) - p = pa.root - except Exception as e: - print(e) - return p - - def update(self): - self._update_disks() - - def _update_disks(self): - self.disks = self.get_disks() - self.disk_text = self.get_disk_text() - self.apfs = self.get_apfs() if self.os_version >= "10.12" else {} - - def get_disks(self): - # Returns a dictionary object of connected disks - disk_list = self.run([self.diskutil, "list", "-plist"])[0] - return self._get_plist(disk_list) - - def get_disk_text(self): - # Returns plain text listing connected disks - return self.run([self.diskutil, "list"])[0] - - def get_apfs(self): - # Returns a dictionary object of apfs disks - output = self.run("echo y | " + self.diskutil + " apfs list -plist", True) - if not output[2] == 0: return {} # Error getting apfs info - return an empty dict - disk_list = output[0] - p_list = disk_list.split("<?xml") - if len(p_list) > 1: disk_list = "<?xml" + p_list[-1] # We had text before the start - get only the plist info - return self._get_plist(disk_list) - - def is_apfs(self, disk): - disk_id = self.get_identifier(disk) - if not disk_id: return None - # Takes a disk identifier, and returns whether or not it's apfs - for d in self.disks.get("AllDisksAndPartitions", []): - if not "APFSVolumes" in d: continue - if d.get("DeviceIdentifier", "").lower() == disk_id.lower(): - return True - for a in d.get("APFSVolumes", []): - if a.get("DeviceIdentifier", "").lower() == disk_id.lower(): - return True - return False - - def is_apfs_container(self, disk): - disk_id = self.get_identifier(disk) - if not disk_id: return None - # Takes a disk identifier, and returns whether or not that specific - # disk/volume is an APFS Container - for d in self.disks.get("AllDisksAndPartitions", []): - # Only check partitions - for p in d.get("Partitions", []): - if disk_id.lower() == p.get("DeviceIdentifier", "").lower(): - return p.get("Content", "").lower() == "apple_apfs" - return False - - def is_cs_container(self, disk): - disk_id = self.get_identifier(disk) - if not disk_id: return None - # Takes a disk identifier, and returns whether or not that specific - # disk/volume is an CoreStorage Container - for d in self.disks.get("AllDisksAndPartitions", []): - # Only check partitions - for p in d.get("Partitions", []): - if disk_id.lower() == p.get("DeviceIdentifier", "").lower(): - return p.get("Content", "").lower() == "apple_corestorage" - return False - - def is_core_storage(self, disk): - disk_id = self.get_identifier(disk) - if not disk_id: return None - if self._get_physical_disk(disk_id, "Logical Volume on "): return True - return False - - def get_identifier(self, disk): - # Should be able to take a mount point, disk name, or disk identifier, - # and return the disk's identifier - # Iterate!! - if not disk or not len(self._get_str(disk)): return None - disk = disk.lower() - if disk.startswith("/dev/r"): disk = disk[len("/dev/r"):] - elif disk.startswith("/dev/"): disk = disk[len("/dev/"):] - if disk in self.disks.get("AllDisks", []): return disk - for d in self.disks.get("AllDisksAndPartitions", []): - for a in d.get("APFSVolumes", []): - if disk in [ a.get(x, "").lower() for x in ["DeviceIdentifier", "VolumeName", "VolumeUUID", "DiskUUID", "MountPoint"] ]: - return a.get("DeviceIdentifier", None) - for a in d.get("Partitions", []): - if disk in [ a.get(x, "").lower() for x in ["DeviceIdentifier", "VolumeName", "VolumeUUID", "DiskUUID", "MountPoint"] ]: - return a.get("DeviceIdentifier", None) - # At this point, we didn't find it - return None - - def get_top_identifier(self, disk): - disk_id = self.get_identifier(disk) - if not disk_id: return None - return disk_id.replace("disk", "didk").split("s")[0].replace("didk", "disk") - - def _get_physical_disk(self, disk, search_term): - # Change disk0s1 to disk0 - our_disk = self.get_top_identifier(disk) - our_term = "/dev/" + our_disk - found_disk = False - our_text = "" - for line in self.disk_text.split("\n"): - if line.lower().startswith(our_term): - found_disk = True - continue - if not found_disk: continue - if line.lower().startswith("/dev/disk"): - # At the next disk - bail - break - if search_term.lower() in line.lower(): - our_text = line - break - if not len(our_text): return None # Nothing found - our_stores = "".join(our_text.strip().split(search_term)[1:]).split(" ,") - if not len(our_stores): return None - for store in our_stores: - efi = self.get_efi(store) - if efi: return store - return None - - def get_physical_store(self, disk): - # Returns the physical store containing the EFI - disk_id = self.get_identifier(disk) - if not disk_id or not self.is_apfs(disk_id): return None - return self._get_physical_disk(disk_id, "Physical Store ") - - def get_core_storage_pv(self, disk): - # Returns the core storage physical volume containing the EFI - disk_id = self.get_identifier(disk) - if not disk_id or not self.is_core_storage(disk_id): return None - return self._get_physical_disk(disk_id, "Logical Volume on ") - - def get_parent(self, disk): - # Disk can be a mount point, disk name, or disk identifier - disk_id = self.get_identifier(disk) - if self.is_apfs(disk_id): disk_id = self.get_physical_store(disk_id) - elif self.is_core_storage(disk_id): disk_id = self.get_core_storage_pv(disk_id) - if not disk_id: return None - if self.is_apfs(disk_id): - # We have apfs - let's get the container ref - for a in self.apfs.get("Containers", []): - # Check if it's the whole container - if a.get("ContainerReference", "").lower() == disk_id.lower(): - return a["ContainerReference"] - # Check through each volume and return the parent's container ref - for v in a.get("Volumes", []): - if v.get("DeviceIdentifier", "").lower() == disk_id.lower(): - return a.get("ContainerReference", None) - else: - # Not apfs - go through all volumes and whole disks - for d in self.disks.get("AllDisksAndPartitions", []): - if d.get("DeviceIdentifier", "").lower() == disk_id.lower(): - return d["DeviceIdentifier"] - for p in d.get("Partitions", []): - if p.get("DeviceIdentifier", "").lower() == disk_id.lower(): - return d["DeviceIdentifier"] - # Didn't find anything - return None - - def get_efi(self, disk): - disk_id = self.get_parent(self.get_identifier(disk)) - if not disk_id: return None - # At this point - we should have the parent - for d in self.disks["AllDisksAndPartitions"]: - if d.get("DeviceIdentifier", "").lower() == disk_id.lower(): - # Found our disk - for p in d.get("Partitions", []): - if p.get("Content", "").lower() == "efi": - return p.get("DeviceIdentifier", None) - return None - - def needs_sudo(self, disk_id = None): - content = "EFI" # Default to EFI content - if disk_id: content = self.get_content(disk_id) - return self.os_version >= self.sudo_mount_version and content.lower() in self.sudo_mount_types - - def is_mounted(self, disk): - disk_id = self.get_identifier(disk) - if not disk_id: return None - m = self.get_mount_point(disk_id) - return (m != None and len(m)>0) - - def _get_value(self, disk, field, default = None, apfs_only = False): - disk_id = self.get_identifier(disk) - if not disk_id: - return None - # Takes a disk identifier, and returns the requested value - for d in self.disks.get("AllDisksAndPartitions", []): - for a in d.get("APFSVolumes", []): - if a.get("DeviceIdentifier", "").lower() == disk_id.lower(): - return a.get(field, default) - if apfs_only: - # Skip looking at regular partitions - continue - if d.get("DeviceIdentifier", "").lower() == disk_id.lower(): - return d.get(field, default) - for a in d.get("Partitions", []): - if a.get("DeviceIdentifier", "").lower() == disk_id.lower(): - return a.get(field, default) - return None - - # Getter methods - def get_content(self, disk): - return self._get_value(disk, "Content") - - def get_mount_point(self, disk): - return self._get_value(disk, "MountPoint") - - def get_volume_name(self, disk): - return self._get_value(disk, "VolumeName") - - def open_mount_point(self, disk, new_window = False): - disk_id = self.get_identifier(disk) - if not disk_id: return None - mount = self.get_mount_point(disk_id) - if not mount: return None - out = self.run(["open", mount]) - return out[2] == 0 - -if __name__ == '__main__': - d = Disk() - # Gather the args - errors = [] - args = [] - for x in sys.argv[1:]: - if x == "/": - args.append(x) - continue - if not x.lower().startswith("/volumes/"): - errors.append("'{}' is not a volume.".format(x)) - continue - if x.endswith("/"): - x = x[:-1] - if len(x.split("/")) > 3: - # Too nested - not a volume - errors.append("'{}' is not a volume.".format(x)) - continue - if not os.path.exists(x): - # Doesn't exist, skip it - errors.append("'{}' does not exist.".format(x)) - continue - args.append(x) - mount_list = [] - needs_sudo = d.needs_sudo() - for x in args: - name = d.get_volume_name(x) - if not name: name = "Untitled" - name = name.replace('"','\\"') # Escape double quotes in names - efi = d.get_efi(x) - if efi: mount_list.append((efi,name,d.is_mounted(efi),"diskutil mount {}".format(efi))) - else: errors.append("'{}' has no ESP.".format(name)) - if len(mount_list): - # We have something to mount - efis = [x[-1] for x in mount_list if not x[2]] # Only mount those that aren't mounted - names = [x[1] for x in mount_list if not x[2]] - if len(efis): # We have something to mount here - command = "do shell script \"{}\" with prompt \"MountEFI would like to mount the ESP{} on {}\"{}".format( - "; ".join(efis), - "s" if len(names) > 1 else "", - ", ".join(names), - " with administrator privileges" if needs_sudo else "") - o,e,r = d.run(["osascript","-e",command]) - if r > 0 and len(e.strip()) and e.strip().lower().endswith("(-128)"): exit() # User canceled, bail - # Update the disks - d.update() - # Walk the mounts and find out which aren't mounted - for efi,name,mounted,comm in mount_list: - mounted_at = d.get_mount_point(efi) - if mounted_at: d.open_mount_point(mounted_at) - else: errors.append("ESP for '{}' failed to mount.".format(name)) - else: - errors.append("No disks with ESPs selected.") - if len(errors): - # Display our errors before we leave - d.run(["osascript","-e","display dialog \"{}\" buttons {{\"OK\"}} default button \"OK\" with icon caution".format("\n".join(errors))]) - CheckedForUserDefaultShell - - inputMethod - 1 - shell - /usr/bin/python - source - - - BundleIdentifier - com.apple.RunShellScript - CFBundleVersion - 2.0.3 - CanShowSelectedItemsWhenRun - - CanShowWhenRun - - Category - - AMCategoryUtilities - - Class Name - RunShellScriptAction - InputUUID - 1EF3F5D2-601A-45CB-A0F3-2073CDF75D6D - Keywords - - Shell - Script - Command - Run - Unix - - OutputUUID - A943503D-31E8-4E6D-91EB-046A35336942 - UUID - 9AE9FCB9-5186-4567-A3FB-08D7175D30F6 - UnlocalizedApplications - - Automator - - arguments - - 0 - - default value - 0 - name - inputMethod - required - 0 - type - 0 - uuid - 0 - - 1 - - default value - - name - CheckedForUserDefaultShell - required - 0 - type - 0 - uuid - 1 - - 2 - - default value - - name - source - required - 0 - type - 0 - uuid - 2 - - 3 - - default value - - name - COMMAND_STRING - required - 0 - type - 0 - uuid - 3 - - 4 - - default value - /bin/sh - name - shell - required - 0 - type - 0 - uuid - 4 - - - isViewVisible - - location - 784.000000:1486.000000 - nibPath - /System/Library/Automator/Run Shell Script.action/Contents/Resources/Base.lproj/main.nib - - isViewVisible - + 1 connectors - - EFDA46C2-B5B5-4365-A046-D576516A3332 - - from - D6C847AF-3800-48CD-B4BA-DBFB7C59F37A - D6C847AF-3800-48CD-B4BA-DBFB7C59F37A - to - 9AE9FCB9-5186-4567-A3FB-08D7175D30F6 - 9AE9FCB9-5186-4567-A3FB-08D7175D30F6 - - + workflowMetaData applicationBundleIDsByPath diff --git a/Utils/ocvalidate/README.md b/Utils/ocvalidate/README.md index 1cef087..0aaf5c6 100644 --- a/Utils/ocvalidate/README.md +++ b/Utils/ocvalidate/README.md @@ -52,6 +52,7 @@ Utility to validate whether a `config.plist` matches requirements and convention - Entry[N]->BundlePath: Filename should have `.kext` suffix. - Entry[N]->PlistPath: Filename should have `.plist` suffix. - Entry[N]: If `Lilu.kext` is used, `DisableLinkeditJettison` should be enabled in `Kernel->Quirks`. +- `BrcmFirmwareRepo.kext` must not be injected by OpenCore. - For some known kexts, their `BundlePath`, `ExecutablePath`, and `PlistPath` must match against each other. Current list of rules can be found [here](https://github.com/acidanthera/OpenCorePkg/blob/master/Utilities/ocvalidate/KextInfo.c). - Plugin kext must be placed after parent kext. For example, [plugins of Lilu](https://github.com/acidanthera/Lilu/blob/master/KnownPlugins.md) must be placed after `Lilu.kext`. #### Delete diff --git a/Utils/ocvalidate/ocvalidate b/Utils/ocvalidate/ocvalidate index 0db3dcb..055016c 100755 Binary files a/Utils/ocvalidate/ocvalidate and b/Utils/ocvalidate/ocvalidate differ diff --git a/Utils/ocvalidate/ocvalidate.exe b/Utils/ocvalidate/ocvalidate.exe index ed23353..47f7de7 100755 Binary files a/Utils/ocvalidate/ocvalidate.exe and b/Utils/ocvalidate/ocvalidate.exe differ diff --git a/Utils/ocvalidate/ocvalidate.linux b/Utils/ocvalidate/ocvalidate.linux index a7e5dea..e288d6f 100755 Binary files a/Utils/ocvalidate/ocvalidate.linux and b/Utils/ocvalidate/ocvalidate.linux differ