├── .gitattributes ├── .gitignore ├── Information.md ├── LICENSE ├── README.md ├── Scripts ├── __init__.py ├── ioreg.py ├── plist.py ├── reveal.py ├── run.py └── utils.py ├── USBMap.command ├── USBMap.py ├── USBMapInjectorEdit.bat ├── USBMapInjectorEdit.command ├── USBMapInjectorEdit.py └── images ├── USB3.png ├── imac171.png └── look-sir-ports.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ensure all .bat scripts use CRLF line endings 2 | # This can prevent a number of odd batch issues 3 | *.bat text eol=crlf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Ignore extras 10 | *.txt 11 | *.plist 12 | *.dsl 13 | *.aml 14 | *.kext 15 | *.json 16 | 17 | # Ignore Results folder 18 | Results 19 | 20 | # Ignore iasl 21 | iasl 22 | 23 | # Ignore the USB.plist as well 24 | USB.plist 25 | 26 | # Don't pull macOS .DS_Store files 27 | .DS_Store 28 | 29 | # Distribution / packaging 30 | .Python 31 | build/ 32 | develop-eggs/ 33 | dist/ 34 | downloads/ 35 | eggs/ 36 | .eggs/ 37 | lib/ 38 | lib64/ 39 | parts/ 40 | sdist/ 41 | var/ 42 | wheels/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | MANIFEST 47 | 48 | # PyInstaller 49 | # Usually these files are written by a python script from a template 50 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 51 | *.manifest 52 | *.spec 53 | 54 | # Installer logs 55 | pip-log.txt 56 | pip-delete-this-directory.txt 57 | 58 | # Unit test / coverage reports 59 | htmlcov/ 60 | .tox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *.cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | db.sqlite3 78 | 79 | # Flask stuff: 80 | instance/ 81 | .webassets-cache 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | 89 | # PyBuilder 90 | target/ 91 | 92 | # Jupyter Notebook 93 | .ipynb_checkpoints 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # celery beat schedule file 99 | celerybeat-schedule 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | -------------------------------------------------------------------------------- /Information.md: -------------------------------------------------------------------------------- 1 | # USBMap 2 | 3 | Python script for mapping USB ports in macOS and creating a custom injector kext. 4 | 5 | *** 6 | 7 | # Features 8 | 9 | - [x] No dependency on USBInjectAll.kext 10 | - [x] Supports mapping XHCI (chipset, third party, and AMD), EHCI, OHCI, and UHCI ports 11 | - [x] Supports mapping USB 2 HUBs (requires the HUB's parent port to use type 255). 12 | - [x] Performs matching based on class name, not port or controller name. 13 | - [x] Allows users to set nicknames for the last-seen populated ports in the discovery process. 14 | - [x] Aggregates connected devices via session id instead of the broken port addressing 15 | - [x] Can use best-guess approaches to generate ACPI to rename controllers or reset RHUB devices as needed 16 | 17 | *** 18 | 19 | # Index 20 | 21 | - [Installation](#installation) 22 | - [Vocab Lesson](#vocab-lesson) 23 | - [What Is USB Mapping?](#what-is-usb-mapping) 24 | - [A Little Background](#a-little-background) 25 | * [Finding Ports](#finding-ports) 26 | * [The Port Limit](#the-port-limit) 27 | - [Mapping Options](#mapping-options) 28 | * [Port Limit Patch](#port-limit-patch) 29 | * [USBInjectAll](#usbinjectall) 30 | * [SSDT Replacement](#ssdt-replacement) 31 | * [Injector Kext](#injector-kext) 32 | 33 | 34 | *** 35 | 36 | ## Installation 37 | 38 | ### With Git 39 | 40 | Run the following one line at a time in Terminal: 41 | 42 | git clone https://github.com/corpnewt/USBMap 43 | cd USBMap 44 | chmod +x USBMap.command 45 | 46 | Then run with either `./USBMap.command` or by double-clicking *USBMap.command* 47 | 48 | ### Without Git 49 | 50 | You can get the latest zip of this repo [here](https://github.com/corpnewt/USBMap/archive/master.zip). Then run by double-clicking *USBMap.command* 51 | 52 | *** 53 | 54 | ## Vocab Lesson 55 | 56 | *Before we even get started, let's get familiar with some words because vocabulary is **fun**!* 57 | 58 | ~~Scary~~ Word | Definition 59 | ---------- | ---------- 60 | `Port` | A physical connection where you can plug a USB device. This could be a USB or USB-C port on your PC or Laptop, etc. 61 | `Header` | Similar to a `Port`, but typically on the motherboard itself. These often take a special connector, and typically either have internal devices plugged in (AiO pump controllers, Bluetooth devices, etc), or extensions that lead to ports at the front of your case when used. 62 | `Chipset` | The hardware on the motherboard responsible for "data flow" between components (on my Maximus X Code, this is Intel's Z370 chipset). 63 | `Controller` | The hardware responsible for managing USB ports. 64 | `RHUB` or `HUBN` | A software device that provides information for each individual port 65 | `OHCI` and `UHCI` | USB 1.1/1.0 protocol - `OHCI` is the "open" variant of `UHCI`. They both do roughly the same thing, but are not interchangable or compatible with each other. 66 | `EHCI` | USB 2.0 protocol with 1.0/1.1 backward compatibility. 67 | `XHCI` | USB protocol used for USB 3 and newer - can emulate USB 2.0/1.1/1.0, but is a completely different protocol. 68 | `Port Personality` | A software representation of a USB port. May correspond to a physical `port`, internal `header`, or may be orphaned. 69 | `Mapping` | In this context, the process of determining which `port personalities` correspond to which `ports` on which `controllers`. 70 | `Full Speed`/`Low Speed` | USB 1.x 71 | `High Speed` | USB 2.0 72 | `Super Speed` | USB 3+ 73 | `Kexts` | A contraction of **K**ernel **Ext**ension - these are much like drivers and extend the functionality of the kernel. 74 | `Injector` or `Codeless` kexts | A special type of kext that has no binary, and just expands the functionality of another kext. Often used to add supported devices, or extra information. 75 | 76 | *** 77 | 78 | ## What Is USB Mapping? 79 | 80 | *Alright kids, get out your cartography kits, we're going mapping!* 81 | 82 | If you've been reading diligently thusfar, you probably caught the short definition in the [Vocab Lesson](#vocab-lesson). We're going to expand on that a fair bit more though! Simply put, USB mapping is the process used to determine which port personalities correspond to which physical ports. 83 | 84 | ### A Little Background 85 | 86 | *Back in the glory days of Yosemite, we were spoiled. Hackintoshes roamed free in the tech fields, grazing lazily on the abundant USB ports that sprouted from the fertile ground... Then El Capitan showed up - touting that mouse cursor trick where it gets bigger when you wiggle it around a bunch (and uh.. probably other useful features), and we Hack Ranchers gathered up our livestock and trotted wide-eyed to its enticingly greener pastures - little did we know, though, that Apple snuck something in the code that would prove to be a thorn in our sides for OS versions to come...* 87 | 88 | There were some *major* under-the-hood changes regarding USB from 10.10 to 10.11! 89 | 90 | #### Finding Ports 91 | 92 | ![Ports!](/images/look-sir-ports.png) 93 | 94 | El Capitan changed the way the OS locates usable USB ports. That discovery is done in 3 ways, prioritized in the following order: 95 | 96 | 1. Ports defined in injector kexts - OSX/macOS has some built-in injectors that define ports based on SMBIOS. 97 | * In 10.11-10.14 the port information is stored in `/System/Library/Extensions/IOUSBHostFamily.kext/Contents/PlugIns/AppleUSB[protocol]PCI.kext/Contents/Info.plist` where `[protocol]` is one of OHCI/UHCI/EHCI/XHCI 98 | * In 10.15+ the port information has been moved to a single file at `/System/Library/Extensions/IOUSBHostFamily.kext/Contents/PlugIns/AppleUSBHostPlatformProperties.kext/Contents/Info.plist` 99 | * These injectors match specific named devices in the IORegistry (XHC1, EHC1, EHC2) 100 | * We **do not** want to match with these, as your motherboard likely doesn't have the same ports, or same port number configuration as an iMac or otherwise... 101 | 2. If no devices match the injectors - OSX/macOS falls back on ACPI data. Many motherboard manufacturers define RHUBs and USB ports in their DSDT, some use SSDTs, others don't define them at all, but *the most frustrating ones* only define **some** ports, causing the OS to ignore any that aren't defined. 102 | 3. If there's no ACPI data for the ports, OSX/macOS will then ask the hardware directly *"Hey bud, what kind of port action you rockin?"* 103 | 104 | *Okay, so if we don't match built-in injectors, and we don't have ACPI information for our ports (or they're all defined properly), we're good, right?* 105 | 106 | Uh... Let me set the scene... 107 | 108 | #### The Port Limit: 109 | 110 | *You finally got your install USB created, sweat pouring down your forehead as you plug that small instrument of black magic into a USB port and shakily press the power button. The machine springs to life, fans whirring and circulating - lights all aglow. Your display blinks, opens its metaphorical eyes and the BIOS splash screen greets you in its "I'm an 80s dream of the future" aesthetic - followed shortly by the boot picker for your boot manager of choice. The selector moves to your install USB and you methodically press the Enter key. Verbose text races across the screen, line by meticulous line, giving you a peek behind the curtain and into the heart of the boot process... but.. something's not right. The text garbles... a large "prohibited" sign affixes itself squarely to the center of your display and seemingly taunts you as booting halts save for one slowly repeating line of garbled text. Your eyes squint as you trace them over the mostly broken text... "Still waiting for root device..."* 111 | 112 | *Wait... what just happened?* 113 | 114 | Well, one of the biggest changes to affect us Hackintoshers is that Apple now imposes a 15 USB port per controller limit. At the surface, this doesn't sound terribly problematic. Most motherboards have far fewer than 15 physical ports - not to mention some have third party chipsets that can share the load (since *each* controller has its own 15 port limit). 115 | 116 | *Why 15?* 117 | 118 | While this seems kinda arbitrary, it's actually a way to limit the addressing of ports to a single 4 bit address. macOS/OSX displays devices using hexadecimal addressing (0-9A-F) - and when a device has other *stuff* attached to it (like a USB RHUB would have ports attached), it uses the first device's address as the starting point, and increments the next digit for the attached device. 119 | 120 | For instance - my chipset XHCI controller's RHUB shows up at `XHC@14000000`. That means the first port we add to that RHUB shows up at address `@14100000`, the second at `@14200000`, the 10th at `@14a00000`, and the 15th at `@14f00000`. Each allowed port coming off the RHUB fits very neatly in one digit of addressing (cute!). It gets a bit worrisome when you find out that *anything* above that `f` address **gets ignored** though... 121 | 122 | *My motherboard doesn't have anywhere near 15 ports, so... what's the catch?* 123 | 124 | I'm glad you asked! Most modern motherboard USB controllers leverage the XHCI protocol to handle all their USB ports, and USB 3 is a bit *sneaky*. Certainly far sneakier than its predecessors. 125 | 126 | When EHCI (USB 2.0) came about, it was really just an expansion upon the existing UHCI/OHCI (USB 1.x) protocol which moved much of the responsibility of routing ports to the hardware side, so backward compatibility using the same physical port layout was pretty easy to ensure. Many early EHCI controllers actually coexisted alongside UHCI or OHCI controllers. 127 | 128 | XHCI sauntered in later with big plans of USB 3 - and to fully replace EHCI/UHCI/OHCI while wrapping all that USB goodness into one neat little package. Our friend XHCI is a bit... *different* than the prior protocols though, so some emulation was required to achieve that functionality (some have had issues with this emulation, but most will never notice). 129 | 130 | (You can read more about the different protocols [here](https://en.wikipedia.org/wiki/Host_controller_interface_(USB,_Firewire))!) 131 | 132 | *Well, how is any of that sneaky though?* 133 | 134 | Let's have a look at the inside of a USB 3 port (image courtesy of usb.com): 135 | 136 | ![USB3](images/USB3.png) 137 | 138 | There are 9 pins in there - but they're setup very specifically. A USB 2 or prior device would only leverage those top 4 pins, while a USB 3+ device actually takes advantage of those 4, *plus* the additional 5. Every physical USB 3 port comes with *split personalities!* When a USB 2 or prior device is plugged into a USB 3 port, it's *seen* as a USB 2 port personality - while a USB 3 device plugged into that same port is seen as a USB 3 port personality. This means that every physical USB 3 port **takes up 2 of our limited 15 ports on that controller**. 139 | 140 | Let's look at the ports on my Asus Maximus X Code and break down how this all works out. Per the [spec page](https://www.asus.com/us/Motherboards/ROG-MAXIMUS-X-CODE/specifications/), we can see the following listed under the *USB Ports* header: 141 | 142 | ``` 143 | ASMedia® USB 3.1 Gen 2 controller : 144 | 1 x USB 3.1 Gen 2 front panel connector port(s) 145 | ASMedia® USB 3.1 Gen 2 controller : 146 | 2 x USB 3.1 Gen 2 port(s) (2 at back panel, black+red, Type-A + USB Type-CTM) 147 | Intel® Z370 Chipset : 148 | 6 x USB 3.1 Gen 1 port(s) (4 at back panel, blue, 2 at mid-board) 149 | Intel® Z370 Chipset : 150 | 6 x USB 2.0 port(s) (4 at back panel, black, 2 at mid-board) 151 | ``` 152 | 153 | Let's break this down - there are 2 *separate* ASMedia controllers, one with a single USB 3.1 Gen 2 front panel connector, the other with 2 USB 3.1 Gen 2 ports on the back panel. Neither of those should surpass the limit, as they're both only going to provide 2 USB 3.x ports, and since we know that each physical USB 3 port *counts as 2*, we can do some quick math and find how many total port personalities each of the ASMedia controllers provide: 154 | 155 | - 1 USB 3.1 Gen 2 front panel connector (which breaks out into 2 physical USB 3.1 ports - *each with split personalities*) gives us **4 port personalities total**. 156 | - 2 USB 3.1 Gen 2 (2 physical USB 3.1 ports - *each with split personalities*) gives us **4 port personalities total**. 157 | 158 | 4 personalities for each of the separate controllers is well under the 15 port limit, so we're **A OK** in that regard. 159 | 160 | 161 | Looking on, there are 2 entries for the Z370 chipset, but this is a bit different. There is only *one chipset*, and as such, both of these entries *share* the same controller. That tightens up our wiggle room a bit, so let's look at how many total port personalities we're working with... 162 | 163 | - 6 USB 3.1 Gen 1 (*each with split personalities*) gives us **12 port personalities**. 164 | - 6 more USB 2.0 ports (these are physically USB 2, and do not have split personalities) gives us **6 port personalities**. 165 | 166 | Combine the two values (since they share the chipset controller), and we're sitting at a toasty **18 port personalities total**. *This is over the 15 port limit!* 167 | 168 | This is where mapping really shines, as it gives us a way to *pick* which ports we'd like to omit, thus keeping us under the limit in a controlled way. Let's look at a few options for mapping next! 169 | 170 | *** 171 | 172 | ### Mapping Options 173 | 174 | As USB mapping has been a necessity since 2015, there have been a few approaches put into place to try and mitigate issues. 175 | 176 | #### Port Limit Patch 177 | 178 | *You stand in the sun, the gentle breeze stealing some of the heat as you walk through the orchard picking apples - you've gotten at least 10lbs put together at this point - a solid amount! Your hands shield your eyes from the sun as you stop to catch your breath; you catch a glimpse of your OS walking up as it hands you a bag for your haul of apples. A measly 5lb bag... How can you fit 10lbs of apples in a 5lb bag?* 179 | 180 | One of the mitigations for the USB port limit is to, well, just patch it out. Seems *epic*, no? The port limit patches have been in circulation for some time and exist to lift the 15 port limit patch in only a few key places so all ports the OS can "see" are available. OpenCore has a quirk that attempts to do this on any OS version called `XhciPortLimit`. 181 | 182 | *That sounds amazing, my 5lb bag is bigger on the inside than the outside? Time to shove all these apples in!* 183 | 184 | While it sounds like the best-case solution, it does come with some drawbacks... The port limit is *hardcoded* in a ton of places all over the OS, and as we're only lifting it in a few, this causes access outside the bounds of a fixed array. We're accessing things that shouldn't even be there, and that can cause some odd or unpredictable side effects. Everyone who sees you skipping along with your bag of apples will *know* that it's only 5lbs, even if it's filled with 10lbs worth. 185 | 186 | Ultimately, it's considered *best practice* to **only** leverage the port limit patch for the mapping process, and then to disable it. 187 | 188 | **Pros:** 189 | 190 | * Gets around the 15 port limit 191 | 192 | **Cons:** 193 | 194 | * Changes with each OS version 195 | * Causes issues with fixed array bounds 196 | * Only patched in some spots 197 | * Can cause unpredictable side effects 198 | 199 | #### USBInjectAll 200 | 201 | Remember those *super cool* ports that were only sorta sometimes defined in firmware/ACPI? Well - RehabMan saw an opportunity, and came up with a solution. He wrote a kext that has a ton of different Intel chipset controller ports hardcoded and named (for real, it's [*a ton*](https://github.com/RehabMan/OS-X-USB-Inject-All/blob/master/USBInjectAll/USBInjectAll-Info.plist)). What USBInjectAll tries to do is inject **all possible ports** for a given controller. From there, you can determine which correspond to physical connections, decide which to keep, and discard whatever you need to bring you under the limit. 202 | 203 | **Pros:** 204 | 205 | * Has a ton of hardware pre-defined 206 | * Utilizes boot args to map in sweeps (USB 2 port personalities or USB 3 port personalities) 207 | * Can be customized with an SSDT 208 | 209 | **Cons:** 210 | 211 | * Another kext to load, with code to execute 212 | * Uses the IORegistry as "scratch paper" 213 | * No longer maintained 214 | * Cannot map third party or AMD ports 215 | 216 | #### SSDT Replacement 217 | 218 | If you feel confident in your [ACPI](https://uefi.org/sites/default/files/resources/ACPI_6_3_final_Jan30.pdf) abilities, you can redefine the RHUB and ports for whichever ports you'd like to utilize. You can see an example of this in Osy's HaC-Mini repo [here](https://github.com/osy86/HaC-Mini/blob/master/ACPI/SSDT-Xhci.asl). 219 | 220 | **Pros:** 221 | 222 | * Clean - fewer kexts (especially to inject) 223 | * Potentially more durable if Apple changes its dependence on injectors 224 | 225 | **Cons:** 226 | 227 | * Tougher to accomplish - ACPI isn't many people's language of choice, and it can be tough to know where to find what you need, what to move over, etc. 228 | 229 | #### Injector Kext 230 | 231 | *So, we know we can leverage the port limit patch temporarily, we don't want to use the IORegistry as scratch paper, and we're all afraid of ACPI - what's the solution for us?* 232 | 233 | Well, we can actually accomplish this *the same way* Apple has! Apple's injector kexts for USB just extend the functionality of a target kext by providing information on which ports should be there, the port types, and what SMBIOS these settings apply to. Let's grab an example from Big Sur's `AppleUSBHostPlatformProperties.kext` for iMac17,1: 234 | 235 | ![iMac17,1 Ports](images/imac171.png) 236 | 237 | Looking at this image, we can see that the `IONameMatch` corresponds to `XHC1`, which means the OS will look for that when running this SMBIOS, and allow the following ports - HS02, HS03, HS04, HS05, HS06, HS10, SSP1, SSP4, SSP5, SSP6. Each of these ports are referenced by their `port` number (HS02 is port number `<02000000>` - which when converted from little-endian Hex to an integer is port 2). They're also given a `UsbConnector` value - which corresponds to the type of connection they use, some common values are 0 for USB 2 physical ports, 3 for USB 3 physical ports, 255 for internal ports. 238 | 239 | We can actually leverage this template to create our own injector kext that maps the ports on our motherboard! This is the approach we'll use for this guide, and the approach that USBMap uses. 240 | 241 | **Pros:** 242 | 243 | * Uses the same approach Apple does 244 | * No extra code to execute 245 | * Can be mostly automated 246 | 247 | **Cons:** 248 | 249 | * May not always be Apple's approach 250 | * Not as clean as an ACPI-only solution 251 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CorpNewt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # USBMap 2 | macOS has a limit on the number of USB ports it can recognize, which might cause some ports to function at lower speeds or not at all. USBMap is a python script which helps to create a custom kext to ensure all ports work correctly by mapping them within macOS's limits. 3 | 4 | *** 5 | 6 | # Features 7 | 8 | - [x] No dependency on USBInjectAll.kext 9 | - [x] Supports mapping XHCI (chipset, third party, and AMD), EHCI, OHCI, and UHCI ports 10 | - [x] Supports mapping USB 2 HUBs (requires the HUB's parent port to use type 255). 11 | - [x] Performs matching based on class name, not port or controller name. 12 | - [x] Allows users to set nicknames for the last-seen populated ports in the discovery process. 13 | - [x] Aggregates connected devices via session id instead of the broken port addressing 14 | - [x] Can use best-guess approaches to generate ACPI to rename controllers or reset RHUB devices as needed 15 | 16 | *** 17 | 18 | ## Installation 19 | 20 | ### With Git 21 | 22 | To install using the latest version from Github, run the following commands one at a time in Terminal.app: 23 | 24 | git clone https://github.com/corpnewt/USBMap 25 | cd USBMap 26 | chmod +x USBMap.command 27 | 28 | Then run with either `./USBMap.command` or by double-clicking *USBMap.command* 29 | 30 | ### Without Git 31 | 32 | You can get the latest zip of this repo [here](https://github.com/corpnewt/USBMap/archive/master.zip). Then run by double-clicking *USBMap.command* 33 | 34 | *** 35 | 36 | ## Quick Start 37 | 38 | ### Why 39 | 40 | macOS supports up to 15 USB ports per controller. On a native Mac, these are directly mapped to the physical ports. However, other motherboards may have more ports than are actually in use, leading macOS to default to using the first 15 ports it detects. This often results in physical ports only achieving USB 2 speeds because USB 3 ports are numbered above 15. USBMap allows you to create a kext customized for your system, ensuring that non-existent ports are ignored and all physical ports are accounted for within the 15-port limit. 41 | 42 | ### Before You Begin 43 | 44 | * Make sure to remove or disable *any* other USB mapping attempts (such as USBInjectAll.kext, USBToolBox.kext, USBPorts.kext, another USBMap.kext, etc) as they can interfere with this process 45 | * It can be helpful to run `R. Reset All Detected Ports` from USBMap's main menu to clear out any prior mapping information and start fresh 46 | 47 | ### General Mapping Process 48 | 49 | 1. Make sure you've run `D. Discover Ports` *at least once* from USBMap's main menu so it knows what USB controllers you have 50 | 2. Choose `K. Create USBMapDummy.kext` via USBMap's main menu 51 | 3. Add the USBMapDummy.kext dummy injector to your `EFI/OC/Kexts` folder and config.plist -> Kernel -> Add 52 | 4. Reboot your machine to apply the dummy map, providing a foundation for mapping. 53 | 5. Go into USBMap's `D. Discover Ports` and plug both a USB 2 and a USB 3 device into **every** port - letting the script refresh between each plug. You can assign nicknames to ports for easier identification using the 'N' key. 54 | 55 | ◦ It is normal that not all port personalities will have devices populate under them at this step as macOS can only see the first 15 per controller here! 56 | 57 | ◦ You can verify the dummy map is applied if all ports use a `UKxx` naming scheme (eg. `UK01`, `UK02`, etc) 58 | 59 | 60 | 6. The USBMap script will save the discovered port information in a file, so you can quit it for now. 61 | 7. Open the `USBMapInjectorEdit.command` and drag the USBMapDummy.kext from your EFI into the Terminal window. Disable **all** of the first 15 port personalities within each of the `IOKitPersonalities` that are not used for a keyboard or mouse - ***EVERYTHING ELSE*** in the first 15 can be disabled 62 | 63 | ◦ Disabling these is ***ONLY TEMPORARY*** and done *for the sake of mapping* - you can still choose which to include in the final map 64 | 65 | ◦ **DO NOT** disable port personalities 16 through 26, these need to stay enabled to continue mapping 66 | 67 | ◦ Make sure you go through each IOKitPersonality that `USBMapInjectorEdit.command` lists for this 68 | 8. Reboot your machine to apply the updated dummy map 69 | 9. Go into USBMap's `D. Discover Ports` and plug a USB 2 and USB 3 device into every port - letting the script refresh between each plug 70 | 71 | ◦ As some port personalities were disabled in step 7, it is normal that not plugged in USB devices will populate under a port personality at this step! 72 | 10. Go into the `P. Edit & Create USBMap.kext` menu and change the types to match the **physical port types** (i.e. for standard USB 2 port use "0" and for USB 3 Type-A port use "3". You can find all codes by pressing T) and enable which port personalities (up to 15) you want to keep 73 | 11. Build the final USBMap.kext and replace the dummy injector in your `EFI/OC/Kexts` folder and config.plist -> Kernel -> Add 74 | 75 | The dummy injector + USBMapInjectorEdit steps are to allow you to map using a "sliding window" of sorts. Since macOS can only see 15 port personalities per controller at one time, you need to map what's visible, then disable some to make room for the next sweep - and map again 76 | 77 | *** 78 | 79 | ## FAQ 80 | 81 | * **Intel Bluetooth Doesn't Show In Discovery** 82 | * Due to the way Intel Bluetooth populates, it does not show in ioreg the same way other USB devices do. You can still find its address in System Information -> USB, then clicking on the bt device and taking note of its `Location ID` 83 | * This should be worked around as of [this commit](https://github.com/corpnewt/USBMap/commit/07beeeba6a1453ad5a38dcdd1c9d9e704f5fb662) which merges info from `system_profiler` with `ioreg` to more completely map. 84 | -------------------------------------------------------------------------------- /Scripts/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile 2 | import glob 3 | modules = glob.glob(dirname(__file__)+"/*.py") 4 | __all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] -------------------------------------------------------------------------------- /Scripts/ioreg.py: -------------------------------------------------------------------------------- 1 | import os, sys, binascii, json 2 | from . import run 3 | 4 | class IOReg: 5 | def __init__(self): 6 | self.ioreg = {} 7 | self.pci_devices = [] 8 | self.r = run.Run() 9 | # Placeholder for a local pci.ids file. You can get it from: https://pci-ids.ucw.cz/ 10 | # and place it next to this file 11 | self.pci_ids = None 12 | 13 | def _get_hex_addr(self,item): 14 | # Attempts to reformat an item from NAME@X,Y to NAME@X000000Y 15 | try: 16 | if not "@" in item: 17 | # If no address - assume 0 18 | item = "{}@0".format(item) 19 | name,addr = item.split("@") 20 | if "," in addr: 21 | cont,port = addr.split(",") 22 | elif len(addr) > 4: 23 | # Using XXXXYYYY formatting already 24 | return name+"@"+addr 25 | else: 26 | # No comma, and 4 or fewer digits 27 | cont,port = addr,"0" 28 | item = name+"@"+hex(int(port,16)+(int(cont,16)<<16))[2:].upper() 29 | except: 30 | pass 31 | return item 32 | 33 | def _get_dec_addr(self,item): 34 | # Attemps to reformat an item from NAME@X000000Y to NAME@X,Y 35 | try: 36 | if not "@" in item: 37 | # If no address - assume 0 38 | item = "{}@0".format(item) 39 | name,addr = item.split("@") 40 | if addr.count(",")==1: 41 | # Using NAME@X,Y formating already 42 | return name+"@"+addr 43 | if len(addr)<5: 44 | return "{}@{},0".format(name,addr) 45 | hexaddr = int(addr,16) 46 | port = hexaddr & 0xFFFF 47 | cont = (hexaddr >> 16) & 0xFFFF 48 | item = name+"@"+hex(cont)[2:].upper() 49 | if port: 50 | item += ","+hex(port)[2:].upper() 51 | except: 52 | pass 53 | return item 54 | 55 | def _get_pcix_uid(self,item,allow_fallback=True,fallback_uid=0,plane="IOService",force=False): 56 | # Helper to look for the passed item's _UID 57 | # Expects a XXXX@Y style string 58 | self.get_ioreg(plane=plane,force=force) 59 | # Ensure our item ends with 2 spaces 60 | item = item.rstrip()+" " 61 | item_uid = None 62 | found_device = False 63 | for line in self.ioreg[plane]: 64 | if item in line: 65 | found_device = True 66 | continue 67 | if not found_device: 68 | continue # Haven't found it yet 69 | # We have the device here - let's look for _UID or a closing 70 | # curly bracket 71 | if line.replace("|","").strip() == "}": 72 | break # Bail on the loop 73 | elif '"_UID" = "' in line: 74 | # Got a _UID - let's rip it 75 | try: 76 | item_uid = int(line.split('"_UID" = "')[1].split('"')[0]) 77 | except: 78 | # Some _UIDs are strings - but we won't accept that here 79 | # as we're ripping it specifically for PciRoot/Pci pathing 80 | break 81 | if item_uid is None and allow_fallback: 82 | return fallback_uid 83 | return item_uid 84 | 85 | def get_ioreg(self,plane="IOService",force=False): 86 | if force or not self.ioreg.get(plane,None): 87 | self.ioreg[plane] = self.r.run({"args":["ioreg", "-lw0", "-p", plane]})[0].split("\n") 88 | return self.ioreg[plane] 89 | 90 | def get_pci_devices(self, force=False): 91 | # Uses system_profiler to build a list of connected 92 | # PCI devices 93 | if force or not self.pci_devices: 94 | try: 95 | self.pci_devices = json.loads(self.r.run({"args":[ 96 | "system_profiler", 97 | "SPPCIDataType", 98 | "-json" 99 | ]})[0])["SPPCIDataType"] 100 | assert isinstance(self.pci_devices,list) 101 | except: 102 | # Failed - reset 103 | self.pci_devices = [] 104 | return self.pci_devices 105 | 106 | def get_pci_device_name_from_pci_ids(self, vendor, device, subvendor=None, subdevice=None): 107 | # Takes 4-digit hex strings (no 0x prefix) for at least the vendor, 108 | # and device ids. Can optionally match subvendor and subdevice ids. 109 | if not self.pci_ids: 110 | # Hasn't already been processed - see if it exists, and load it if so 111 | pci_ids_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"pci.ids") 112 | if os.path.isfile(pci_ids_path): 113 | # Try loading the file 114 | try: 115 | with open(pci_ids_path,"rb") as f: 116 | self.pci_ids = f.read().decode(errors="ignore").replace("\r","").split("\n") 117 | except: 118 | return None 119 | # Check again 120 | if not self.pci_ids: 121 | return None 122 | # Helper to normalize all ids to 4 digit, lowercase 123 | # hex strings 124 | def normalize_id(_id): 125 | if not isinstance(_id,(int,str)): 126 | return None 127 | if isinstance(_id,str): 128 | if _id.startswith("<") and _id.endswith(">"): 129 | _id = _id.strip("<>") 130 | try: 131 | _id = binascii.hexlify(binascii.unhexlify(_id)[::-1]).decode() 132 | except: 133 | return None 134 | try: 135 | _id = int(_id,16) 136 | except: 137 | return None 138 | try: 139 | return hex(_id)[2:].lower().rjust(4,"0") 140 | except: 141 | return None 142 | # Ensure our ids are all lowercase 143 | vendor = normalize_id(vendor) 144 | device = normalize_id(device) 145 | if not vendor or not device: 146 | return None 147 | sub_check = None 148 | if subvendor and subdevice: 149 | v = normalize_id(subvendor) 150 | d = normalize_id(subdevice) 151 | if v and d: 152 | sub_check = "{} {}".format(v,d) 153 | # Walk the pci ids and check for our info sequentially 154 | vm = dm = sm = None 155 | for line in self.pci_ids: 156 | if line.strip().startswith("#"): 157 | continue # Skip comments 158 | if vm is None: 159 | if line.startswith(vendor): 160 | vm = " ".join(line.split(" ")[1:]).strip() 161 | continue 162 | # We should have a vendor here - make sure we 163 | # don't jump out of scope 164 | if not line.startswith("\t"): 165 | break # Jumped scope 166 | if dm is None: 167 | if line.startswith("\t"+device): 168 | dm = " ".join(line.split(" ")[1:]).strip() 169 | if sub_check is None: 170 | break # Nothing else to look for 171 | continue 172 | else: 173 | # Looking for subdevice info 174 | if not line.startswith("\t\t"): 175 | break # Jumped scope 176 | if line.startswith("\t\t"+sub_check): 177 | sm = " ".join(line.split(" ")[1:]).strip() 178 | break 179 | return sm or dm 180 | 181 | def get_pci_device_name(self, device_dict, pci_devices=None, force=False, use_unknown=True, use_pci_ids=True): 182 | device_name = "Unknown PCI Device" if use_unknown else None 183 | if not device_dict or not isinstance(device_dict,dict): 184 | return device_name 185 | if "info" in device_dict: 186 | # Expand the info 187 | device_dict = device_dict["info"] 188 | # Compare the vendor-id, device-id, revision-id, 189 | # subsystem-id, and subsystem-vendor-id if found 190 | # The system_profiler output prefixes those with "sppci-" 191 | def normalize_id(_id): 192 | if not _id: 193 | return None 194 | if _id.startswith("<") and _id.endswith(">"): 195 | _id = _id.strip("<>") 196 | try: 197 | _id = binascii.hexlify(binascii.unhexlify(_id)[::-1]).decode() 198 | except: 199 | return None 200 | try: 201 | return int(_id,16) 202 | except: 203 | return None 204 | # Order is important here for scraping pci.ids 205 | key_list = ( 206 | "vendor-id", 207 | "device-id", 208 | "subsystem-vendor-id", 209 | "subsystem-id" 210 | ) 211 | # Normalize the ids 212 | d_keys = [normalize_id(device_dict.get(key)) for key in key_list] 213 | if any(k is None for k in d_keys[:2]): 214 | # vendor and device ids are required 215 | return device_name 216 | if use_pci_ids: 217 | # Try our pci.ids list if we have one 218 | pci_ids_name = self.get_pci_device_name_from_pci_ids(*d_keys) 219 | if pci_ids_name: 220 | return pci_ids_name 221 | # Didn't get anything, or didn't check pci.ids 222 | # - check our system_profiler info 223 | if not isinstance(pci_devices,list): 224 | pci_devices = self.get_pci_devices(force=force) 225 | for pci_device in pci_devices: 226 | p_keys = [normalize_id(pci_device.get("sppci_"+key)) for key in key_list] 227 | if p_keys == d_keys: 228 | # Got a match - save the name if present 229 | device_name = pci_device.get("_name",device_name) 230 | break 231 | return device_name 232 | 233 | def get_all_devices(self, plane=None, force=False): 234 | # Let's build a device dict - and retain any info for each 235 | if plane is None: 236 | # Try to use IODeviceTree if it's populated, or if 237 | # IOService is not populated 238 | if self.ioreg.get("IODeviceTree") or not self.ioreg.get("IOService"): 239 | plane = "IODeviceTree" 240 | else: 241 | plane = "IOService" 242 | self.get_ioreg(plane=plane,force=force) 243 | # We're only interested in these two classes 244 | class_match = ( 245 | "= pad: 268 | del _path[-1] 269 | else: 270 | break 271 | if class_match and not any(c in line for c in class_match): 272 | continue # Not the right class 273 | # We found a device of our class - let's 274 | # retain info about it 275 | name = parts[1].split(" ")[0] 276 | clss = parts[1].split("= (3, 0) 43 | 44 | def _is_binary(fp): 45 | if isinstance(fp, basestring): 46 | return fp.startswith(b"bplist00") 47 | header = fp.read(32) 48 | fp.seek(0) 49 | return header[:8] == b'bplist00' 50 | 51 | ### ### 52 | # Deprecated Functions - Remapped # 53 | ### ### 54 | 55 | def readPlist(pathOrFile): 56 | if not isinstance(pathOrFile, basestring): 57 | return load(pathOrFile) 58 | with open(pathOrFile, "rb") as f: 59 | return load(f) 60 | 61 | def writePlist(value, pathOrFile): 62 | if not isinstance(pathOrFile, basestring): 63 | return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False) 64 | with open(pathOrFile, "wb") as f: 65 | return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False) 66 | 67 | ### ### 68 | # Remapped Functions # 69 | ### ### 70 | 71 | def load(fp, fmt=None, use_builtin_types=None, dict_type=dict): 72 | if _is_binary(fp): 73 | use_builtin_types = False if use_builtin_types is None else use_builtin_types 74 | try: 75 | p = _BinaryPlistParser(use_builtin_types=use_builtin_types, dict_type=dict_type) 76 | except: 77 | # Python 3.9 removed use_builtin_types 78 | p = _BinaryPlistParser(dict_type=dict_type) 79 | return p.parse(fp) 80 | elif _check_py3(): 81 | use_builtin_types = True if use_builtin_types is None else use_builtin_types 82 | # We need to monkey patch this to allow for hex integers - code taken/modified from 83 | # https://github.com/python/cpython/blob/3.8/Lib/plistlib.py 84 | if fmt is None: 85 | header = fp.read(32) 86 | fp.seek(0) 87 | for info in plistlib._FORMATS.values(): 88 | if info['detect'](header): 89 | P = info['parser'] 90 | break 91 | else: 92 | raise plistlib.InvalidFileException() 93 | else: 94 | P = plistlib._FORMATS[fmt]['parser'] 95 | try: 96 | p = P(use_builtin_types=use_builtin_types, dict_type=dict_type) 97 | except: 98 | # Python 3.9 removed use_builtin_types 99 | p = P(dict_type=dict_type) 100 | if isinstance(p,plistlib._PlistParser): 101 | # Monkey patch! 102 | def end_integer(): 103 | d = p.get_data() 104 | value = int(d,16) if d.lower().startswith("0x") else int(d) 105 | if -1 << 63 <= value < 1 << 64: 106 | p.add_object(value) 107 | else: 108 | raise OverflowError("Integer overflow at line {}".format(p.parser.CurrentLineNumber)) 109 | def end_data(): 110 | try: 111 | p.add_object(plistlib._decode_base64(p.get_data())) 112 | except Exception as e: 113 | raise Exception("Data error at line {}: {}".format(p.parser.CurrentLineNumber,e)) 114 | p.end_integer = end_integer 115 | p.end_data = end_data 116 | return p.parse(fp) 117 | else: 118 | # Is not binary - assume a string - and try to load 119 | # We avoid using readPlistFromString() as that uses 120 | # cStringIO and fails when Unicode strings are detected 121 | # Don't subclass - keep the parser local 122 | from xml.parsers.expat import ParserCreate 123 | # Create a new PlistParser object - then we need to set up 124 | # the values and parse. 125 | p = plistlib.PlistParser() 126 | parser = ParserCreate() 127 | parser.StartElementHandler = p.handleBeginElement 128 | parser.EndElementHandler = p.handleEndElement 129 | parser.CharacterDataHandler = p.handleData 130 | # We also need to monkey patch this to allow for other dict_types, hex int support 131 | # proper line output for data errors, and for unicode string decoding 132 | def begin_dict(attrs): 133 | d = dict_type() 134 | p.addObject(d) 135 | p.stack.append(d) 136 | def end_integer(): 137 | d = p.getData() 138 | value = int(d,16) if d.lower().startswith("0x") else int(d) 139 | if -1 << 63 <= value < 1 << 64: 140 | p.addObject(value) 141 | else: 142 | raise OverflowError("Integer overflow at line {}".format(parser.CurrentLineNumber)) 143 | def end_data(): 144 | try: 145 | p.addObject(plistlib.Data.fromBase64(p.getData())) 146 | except Exception as e: 147 | raise Exception("Data error at line {}: {}".format(parser.CurrentLineNumber,e)) 148 | def end_string(): 149 | d = p.getData() 150 | if isinstance(d,unicode): 151 | d = d.encode("utf-8") 152 | p.addObject(d) 153 | p.begin_dict = begin_dict 154 | p.end_integer = end_integer 155 | p.end_data = end_data 156 | p.end_string = end_string 157 | if isinstance(fp, unicode): 158 | # Encode unicode -> string; use utf-8 for safety 159 | fp = fp.encode("utf-8") 160 | if isinstance(fp, basestring): 161 | # It's a string - let's wrap it up 162 | fp = StringIO(fp) 163 | # Parse it 164 | parser.ParseFile(fp) 165 | return p.root 166 | 167 | def loads(value, fmt=None, use_builtin_types=None, dict_type=dict): 168 | if _check_py3() and isinstance(value, basestring): 169 | # If it's a string - encode it 170 | value = value.encode() 171 | try: 172 | return load(BytesIO(value),fmt=fmt,use_builtin_types=use_builtin_types,dict_type=dict_type) 173 | except: 174 | # Python 3.9 removed use_builtin_types 175 | return load(BytesIO(value),fmt=fmt,dict_type=dict_type) 176 | 177 | def dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False): 178 | if fmt == FMT_BINARY: 179 | # Assume binary at this point 180 | writer = _BinaryPlistWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys) 181 | writer.write(value) 182 | elif fmt == FMT_XML: 183 | if _check_py3(): 184 | plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) 185 | else: 186 | # We need to monkey patch a bunch here too in order to avoid auto-sorting 187 | # of keys 188 | writer = plistlib.PlistWriter(fp) 189 | def writeDict(d): 190 | if d: 191 | writer.beginElement("dict") 192 | items = sorted(d.items()) if sort_keys else d.items() 193 | for key, value in items: 194 | if not isinstance(key, basestring): 195 | if skipkeys: 196 | continue 197 | raise TypeError("keys must be strings") 198 | writer.simpleElement("key", key) 199 | writer.writeValue(value) 200 | writer.endElement("dict") 201 | else: 202 | writer.simpleElement("dict") 203 | writer.writeDict = writeDict 204 | writer.writeln("") 205 | writer.writeValue(value) 206 | writer.writeln("") 207 | else: 208 | # Not a proper format 209 | raise ValueError("Unsupported format: {}".format(fmt)) 210 | 211 | def dumps(value, fmt=FMT_XML, skipkeys=False, sort_keys=True): 212 | # We avoid using writePlistToString() as that uses 213 | # cStringIO and fails when Unicode strings are detected 214 | f = BytesIO() if _check_py3() else StringIO() 215 | dump(value, f, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys) 216 | value = f.getvalue() 217 | if _check_py3(): 218 | value = value.decode("utf-8") 219 | return value 220 | 221 | ### ### 222 | # Binary Plist Stuff For Py2 # 223 | ### ### 224 | 225 | # From the python 3 plistlib.py source: https://github.com/python/cpython/blob/3.11/Lib/plistlib.py 226 | # Tweaked to function on both Python 2 and 3 227 | 228 | class UID: 229 | def __init__(self, data): 230 | if not isinstance(data, int): 231 | raise TypeError("data must be an int") 232 | # It seems Apple only uses 32-bit unsigned ints for UIDs. Although the comment in 233 | # CoreFoundation's CFBinaryPList.c detailing the binary plist format theoretically 234 | # allows for 64-bit UIDs, most functions in the same file use 32-bit unsigned ints, 235 | # with the sole function hinting at 64-bits appearing to be a leftover from copying 236 | # and pasting integer handling code internally, and this code has not changed since 237 | # it was added. (In addition, code in CFPropertyList.c to handle CF$UID also uses a 238 | # 32-bit unsigned int.) 239 | # 240 | # if data >= 1 << 64: 241 | # raise ValueError("UIDs cannot be >= 2**64") 242 | if data >= 1 << 32: 243 | raise ValueError("UIDs cannot be >= 2**32 (4294967296)") 244 | if data < 0: 245 | raise ValueError("UIDs must be positive") 246 | self.data = data 247 | 248 | def __index__(self): 249 | return self.data 250 | 251 | def __repr__(self): 252 | return "%s(%s)" % (self.__class__.__name__, repr(self.data)) 253 | 254 | def __reduce__(self): 255 | return self.__class__, (self.data,) 256 | 257 | def __eq__(self, other): 258 | if not isinstance(other, UID): 259 | return NotImplemented 260 | return self.data == other.data 261 | 262 | def __hash__(self): 263 | return hash(self.data) 264 | 265 | class InvalidFileException (ValueError): 266 | def __init__(self, message="Invalid file"): 267 | ValueError.__init__(self, message) 268 | 269 | _BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'} 270 | 271 | _undefined = object() 272 | 273 | class _BinaryPlistParser: 274 | """ 275 | Read or write a binary plist file, following the description of the binary 276 | format. Raise InvalidFileException in case of error, otherwise return the 277 | root object. 278 | see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c 279 | """ 280 | def __init__(self, use_builtin_types, dict_type): 281 | self._use_builtin_types = use_builtin_types 282 | self._dict_type = dict_type 283 | 284 | def parse(self, fp): 285 | try: 286 | # The basic file format: 287 | # HEADER 288 | # object... 289 | # refid->offset... 290 | # TRAILER 291 | self._fp = fp 292 | self._fp.seek(-32, os.SEEK_END) 293 | trailer = self._fp.read(32) 294 | if len(trailer) != 32: 295 | raise InvalidFileException() 296 | ( 297 | offset_size, self._ref_size, num_objects, top_object, 298 | offset_table_offset 299 | ) = struct.unpack('>6xBBQQQ', trailer) 300 | self._fp.seek(offset_table_offset) 301 | self._object_offsets = self._read_ints(num_objects, offset_size) 302 | self._objects = [_undefined] * num_objects 303 | return self._read_object(top_object) 304 | 305 | except (OSError, IndexError, struct.error, OverflowError, 306 | UnicodeDecodeError): 307 | raise InvalidFileException() 308 | 309 | def _get_size(self, tokenL): 310 | """ return the size of the next object.""" 311 | if tokenL == 0xF: 312 | m = self._fp.read(1)[0] 313 | if not _check_py3(): 314 | m = ord(m) 315 | m = m & 0x3 316 | s = 1 << m 317 | f = '>' + _BINARY_FORMAT[s] 318 | return struct.unpack(f, self._fp.read(s))[0] 319 | 320 | return tokenL 321 | 322 | def _read_ints(self, n, size): 323 | data = self._fp.read(size * n) 324 | if size in _BINARY_FORMAT: 325 | return struct.unpack('>' + _BINARY_FORMAT[size] * n, data) 326 | else: 327 | if not size or len(data) != size * n: 328 | raise InvalidFileException() 329 | return tuple(int(binascii.hexlify(data[i: i + size]),16) 330 | for i in range(0, size * n, size)) 331 | '''return tuple(int.from_bytes(data[i: i + size], 'big') 332 | for i in range(0, size * n, size))''' 333 | 334 | def _read_refs(self, n): 335 | return self._read_ints(n, self._ref_size) 336 | 337 | def _read_object(self, ref): 338 | """ 339 | read the object by reference. 340 | May recursively read sub-objects (content of an array/dict/set) 341 | """ 342 | result = self._objects[ref] 343 | if result is not _undefined: 344 | return result 345 | 346 | offset = self._object_offsets[ref] 347 | self._fp.seek(offset) 348 | token = self._fp.read(1)[0] 349 | if not _check_py3(): 350 | token = ord(token) 351 | tokenH, tokenL = token & 0xF0, token & 0x0F 352 | 353 | if token == 0x00: # \x00 or 0x00 354 | result = None 355 | 356 | elif token == 0x08: # \x08 or 0x08 357 | result = False 358 | 359 | elif token == 0x09: # \x09 or 0x09 360 | result = True 361 | 362 | # The referenced source code also mentions URL (0x0c, 0x0d) and 363 | # UUID (0x0e), but neither can be generated using the Cocoa libraries. 364 | 365 | elif token == 0x0f: # \x0f or 0x0f 366 | result = b'' 367 | 368 | elif tokenH == 0x10: # int 369 | result = int(binascii.hexlify(self._fp.read(1 << tokenL)),16) 370 | if tokenL >= 3: # Signed - adjust 371 | result = result-((result & 0x8000000000000000) << 1) 372 | 373 | elif token == 0x22: # real 374 | result = struct.unpack('>f', self._fp.read(4))[0] 375 | 376 | elif token == 0x23: # real 377 | result = struct.unpack('>d', self._fp.read(8))[0] 378 | 379 | elif token == 0x33: # date 380 | f = struct.unpack('>d', self._fp.read(8))[0] 381 | # timestamp 0 of binary plists corresponds to 1/1/2001 382 | # (year of Mac OS X 10.0), instead of 1/1/1970. 383 | result = (datetime.datetime(2001, 1, 1) + 384 | datetime.timedelta(seconds=f)) 385 | 386 | elif tokenH == 0x40: # data 387 | s = self._get_size(tokenL) 388 | if self._use_builtin_types or not hasattr(plistlib, "Data"): 389 | result = self._fp.read(s) 390 | else: 391 | result = plistlib.Data(self._fp.read(s)) 392 | 393 | elif tokenH == 0x50: # ascii string 394 | s = self._get_size(tokenL) 395 | result = self._fp.read(s).decode('ascii') 396 | result = result 397 | 398 | elif tokenH == 0x60: # unicode string 399 | s = self._get_size(tokenL) 400 | result = self._fp.read(s * 2).decode('utf-16be') 401 | 402 | elif tokenH == 0x80: # UID 403 | # used by Key-Archiver plist files 404 | result = UID(int(binascii.hexlify(self._fp.read(1 + tokenL)),16)) 405 | 406 | elif tokenH == 0xA0: # array 407 | s = self._get_size(tokenL) 408 | obj_refs = self._read_refs(s) 409 | result = [] 410 | self._objects[ref] = result 411 | result.extend(self._read_object(x) for x in obj_refs) 412 | 413 | # tokenH == 0xB0 is documented as 'ordset', but is not actually 414 | # implemented in the Apple reference code. 415 | 416 | # tokenH == 0xC0 is documented as 'set', but sets cannot be used in 417 | # plists. 418 | 419 | elif tokenH == 0xD0: # dict 420 | s = self._get_size(tokenL) 421 | key_refs = self._read_refs(s) 422 | obj_refs = self._read_refs(s) 423 | result = self._dict_type() 424 | self._objects[ref] = result 425 | for k, o in zip(key_refs, obj_refs): 426 | key = self._read_object(k) 427 | if hasattr(plistlib, "Data") and isinstance(key, plistlib.Data): 428 | key = key.data 429 | result[key] = self._read_object(o) 430 | 431 | else: 432 | raise InvalidFileException() 433 | 434 | self._objects[ref] = result 435 | return result 436 | 437 | def _count_to_size(count): 438 | if count < 1 << 8: 439 | return 1 440 | 441 | elif count < 1 << 16: 442 | return 2 443 | 444 | elif count < 1 << 32: 445 | return 4 446 | 447 | else: 448 | return 8 449 | 450 | _scalars = (str, int, float, datetime.datetime, bytes) 451 | 452 | class _BinaryPlistWriter (object): 453 | def __init__(self, fp, sort_keys, skipkeys): 454 | self._fp = fp 455 | self._sort_keys = sort_keys 456 | self._skipkeys = skipkeys 457 | 458 | def write(self, value): 459 | 460 | # Flattened object list: 461 | self._objlist = [] 462 | 463 | # Mappings from object->objectid 464 | # First dict has (type(object), object) as the key, 465 | # second dict is used when object is not hashable and 466 | # has id(object) as the key. 467 | self._objtable = {} 468 | self._objidtable = {} 469 | 470 | # Create list of all objects in the plist 471 | self._flatten(value) 472 | 473 | # Size of object references in serialized containers 474 | # depends on the number of objects in the plist. 475 | num_objects = len(self._objlist) 476 | self._object_offsets = [0]*num_objects 477 | self._ref_size = _count_to_size(num_objects) 478 | 479 | self._ref_format = _BINARY_FORMAT[self._ref_size] 480 | 481 | # Write file header 482 | self._fp.write(b'bplist00') 483 | 484 | # Write object list 485 | for obj in self._objlist: 486 | self._write_object(obj) 487 | 488 | # Write refnum->object offset table 489 | top_object = self._getrefnum(value) 490 | offset_table_offset = self._fp.tell() 491 | offset_size = _count_to_size(offset_table_offset) 492 | offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects 493 | self._fp.write(struct.pack(offset_format, *self._object_offsets)) 494 | 495 | # Write trailer 496 | sort_version = 0 497 | trailer = ( 498 | sort_version, offset_size, self._ref_size, num_objects, 499 | top_object, offset_table_offset 500 | ) 501 | self._fp.write(struct.pack('>5xBBBQQQ', *trailer)) 502 | 503 | def _flatten(self, value): 504 | # First check if the object is in the object table, not used for 505 | # containers to ensure that two subcontainers with the same contents 506 | # will be serialized as distinct values. 507 | if isinstance(value, _scalars): 508 | if (type(value), value) in self._objtable: 509 | return 510 | 511 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 512 | if (type(value.data), value.data) in self._objtable: 513 | return 514 | 515 | elif id(value) in self._objidtable: 516 | return 517 | 518 | # Add to objectreference map 519 | refnum = len(self._objlist) 520 | self._objlist.append(value) 521 | if isinstance(value, _scalars): 522 | self._objtable[(type(value), value)] = refnum 523 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 524 | self._objtable[(type(value.data), value.data)] = refnum 525 | else: 526 | self._objidtable[id(value)] = refnum 527 | 528 | # And finally recurse into containers 529 | if isinstance(value, dict): 530 | keys = [] 531 | values = [] 532 | items = value.items() 533 | if self._sort_keys: 534 | items = sorted(items) 535 | 536 | for k, v in items: 537 | if not isinstance(k, basestring): 538 | if self._skipkeys: 539 | continue 540 | raise TypeError("keys must be strings") 541 | keys.append(k) 542 | values.append(v) 543 | 544 | for o in itertools.chain(keys, values): 545 | self._flatten(o) 546 | 547 | elif isinstance(value, (list, tuple)): 548 | for o in value: 549 | self._flatten(o) 550 | 551 | def _getrefnum(self, value): 552 | if isinstance(value, _scalars): 553 | return self._objtable[(type(value), value)] 554 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 555 | return self._objtable[(type(value.data), value.data)] 556 | else: 557 | return self._objidtable[id(value)] 558 | 559 | def _write_size(self, token, size): 560 | if size < 15: 561 | self._fp.write(struct.pack('>B', token | size)) 562 | 563 | elif size < 1 << 8: 564 | self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size)) 565 | 566 | elif size < 1 << 16: 567 | self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size)) 568 | 569 | elif size < 1 << 32: 570 | self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size)) 571 | 572 | else: 573 | self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size)) 574 | 575 | def _write_object(self, value): 576 | ref = self._getrefnum(value) 577 | self._object_offsets[ref] = self._fp.tell() 578 | if value is None: 579 | self._fp.write(b'\x00') 580 | 581 | elif value is False: 582 | self._fp.write(b'\x08') 583 | 584 | elif value is True: 585 | self._fp.write(b'\x09') 586 | 587 | elif isinstance(value, int): 588 | if value < 0: 589 | try: 590 | self._fp.write(struct.pack('>Bq', 0x13, value)) 591 | except struct.error: 592 | raise OverflowError(value) # from None 593 | elif value < 1 << 8: 594 | self._fp.write(struct.pack('>BB', 0x10, value)) 595 | elif value < 1 << 16: 596 | self._fp.write(struct.pack('>BH', 0x11, value)) 597 | elif value < 1 << 32: 598 | self._fp.write(struct.pack('>BL', 0x12, value)) 599 | elif value < 1 << 63: 600 | self._fp.write(struct.pack('>BQ', 0x13, value)) 601 | elif value < 1 << 64: 602 | self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True)) 603 | else: 604 | raise OverflowError(value) 605 | 606 | elif isinstance(value, float): 607 | self._fp.write(struct.pack('>Bd', 0x23, value)) 608 | 609 | elif isinstance(value, datetime.datetime): 610 | f = (value - datetime.datetime(2001, 1, 1)).total_seconds() 611 | self._fp.write(struct.pack('>Bd', 0x33, f)) 612 | 613 | elif (_check_py3() and isinstance(value, (bytes, bytearray))) or (hasattr(plistlib, "Data") and isinstance(value, plistlib.Data)): 614 | if not isinstance(value, (bytes, bytearray)): 615 | value = value.data # Unpack it 616 | self._write_size(0x40, len(value)) 617 | self._fp.write(value) 618 | 619 | elif isinstance(value, basestring): 620 | try: 621 | t = value.encode('ascii') 622 | self._write_size(0x50, len(value)) 623 | except UnicodeEncodeError: 624 | t = value.encode('utf-16be') 625 | self._write_size(0x60, len(t) // 2) 626 | self._fp.write(t) 627 | 628 | elif isinstance(value, UID) or (hasattr(plistlib,"UID") and isinstance(value, plistlib.UID)): 629 | if value.data < 0: 630 | raise ValueError("UIDs must be positive") 631 | elif value.data < 1 << 8: 632 | self._fp.write(struct.pack('>BB', 0x80, value)) 633 | elif value.data < 1 << 16: 634 | self._fp.write(struct.pack('>BH', 0x81, value)) 635 | elif value.data < 1 << 32: 636 | self._fp.write(struct.pack('>BL', 0x83, value)) 637 | # elif value.data < 1 << 64: 638 | # self._fp.write(struct.pack('>BQ', 0x87, value)) 639 | else: 640 | raise OverflowError(value) 641 | 642 | elif isinstance(value, (list, tuple)): 643 | refs = [self._getrefnum(o) for o in value] 644 | s = len(refs) 645 | self._write_size(0xA0, s) 646 | self._fp.write(struct.pack('>' + self._ref_format * s, *refs)) 647 | 648 | elif isinstance(value, dict): 649 | keyRefs, valRefs = [], [] 650 | 651 | if self._sort_keys: 652 | rootItems = sorted(value.items()) 653 | else: 654 | rootItems = value.items() 655 | 656 | for k, v in rootItems: 657 | if not isinstance(k, basestring): 658 | if self._skipkeys: 659 | continue 660 | raise TypeError("keys must be strings") 661 | keyRefs.append(self._getrefnum(k)) 662 | valRefs.append(self._getrefnum(v)) 663 | 664 | s = len(keyRefs) 665 | self._write_size(0xD0, s) 666 | self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs)) 667 | self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs)) 668 | 669 | else: 670 | raise TypeError(value) 671 | -------------------------------------------------------------------------------- /Scripts/reveal.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | from . import run 3 | 4 | class Reveal: 5 | 6 | def __init__(self): 7 | self.r = run.Run() 8 | return 9 | 10 | def get_parent(self, path): 11 | return os.path.normpath(os.path.join(path, os.pardir)) 12 | 13 | def reveal(self, path, new_window = False): 14 | # Reveals the passed path in Finder - only works on macOS 15 | if not sys.platform == "darwin": 16 | return ("", "macOS Only", 1) 17 | if not path: 18 | # No path sent - nothing to reveal 19 | return ("", "No path specified", 1) 20 | # Build our script - then convert it to a single line task 21 | if not os.path.exists(path): 22 | # Not real - bail 23 | return ("", "{} - doesn't exist".format(path), 1) 24 | # Get the absolute path 25 | path = os.path.abspath(path) 26 | command = ["osascript"] 27 | if new_window: 28 | command.extend([ 29 | "-e", "set p to \"{}\"".format(path.replace("\"", "\\\"")), 30 | "-e", "tell application \"Finder\"", 31 | "-e", "reveal POSIX file p as text", 32 | "-e", "activate", 33 | "-e", "end tell" 34 | ]) 35 | else: 36 | if path == self.get_parent(path): 37 | command.extend([ 38 | "-e", "set p to \"{}\"".format(path.replace("\"", "\\\"")), 39 | "-e", "tell application \"Finder\"", 40 | "-e", "reopen", 41 | "-e", "activate", 42 | "-e", "set target of window 1 to (POSIX file p as text)", 43 | "-e", "end tell" 44 | ]) 45 | else: 46 | command.extend([ 47 | "-e", "set o to \"{}\"".format(self.get_parent(path).replace("\"", "\\\"")), 48 | "-e", "set p to \"{}\"".format(path.replace("\"", "\\\"")), 49 | "-e", "tell application \"Finder\"", 50 | "-e", "reopen", 51 | "-e", "activate", 52 | "-e", "set target of window 1 to (POSIX file o as text)", 53 | "-e", "select (POSIX file p as text)", 54 | "-e", "end tell" 55 | ]) 56 | return self.r.run({"args" : command}) 57 | 58 | def notify(self, title = None, subtitle = None, sound = None): 59 | # Sends a notification 60 | if not title: 61 | return ("", "Malformed dict", 1) 62 | # Build our notification 63 | n_text = "display notification with title \"{}\"".format(title.replace("\"", "\\\"")) 64 | if subtitle: 65 | n_text += " subtitle \"{}\"".format(subtitle.replace("\"", "\\\"")) 66 | if sound: 67 | n_text += " sound name \"{}\"".format(sound.replace("\"", "\\\"")) 68 | command = ["osascript", "-e", n_text] 69 | return self.r.run({"args" : command}) 70 | -------------------------------------------------------------------------------- /Scripts/run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | import threading 4 | import shlex 5 | try: 6 | from Queue import Queue, Empty 7 | except: 8 | from queue import Queue, Empty 9 | 10 | ON_POSIX = 'posix' in sys.builtin_module_names 11 | 12 | class Run: 13 | 14 | def __init__(self): 15 | return 16 | 17 | def _read_output(self, pipe, q): 18 | try: 19 | for line in iter(lambda: pipe.read(1), b''): 20 | q.put(line) 21 | except ValueError: 22 | pass 23 | pipe.close() 24 | 25 | def _stream_output(self, comm, shell = False): 26 | output = error = "" 27 | p = ot = et = None 28 | try: 29 | if shell and type(comm) is list: 30 | comm = " ".join(shlex.quote(x) for x in comm) 31 | if not shell and type(comm) is str: 32 | comm = shlex.split(comm) 33 | p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=True, close_fds=ON_POSIX) 34 | # Setup the stdout thread/queue 35 | q = Queue() 36 | t = threading.Thread(target=self._read_output, args=(p.stdout, q)) 37 | t.daemon = True # thread dies with the program 38 | # Setup the stderr thread/queue 39 | qe = Queue() 40 | te = threading.Thread(target=self._read_output, args=(p.stderr, qe)) 41 | te.daemon = True # thread dies with the program 42 | # Start both threads 43 | t.start() 44 | te.start() 45 | 46 | while True: 47 | c = z = "" 48 | try: 49 | c = q.get_nowait() 50 | except Empty: 51 | pass 52 | else: 53 | sys.stdout.write(c) 54 | output += c 55 | sys.stdout.flush() 56 | try: 57 | z = qe.get_nowait() 58 | except Empty: 59 | pass 60 | else: 61 | sys.stderr.write(z) 62 | error += z 63 | sys.stderr.flush() 64 | p.poll() 65 | if c==z=="" and p.returncode != None: 66 | break 67 | 68 | o, e = p.communicate() 69 | ot.exit() 70 | et.exit() 71 | return (output+o, error+e, p.returncode) 72 | except: 73 | if ot or et: 74 | try: ot.exit() 75 | except: pass 76 | try: et.exit() 77 | except: pass 78 | if p: 79 | return (output, error, p.returncode) 80 | return ("", "Command not found!", 1) 81 | 82 | def _decode(self, value): 83 | # Helper method to only decode if bytes type 84 | if sys.version_info >= (3,0) and isinstance(value, bytes): 85 | return value.decode("utf-8","ignore") 86 | return value 87 | 88 | def _run_command(self, comm, shell = False): 89 | c = None 90 | try: 91 | if shell and type(comm) is list: 92 | comm = " ".join(shlex.quote(x) for x in comm) 93 | if not shell and type(comm) is str: 94 | comm = shlex.split(comm) 95 | p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 96 | c = p.communicate() 97 | except: 98 | if c == None: 99 | return ("", "Command not found!", 1) 100 | return (self._decode(c[0]), self._decode(c[1]), p.returncode) 101 | 102 | def run(self, command_list, leave_on_fail = False): 103 | # Command list should be an array of dicts 104 | if type(command_list) is dict: 105 | # We only have one command 106 | command_list = [command_list] 107 | output_list = [] 108 | for comm in command_list: 109 | args = comm.get("args", []) 110 | shell = comm.get("shell", False) 111 | stream = comm.get("stream", False) 112 | sudo = comm.get("sudo", False) 113 | stdout = comm.get("stdout", False) 114 | stderr = comm.get("stderr", False) 115 | mess = comm.get("message", None) 116 | show = comm.get("show", False) 117 | 118 | if not mess == None: 119 | print(mess) 120 | 121 | if not len(args): 122 | # nothing to process 123 | continue 124 | if sudo: 125 | # Check if we have sudo 126 | out = self._run_command(["which", "sudo"]) 127 | if "sudo" in out[0]: 128 | # Can sudo 129 | if type(args) is list: 130 | args.insert(0, out[0].replace("\n", "")) # add to start of list 131 | elif type(args) is str: 132 | args = out[0].replace("\n", "") + " " + args # add to start of string 133 | 134 | if show: 135 | print(" ".join(args)) 136 | 137 | if stream: 138 | # Stream it! 139 | out = self._stream_output(args, shell) 140 | else: 141 | # Just run and gather output 142 | out = self._run_command(args, shell) 143 | if stdout and len(out[0]): 144 | print(out[0]) 145 | if stderr and len(out[1]): 146 | print(out[1]) 147 | # Append output 148 | output_list.append(out) 149 | # Check for errors 150 | if leave_on_fail and out[2] != 0: 151 | # Got an error - leave 152 | break 153 | if len(output_list) == 1: 154 | # We only ran one command - just return that output 155 | return output_list[0] 156 | return output_list 157 | -------------------------------------------------------------------------------- /Scripts/utils.py: -------------------------------------------------------------------------------- 1 | import sys, os, time, re, json, datetime, ctypes, subprocess 2 | 3 | if os.name == "nt": 4 | # Windows 5 | import msvcrt 6 | else: 7 | # Not Windows \o/ 8 | import select 9 | 10 | class Utils: 11 | 12 | def __init__(self, name = "Python Script"): 13 | self.name = name 14 | # Init our colors before we need to print anything 15 | cwd = os.getcwd() 16 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 17 | if os.path.exists("colors.json"): 18 | self.colors_dict = json.load(open("colors.json")) 19 | else: 20 | self.colors_dict = {} 21 | os.chdir(cwd) 22 | 23 | def check_admin(self): 24 | # Returns whether or not we're admin 25 | try: 26 | is_admin = os.getuid() == 0 27 | except AttributeError: 28 | is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 29 | return is_admin 30 | 31 | def elevate(self, file): 32 | # Runs the passed file as admin 33 | if self.check_admin(): 34 | return 35 | if os.name == "nt": 36 | ctypes.windll.shell32.ShellExecuteW(None, "runas", '"{}"'.format(sys.executable), '"{}"'.format(file), None, 1) 37 | else: 38 | try: 39 | p = subprocess.Popen(["which", "sudo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 40 | c = p.communicate()[0].decode("utf-8", "ignore").replace("\n", "") 41 | os.execv(c, [ sys.executable, 'python'] + sys.argv) 42 | except: 43 | exit(1) 44 | 45 | def compare_versions(self, vers1, vers2, **kwargs): 46 | # Helper method to compare ##.## strings 47 | # 48 | # vers1 < vers2 = True 49 | # vers1 = vers2 = None 50 | # vers1 > vers2 = False 51 | 52 | # Sanitize the pads 53 | pad = str(kwargs.get("pad", "")) 54 | sep = str(kwargs.get("separator", ".")) 55 | 56 | ignore_case = kwargs.get("ignore_case", True) 57 | 58 | # Cast as strings 59 | vers1 = str(vers1) 60 | vers2 = str(vers2) 61 | 62 | if ignore_case: 63 | vers1 = vers1.lower() 64 | vers2 = vers2.lower() 65 | 66 | # Split and pad lists 67 | v1_parts, v2_parts = self.pad_length(vers1.split(sep), vers2.split(sep)) 68 | 69 | # Iterate and compare 70 | for i in range(len(v1_parts)): 71 | # Remove non-numeric 72 | v1 = ''.join(c.lower() for c in v1_parts[i] if c.isalnum()) 73 | v2 = ''.join(c.lower() for c in v2_parts[i] if c.isalnum()) 74 | # Equalize the lengths 75 | v1, v2 = self.pad_length(v1, v2) 76 | # Compare 77 | if str(v1) < str(v2): 78 | return True 79 | elif str(v1) > str(v2): 80 | return False 81 | # Never differed - return None, must be equal 82 | return None 83 | 84 | def pad_length(self, var1, var2, pad = "0"): 85 | # Pads the vars on the left side to make them equal length 86 | pad = "0" if len(str(pad)) < 1 else str(pad)[0] 87 | if not type(var1) == type(var2): 88 | # Type mismatch! Just return what we got 89 | return (var1, var2) 90 | if len(var1) < len(var2): 91 | if type(var1) is list: 92 | var1.extend([str(pad) for x in range(len(var2) - len(var1))]) 93 | else: 94 | var1 = "{}{}".format((pad*(len(var2)-len(var1))), var1) 95 | elif len(var2) < len(var1): 96 | if type(var2) is list: 97 | var2.extend([str(pad) for x in range(len(var1) - len(var2))]) 98 | else: 99 | var2 = "{}{}".format((pad*(len(var1)-len(var2))), var2) 100 | return (var1, var2) 101 | 102 | def check_path(self, path): 103 | # Let's loop until we either get a working path, or no changes 104 | test_path = path 105 | last_path = None 106 | while True: 107 | # Bail if we've looped at least once and the path didn't change 108 | if last_path != None and last_path == test_path: return None 109 | last_path = test_path 110 | # Check if we stripped everything out 111 | if not len(test_path): return None 112 | # Check if we have a valid path 113 | if os.path.exists(test_path): 114 | return os.path.abspath(test_path) 115 | # Check for quotes 116 | if test_path[0] == test_path[-1] and test_path[0] in ('"',"'"): 117 | test_path = test_path[1:-1] 118 | continue 119 | # Check for a tilde and expand if needed 120 | if test_path[0] == "~": 121 | tilde_expanded = os.path.expanduser(test_path) 122 | if tilde_expanded != test_path: 123 | # Got a change 124 | test_path = tilde_expanded 125 | continue 126 | # Let's check for spaces - strip from the left first, then the right 127 | if test_path[0] in (" ","\t"): 128 | test_path = test_path[1:] 129 | continue 130 | if test_path[-1] in (" ","\t"): 131 | test_path = test_path[:-1] 132 | continue 133 | # Maybe we have escapes to handle? 134 | test_path = "\\".join([x.replace("\\", "") for x in test_path.split("\\\\")]) 135 | 136 | def grab(self, prompt, **kwargs): 137 | # Takes a prompt, a default, and a timeout and shows it with that timeout 138 | # returning the result 139 | timeout = kwargs.get("timeout", 0) 140 | default = kwargs.get("default", None) 141 | # If we don't have a timeout - then skip the timed sections 142 | if timeout <= 0: 143 | if sys.version_info >= (3, 0): 144 | return input(prompt) 145 | else: 146 | return str(raw_input(prompt)) 147 | # Write our prompt 148 | sys.stdout.write(prompt) 149 | sys.stdout.flush() 150 | if os.name == "nt": 151 | start_time = time.time() 152 | i = '' 153 | while True: 154 | if msvcrt.kbhit(): 155 | c = msvcrt.getche() 156 | if ord(c) == 13: # enter_key 157 | break 158 | elif ord(c) >= 32: #space_char 159 | i += c 160 | if len(i) == 0 and (time.time() - start_time) > timeout: 161 | break 162 | else: 163 | i, o, e = select.select( [sys.stdin], [], [], timeout ) 164 | if i: 165 | i = sys.stdin.readline().strip() 166 | print('') # needed to move to next line 167 | if len(i) > 0: 168 | return i 169 | else: 170 | return default 171 | 172 | def cls(self): 173 | os.system('cls' if os.name=='nt' else 'clear') 174 | 175 | def cprint(self, message, **kwargs): 176 | strip_colors = kwargs.get("strip_colors", False) 177 | if os.name == "nt": 178 | strip_colors = True 179 | reset = u"\u001b[0m" 180 | # Requires sys import 181 | for c in self.colors: 182 | if strip_colors: 183 | message = message.replace(c["find"], "") 184 | else: 185 | message = message.replace(c["find"], c["replace"]) 186 | if strip_colors: 187 | return message 188 | sys.stdout.write(message) 189 | print(reset) 190 | 191 | # Needs work to resize the string if color chars exist 192 | '''# Header drawing method 193 | def head(self, text = None, width = 55): 194 | if text == None: 195 | text = self.name 196 | self.cls() 197 | print(" {}".format("#"*width)) 198 | len_text = self.cprint(text, strip_colors=True) 199 | mid_len = int(round(width/2-len(len_text)/2)-2) 200 | middle = " #{}{}{}#".format(" "*mid_len, len_text, " "*((width - mid_len - len(len_text))-2)) 201 | if len(middle) > width+1: 202 | # Get the difference 203 | di = len(middle) - width 204 | # Add the padding for the ...# 205 | di += 3 206 | # Trim the string 207 | middle = middle[:-di] 208 | newlen = len(middle) 209 | middle += "...#" 210 | find_list = [ c["find"] for c in self.colors ] 211 | 212 | # Translate colored string to len 213 | middle = middle.replace(len_text, text + self.rt_color) # always reset just in case 214 | self.cprint(middle) 215 | print("#"*width)''' 216 | 217 | # Header drawing method 218 | def head(self, text = None, width = 55): 219 | if text == None: 220 | text = self.name 221 | self.cls() 222 | print(" {}".format("#"*width)) 223 | mid_len = int(round(width/2-len(text)/2)-2) 224 | middle = " #{}{}{}#".format(" "*mid_len, text, " "*((width - mid_len - len(text))-2)) 225 | if len(middle) > width+1: 226 | # Get the difference 227 | di = len(middle) - width 228 | # Add the padding for the ...# 229 | di += 3 230 | # Trim the string 231 | middle = middle[:-di] + "...#" 232 | print(middle) 233 | print("#"*width) 234 | 235 | def resize(self, width, height): 236 | print('\033[8;{};{}t'.format(height, width)) 237 | 238 | def custom_quit(self): 239 | self.head() 240 | print("by CorpNewt\n") 241 | print("Thanks for testing it out, for bugs/comments/complaints") 242 | print("send me a message on Reddit, or check out my GitHub:\n") 243 | print("www.reddit.com/u/corpnewt") 244 | print("www.github.com/corpnewt\n") 245 | # Get the time and wish them a good morning, afternoon, evening, and night 246 | hr = datetime.datetime.now().time().hour 247 | if hr > 3 and hr < 12: 248 | print("Have a nice morning!\n\n") 249 | elif hr >= 12 and hr < 17: 250 | print("Have a nice afternoon!\n\n") 251 | elif hr >= 17 and hr < 21: 252 | print("Have a nice evening!\n\n") 253 | else: 254 | print("Have a nice night!\n\n") 255 | exit(0) 256 | -------------------------------------------------------------------------------- /USBMap.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the curent directory, the script name 4 | # and the script name with "py" substituted for the extension. 5 | args=( "$@" ) 6 | dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" 7 | script="${0##*/}" 8 | target="${script%.*}.py" 9 | 10 | # use_py3: 11 | # TRUE = Use if found, use py2 otherwise 12 | # FALSE = Use py2 13 | # FORCE = Use py3 14 | use_py3="TRUE" 15 | 16 | # We'll parse if the first argument passed is 17 | # --install-python and if so, we'll just install 18 | just_installing="FALSE" 19 | 20 | tempdir="" 21 | 22 | compare_to_version () { 23 | # Compares our OS version to the passed OS version, and 24 | # return a 1 if we match the passed compare type, or a 0 if we don't. 25 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 26 | # $2 = OS version to compare ours to 27 | if [ -z "$1" ] || [ -z "$2" ]; then 28 | # Missing info - bail. 29 | return 30 | fi 31 | local current_os= comp= 32 | current_os="$(sw_vers -productVersion)" 33 | comp="$(vercomp "$current_os" "$2")" 34 | # Check gequal and lequal first 35 | if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then 36 | # Matched 37 | echo "1" 38 | else 39 | # No match 40 | echo "0" 41 | fi 42 | } 43 | 44 | set_use_py3_if () { 45 | # Auto sets the "use_py3" variable based on 46 | # conditions passed 47 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 48 | # $2 = OS version to compare 49 | # $3 = TRUE/FALSE/FORCE in case of match 50 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 51 | # Missing vars - bail with no changes. 52 | return 53 | fi 54 | if [ "$(compare_to_version "$1" "$2")" == "1" ]; then 55 | use_py3="$3" 56 | fi 57 | } 58 | 59 | get_remote_py_version () { 60 | local pyurl= py_html= py_vers= py_num="3" 61 | pyurl="https://www.python.org/downloads/macos/" 62 | py_html="$(curl -L $pyurl --compressed 2>&1)" 63 | if [ -z "$use_py3" ]; then 64 | use_py3="TRUE" 65 | fi 66 | if [ "$use_py3" == "FALSE" ]; then 67 | py_num="2" 68 | fi 69 | py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" 70 | echo "$py_vers" 71 | } 72 | 73 | download_py () { 74 | local vers="$1" url= 75 | clear 76 | echo " ### ###" 77 | echo " # Downloading Python #" 78 | echo "### ###" 79 | echo 80 | if [ -z "$vers" ]; then 81 | echo "Gathering latest version..." 82 | vers="$(get_remote_py_version)" 83 | fi 84 | if [ -z "$vers" ]; then 85 | # Didn't get it still - bail 86 | print_error 87 | fi 88 | echo "Located Version: $vers" 89 | echo 90 | echo "Building download url..." 91 | url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }')" 92 | if [ -z "$url" ]; then 93 | # Couldn't get the URL - bail 94 | print_error 95 | fi 96 | echo " - $url" 97 | echo 98 | echo "Downloading..." 99 | echo 100 | # Create a temp dir and download to it 101 | tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" 102 | curl "$url" -o "$tempdir/python.pkg" 103 | if [ "$?" != "0" ]; then 104 | echo 105 | echo " - Failed to download python installer!" 106 | echo 107 | exit $? 108 | fi 109 | echo 110 | echo "Running python install package..." 111 | echo 112 | sudo installer -pkg "$tempdir/python.pkg" -target / 113 | if [ "$?" != "0" ]; then 114 | echo 115 | echo " - Failed to install python!" 116 | echo 117 | exit $? 118 | fi 119 | # Now we expand the package and look for a shell update script 120 | pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" 121 | if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then 122 | # Run the script 123 | echo 124 | echo "Updating PATH..." 125 | echo 126 | "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" 127 | fi 128 | vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" 129 | if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then 130 | # Certs script exists - let's execute that to make sure our certificates are updated 131 | echo 132 | echo "Updating Certificates..." 133 | echo 134 | "/Applications/$vers_folder/Install Certificates.command" 135 | fi 136 | echo 137 | echo "Cleaning up..." 138 | cleanup 139 | echo 140 | if [ "$just_installing" == "TRUE" ]; then 141 | echo "Done." 142 | else 143 | # Now we check for py again 144 | echo "Rechecking py..." 145 | downloaded="TRUE" 146 | clear 147 | main 148 | fi 149 | } 150 | 151 | cleanup () { 152 | if [ -d "$tempdir" ]; then 153 | rm -Rf "$tempdir" 154 | fi 155 | } 156 | 157 | print_error() { 158 | clear 159 | cleanup 160 | echo " ### ###" 161 | echo " # Python Not Found #" 162 | echo "### ###" 163 | echo 164 | echo "Python is not installed or not found in your PATH var." 165 | echo 166 | if [ "$kernel" == "Darwin" ]; then 167 | echo "Please go to https://www.python.org/downloads/macos/ to" 168 | echo "download and install the latest version, then try again." 169 | else 170 | echo "Please install python through your package manager and" 171 | echo "try again." 172 | fi 173 | echo 174 | exit 1 175 | } 176 | 177 | print_target_missing() { 178 | clear 179 | cleanup 180 | echo " ### ###" 181 | echo " # Target Not Found #" 182 | echo "### ###" 183 | echo 184 | echo "Could not locate $target!" 185 | echo 186 | exit 1 187 | } 188 | 189 | format_version () { 190 | local vers="$1" 191 | echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" 192 | } 193 | 194 | vercomp () { 195 | # Modified from: https://apple.stackexchange.com/a/123408/11374 196 | local ver1="$(format_version "$1")" ver2="$(format_version "$2")" 197 | if [ $ver1 -gt $ver2 ]; then 198 | echo "1" 199 | elif [ $ver1 -lt $ver2 ]; then 200 | echo "2" 201 | else 202 | echo "0" 203 | fi 204 | } 205 | 206 | get_local_python_version() { 207 | # $1 = Python bin name (defaults to python3) 208 | # Echoes the path to the highest version of the passed python bin if any 209 | local py_name="$1" max_version= python= python_version= python_path= 210 | if [ -z "$py_name" ]; then 211 | py_name="python3" 212 | fi 213 | py_list="$(which -a "$py_name" 2>/dev/null)" 214 | # Walk that newline separated list 215 | while read python; do 216 | if [ -z "$python" ]; then 217 | # Got a blank line - skip 218 | continue 219 | fi 220 | if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then 221 | # See if we have a valid developer path 222 | xcode-select -p > /dev/null 2>&1 223 | if [ "$?" != "0" ]; then 224 | # /usr/bin/python3 path - but no valid developer dir 225 | continue 226 | fi 227 | fi 228 | python_version="$(get_python_version $python)" 229 | if [ -z "$python_version" ]; then 230 | # Didn't find a py version - skip 231 | continue 232 | fi 233 | # Got the py version - compare to our max 234 | if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then 235 | # Max not set, or less than the current - update it 236 | max_version="$python_version" 237 | python_path="$python" 238 | fi 239 | done <<< "$py_list" 240 | echo "$python_path" 241 | } 242 | 243 | get_python_version() { 244 | local py_path="$1" py_version= 245 | # Get the python version by piping stderr into stdout (for py2), then grepping the output for 246 | # the word "python", getting the second element, and grepping for an alphanumeric version number 247 | py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" 248 | if [ ! -z "$py_version" ]; then 249 | echo "$py_version" 250 | fi 251 | } 252 | 253 | prompt_and_download() { 254 | if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then 255 | # We already tried to download, or we're not on macOS - just bail 256 | print_error 257 | fi 258 | clear 259 | echo " ### ###" 260 | echo " # Python Not Found #" 261 | echo "### ###" 262 | echo 263 | target_py="Python 3" 264 | printed_py="Python 2 or 3" 265 | if [ "$use_py3" == "FORCE" ]; then 266 | printed_py="Python 3" 267 | elif [ "$use_py3" == "FALSE" ]; then 268 | target_py="Python 2" 269 | printed_py="Python 2" 270 | fi 271 | echo "Could not locate $printed_py!" 272 | echo 273 | echo "This script requires $printed_py to run." 274 | echo 275 | while true; do 276 | read -p "Would you like to install the latest $target_py now? (y/n): " yn 277 | case $yn in 278 | [Yy]* ) download_py;break;; 279 | [Nn]* ) print_error;; 280 | esac 281 | done 282 | } 283 | 284 | main() { 285 | local python= version= 286 | # Verify our target exists 287 | if [ ! -f "$dir/$target" ]; then 288 | # Doesn't exist 289 | print_target_missing 290 | fi 291 | if [ -z "$use_py3" ]; then 292 | use_py3="TRUE" 293 | fi 294 | if [ "$use_py3" != "FALSE" ]; then 295 | # Check for py3 first 296 | python="$(get_local_python_version python3)" 297 | fi 298 | if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then 299 | # We aren't using py3 explicitly, and we don't already have a path 300 | python="$(get_local_python_version python2)" 301 | if [ -z "$python" ]; then 302 | # Try just looking for "python" 303 | python="$(get_local_python_version python)" 304 | fi 305 | fi 306 | if [ -z "$python" ]; then 307 | # Didn't ever find it - prompt 308 | prompt_and_download 309 | return 1 310 | fi 311 | # Found it - start our script and pass all args 312 | "$python" "$dir/$target" "${args[@]}" 313 | } 314 | 315 | # Keep track of whether or not we're on macOS to determine if 316 | # we can download and install python for the user as needed. 317 | kernel="$(uname -s)" 318 | # Check to see if we need to force based on 319 | # macOS version. 10.15 has a dummy python3 version 320 | # that can trip up some py3 detection in other scripts. 321 | # set_use_py3_if "3" "10.15" "FORCE" 322 | downloaded="FALSE" 323 | # Check for the aforementioned /usr/bin/python3 stub if 324 | # our OS version is 10.15 or greater. 325 | check_py3_stub="$(compare_to_version "3" "10.15")" 326 | trap cleanup EXIT 327 | if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then 328 | just_installing="TRUE" 329 | download_py 330 | else 331 | main 332 | fi 333 | -------------------------------------------------------------------------------- /USBMapInjectorEdit.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Get our local path before delayed expansion - allows ! in path 3 | set "thisDir=%~dp0" 4 | 5 | setlocal enableDelayedExpansion 6 | REM Setup initial vars 7 | set "script_name=" 8 | set /a tried=0 9 | set "toask=yes" 10 | set "pause_on_error=yes" 11 | set "py2v=" 12 | set "py2path=" 13 | set "py3v=" 14 | set "py3path=" 15 | set "pypath=" 16 | set "targetpy=3" 17 | 18 | REM use_py3: 19 | REM TRUE = Use if found, use py2 otherwise 20 | REM FALSE = Use py2 21 | REM FORCE = Use py3 22 | set "use_py3=TRUE" 23 | 24 | REM We'll parse if the first argument passed is 25 | REM --install-python and if so, we'll just install 26 | set "just_installing=FALSE" 27 | 28 | REM Get the system32 (or equivalent) path 29 | call :getsyspath "syspath" 30 | 31 | REM Make sure the syspath exists 32 | if "!syspath!" == "" ( 33 | if exist "%SYSTEMROOT%\system32\cmd.exe" ( 34 | if exist "%SYSTEMROOT%\system32\reg.exe" ( 35 | if exist "%SYSTEMROOT%\system32\where.exe" ( 36 | REM Fall back on the default path if it exists 37 | set "ComSpec=%SYSTEMROOT%\system32\cmd.exe" 38 | set "syspath=%SYSTEMROOT%\system32\" 39 | ) 40 | ) 41 | ) 42 | if "!syspath!" == "" ( 43 | cls 44 | echo ### ### 45 | echo # Warning # 46 | echo ### ### 47 | echo. 48 | echo Could not locate cmd.exe, reg.exe, or where.exe 49 | echo. 50 | echo Please ensure your ComSpec environment variable is properly configured and 51 | echo points directly to cmd.exe, then try again. 52 | echo. 53 | echo Current CompSpec Value: "%ComSpec%" 54 | echo. 55 | echo Press [enter] to quit. 56 | pause > nul 57 | exit /b 1 58 | ) 59 | ) 60 | 61 | if "%~1" == "--install-python" ( 62 | set "just_installing=TRUE" 63 | goto installpy 64 | ) 65 | 66 | goto checkscript 67 | 68 | :checkscript 69 | REM Check for our script first 70 | set "looking_for=!script_name!" 71 | if "!script_name!" == "" ( 72 | set "looking_for=%~n0.py or %~n0.command" 73 | set "script_name=%~n0.py" 74 | if not exist "!thisDir!\!script_name!" ( 75 | set "script_name=%~n0.command" 76 | ) 77 | ) 78 | if not exist "!thisDir!\!script_name!" ( 79 | echo Could not find !looking_for!. 80 | echo Please make sure to run this script from the same directory 81 | echo as !looking_for!. 82 | echo. 83 | echo Press [enter] to quit. 84 | pause > nul 85 | exit /b 1 86 | ) 87 | goto checkpy 88 | 89 | :checkpy 90 | call :updatepath 91 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 92 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 93 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) 94 | REM Walk our returns to see if we need to install 95 | if /i "!use_py3!" == "FALSE" ( 96 | set "targetpy=2" 97 | set "pypath=!py2path!" 98 | ) else if /i "!use_py3!" == "FORCE" ( 99 | set "pypath=!py3path!" 100 | ) else if /i "!use_py3!" == "TRUE" ( 101 | set "pypath=!py3path!" 102 | if "!pypath!" == "" set "pypath=!py2path!" 103 | ) 104 | if not "!pypath!" == "" ( 105 | goto runscript 106 | ) 107 | if !tried! lss 1 ( 108 | if /i "!toask!"=="yes" ( 109 | REM Better ask permission first 110 | goto askinstall 111 | ) else ( 112 | goto installpy 113 | ) 114 | ) else ( 115 | cls 116 | echo ### ### 117 | echo # Warning # 118 | echo ### ### 119 | echo. 120 | REM Couldn't install for whatever reason - give the error message 121 | echo Python is not installed or not found in your PATH var. 122 | echo Please install it from https://www.python.org/downloads/windows/ 123 | echo. 124 | echo Make sure you check the box labeled: 125 | echo. 126 | echo "Add Python X.X to PATH" 127 | echo. 128 | echo Where X.X is the py version you're installing. 129 | echo. 130 | echo Press [enter] to quit. 131 | pause > nul 132 | exit /b 1 133 | ) 134 | goto runscript 135 | 136 | :checkpylauncher 137 | REM Attempt to check the latest python 2 and 3 versions via the py launcher 138 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 139 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 140 | goto :EOF 141 | 142 | :checkpyversion 143 | set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( 144 | REM Ensure we have a version number 145 | call :isnumber "%%a" 146 | if not "!errorlevel!" == "0" goto :EOF 147 | set "version=%%a" 148 | ) 149 | if not defined version goto :EOF 150 | if "!version:~0,1!" == "2" ( 151 | REM Python 2 152 | call :comparepyversion "!version!" "!%~2!" 153 | if "!errorlevel!" == "1" ( 154 | set "%~2=!version!" 155 | set "%~3=%~1" 156 | ) 157 | ) else ( 158 | REM Python 3 159 | call :comparepyversion "!version!" "!%~4!" 160 | if "!errorlevel!" == "1" ( 161 | set "%~4=!version!" 162 | set "%~5=%~1" 163 | ) 164 | ) 165 | goto :EOF 166 | 167 | :isnumber 168 | set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i 169 | if defined var (exit /b 1) 170 | exit /b 0 171 | 172 | :comparepyversion 173 | REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 174 | for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( 175 | set a1=%%a 176 | set a2=%%b 177 | set a3=%%c 178 | ) 179 | for /f "tokens=1,2,3 delims=." %%a in ("%~2") do ( 180 | set b1=%%a 181 | set b2=%%b 182 | set b3=%%c 183 | ) 184 | if not defined a1 set a1=0 185 | if not defined a2 set a2=0 186 | if not defined a3 set a3=0 187 | if not defined b1 set b1=0 188 | if not defined b2 set b2=0 189 | if not defined b3 set b3=0 190 | if %a1% gtr %b1% exit /b 1 191 | if %a1% lss %b1% exit /b 2 192 | if %a2% gtr %b2% exit /b 1 193 | if %a2% lss %b2% exit /b 2 194 | if %a3% gtr %b3% exit /b 1 195 | if %a3% lss %b3% exit /b 2 196 | exit /b 0 197 | 198 | :askinstall 199 | cls 200 | echo ### ### 201 | echo # Python Not Found # 202 | echo ### ### 203 | echo. 204 | echo Python !targetpy! was not found on the system or in the PATH var. 205 | echo. 206 | set /p "menu=Would you like to install it now? [y/n]: " 207 | if /i "!menu!"=="y" ( 208 | REM We got the OK - install it 209 | goto installpy 210 | ) else if "!menu!"=="n" ( 211 | REM No OK here... 212 | set /a tried=!tried!+1 213 | goto checkpy 214 | ) 215 | REM Incorrect answer - go back 216 | goto askinstall 217 | 218 | :installpy 219 | REM This will attempt to download and install python 220 | REM First we get the html for the python downloads page for Windows 221 | set /a tried=!tried!+1 222 | cls 223 | echo ### ### 224 | echo # Installing Python # 225 | echo ### ### 226 | echo. 227 | echo Gathering info from https://www.python.org/downloads/windows/... 228 | powershell -command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" 229 | REM Extract it if it's gzip compressed 230 | powershell -command "$infile='%TEMP%\pyurl.txt';$outfile='%TEMP%\pyurl.temp';try{$input=New-Object System.IO.FileStream $infile,([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read);$output=New-Object System.IO.FileStream $outfile,([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::None);$gzipStream=New-Object System.IO.Compression.GzipStream $input,([IO.Compression.CompressionMode]::Decompress);$buffer=New-Object byte[](1024);while($true){$read=$gzipstream.Read($buffer,0,1024);if($read -le 0){break};$output.Write($buffer,0,$read)};$gzipStream.Close();$output.Close();$input.Close();Move-Item -Path $outfile -Destination $infile -Force}catch{}" 231 | if not exist "%TEMP%\pyurl.txt" ( 232 | if /i "!just_installing!" == "TRUE" ( 233 | echo Failed to get info 234 | exit /b 1 235 | ) else ( 236 | goto checkpy 237 | ) 238 | ) 239 | echo Parsing for latest... 240 | pushd "%TEMP%" 241 | :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) 242 | for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) 243 | popd 244 | if "!release!" == "" ( 245 | if /i "!just_installing!" == "TRUE" ( 246 | echo Failed to get python version 247 | exit /b 1 248 | ) else ( 249 | goto checkpy 250 | ) 251 | ) 252 | echo Found Python !release! - Downloading... 253 | REM Let's delete our txt file now - we no longer need it 254 | del "%TEMP%\pyurl.txt" 255 | REM At this point - we should have the version number. 256 | REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" 257 | set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" 258 | set "pytype=exe" 259 | if "!targetpy!" == "2" ( 260 | set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" 261 | set "pytype=msi" 262 | ) 263 | REM Now we download it with our slick powershell command 264 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" 265 | REM If it doesn't exist - we bail 266 | if not exist "%TEMP%\pyinstall.!pytype!" ( 267 | if /i "!just_installing!" == "TRUE" ( 268 | echo Failed to download installer 269 | exit /b 1 270 | ) else ( 271 | goto checkpy 272 | ) 273 | ) 274 | REM It should exist at this point - let's run it to install silently 275 | echo Installing... 276 | pushd "%TEMP%" 277 | if /i "!pytype!" == "exe" ( 278 | echo pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 279 | pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 280 | ) else ( 281 | set "foldername=!release:.=!" 282 | echo msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 283 | msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 284 | ) 285 | popd 286 | echo Installer finished with %ERRORLEVEL% status. 287 | REM Now we should be able to delete the installer and check for py again 288 | del "%TEMP%\pyinstall.!pytype!" 289 | REM If it worked, then we should have python in our PATH 290 | REM this does not get updated right away though - let's try 291 | REM manually updating the local PATH var 292 | call :updatepath 293 | if /i "!just_installing!" == "TRUE" ( 294 | echo. 295 | echo Done. 296 | ) else ( 297 | goto checkpy 298 | ) 299 | exit /b 300 | 301 | :runscript 302 | REM Python found 303 | cls 304 | set "args=%*" 305 | set "args=!args:"=!" 306 | if "!args!"=="" ( 307 | "!pypath!" "!thisDir!!script_name!" 308 | ) else ( 309 | "!pypath!" "!thisDir!!script_name!" %* 310 | ) 311 | if /i "!pause_on_error!" == "yes" ( 312 | if not "%ERRORLEVEL%" == "0" ( 313 | echo. 314 | echo Script exited with error code: %ERRORLEVEL% 315 | echo. 316 | echo Press [enter] to exit... 317 | pause > nul 318 | ) 319 | ) 320 | goto :EOF 321 | 322 | :undouble 323 | REM Helper function to strip doubles of a single character out of a string recursively 324 | set "string_value=%~2" 325 | :undouble_continue 326 | set "check=!string_value:%~3%~3=%~3!" 327 | if not "!check!" == "!string_value!" ( 328 | set "string_value=!check!" 329 | goto :undouble_continue 330 | ) 331 | set "%~1=!check!" 332 | goto :EOF 333 | 334 | :updatepath 335 | set "spath=" 336 | set "upath=" 337 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKCU\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "upath=%%j" ) 338 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "spath=%%j" ) 339 | if not "%spath%" == "" ( 340 | REM We got something in the system path 341 | set "PATH=%spath%" 342 | if not "%upath%" == "" ( 343 | REM We also have something in the user path 344 | set "PATH=%PATH%;%upath%" 345 | ) 346 | ) else if not "%upath%" == "" ( 347 | set "PATH=%upath%" 348 | ) 349 | REM Remove double semicolons from the adjusted PATH 350 | call :undouble "PATH" "%PATH%" ";" 351 | goto :EOF 352 | 353 | :getsyspath 354 | REM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by 355 | REM walking the ComSpec var - will also repair it in memory if need be 356 | REM Strip double semi-colons 357 | call :undouble "temppath" "%ComSpec%" ";" 358 | 359 | REM Dirty hack to leverage the "line feed" approach - there are some odd side 360 | REM effects with this. Do not use this variable name in comments near this 361 | REM line - as it seems to behave erradically. 362 | (set LF=^ 363 | %=this line is empty=% 364 | ) 365 | REM Replace instances of semi-colons with a line feed and wrap 366 | REM in parenthesis to work around some strange batch behavior 367 | set "testpath=%temppath:;=!LF!%" 368 | 369 | REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there 370 | set /a found=0 371 | for /f "tokens=* delims=" %%i in ("!testpath!") do ( 372 | REM Only continue if we haven't found it yet 373 | if not "%%i" == "" ( 374 | if !found! lss 1 ( 375 | set "checkpath=%%i" 376 | REM Remove "cmd.exe" from the end if it exists 377 | if /i "!checkpath:~-7!" == "cmd.exe" ( 378 | set "checkpath=!checkpath:~0,-7!" 379 | ) 380 | REM Pad the end with a backslash if needed 381 | if not "!checkpath:~-1!" == "\" ( 382 | set "checkpath=!checkpath!\" 383 | ) 384 | REM Let's see if cmd, reg, and where exist there - and set it if so 385 | if EXIST "!checkpath!cmd.exe" ( 386 | if EXIST "!checkpath!reg.exe" ( 387 | if EXIST "!checkpath!where.exe" ( 388 | set /a found=1 389 | set "ComSpec=!checkpath!cmd.exe" 390 | set "%~1=!checkpath!" 391 | ) 392 | ) 393 | ) 394 | ) 395 | ) 396 | ) 397 | goto :EOF 398 | -------------------------------------------------------------------------------- /USBMapInjectorEdit.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the curent directory, the script name 4 | # and the script name with "py" substituted for the extension. 5 | args=( "$@" ) 6 | dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" 7 | script="${0##*/}" 8 | target="${script%.*}.py" 9 | 10 | # use_py3: 11 | # TRUE = Use if found, use py2 otherwise 12 | # FALSE = Use py2 13 | # FORCE = Use py3 14 | use_py3="TRUE" 15 | 16 | # We'll parse if the first argument passed is 17 | # --install-python and if so, we'll just install 18 | just_installing="FALSE" 19 | 20 | tempdir="" 21 | 22 | compare_to_version () { 23 | # Compares our OS version to the passed OS version, and 24 | # return a 1 if we match the passed compare type, or a 0 if we don't. 25 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 26 | # $2 = OS version to compare ours to 27 | if [ -z "$1" ] || [ -z "$2" ]; then 28 | # Missing info - bail. 29 | return 30 | fi 31 | local current_os= comp= 32 | current_os="$(sw_vers -productVersion)" 33 | comp="$(vercomp "$current_os" "$2")" 34 | # Check gequal and lequal first 35 | if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then 36 | # Matched 37 | echo "1" 38 | else 39 | # No match 40 | echo "0" 41 | fi 42 | } 43 | 44 | set_use_py3_if () { 45 | # Auto sets the "use_py3" variable based on 46 | # conditions passed 47 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 48 | # $2 = OS version to compare 49 | # $3 = TRUE/FALSE/FORCE in case of match 50 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 51 | # Missing vars - bail with no changes. 52 | return 53 | fi 54 | if [ "$(compare_to_version "$1" "$2")" == "1" ]; then 55 | use_py3="$3" 56 | fi 57 | } 58 | 59 | get_remote_py_version () { 60 | local pyurl= py_html= py_vers= py_num="3" 61 | pyurl="https://www.python.org/downloads/macos/" 62 | py_html="$(curl -L $pyurl --compressed 2>&1)" 63 | if [ -z "$use_py3" ]; then 64 | use_py3="TRUE" 65 | fi 66 | if [ "$use_py3" == "FALSE" ]; then 67 | py_num="2" 68 | fi 69 | py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" 70 | echo "$py_vers" 71 | } 72 | 73 | download_py () { 74 | local vers="$1" url= 75 | clear 76 | echo " ### ###" 77 | echo " # Downloading Python #" 78 | echo "### ###" 79 | echo 80 | if [ -z "$vers" ]; then 81 | echo "Gathering latest version..." 82 | vers="$(get_remote_py_version)" 83 | fi 84 | if [ -z "$vers" ]; then 85 | # Didn't get it still - bail 86 | print_error 87 | fi 88 | echo "Located Version: $vers" 89 | echo 90 | echo "Building download url..." 91 | url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }')" 92 | if [ -z "$url" ]; then 93 | # Couldn't get the URL - bail 94 | print_error 95 | fi 96 | echo " - $url" 97 | echo 98 | echo "Downloading..." 99 | echo 100 | # Create a temp dir and download to it 101 | tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" 102 | curl "$url" -o "$tempdir/python.pkg" 103 | if [ "$?" != "0" ]; then 104 | echo 105 | echo " - Failed to download python installer!" 106 | echo 107 | exit $? 108 | fi 109 | echo 110 | echo "Running python install package..." 111 | echo 112 | sudo installer -pkg "$tempdir/python.pkg" -target / 113 | if [ "$?" != "0" ]; then 114 | echo 115 | echo " - Failed to install python!" 116 | echo 117 | exit $? 118 | fi 119 | # Now we expand the package and look for a shell update script 120 | pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" 121 | if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then 122 | # Run the script 123 | echo 124 | echo "Updating PATH..." 125 | echo 126 | "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" 127 | fi 128 | vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" 129 | if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then 130 | # Certs script exists - let's execute that to make sure our certificates are updated 131 | echo 132 | echo "Updating Certificates..." 133 | echo 134 | "/Applications/$vers_folder/Install Certificates.command" 135 | fi 136 | echo 137 | echo "Cleaning up..." 138 | cleanup 139 | echo 140 | if [ "$just_installing" == "TRUE" ]; then 141 | echo "Done." 142 | else 143 | # Now we check for py again 144 | echo "Rechecking py..." 145 | downloaded="TRUE" 146 | clear 147 | main 148 | fi 149 | } 150 | 151 | cleanup () { 152 | if [ -d "$tempdir" ]; then 153 | rm -Rf "$tempdir" 154 | fi 155 | } 156 | 157 | print_error() { 158 | clear 159 | cleanup 160 | echo " ### ###" 161 | echo " # Python Not Found #" 162 | echo "### ###" 163 | echo 164 | echo "Python is not installed or not found in your PATH var." 165 | echo 166 | if [ "$kernel" == "Darwin" ]; then 167 | echo "Please go to https://www.python.org/downloads/macos/ to" 168 | echo "download and install the latest version, then try again." 169 | else 170 | echo "Please install python through your package manager and" 171 | echo "try again." 172 | fi 173 | echo 174 | exit 1 175 | } 176 | 177 | print_target_missing() { 178 | clear 179 | cleanup 180 | echo " ### ###" 181 | echo " # Target Not Found #" 182 | echo "### ###" 183 | echo 184 | echo "Could not locate $target!" 185 | echo 186 | exit 1 187 | } 188 | 189 | format_version () { 190 | local vers="$1" 191 | echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" 192 | } 193 | 194 | vercomp () { 195 | # Modified from: https://apple.stackexchange.com/a/123408/11374 196 | local ver1="$(format_version "$1")" ver2="$(format_version "$2")" 197 | if [ $ver1 -gt $ver2 ]; then 198 | echo "1" 199 | elif [ $ver1 -lt $ver2 ]; then 200 | echo "2" 201 | else 202 | echo "0" 203 | fi 204 | } 205 | 206 | get_local_python_version() { 207 | # $1 = Python bin name (defaults to python3) 208 | # Echoes the path to the highest version of the passed python bin if any 209 | local py_name="$1" max_version= python= python_version= python_path= 210 | if [ -z "$py_name" ]; then 211 | py_name="python3" 212 | fi 213 | py_list="$(which -a "$py_name" 2>/dev/null)" 214 | # Walk that newline separated list 215 | while read python; do 216 | if [ -z "$python" ]; then 217 | # Got a blank line - skip 218 | continue 219 | fi 220 | if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then 221 | # See if we have a valid developer path 222 | xcode-select -p > /dev/null 2>&1 223 | if [ "$?" != "0" ]; then 224 | # /usr/bin/python3 path - but no valid developer dir 225 | continue 226 | fi 227 | fi 228 | python_version="$(get_python_version $python)" 229 | if [ -z "$python_version" ]; then 230 | # Didn't find a py version - skip 231 | continue 232 | fi 233 | # Got the py version - compare to our max 234 | if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then 235 | # Max not set, or less than the current - update it 236 | max_version="$python_version" 237 | python_path="$python" 238 | fi 239 | done <<< "$py_list" 240 | echo "$python_path" 241 | } 242 | 243 | get_python_version() { 244 | local py_path="$1" py_version= 245 | # Get the python version by piping stderr into stdout (for py2), then grepping the output for 246 | # the word "python", getting the second element, and grepping for an alphanumeric version number 247 | py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" 248 | if [ ! -z "$py_version" ]; then 249 | echo "$py_version" 250 | fi 251 | } 252 | 253 | prompt_and_download() { 254 | if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then 255 | # We already tried to download, or we're not on macOS - just bail 256 | print_error 257 | fi 258 | clear 259 | echo " ### ###" 260 | echo " # Python Not Found #" 261 | echo "### ###" 262 | echo 263 | target_py="Python 3" 264 | printed_py="Python 2 or 3" 265 | if [ "$use_py3" == "FORCE" ]; then 266 | printed_py="Python 3" 267 | elif [ "$use_py3" == "FALSE" ]; then 268 | target_py="Python 2" 269 | printed_py="Python 2" 270 | fi 271 | echo "Could not locate $printed_py!" 272 | echo 273 | echo "This script requires $printed_py to run." 274 | echo 275 | while true; do 276 | read -p "Would you like to install the latest $target_py now? (y/n): " yn 277 | case $yn in 278 | [Yy]* ) download_py;break;; 279 | [Nn]* ) print_error;; 280 | esac 281 | done 282 | } 283 | 284 | main() { 285 | local python= version= 286 | # Verify our target exists 287 | if [ ! -f "$dir/$target" ]; then 288 | # Doesn't exist 289 | print_target_missing 290 | fi 291 | if [ -z "$use_py3" ]; then 292 | use_py3="TRUE" 293 | fi 294 | if [ "$use_py3" != "FALSE" ]; then 295 | # Check for py3 first 296 | python="$(get_local_python_version python3)" 297 | fi 298 | if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then 299 | # We aren't using py3 explicitly, and we don't already have a path 300 | python="$(get_local_python_version python2)" 301 | if [ -z "$python" ]; then 302 | # Try just looking for "python" 303 | python="$(get_local_python_version python)" 304 | fi 305 | fi 306 | if [ -z "$python" ]; then 307 | # Didn't ever find it - prompt 308 | prompt_and_download 309 | return 1 310 | fi 311 | # Found it - start our script and pass all args 312 | "$python" "$dir/$target" "${args[@]}" 313 | } 314 | 315 | # Keep track of whether or not we're on macOS to determine if 316 | # we can download and install python for the user as needed. 317 | kernel="$(uname -s)" 318 | # Check to see if we need to force based on 319 | # macOS version. 10.15 has a dummy python3 version 320 | # that can trip up some py3 detection in other scripts. 321 | # set_use_py3_if "3" "10.15" "FORCE" 322 | downloaded="FALSE" 323 | # Check for the aforementioned /usr/bin/python3 stub if 324 | # our OS version is 10.15 or greater. 325 | check_py3_stub="$(compare_to_version "3" "10.15")" 326 | trap cleanup EXIT 327 | if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then 328 | just_installing="TRUE" 329 | download_py 330 | else 331 | main 332 | fi 333 | -------------------------------------------------------------------------------- /USBMapInjectorEdit.py: -------------------------------------------------------------------------------- 1 | import os, sys, re, json, binascii, shutil, subprocess 2 | from Scripts import run, utils, ioreg, plist, reveal 3 | from collections import OrderedDict 4 | from datetime import datetime 5 | 6 | class USBMap: 7 | def __init__(self): 8 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 9 | self.output = "./Results" 10 | self.w = 80 11 | self.h = 24 12 | if os.name == "nt": 13 | self.w = 120 14 | self.h = 30 15 | os.system("color") # Run this once on Windows to enable ansi colors 16 | self.u = utils.Utils("USBMap Injector Edit") 17 | self.plist_path = None 18 | self.plist_data = None 19 | self.smbios = self.current_smbios() 20 | self.cs = u"\u001b[32;1m" 21 | self.ce = u"\u001b[0m" 22 | self.bs = u"\u001b[36;1m" 23 | self.rs = u"\u001b[31;1m" 24 | self.nm = u"\u001b[35;1m" 25 | 26 | # Helper methods 27 | def check_hex(self, value): 28 | # Remove 0x 29 | return re.sub(r'[^0-9A-Fa-f]+', '', value.lower().replace("0x", "")) 30 | 31 | def hex_swap(self, value): 32 | input_hex = self.check_hex(value) 33 | if not len(input_hex): return None 34 | # Normalize hex into pairs 35 | input_hex = list("0"*(len(input_hex)%2)+input_hex) 36 | hex_pairs = [input_hex[i:i + 2] for i in range(0, len(input_hex), 2)] 37 | hex_rev = hex_pairs[::-1] 38 | hex_str = "".join(["".join(x) for x in hex_rev]) 39 | return hex_str.upper() 40 | 41 | def hex_dec(self, value): 42 | value = self.check_hex(value) 43 | try: dec = int(value, 16) 44 | except: return None 45 | return dec 46 | 47 | def hex_data(self, hex_str): 48 | hex_str = self.check_hex(hex_str) 49 | try: return plist.wrap_data(binascii.unhexlify(hex_str.encode("utf-8"))) 50 | except: return None 51 | 52 | def port_to_num(self, value, pad_to=2): 53 | value = self.check_hex(value) 54 | try: return str(int(self.hex_swap(value),16)).rjust(pad_to) 55 | except: pass 56 | return "-1".rjust(pad_to) 57 | 58 | def print_types(self): 59 | self.u.resize(self.w, self.h) 60 | self.u.head("USB Types") 61 | print("") 62 | types = "\n".join([ 63 | "0: Type A connector", 64 | "1: Mini-AB connector", 65 | "2: ExpressCard", 66 | "3: USB 3 Standard-A connector", 67 | "4: USB 3 Standard-B connector", 68 | "5: USB 3 Micro-B connector", 69 | "6: USB 3 Micro-AB connector", 70 | "7: USB 3 Power-B connector", 71 | "8: Type C connector - USB2-only", 72 | "9: Type C connector - USB2 and SS with Switch", 73 | "10: Type C connector - USB2 and SS without Switch", 74 | "11 - 254: Reserved", 75 | "255: Proprietary connector" 76 | ]) 77 | print(types) 78 | print("") 79 | print("Per the ACPI 6.2 Spec.") 80 | print("") 81 | return self.u.grab("Press [enter] to return to the menu...") 82 | 83 | def current_smbios(self): 84 | if not sys.platform.lower() == "darwin": return None 85 | try: return subprocess.Popen(["system_profiler","SPHardwareDataType"],stdout=subprocess.PIPE).stdout.read().decode("utf-8").split("Model Identifier: ")[1].split("\n")[0].strip() 86 | except: pass 87 | return None 88 | 89 | def choose_smbios(self,current=None,allow_return=True,prompt=None): 90 | self.u.resize(self.w, self.h) 91 | while True: 92 | self.u.head("Choose SMBIOS Target") 93 | print("") 94 | if current: 95 | print("Current: {}".format(current)) 96 | print("") 97 | if prompt: print(prompt+"\n") 98 | if self.smbios: print("C. Use Current Machine's SMBIOS ({})".format(self.smbios)) 99 | if allow_return: print("M. Return to Menu") 100 | print("Q. Quit") 101 | print("") 102 | menu = self.u.grab("Please type the new target SMBIOS (eg. iMac18,1): ") 103 | if not len(menu): continue 104 | elif menu.lower() == "c" and self.smbios: return self.smbios 105 | elif menu.lower() == "m" and allow_return: return 106 | elif menu.lower() == "q": self.u.custom_quit() 107 | else: return menu 108 | 109 | def save_plist(self): 110 | # Ensure the lists are the same 111 | try: 112 | with open(self.plist_path,"wb") as f: 113 | plist.dump(self.plist_data,f,sort_keys=False) 114 | return True 115 | except Exception as e: 116 | self.show_error("Error Saving","Could not save to {}! {}".format(os.path.basename(self.plist_path),e)) 117 | return False 118 | 119 | def change_personality_name(self,personality): 120 | self.u.resize(self.w, self.h) 121 | while True: 122 | pad = 4 123 | print_text = [""] 124 | print_text.append("Existing Personalities:") 125 | print_text.append("") 126 | print_text.extend([" - {}{}{}{}".format( 127 | self.bs if x==personality else "", 128 | x, 129 | self.ce if x==personality else "", 130 | " (Currently Editing)" if x == personality else "" 131 | ) for x in self.plist_data["IOKitPersonalities"]]) 132 | print_text.append("") 133 | print_text.append("M. Return to Menu") 134 | print_text.append("Q. Quit") 135 | print_text.append("") 136 | w_adj = max((len(x) for x in print_text)) 137 | h_adj = len(print_text) + pad 138 | self.u.resize(w_adj if w_adj>self.w else self.w, h_adj if h_adj>self.h else self.h) 139 | self.u.head("Change IOKitPersonality Name") 140 | print("\n".join(print_text)) 141 | menu = self.u.grab("Please type the new IOKitPersonality name: ") 142 | if not len(menu): continue 143 | elif menu.lower() == "m" or menu == personality: return personality 144 | elif menu.lower() == "q": self.u.custom_quit() 145 | elif menu in self.plist_data["IOKitPersonalities"]: 146 | self.u.resize(self.w, self.h) 147 | self.u.head("Personality Exists") 148 | print("") 149 | print("The following IOKitPersonality already exists:\n\n - {}".format(menu)) 150 | print("") 151 | self.u.grab("Press [enter] to return...") 152 | continue 153 | # Should have a valid name - let's pop our current value into the new one 154 | self.plist_data["IOKitPersonalities"][menu] = self.plist_data["IOKitPersonalities"].pop(personality,None) 155 | return menu 156 | 157 | def edit_ports(self,personality): 158 | pers = self.plist_data["IOKitPersonalities"][personality] 159 | if not pers.get("IOProviderMergeProperties",{}).get("ports",{}): 160 | return self.show_error("No Ports Defined","There are no ports defined for {}!".format(personality)) 161 | ports = pers["IOProviderMergeProperties"]["ports"] 162 | sorted_ports = sorted(ports,key=lambda x:ports[x].get("port",ports[x].get("#port"))) 163 | port_list = list(ports) 164 | next_class = "AppleUSBHostMergeProperties" 165 | while True: 166 | pad = 4 167 | enabled = 0 168 | highest = b"\x00\x00\x00\x00" 169 | print_text = [""] 170 | for i,x in enumerate(sorted_ports,start=1): 171 | port = ports[x] 172 | try: 173 | addr = binascii.hexlify(plist.extract_data(port.get("port",port.get("#port")))).decode("utf-8") 174 | except Exception as e: 175 | print(str(e)) 176 | continue 177 | if "port" in port: 178 | enabled += 1 179 | if self.hex_dec(self.hex_swap(addr)) > self.hex_dec(self.hex_swap(binascii.hexlify(highest).decode("utf-8"))): 180 | highest = plist.extract_data(port["port"]) 181 | line = "[{}] {}. {} | {} ({}) | Type {}".format( 182 | "#" if "port" in port else " ", 183 | str(i).rjust(2), 184 | x, 185 | self.port_to_num(addr), 186 | addr, 187 | port.get("UsbConnector",-1), 188 | ) 189 | print_text.append("{}{}{}".format( 190 | self.bs if "port" in port else "", 191 | line, 192 | self.ce if "port" in port else "" 193 | )) 194 | comment = port.get("Comment",port.get("comment",None)) 195 | if comment: 196 | print_text.append(" {}{}{}".format(self.nm,comment,self.ce)) 197 | # Update the highest selected 198 | pers["IOProviderMergeProperties"]["port-count"] = plist.wrap_data(highest) 199 | print_text.append("") 200 | print_text.append("Total Enabled: {}{:,}{}".format( 201 | self.cs if 0 < enabled < 16 else self.rs, 202 | enabled, 203 | self.ce 204 | )) 205 | if "model" in pers: 206 | print_text.append("Target SMBIOS: {}".format(pers["model"])) 207 | if "IOClass" in pers: 208 | print_text.append("Target Class: {}".format(pers["IOClass"])) 209 | print_text.append("") 210 | print_text.append("I. Change IOKitPersonality Name") 211 | if "model" in pers: 212 | print_text.append("S. Change SMBIOS Target") 213 | if "IOClass" in pers: 214 | next_class = "AppleUSBMergeNub" if pers["IOClass"] == "AppleUSBHostMergeProperties" else "AppleUSBHostMergeProperties" 215 | print_text.append("C. Toggle IOClass to {}".format(next_class)) 216 | print_text.append("") 217 | print_text.append("A. Select All") 218 | print_text.append("N. Select None") 219 | print_text.append("T. Show Types") 220 | print_text.append("M. IOKitPersonality Menu") 221 | print_text.append("Q. Quit") 222 | print_text.append("") 223 | print_text.append("- Select ports to toggle with comma-delimited lists (eg. 1,2,3,4,5)") 224 | print_text.append("- Set a range of ports using this formula R:1-15:On/Off") 225 | print_text.append("- Change types using this formula T:1,2,3,4,5:t where t is the type") 226 | print_text.append("- Set custom names using this formula C:1,2:Name - Name = None to clear") 227 | print_text.append("") 228 | self.save_plist() 229 | w_adj = max((len(x) for x in print_text)) 230 | h_adj = len(print_text) + pad 231 | self.u.resize(w_adj if w_adj>self.w else self.w, h_adj if h_adj>self.h else self.h) 232 | self.u.head("{} Ports".format(personality)) 233 | print("\n".join(print_text)) 234 | menu = self.u.grab("Please make your selection: ") 235 | if not len(menu): continue 236 | elif menu.lower() == "m": return 237 | elif menu.lower() == "q": 238 | self.u.resize(self.w, self.h) 239 | self.u.custom_quit() 240 | elif menu.lower() == "i": 241 | personality = self.change_personality_name(personality) 242 | pers = self.plist_data["IOKitPersonalities"][personality] 243 | elif menu.lower() == "s" and "model" in pers: 244 | smbios = self.choose_smbios(pers["model"]) 245 | if smbios: pers["model"] = smbios 246 | elif menu.lower() == "c" and "IOClass" in pers: 247 | pers["IOClass"] = next_class 248 | pers["CFBundleIdentifier"] = "com.apple.driver."+next_class 249 | elif menu.lower() in ("a","n"): 250 | find,repl = ("#port","port") if menu.lower() == "a" else ("port","#port") 251 | for x in ports: 252 | if find in ports[x]: ports[x][repl] = ports[x].pop(find) 253 | elif menu.lower() == "t": 254 | self.print_types() 255 | elif menu[0].lower() == "r": 256 | # Should be a range 257 | try: 258 | nums = [int(x) for x in menu.split(":")[1].replace(" ","").split("-")] 259 | a,b = nums[0]-1,nums[-1]-1 # Get the first and last - then determine which is larger 260 | if b < a: a,b = b,a # Flip them around if need be 261 | if not all((0 <= x < len(ports) for x in (a,b))): continue # Out of bounds, skip 262 | # Ge the on/off value 263 | toggle = menu.split(":")[-1].lower() 264 | if not toggle in ("on","off"): continue # Invalid - skip 265 | find,repl = ("#port","port") if toggle == "on" else ("port","#port") 266 | for x in range(a,b+1): 267 | if find in ports[port_list[x]]: ports[port_list[x]][repl] = ports[port_list[x]].pop(find) 268 | except: 269 | continue 270 | # Check if we need to toggle 271 | elif menu[0].lower() == "t": 272 | # We should have a type 273 | try: 274 | nums = [int(x) for x in menu.split(":")[1].replace(" ","").split(",")] 275 | t = int(menu.split(":")[-1]) 276 | for x in nums: 277 | x -= 1 278 | if not 0 <= x < len(ports): continue # Out of bounds, skip 279 | # Valid index 280 | ports[port_list[x]]["UsbConnector"] = t 281 | except: 282 | continue 283 | elif menu[0].lower() == "c": 284 | # We should have a new name 285 | try: 286 | nums = [int(x) for x in menu.split(":")[1].replace(" ","").split(",")] 287 | name = menu.split(":")[-1] 288 | for x in nums: 289 | x -= 1 290 | if not 0 <= x < len(ports): continue # Out of bounds, skip 291 | # Valid index - let's pop any lowercase comments first 292 | ports[port_list[x]].pop("comment",None) 293 | if name.lower() == "none": ports[port_list[x]].pop("Comment",None) 294 | else: ports[port_list[x]]["Comment"] = name 295 | except: 296 | continue 297 | else: 298 | # At this point, check for indexes and toggle 299 | try: 300 | nums = [int(x) for x in menu.replace(" ","").split(",")] 301 | for x in nums: 302 | x -= 1 303 | if not 0 <= x < len(ports): continue # Out of bounds, skip 304 | find,repl = ("#port","port") if "#port" in ports[port_list[x]] else ("port","#port") 305 | ports[port_list[x]][repl] = ports[port_list[x]].pop(find) 306 | except: 307 | continue 308 | 309 | def pick_personality(self): 310 | if not self.plist_path or not self.plist_data: return 311 | while True: 312 | pad = 4 313 | print_text = [""] 314 | pers = list(self.plist_data["IOKitPersonalities"]) 315 | for i,x in enumerate(pers,start=1): 316 | personality = self.plist_data["IOKitPersonalities"][x] 317 | ports = personality.get("IOProviderMergeProperties",{}).get("ports",{}) 318 | enabled = len([p for p in ports if "port" in ports[p]]) 319 | print_text.append("{}. {} - {}{:,}{}/{:,} enabled".format( 320 | str(i).rjust(2), 321 | x, 322 | self.cs if 0 < enabled < 16 else self.rs, 323 | enabled, 324 | self.ce, 325 | len(ports) 326 | )) 327 | if "model" in personality: 328 | print_text.append(" {}SMBIOS: {}{}".format(self.bs,personality["model"],self.ce)) 329 | if "IOClass" in personality: 330 | print_text.append(" {}Class: {}{}".format(self.bs,personality["IOClass"],self.ce)) 331 | print_text.append("") 332 | print_text.append("S. Set All SMBIOS Targets") 333 | print_text.append("C. Set All Classes to AppleUSBHostMergeProperties") 334 | print_text.append("L. Set All Classes to AppleUSBMergeNub (Legacy)") 335 | print_text.append("M. Return To Injector Selection Menu") 336 | print_text.append("Q. Quit") 337 | print_text.append("") 338 | w_adj = max((len(x) for x in print_text)) 339 | h_adj = len(print_text) + pad 340 | self.u.resize(w_adj if w_adj>self.w else self.w, h_adj if h_adj>self.h else self.h) 341 | self.u.head("Available IOKitPersonalities") 342 | print("\n".join(print_text)) 343 | menu = self.u.grab("Please select an IOKitPersonality to edit (1-{:,}): ".format(len(pers))) 344 | if not len(menu): continue 345 | elif menu.lower() == "m": return 346 | elif menu.lower() == "q": 347 | self.u.resize(self.w, self.h) 348 | self.u.custom_quit() 349 | elif menu.lower() == "s": 350 | smbios = self.choose_smbios() 351 | if smbios: 352 | for x in pers: 353 | self.plist_data["IOKitPersonalities"][x]["model"] = smbios 354 | self.save_plist() 355 | elif menu.lower() in ("c","l"): 356 | next_class = "AppleUSBHostMergeProperties" if menu.lower() == "c" else "AppleUSBMergeNub" 357 | for x in pers: 358 | self.plist_data["IOKitPersonalities"][x]["IOClass"] = next_class 359 | self.plist_data["IOKitPersonalities"][x]["CFBundleIdentifier"] = "com.apple.driver."+next_class 360 | self.save_plist() 361 | else: 362 | # Cast as int and ensure we're in range 363 | try: 364 | menu = int(menu)-1 365 | assert 0 <= menu < len(pers) 366 | except: 367 | continue 368 | self.edit_ports(pers[menu]) 369 | 370 | def show_error(self,header,error): 371 | self.u.head(header) 372 | print("") 373 | print(str(error)) 374 | print("") 375 | return self.u.grab("Press [enter] to continue...") 376 | 377 | def parse_usb_txt(self,raw): 378 | model = self.choose_smbios(current=None,prompt="Please enter the target SMBIOS for this injector.") 379 | if not model: return 380 | self.u.head("Parsing USB Info") 381 | print("") 382 | print("Got SMBIOS: {}".format(model)) 383 | print("Walking UsbDumpEfi output...") 384 | try: 385 | output_plist = { 386 | "CFBundleDevelopmentRegion": "English", 387 | "CFBundleGetInfoString": "v1.0", 388 | "CFBundleIdentifier": "com.corpnewt.USBMap", 389 | "CFBundleInfoDictionaryVersion": "6.0", 390 | "CFBundleName": "USBMap", 391 | "CFBundlePackageType": "KEXT", 392 | "CFBundleShortVersionString": "1.0", 393 | "CFBundleSignature": "????", 394 | "CFBundleVersion": "1.0", 395 | "IOKitPersonalities": {}, 396 | "OSBundleRequired": "Root" 397 | } 398 | controllers = output_plist["IOKitPersonalities"] 399 | types = {"0":"OHCI","1":"OHCI","2":"EHCI","3":"XHCI"} # Use OHCI as a placeholder for 0, and 1 400 | info = raw.split("UsbDumpEfi start")[1] 401 | last_name = None 402 | for line in info.split("\n"): 403 | line = line.strip() 404 | if not line: continue 405 | if line.startswith("Found"): # Got a controller 406 | addr = ":".join([str(int(x,16)) for x in line.split(" @ ")[-1].replace(".",":").split(":")]) 407 | t = types.get(line.split("speed ")[1].split(")")[0],"Unknown") 408 | last_name = t 409 | if last_name in controllers: 410 | n = 1 411 | while True: 412 | temp = "{}-{}".format(last_name,n) 413 | if not temp in controllers: 414 | last_name = temp 415 | break 416 | n += 1 417 | controllers[last_name] = { 418 | "CFBundleItentifier": "com.apple.driver.AppleUSBHostMergeProperties", 419 | "IOClass": "AppleUSBHostMergeProperties", 420 | "IOParentMatch": {"IOPropertyMatch":{"pcidebug":addr}}, 421 | "IOProviderClass":"AppleUSB{}PCI".format(t), 422 | "IOProviderMergeProperties": { 423 | "port-count": self.hex_data(self.hex_swap(hex(int(line.split("(")[1].split(" ports")[0]))[2:].upper().rjust(8,"0"))), 424 | "ports": {} 425 | }, 426 | "model": model 427 | } 428 | if t == "XHCI": controllers[last_name]["IOProviderMergeProperties"]["kUSBMuxEnabled"] = True 429 | elif line.startswith("Port") and last_name != None: 430 | usb_connector = 3 if "XHCI" in controllers[last_name]["IOProviderClass"] else 0 431 | num = int(line.split("Port ")[1].split(" status")[0])+1 432 | name = "UK{}".format(str(num).rjust(2,"0")) 433 | hex_num = self.hex_data(self.hex_swap(hex(num)[2:].upper().rjust(8,"0"))) 434 | controllers[last_name]["IOProviderMergeProperties"]["ports"][name] = {"UsbConnector":usb_connector,"port":hex_num} 435 | except Exception as e: 436 | return self.show_error("Error Parsing".format(os.path.basename(path)),e) 437 | print("Generating kexts...") 438 | if not os.path.exists(self.output): os.mkdir(self.output) 439 | for k,t in (("USBMap.kext","AppleUSBHostMergeProperties"),("USBMapLegacy.kext","AppleUSBMergeNub")): 440 | print(" - {}".format(k)) 441 | kp = os.path.join(self.output,k) 442 | if os.path.exists(kp): 443 | print(" --> Located existing {} - removing...".format(k)) 444 | shutil.rmtree(kp,ignore_errors=True) 445 | print(" --> Creating bundle structure...") 446 | os.makedirs(os.path.join(kp,"Contents")) 447 | print(" --> Setting IOClass types...") 448 | for c in controllers: 449 | controllers[c]["CFBundleItentifier"] = "com.apple.driver.{}".format(t) 450 | controllers[c]["IOClass"] = t 451 | print(" --> Writing Info.plist...") 452 | with open(os.path.join(kp,"Contents","Info.plist"),"wb") as f: 453 | plist.dump(output_plist,f) 454 | print(" - Saved to: {}".format(kp)) 455 | print("") 456 | print("Done.") 457 | print("") 458 | self.u.grab("Press [enter] to return...") 459 | 460 | def main(self,path=None): 461 | if path is None: 462 | self.u.resize(self.w, self.h) 463 | self.u.head() 464 | print("") 465 | print("NOTE: All changes are done in-place, and happen immediately.") 466 | print(" Please make sure you keep backups.") 467 | print("") 468 | print("Q. Quit") 469 | print("") 470 | print("Please drag and drop a USBMap(Legacy).kext, Info.plist,") 471 | menu = self.u.grab("or UsbDumpEfi.efi output here to continue: ") 472 | if not len(menu): return 473 | if menu.lower() == "q": self.u.custom_quit() 474 | else: 475 | menu = path 476 | # Check the path 477 | path = self.u.check_path(menu) 478 | try: 479 | # Ensure we have a valid path 480 | if not path: raise Exception("{} does not exist!".format(menu)) 481 | if os.path.isdir(path): path = os.path.join(path,"Contents","Info.plist") 482 | if not os.path.exists(path): raise Exception("{} does not exist!".format(path)) 483 | if not os.path.isfile(path): raise Exception("{} is a directory!".format(path)) 484 | except Exception as e: 485 | return self.show_error("Error Selecting Target",e) 486 | try: 487 | # Load it and ensure the plist is valid 488 | with open(path,"rb") as f: 489 | raw = f.read().replace(b"\x00",b"").decode("utf-8",errors="ignore") 490 | if "UsbDumpEfi start" in raw: 491 | return self.parse_usb_txt(raw) 492 | else: 493 | f.seek(0) 494 | plist_data = plist.load(f,dict_type=OrderedDict) 495 | except Exception as e: 496 | return self.show_error("Error Loading {}".format(os.path.basename(path)),e) 497 | if not len(plist_data.get("IOKitPersonalities",{})): 498 | return self.show_error("Missing Personalities","No IOKitPersonalities found in {}!".format(os.path.basename(path))) 499 | self.plist_path = path 500 | self.plist_data = plist_data 501 | self.pick_personality() 502 | 503 | if __name__ == '__main__': 504 | u = USBMap() 505 | path = sys.argv[1] if len(sys.argv)>1 else None 506 | while True: 507 | u.main(path=path) 508 | path = None # Prevent a loop on exception 509 | -------------------------------------------------------------------------------- /images/USB3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corpnewt/USBMap/fa6b77309283a4f1e9e474b15b41224312cf7c84/images/USB3.png -------------------------------------------------------------------------------- /images/imac171.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corpnewt/USBMap/fa6b77309283a4f1e9e474b15b41224312cf7c84/images/imac171.png -------------------------------------------------------------------------------- /images/look-sir-ports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corpnewt/USBMap/fa6b77309283a4f1e9e474b15b41224312cf7c84/images/look-sir-ports.png --------------------------------------------------------------------------------