├── .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 | 
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 | 
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 | 
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
--------------------------------------------------------------------------------