├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── build.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── Scripts ├── __init__.py ├── _build.py ├── iokit.py ├── shared.py ├── usbdump.py └── utils.py ├── TYPES.md ├── Windows.py ├── base.py ├── debug_dump.py ├── macOS.py ├── requirements.txt ├── resources ├── Info.plist └── usbdump.exe └── spec ├── Windows.spec ├── Windows_dir.spec ├── debug_dump.spec ├── debug_dump_dir.spec ├── insert_version.py └── macOS.spec /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501, E203 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report bugs with the USBToolBox tool 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please make sure you are testing with the **latest** version of USBToolBox, available from https://github.com/USBToolBox/tool/releases. 11 | 12 | The issue tracker for the **kext** is located at https://github.com/USBToolBox/kext. However, if you're not sure where an issue should go, open it somewhere and it will be moved to the appropriate place. 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. If the program is closing and you're on Windows, make sure to run it through `cmd` (or other preferred Terminal). Add screenshots if possible. 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Debugging information** 25 | If on Windows, please download debug_dump from https://github.com/USBToolBox/tool/releases. It will save a file with debugging information, please upload it to this bug report. 26 | 27 | **Hardware:** 28 | - Motherboard (or if laptop/OEM desktop, model): [e.g. ASRock B450 Pro4] 29 | - OS: [e.g. iOS] 30 | - Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | jobs: 8 | macOS: 9 | name: Build macOS 10 | runs-on: self-hosted 11 | steps: 12 | - uses: actions/checkout@v2 13 | # - name: Set up Python 3 14 | # run: brew install python3 15 | 16 | - name: Install Python Dependencies 17 | run: pip3 install -r requirements.txt 18 | 19 | - name: Install Debug Dependencies 20 | run: pip3 install pyinstaller 21 | - name: Build for macOS 22 | run: pyinstaller spec/macOS.spec 23 | 24 | - name: Zip 25 | run: cd dist; zip macOS.zip macOS 26 | - name: Upload to Artifacts 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: Artifacts macOS 30 | path: dist/macOS.zip 31 | - name: Upload to Release 32 | if: github.event_name == 'release' 33 | uses: svenstaro/upload-release-action@e74ff71f7d8a4c4745b560a485cc5fdb9b5b999d 34 | with: 35 | repo_token: ${{ secrets.GITHUB_TOKEN }} 36 | file: dist/macOS.zip 37 | tag: ${{ github.ref }} 38 | file_glob: true 39 | 40 | windows: 41 | name: Build Windows 42 | runs-on: windows-latest 43 | steps: 44 | - uses: actions/checkout@v2 45 | 46 | - name: Set up Python 3 47 | uses: actions/setup-python@v2 48 | with: 49 | python-version: "3.x" 50 | 51 | - name: Install Python Dependencies 52 | run: pip3 install -r requirements.txt 53 | 54 | - name: Install Debug Dependencies 55 | run: pip3 install pyinstaller 56 | 57 | - name: Build for Windows 58 | run: pyinstaller spec\Windows.spec && pyinstaller spec\Windows_dir.spec && pyinstaller spec\debug_dump.spec && pyinstaller spec\debug_dump_dir.spec 59 | 60 | - uses: vimtor/action-zip@v1 61 | with: 62 | files: dist/Windows/ 63 | recursive: false 64 | dest: dist/Windows.zip 65 | - uses: vimtor/action-zip@v1 66 | with: 67 | files: dist/debug_dump/ 68 | recursive: false 69 | dest: dist/debug_dump.zip 70 | 71 | - name: Upload to Artifacts 72 | uses: actions/upload-artifact@v2 73 | with: 74 | name: Artifacts Windows 75 | path: | 76 | dist/*.exe 77 | dist/*.zip 78 | - name: Upload to Release 79 | if: github.event_name == 'release' 80 | uses: svenstaro/upload-release-action@e74ff71f7d8a4c4745b560a485cc5fdb9b5b999d 81 | with: 82 | repo_token: ${{ secrets.GITHUB_TOKEN }} 83 | file: dist/*.* 84 | tag: ${{ github.ref }} 85 | file_glob: true 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # static files generated from Django application using `collectstatic` 141 | media 142 | static 143 | 144 | # General 145 | .DS_Store 146 | .AppleDouble 147 | .LSOverride 148 | 149 | # Icon must end with two \r 150 | Icon 151 | 152 | # Thumbnails 153 | ._* 154 | 155 | # Files that might appear in the root of a volume 156 | .DocumentRevisions-V100 157 | .fseventsd 158 | .Spotlight-V100 159 | .TemporaryItems 160 | .Trashes 161 | .VolumeIcon.icns 162 | .com.apple.timemachine.donotpresent 163 | 164 | # Directories potentially created on remote AFP share 165 | .AppleDB 166 | .AppleDesktop 167 | Network Trash Folder 168 | Temporary Items 169 | .apdisk 170 | 171 | usb.json 172 | USBMap.kext 173 | UTBMap.kext 174 | *USBMap.json 175 | settings.json 176 | 177 | .vscode 178 | samples/ -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" 4 | 5 | [MESSAGES CONTROL] 6 | 7 | disable=unused-import, 8 | subprocess-run-check, 9 | line-too-long, 10 | too-few-public-methods, 11 | missing-module-docstring, 12 | missing-class-docstring, 13 | missing-function-docstring -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Dhinak G 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 | # USBToolBoxᵇᵉᵗᵃ 2 | 3 | *Making USB mapping simple(r)* 4 | 5 | The USBToolBox tool is a USB mapping tool supporting Windows and macOS. It allows for building a custom injector kext from Windows and macOS. 6 | 7 | ## Features 8 | 9 | * Supports mapping from Windows and macOS 10 | * Can build a map using either the USBToolBox kext or native Apple kexts (AppleUSBHostMergeProperties) 11 | * Supports multiple ways of matching 12 | * Supports companion ports (on Windows) 13 | * Make educated guesses for port types (on Windows) 14 | 15 | ## Supported Methods 16 | 17 | ### From Windows 18 | 19 | Windows 10 or 11 64-bit are recommended for the full feature set (companion port binding, port type guessing.). Windows 8 may work, Windows 7 and below will very likely crash. 32-bit is not supported, macOS needs 64-bit anyway. 20 | 21 | Simply download the latest `Windows.exe` from releases. If Windows Defender/other antivirus complains, you can either whitelist the download or use `Windows.zip`, which doesn't have a self extractor (which is what most antiviruses seem to complain about). 22 | 23 | ### From Windows PE 24 | 25 | Yes this works lol. Some device names may not be as descriptive but if you really don't want to install Windows, you can create a Windows PE USB and hit Shift + F10 to open `cmd`, then run the program. 26 | 27 | ### From macOS 28 | 29 | macOS is *not* recommended for several reasons. You won't have features like guessing port types (as there simply isn't enough info for this) as well as binding companion ports (again, no info). However, there's also port limits to deal with, and in macOS 11.3, `XhciPortLimit` is broken, resulting in a lot more hoops to go through. If you are forced to use macOS, you should probably use [USBMap](https://github.com/CorpNewt/USBMap) instead, as it has code to handle the port limit. 30 | 31 | If you still want to use USBToolBox on macOS, download `macOS.zip` from releases. 32 | 33 | ## Usage 34 | 35 | This is gonna be a very basic guide for now. A fully-fleshed guide will be released in the future. 36 | 37 | 1. Download the appropriate download for your OS. 38 | 2. Open and adjust settings if necessary. 39 | 3. Select Discover Ports and wait for the listing to populate. 40 | 4. Plug in a USB device into each port. Wait for the listing to show your USB device before unplugging it and plugging it into another port. 41 | * If on Windows, you only need to plug in 1 device to USB 3 ports (as companion detection should be working). If on macOS, you will have to plug in a USB 2 device and a USB 3 device into each USB 3 port. 42 | * For old computers with OHCI/UHCI and EHCI controllers, you will need to plug in a mouse/keyboard to map the USB 1.1 personalities, as most USB 2 devices will end on the USB 2 personality. 43 | 5. Once mapping is done, go to the Select Ports screen. 44 | 6. Select your ports and adjust port types as neccesary. 45 | 7. Press K to build the kext! 46 | 8. Add the resulting USB map to your `EFI/OC/Kexts` folder, and make sure to update your `config.plist`. 47 | * If building a map that uses the USBToolBox kext, make sure to grab the [latest release](https://github.com/USBToolBox/kext/releases) of the kext too. 48 | * Make sure to remove `UTBDefault.kext` , if you have it. 49 | 9. Reboot and you should have your USB map working! 50 | 51 | ## Known Issues/FAQ 52 | 53 | See the [issues tab](https://github.com/USBToolBox/tool/issues) for known issues. 54 | 55 | ### FAQ 56 | 57 | * Q: Why is some information missing? 58 | 59 | A: Make sure you have drivers installed for all your devices. On Windows, some information is missing if you don't have drivers installed, leading USBToolBox to report them as unknown. 60 | 61 | * Q: How do I report a bug? 62 | 63 | A: Please go to the [new issue](https://github.com/USBToolBox/tool/issues/new/choose) page, click on "Bug report", and read through the steps before filling them out. Please ensure that you respond to my inquiries as there's no other way I can fix bugs. 64 | 65 | ## Credits 66 | 67 | @CorpNewt for [USBMap](https://github.com/corpnewt/USBMap). This project was heavily inspired by USBMap (and some functions are from USBMap). 68 | 69 | My testing team (you know who you are) for testing 70 | -------------------------------------------------------------------------------- /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/_build.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | try: 4 | result = subprocess.run("git describe --tags --always".split(), stdout=subprocess.PIPE) 5 | BUILD = result.stdout.decode().strip() 6 | except: 7 | BUILD = None 8 | -------------------------------------------------------------------------------- /Scripts/iokit.py: -------------------------------------------------------------------------------- 1 | from typing import NewType, Union 2 | 3 | import objc 4 | from CoreFoundation import CFRelease # type: ignore # pylint: disable=no-name-in-module 5 | from Foundation import NSBundle # type: ignore # pylint: disable=no-name-in-module 6 | from PyObjCTools import Conversion 7 | 8 | IOKit_bundle = NSBundle.bundleWithIdentifier_("com.apple.framework.IOKit") 9 | 10 | io_name_t_ref = b"[128c]" # pylint: disable=invalid-name 11 | CFStringRef = b"^{__CFString=}" 12 | CFDictionaryRef = b"^{__CFDictionary=}" 13 | # https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html 14 | functions = [ 15 | ("IORegistryEntryCreateCFProperties", b"IIo^@II"), 16 | ("IOServiceMatching", CFDictionaryRef + b"r*"), 17 | ("IOServiceGetMatchingServices", b"II" + CFDictionaryRef + b"o^I"), 18 | ("IOIteratorNext", b"II"), 19 | ("IORegistryEntryGetParentEntry", b"IIr*o^I"), 20 | ("IOObjectRelease", b"II"), 21 | # io_name_t is char[128] 22 | ("IORegistryEntryGetName", b"IIo" + io_name_t_ref), 23 | ("IOObjectGetClass", b"IIo" + io_name_t_ref), 24 | ("IOObjectCopyClass", CFStringRef + b"I"), 25 | ("IOObjectCopySuperclassForClass", CFStringRef + CFStringRef), 26 | ("IORegistryEntryGetChildIterator", b"IIr*o^I"), 27 | ("IORegistryCreateIterator", b"IIr*Io^I"), 28 | ("IORegistryEntryCreateIterator", b"IIr*Io^I"), 29 | ("IORegistryIteratorEnterEntry", b"II"), 30 | ("IORegistryIteratorExitEntry", b"II"), 31 | ("IORegistryEntryCreateCFProperty", b"@I" + CFStringRef + b"II"), 32 | ("IORegistryEntryGetPath", b"IIr*oI"), 33 | ("IORegistryEntryCopyPath", CFStringRef + b"Ir*"), 34 | ] 35 | 36 | # TODO: Proper typing 37 | 38 | # pylint: disable=invalid-name 39 | pointer = type(None) 40 | 41 | kern_return_t = NewType("kern_return_t", int) 42 | 43 | io_object_t = NewType("io_object_t", object) 44 | io_name_t = bytes 45 | io_string_t = bytes 46 | 47 | # io_registry_entry_t = NewType("io_registry_entry_t", io_object_t) 48 | io_registry_entry_t = io_object_t 49 | io_iterator_t = NewType("io_iterator_t", io_object_t) 50 | 51 | CFTypeRef = Union[int, float, bytes, dict, list] 52 | 53 | IOOptionBits = int 54 | mach_port_t = int 55 | CFAllocatorRef = int 56 | 57 | NULL = 0 58 | 59 | kIOMasterPortDefault: mach_port_t = NULL 60 | kCFAllocatorDefault: CFAllocatorRef = NULL 61 | kNilOptions: IOOptionBits = NULL 62 | 63 | kIORegistryIterateRecursively = 1 64 | kIORegistryIterateParents = 2 65 | 66 | # pylint: enable=invalid-name 67 | 68 | 69 | # kern_return_t IORegistryEntryCreateCFProperties(io_registry_entry_t entry, CFMutableDictionaryRef * properties, CFAllocatorRef allocator, IOOptionBits options); 70 | def IORegistryEntryCreateCFProperties(entry: io_registry_entry_t, properties: pointer, allocator: CFAllocatorRef, options: IOOptionBits) -> tuple[kern_return_t, dict]: # pylint: disable=invalid-name 71 | raise NotImplementedError 72 | 73 | 74 | # CFMutableDictionaryRef IOServiceMatching(const char * name); 75 | def IOServiceMatching(name: bytes) -> dict: # pylint: disable=invalid-name 76 | raise NotImplementedError 77 | 78 | 79 | # kern_return_t IOServiceGetMatchingServices(mach_port_t masterPort, CFDictionaryRef matching CF_RELEASES_ARGUMENT, io_iterator_t * existing); 80 | def IOServiceGetMatchingServices(masterPort: mach_port_t, matching: dict, existing: pointer) -> tuple[kern_return_t, io_iterator_t]: # pylint: disable=invalid-name 81 | raise NotImplementedError 82 | 83 | 84 | # io_object_t IOIteratorNext(io_iterator_t iterator); 85 | def IOIteratorNext(iterator: io_iterator_t) -> io_object_t: # pylint: disable=invalid-name 86 | raise NotImplementedError 87 | 88 | 89 | # kern_return_t IORegistryEntryGetParentEntry(io_registry_entry_t entry, const io_name_t plane, io_registry_entry_t * parent); 90 | def IORegistryEntryGetParentEntry(entry: io_registry_entry_t, plane: io_name_t, parent: pointer) -> tuple[kern_return_t, io_registry_entry_t]: # pylint: disable=invalid-name 91 | raise NotImplementedError 92 | 93 | 94 | # kern_return_t IOObjectRelease(io_object_t object); 95 | def IOObjectRelease(object: io_object_t) -> kern_return_t: # pylint: disable=invalid-name 96 | raise NotImplementedError 97 | 98 | 99 | # kern_return_t IORegistryEntryGetName(io_registry_entry_t entry, io_name_t name); 100 | def IORegistryEntryGetName(entry: io_registry_entry_t, name: pointer) -> tuple[kern_return_t, str]: # pylint: disable=invalid-name 101 | raise NotImplementedError 102 | 103 | 104 | # kern_return_t IOObjectGetClass(io_object_t object, io_name_t className); 105 | def IOObjectGetClass(object: io_object_t, className: pointer) -> tuple[kern_return_t, str]: # pylint: disable=invalid-name 106 | raise NotImplementedError 107 | 108 | 109 | # CFStringRef IOObjectCopyClass(io_object_t object); 110 | def IOObjectCopyClass(object: io_object_t) -> str: # pylint: disable=invalid-name 111 | raise NotImplementedError 112 | 113 | 114 | # CFStringRef IOObjectCopySuperclassForClass(CFStringRef classname) 115 | def IOObjectCopySuperclassForClass(classname: str) -> str: # pylint: disable=invalid-name 116 | raise NotImplementedError 117 | 118 | 119 | # kern_return_t IORegistryEntryGetChildIterator(io_registry_entry_t entry, const io_name_t plane, io_iterator_t * iterator); 120 | def IORegistryEntryGetChildIterator(entry: io_registry_entry_t, plane: io_name_t, iterator: pointer) -> tuple[kern_return_t, io_iterator_t]: # pylint: disable=invalid-name 121 | raise NotImplementedError 122 | 123 | 124 | # kern_return_t IORegistryCreateIterator(mach_port_t masterPort, const io_name_t plane, IOOptionBits options, io_iterator_t * iterator) 125 | def IORegistryCreateIterator(masterPort: mach_port_t, plane: io_name_t, options: IOOptionBits, iterator: pointer) -> tuple[kern_return_t, io_iterator_t]: # pylint: disable=invalid-name 126 | raise NotImplementedError 127 | 128 | 129 | # kern_return_t IORegistryEntryCreateIterator(io_registry_entry_t entry, const io_name_t plane, IOOptionBits options, io_iterator_t * iterator) 130 | def IORegistryEntryCreateIterator(entry: io_registry_entry_t, plane: io_name_t, options: IOOptionBits, iterator: pointer) -> tuple[kern_return_t, io_iterator_t]: # pylint: disable=invalid-name 131 | raise NotImplementedError 132 | 133 | 134 | # kern_return_t IORegistryIteratorEnterEntry(io_iterator_t iterator) 135 | def IORegistryIteratorEnterEntry(iterator: io_iterator_t) -> kern_return_t: # pylint: disable=invalid-name 136 | raise NotImplementedError 137 | 138 | 139 | # kern_return_t IORegistryIteratorExitEntry(io_iterator_t iterator) 140 | def IORegistryIteratorExitEntry(iterator: io_iterator_t) -> kern_return_t: # pylint: disable=invalid-name 141 | raise NotImplementedError 142 | 143 | 144 | # CFTypeRef IORegistryEntryCreateCFProperty(io_registry_entry_t entry, CFStringRef key, CFAllocatorRef allocator, IOOptionBits options); 145 | def IORegistryEntryCreateCFProperty(entry: io_registry_entry_t, key: str, allocator: CFAllocatorRef, options: IOOptionBits) -> CFTypeRef: # pylint: disable=invalid-name 146 | raise NotImplementedError 147 | 148 | 149 | # kern_return_t IORegistryEntryGetPath(io_registry_entry_t entry, const io_name_t plane, io_string_t path); 150 | def IORegistryEntryGetPath(entry: io_registry_entry_t, plane: io_name_t, path: pointer) -> tuple[kern_return_t, io_string_t]: # pylint: disable=invalid-name 151 | raise NotImplementedError 152 | 153 | 154 | # CFStringRef IORegistryEntryCopyPath(io_registry_entry_t entry, const io_name_t plane) 155 | def IORegistryEntryCopyPath(entry: io_registry_entry_t, plane: bytes) -> str: # pylint: disable=invalid-name 156 | raise NotImplementedError 157 | 158 | 159 | objc.loadBundleFunctions(IOKit_bundle, globals(), functions) # type: ignore # pylint: disable=no-member 160 | 161 | 162 | def ioiterator_to_list(iterator: io_iterator_t): 163 | # items = [] 164 | item = IOIteratorNext(iterator) # noqa: F821 165 | while item: 166 | # items.append(next) 167 | yield item 168 | item = IOIteratorNext(iterator) # noqa: F821 169 | IOObjectRelease(iterator) # noqa: F821 170 | # return items 171 | 172 | 173 | def corefoundation_to_native(collection): 174 | native = Conversion.pythonCollectionFromPropertyList(collection) 175 | CFRelease(collection) 176 | return native 177 | 178 | 179 | def native_to_corefoundation(native): 180 | return Conversion.propertyListFromPythonCollection(native) 181 | 182 | 183 | def io_name_t_to_str(name): 184 | return name.partition(b"\0")[0].decode() 185 | 186 | 187 | def get_class_inheritance(io_object): 188 | classes = [] 189 | cls = IOObjectCopyClass(io_object) 190 | while cls: 191 | # yield cls 192 | classes.append(cls) 193 | CFRelease(cls) 194 | cls = IOObjectCopySuperclassForClass(cls) 195 | return classes 196 | -------------------------------------------------------------------------------- /Scripts/shared.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | import enum 3 | import sys 4 | from time import time 5 | from typing import Callable 6 | from pathlib import Path 7 | 8 | from Scripts._build import BUILD 9 | 10 | VERSION = "0.2" 11 | 12 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): 13 | current_dir = Path(sys.executable).parent 14 | resource_dir = Path(sys._MEIPASS) / Path("resources") 15 | else: 16 | current_dir = Path(__file__).parent.parent 17 | resource_dir = current_dir / Path("resources") 18 | 19 | 20 | class USBDeviceSpeeds(enum.IntEnum): 21 | LowSpeed = 0 22 | FullSpeed = 1 23 | HighSpeed = 2 24 | SuperSpeed = 3 25 | # The integer value of this only applies for macOS 26 | SuperSpeedPlus = 4 27 | # This is not an actual value 28 | Unknown = 9999 29 | 30 | def __str__(self) -> str: 31 | return _usb_protocol_names[self] 32 | 33 | def __bool__(self) -> bool: 34 | return True 35 | 36 | 37 | class USBPhysicalPortTypes(enum.IntEnum): 38 | USBTypeA = 0 39 | USBTypeMiniAB = 1 40 | ExpressCard = 2 41 | USB3TypeA = 3 42 | USB3TypeB = 4 43 | USB3TypeMicroB = 5 44 | USB3TypeMicroAB = 6 45 | USB3TypePowerB = 7 46 | USB3TypeC_USB2Only = 8 47 | USB3TypeC_WithSwitch = 9 48 | USB3TypeC_WithoutSwitch = 10 49 | Internal = 255 50 | 51 | def __str__(self) -> str: 52 | return _usb_physical_port_types[self] 53 | 54 | def __bool__(self) -> bool: 55 | return True 56 | 57 | 58 | class USBControllerTypes(enum.IntEnum): 59 | UHCI = int("0x00", 16) 60 | OHCI = int("0x10", 16) 61 | EHCI = int("0x20", 16) 62 | XHCI = int("0x30", 16) 63 | Unknown = 9999 64 | 65 | def __str__(self) -> str: 66 | return _usb_controller_types[self] 67 | 68 | def __bool__(self) -> bool: 69 | return True 70 | 71 | 72 | _usb_controller_types = { 73 | USBControllerTypes.UHCI: "USB 1.1 (UHCI)", 74 | USBControllerTypes.OHCI: "USB 1.1 (OHCI)", 75 | USBControllerTypes.EHCI: "USB 2.0 (EHCI)", 76 | USBControllerTypes.XHCI: "USB 3.0 (XHCI)", 77 | USBControllerTypes.Unknown: "Unknown", 78 | } 79 | 80 | _usb_physical_port_types = { 81 | USBPhysicalPortTypes.USBTypeA: "Type A", 82 | USBPhysicalPortTypes.USBTypeMiniAB: "Type Mini-AB", 83 | USBPhysicalPortTypes.ExpressCard: "ExpressCard", 84 | USBPhysicalPortTypes.USB3TypeA: "USB 3 Type A", 85 | USBPhysicalPortTypes.USB3TypeB: "USB 3 Type B", 86 | USBPhysicalPortTypes.USB3TypeMicroB: "USB 3 Type Micro-B", 87 | USBPhysicalPortTypes.USB3TypeMicroAB: "USB 3 Type Micro-AB", 88 | USBPhysicalPortTypes.USB3TypePowerB: "USB 3 Type Power-B", 89 | USBPhysicalPortTypes.USB3TypeC_USB2Only: "Type C - USB 2 only", 90 | USBPhysicalPortTypes.USB3TypeC_WithSwitch: "Type C - with switch", 91 | USBPhysicalPortTypes.USB3TypeC_WithoutSwitch: "Type C - without switch", 92 | USBPhysicalPortTypes.Internal: "Internal", 93 | } 94 | 95 | _short_names = True 96 | 97 | _usb_protocol_names_full = { 98 | USBDeviceSpeeds.LowSpeed: "USB 1.1 (Low Speed)", 99 | USBDeviceSpeeds.FullSpeed: "USB 1.1 (Full Speed)", 100 | USBDeviceSpeeds.HighSpeed: "USB 2.0 (High Speed)", 101 | USBDeviceSpeeds.SuperSpeed: "USB 3.0/USB 3.1 Gen 1/USB 3.2 Gen 1x1 (SuperSpeed)", 102 | USBDeviceSpeeds.SuperSpeedPlus: "USB 3.1 Gen 2/USB 3.2 Gen 2×1 (SuperSpeed+)", 103 | USBDeviceSpeeds.Unknown: "Unknown", 104 | } 105 | 106 | _usb_protocol_names_short = { 107 | USBDeviceSpeeds.LowSpeed: "USB 1.1", 108 | USBDeviceSpeeds.FullSpeed: "USB 1.1", 109 | USBDeviceSpeeds.HighSpeed: "USB 2.0", 110 | USBDeviceSpeeds.SuperSpeed: "USB 3.0", 111 | USBDeviceSpeeds.SuperSpeedPlus: "USB 3.1 Gen 2", 112 | USBDeviceSpeeds.Unknown: "Unknown", 113 | } 114 | 115 | _usb_protocol_names = _usb_protocol_names_short if _short_names else _usb_protocol_names_full 116 | 117 | 118 | def time_it(func: Callable, text: str, *args, **kwargs): 119 | start = time() 120 | result = func(*args, **kwargs) 121 | end = time() 122 | input(f"{text} took {end - start}, press enter to continue".strip()) 123 | return result 124 | 125 | debugging = False 126 | 127 | def debug(str): 128 | if debugging: 129 | input(f"DEBUG: {str}\nPress enter to continue") 130 | 131 | test_mode = False and debugging 132 | if test_mode: 133 | debug_dump_path = Path(input("Debug dump path: ").strip().replace("'", "").replace('"', "")) 134 | else: 135 | debug_dump_path = None 136 | 137 | # def speed_to_name(speed: USBDeviceSpeeds): 138 | # return _usb_protocol_names[speed] 139 | -------------------------------------------------------------------------------- /Scripts/usbdump.py: -------------------------------------------------------------------------------- 1 | # USBDump Conversion Interface 2 | import itertools 3 | import json 4 | import subprocess 5 | import sys 6 | from operator import itemgetter 7 | from pathlib import Path 8 | 9 | from Scripts import shared 10 | 11 | # input_path = input("File path: ") 12 | # if input_path: 13 | # file_path = Path("samples/" + input_path) 14 | # else: 15 | # file_path = Path("samples/tablet.json") 16 | # info = json.load(file_path.open()) 17 | 18 | 19 | hub_map = {} 20 | 21 | 22 | def get_port_type(port): 23 | if not port["ConnectionInfoV2"]: 24 | return shared.USBDeviceSpeeds.Unknown 25 | supported_usb_protocols = port["ConnectionInfoV2"]["SupportedUsbProtocols"] 26 | if supported_usb_protocols["Usb300"]: 27 | return shared.USBDeviceSpeeds.SuperSpeed 28 | elif supported_usb_protocols["Usb200"] and supported_usb_protocols["Usb110"]: 29 | return shared.USBDeviceSpeeds.HighSpeed 30 | elif supported_usb_protocols["Usb110"]: 31 | return shared.USBDeviceSpeeds.FullSpeed 32 | else: 33 | return shared.USBDeviceSpeeds.Unknown 34 | 35 | 36 | def get_device_speed(port): 37 | speed = port["ConnectionInfo"]["Speed"] 38 | if speed == shared.USBDeviceSpeeds.LowSpeed: 39 | return (shared.USBDeviceSpeeds.LowSpeed, None) 40 | elif speed == shared.USBDeviceSpeeds.FullSpeed: 41 | speed = shared.USBDeviceSpeeds.FullSpeed 42 | elif speed == shared.USBDeviceSpeeds.HighSpeed: 43 | speed = shared.USBDeviceSpeeds.HighSpeed 44 | elif speed == shared.USBDeviceSpeeds.SuperSpeed and port["ConnectionInfoV2"] and port["ConnectionInfoV2"]["Flags"]["DeviceIsOperatingAtSuperSpeedPlusOrHigher"]: 45 | return (shared.USBDeviceSpeeds.SuperSpeedPlus, None) 46 | elif speed == shared.USBDeviceSpeeds.SuperSpeed: 47 | speed = shared.USBDeviceSpeeds.SuperSpeed 48 | else: 49 | return (shared.USBDeviceSpeeds.Unknown, speed) 50 | 51 | if port["ConnectionInfoV2"] and port["ConnectionInfoV2"]["Flags"]["DeviceIsSuperSpeedPlusCapableOrHigher"]: 52 | return (speed, shared.USBDeviceSpeeds.SuperSpeedPlus) 53 | elif speed < shared.USBDeviceSpeeds.SuperSpeed and port["ConnectionInfoV2"] and port["ConnectionInfoV2"]["Flags"]["DeviceIsSuperSpeedCapableOrHigher"]: 54 | return (speed, shared.USBDeviceSpeeds.SuperSpeed) 55 | else: 56 | return (speed, None) 57 | 58 | 59 | def get_device_speed_string(port, hub_port_count=None): 60 | speed = get_device_speed(port) 61 | # return f"{speed[0]}{(', ' + speed[1] + ' capable') if speed[1] else ''}{(', ' + str(hub_port_count) + ' ports') if hub_port_count else ''}" 62 | return speed[0] 63 | 64 | 65 | def get_device_name(port): 66 | if not port["UsbDeviceProperties"]: 67 | port["UsbDeviceProperties"] = {} 68 | if not port["DeviceInfoNode"]: 69 | port["DeviceInfoNode"] = {} 70 | 71 | if not port["ConnectionInfo"]["DeviceDescriptor"]["iProduct"]: 72 | return port["UsbDeviceProperties"].get("DeviceDesc") or port["DeviceInfoNode"].get("DeviceDescName", "Unknown Device") 73 | for string_desc in port["StringDescs"] or []: 74 | if string_desc["DescriptorIndex"] == port["ConnectionInfo"]["DeviceDescriptor"]["iProduct"]: 75 | return string_desc["StringDescriptor"][0]["bString"] 76 | return port["UsbDeviceProperties"].get("DeviceDesc") or port["DeviceInfoNode"].get("DeviceDescName", "Unknown Device") 77 | 78 | 79 | def get_hub_type(port): 80 | return shared.USBDeviceSpeeds(port["HubInfoEx"]["HubType"]) 81 | 82 | 83 | # def merge_companions(controllers): 84 | # controllers = copy.deepcopy(controllers) 85 | # for controller in controllers: 86 | # for port in controller["ports"]: 87 | # if port["companion_info"]["port"]: 88 | # companion_hub = [i for i in controllers if i["hub_name"] == port["companion_info"]["hub"]][0] 89 | # companion_port = [i for i in companion_hub["ports"] if i["index"] == port["companion_info"]["port"]][0] 90 | # companion_port["companion_info"]["port"] = 0 91 | # port["companion_info"]["port"] = companion_port 92 | # for controller in controllers: 93 | # for port in list(controller["ports"]): 94 | # if port["companion_info"]["port"]: 95 | # companion_hub = [i for i in controllers if i["hub_name"] == port["companion_info"]["hub"]][0] 96 | # companion_port = [i for i in companion_hub["ports"] if i["index"] == port["companion_info"]["port"]["index"]][0] 97 | # companion_hub["ports"].remove(companion_port) 98 | # return controllers 99 | 100 | 101 | def get_hub_by_name(name): 102 | return hub_map.get(name) 103 | 104 | # TODO: Figure out how to deal with the hub name not matching 105 | def get_companion_port(port): 106 | return ([i for i in hub_map.get(port["companion_info"]["hub"], {"ports": []})["ports"] if i["index"] == port["companion_info"]["port"]] or [None])[0] 107 | 108 | 109 | def guess_ports(): 110 | for hub in hub_map: 111 | for port in hub_map[hub]["ports"]: 112 | if not port["status"].endswith("DeviceConnected"): 113 | # we don't have info. anything else is going to error 114 | port["guessed"] = None 115 | elif port["type_c"] or port["companion_info"]["port"] and get_companion_port(port) and get_companion_port(port).get("type_c", None): 116 | port["guessed"] = shared.USBPhysicalPortTypes.USB3TypeC_WithSwitch 117 | elif not port["user_connectable"]: 118 | port["guessed"] = shared.USBPhysicalPortTypes.Internal 119 | elif ( 120 | port["class"] == shared.USBDeviceSpeeds.SuperSpeed 121 | and port["companion_info"]["port"] 122 | and get_companion_port(port) 123 | and get_companion_port(port)["class"] == shared.USBDeviceSpeeds.HighSpeed 124 | or port["class"] == shared.USBDeviceSpeeds.HighSpeed 125 | and port["companion_info"]["port"] 126 | and get_companion_port(port) 127 | and get_companion_port(port)["class"] == shared.USBDeviceSpeeds.SuperSpeed 128 | ): 129 | port["guessed"] = shared.USBPhysicalPortTypes.USB3TypeA 130 | elif port["class"] == shared.USBDeviceSpeeds.SuperSpeed and not port["companion_info"]["port"]: 131 | port["guessed"] = shared.USBPhysicalPortTypes.Internal 132 | else: 133 | port["guessed"] = shared.USBPhysicalPortTypes.USBTypeA 134 | 135 | 136 | def serialize_hub(hub): 137 | hub_info = { 138 | "hub_name": hub["HubName"], 139 | # "class": get_hub_type(hub), 140 | "port_count": hub["HubInfo"]["HubInformation"]["HubDescriptor"]["bNumberOfPorts"], 141 | # "highest_port_number": hub["HubInfoEx"]["HighestPortNumber"], 142 | "ports": [], 143 | } 144 | 145 | # HubPorts 146 | hub_ports = hub["HubPorts"] 147 | if hub_ports: # For some reason, this is sometimes null? Botched driver? 148 | for i, port in enumerate(hub_ports): 149 | if not port: 150 | continue 151 | port_info = { 152 | "index": (port.get("PortConnectorProps") or {}).get("ConnectionIndex") 153 | or (port.get("ConnectionInfo") or {}).get("ConnectionIndex") 154 | or (port.get("ConnectionInfoV2") or {}).get("ConnectionIndex") 155 | or i + 1, 156 | "comment": None, 157 | "class": shared.USBDeviceSpeeds.Unknown, 158 | "status": port["ConnectionInfo"]["ConnectionStatus"], 159 | "type": None, 160 | "guessed": None, 161 | "devices": [], 162 | } 163 | port_info["name"] = f"Port {port_info['index']}" 164 | 165 | friendly_error = {"DeviceCausedOvercurrent": "Device connected to port pulled too much current."} 166 | 167 | if not port_info["status"].endswith("DeviceConnected"): 168 | # shared.debug(f"Device connected to port {port_info['index']} errored. Please unplug or connect a different device.") 169 | port_info["devices"] = [{"error": friendly_error.get(port_info["status"], True)}] 170 | hub_info["ports"].append(port_info) 171 | continue 172 | 173 | port_info["class"] = get_port_type(port) 174 | if not port["PortConnectorProps"]: 175 | port["PortConnectorProps"] = {} 176 | 177 | port_info["companion_info"] = { 178 | "port": port["PortConnectorProps"].get("CompanionPortNumber", ""), 179 | "hub": port["PortConnectorProps"].get("CompanionHubSymbolicLinkName", ""), 180 | "multiple_companions": bool(port["PortConnectorProps"].get("UsbPortProperties", {}).get("PortHasMultipleCompanions", False)), 181 | } 182 | port_info["type_c"] = bool(port["PortConnectorProps"].get("UsbPortProperties", {}).get("PortConnectorIsTypeC", False)) 183 | port_info["user_connectable"] = bool(port["PortConnectorProps"].get("UsbPortProperties", {}).get("PortIsUserConnectable", True)) 184 | 185 | # Guess port type 186 | 187 | if port["ConnectionInfo"]["ConnectionStatus"] == "DeviceConnected": 188 | device_info = {"name": get_device_name(port), "instance_id": port["UsbDeviceProperties"].get("DeviceId"), "devices": []} 189 | 190 | if port["DeviceInfoType"] == "ExternalHubInfo": 191 | external_hub = serialize_hub(port) 192 | device_info["speed"] = get_device_speed_string(port, external_hub["port_count"]) 193 | device_info["devices"] = [i for i in itertools.chain.from_iterable([hub_port["devices"] for hub_port in external_hub["ports"]]) if i] 194 | # device_info["hub_type"] = get_hub_type(port) 195 | # device_info["hub"] = serialize_hub(port) 196 | else: 197 | device_info["speed"] = get_device_speed_string(port) 198 | 199 | port_info["devices"].append(device_info) 200 | 201 | hub_info["ports"].append(port_info) 202 | hub_info["ports"].sort(key=itemgetter("index")) 203 | hub_map[hub_info["hub_name"]] = hub_info 204 | return hub_info 205 | 206 | 207 | def get_controllers(): 208 | new_info = [] 209 | 210 | usbdump_path = Path("resources/usbdump.exe") 211 | 212 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): 213 | usbdump_path = Path(sys._MEIPASS) / usbdump_path 214 | 215 | info = json.load(shared.debug_dump_path.open())["usbdump"] if shared.test_mode else json.loads(subprocess.run(usbdump_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.decode()) 216 | for controller in info: 217 | if not controller["RootHub"]: 218 | # This is useless 219 | continue 220 | 221 | # root 222 | controller_info = { 223 | "name": controller["UsbDeviceProperties"]["DeviceDesc"], 224 | "identifiers": { 225 | "instance_id": controller["UsbDeviceProperties"]["DeviceId"], 226 | # "revision": controller["Revision"], 227 | }, 228 | # "port_count_no3": controller["ControllerInfo"]["NumberOfRootPorts"], 229 | "class": "", 230 | } | serialize_hub(controller["RootHub"]) 231 | 232 | if all(controller[i] not in [0, int("0xFFFF", 16)] for i in ["VendorID", "DeviceID"]): 233 | controller_info["identifiers"]["pci_id"] = [hex(controller[i])[2:] for i in ["VendorID", "DeviceID"]] 234 | 235 | if controller["SubSysID"] not in [0, int("0xFFFFFFFF", 16)]: 236 | controller_info["identifiers"]["pci_id"] += [hex(controller["SubSysID"])[2:6], hex(controller["SubSysID"])[6:]] 237 | 238 | if (controller.get("ControllerInfo") or {}).get("PciRevision", 0) not in [0, int("0xFF", 16)]: 239 | controller_info["identifiers"]["pci_revision"] = int(controller["ControllerInfo"]["PciRevision"]) 240 | 241 | if controller["BusDeviceFunctionValid"]: 242 | controller_info["identifiers"]["bdf"] = [controller["BusNumber"], controller["BusDevice"], controller["BusFunction"]] 243 | 244 | new_info.append(controller_info) 245 | guess_ports() 246 | if False: 247 | for hub in hub_map: 248 | for port in hub_map[hub]["ports"]: 249 | if port["companion_info"]["hub"]: 250 | port["companion_info"]["hub"] = hub_map[port["companion_info"]["hub"]] 251 | return new_info 252 | -------------------------------------------------------------------------------- /Scripts/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import os 4 | import sys 5 | import time 6 | from sys import exit 7 | from typing import Callable, Optional, Union 8 | import ansiescapes 9 | 10 | if os.name == "nt": 11 | # Windows 12 | import msvcrt 13 | else: 14 | # Not Windows \o/ 15 | import select 16 | 17 | 18 | class Utils: 19 | def __init__(self, name="Python Script"): 20 | self.name = name 21 | # Init our colors before we need to print anything 22 | cwd = os.getcwd() 23 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 24 | if os.path.exists("colors.json"): 25 | self.colors_dict = json.load(open("colors.json")) 26 | else: 27 | self.colors_dict = {} 28 | os.chdir(cwd) 29 | 30 | def grab(self, prompt, **kwargs): 31 | # Takes a prompt, a default, and a timeout and shows it with that timeout 32 | # returning the result 33 | timeout = kwargs.get("timeout", 0) 34 | default = kwargs.get("default", None) 35 | # If we don't have a timeout - then skip the timed sections 36 | if timeout <= 0: 37 | return input(prompt) 38 | # Write our prompt 39 | sys.stdout.write(prompt) 40 | sys.stdout.flush() 41 | if os.name == "nt": 42 | start_time = time.time() 43 | i = "" 44 | while True: 45 | if msvcrt.kbhit(): 46 | c = msvcrt.getche() 47 | if ord(c) == 13: # enter_key 48 | break 49 | elif ord(c) >= 32: # space_char 50 | i += c.decode("utf-8") 51 | if len(i) == 0 and (time.time() - start_time) > timeout: 52 | break 53 | else: 54 | i, o, e = select.select([sys.stdin], [], [], timeout) 55 | if i: 56 | i = sys.stdin.readline().strip() 57 | print("") # needed to move to next line 58 | if len(i) > 0: 59 | return i 60 | else: 61 | return default 62 | 63 | def cls(self): 64 | os.system("cls" if os.name == "nt" else "clear") 65 | 66 | # Header drawing method 67 | def head(self, text=None, width=55): 68 | if text == None: 69 | text = self.name 70 | self.cls() 71 | print(" {}".format("#" * width)) 72 | mid_len = int(round(width / 2 - len(text) / 2) - 2) 73 | middle = " #{}{}{}#".format(" " * mid_len, text, " " * ((width - mid_len - len(text)) - 2)) 74 | if len(middle) > width + 1: 75 | # Get the difference 76 | di = len(middle) - width 77 | # Add the padding for the ...# 78 | di += 3 79 | # Trim the string 80 | middle = middle[:-di] + "...#" 81 | print(middle) 82 | print("#" * width) 83 | 84 | def custom_quit(self): 85 | self.head() 86 | print("by DhinakG") 87 | print("with code from CorpNewt's USBMap\n") 88 | print("Thanks for testing it out!\n") 89 | # Get the time and wish them a good morning, afternoon, evening, and night 90 | hr = datetime.datetime.now().time().hour 91 | if hr > 3 and hr < 12: 92 | print("Have a nice morning!\n\n") 93 | elif hr >= 12 and hr < 17: 94 | print("Have a nice afternoon!\n\n") 95 | elif hr >= 17 and hr < 21: 96 | print("Have a nice evening!\n\n") 97 | else: 98 | print("Have a nice night!\n\n") 99 | exit(0) 100 | 101 | 102 | def cls(): 103 | os.system("cls" if os.name == "nt" else "clear") 104 | 105 | 106 | def header(text, width=55): 107 | cls() 108 | print(" {}".format("#" * width)) 109 | mid_len = int(round(width / 2 - len(text) / 2) - 2) 110 | middle = " #{}{}{}#".format(" " * mid_len, text, " " * ((width - mid_len - len(text)) - 2)) 111 | if len(middle) > width + 1: 112 | # Get the difference 113 | di = len(middle) - width 114 | # Add the padding for the ...# 115 | di += 3 116 | # Trim the string 117 | middle = middle[:-di] + "...#" 118 | print(middle) 119 | print("#" * width) 120 | 121 | 122 | class TUIMenu: 123 | EXIT_MENU = object() 124 | 125 | def __init__( 126 | self, 127 | title: str, 128 | prompt: str, 129 | return_number: bool = False, 130 | add_quit: bool = True, 131 | auto_number: bool = False, 132 | in_between: Optional[Union[list, Callable]] = None, 133 | top_level: bool = False, 134 | loop: bool = False, 135 | ): 136 | self.title = title 137 | self.prompt = prompt 138 | self.in_between = in_between or [] 139 | self.options = [] 140 | self.return_number = return_number 141 | self.auto_number = auto_number 142 | self.add_quit = add_quit 143 | self.top_level = top_level 144 | self.loop = loop 145 | self.add_quit = add_quit 146 | 147 | self.return_option = (["Q", "Quit"] if self.top_level else ["B", "Back"]) if self.add_quit else None 148 | 149 | def add_menu_option(self, name: Union[str, Callable], description: Optional[list[str]] = None, function: Optional[Callable] = None, key: Optional[str] = None): 150 | if not key and not self.auto_number: 151 | raise TypeError("Key must be specified if auto_number is false") 152 | self.options.append([key, name, description or [], function]) 153 | 154 | def head(self): 155 | cls() 156 | header(self.title) 157 | print() 158 | 159 | def print_options(self): 160 | for index, option in enumerate(self.options): 161 | if self.auto_number: 162 | option[0] = str((index + 1)) 163 | print(option[0] + ". " + (option[1]() if callable(option[1]) else option[1])) 164 | for i in option[2]: 165 | print(" " + i) 166 | if self.add_quit: 167 | print(f"{self.return_option[0]}. {self.return_option[1]}") 168 | print() 169 | 170 | def select(self): 171 | print(ansiescapes.cursorSavePosition, end="") 172 | print(ansiescapes.eraseDown, end="") 173 | selected = input(self.prompt) 174 | 175 | keys = [option[0].upper() for option in self.options] 176 | if self.add_quit: 177 | keys += [self.return_option[0]] 178 | 179 | while not selected or selected.upper() not in keys: 180 | nl_count = self.prompt.count("\n") + selected.count("\n") + 1 181 | selected = input(f"{nl_count * ansiescapes.cursorPrevLine}{ansiescapes.eraseDown}{self.prompt}") 182 | 183 | if self.add_quit and selected.upper() == self.return_option[0]: 184 | return self.EXIT_MENU 185 | elif self.return_number: 186 | return self.options[keys.index(selected.upper())][0] 187 | else: 188 | return self.options[keys.index(selected.upper())][3]() if self.options[keys.index(selected.upper())][3] else None 189 | 190 | def start(self): 191 | while True: 192 | self.head() 193 | 194 | if callable(self.in_between): 195 | self.in_between() 196 | print() 197 | elif self.in_between: 198 | for i in self.in_between: 199 | print(i) 200 | print() 201 | 202 | self.print_options() 203 | 204 | result = self.select() 205 | 206 | if result is self.EXIT_MENU: 207 | return self.EXIT_MENU 208 | elif not self.loop: 209 | return result 210 | 211 | 212 | class TUIOnlyPrint: 213 | def __init__(self, title, prompt, in_between=None): 214 | self.title = title 215 | self.prompt = prompt 216 | self.in_between = in_between or [] 217 | 218 | def start(self): 219 | cls() 220 | header(self.title) 221 | print() 222 | 223 | if callable(self.in_between): 224 | self.in_between() 225 | else: 226 | for i in self.in_between: 227 | print(i) 228 | if self.in_between: 229 | print() 230 | 231 | return input(self.prompt) 232 | -------------------------------------------------------------------------------- /TYPES.md: -------------------------------------------------------------------------------- 1 | # USB Types 2 | 3 | ## USB Type A 4 | 5 | Connector type 0. The standard USB 2.0 ports we all are accustomed to. 6 | 7 | ![Type A](https://upload.wikimedia.org/wikipedia/commons/7/7e/USB_Type-A_receptacle.svg) 8 | 9 | ## USB Type Mini-AB 10 | 11 | Connector type 1. Probably one of the most obscure USB port types. Designed to mate with both USB Mini-A and USB Mini-B. 12 | 13 | ![Mini-AB](https://upload.wikimedia.org/wikipedia/commons/f/f3/USB_Mini-AB_receptacle.svg) 14 | 15 | ## ExpressCard 16 | 17 | Connector type 2. A connector type almost never used, this is for the USB side of ExpressCards. 18 | 19 | ## USB 3 Type A 20 | 21 | Connector type 3. The standard USB 3.0 ports we are all accustomed to. 22 | 23 | ![USB 3 Type A](https://upload.wikimedia.org/wikipedia/commons/f/f4/USB_3.0_Type-A_receptacle_blue.svg) 24 | 25 | ## USB 3 Type B 26 | 27 | Connector type 4. Commonly seen on hard drives, but not really on computers. 28 | 29 | ![USB 3 Type B](https://upload.wikimedia.org/wikipedia/commons/8/8c/USB_3.0_Type-B_receptacle_blue.svg) 30 | 31 | ## USB 3 Type Micro-B 32 | 33 | Connector type 5. Commonly seen on smaller hard drives and some old Galaxy phones, but again, not really on computers. 34 | 35 | ![USB 3 Type Micro-B](https://upload.wikimedia.org/wikipedia/commons/a/a8/USB_3.0_Micro-B_receptacle.svg) 36 | 37 | ## USB 3 Type Micro-AB 38 | 39 | Connector type 6. Another one of those obscure port types. Designed to mate with both USB Micro-A and USB Micro-B. 40 | 41 | ![USB 3 Type Micro-AB](https://upload.wikimedia.org/wikipedia/commons/6/6c/USB_Micro-AB_receptacle.svg) 42 | 43 | ## USB 3 Type Power-B 44 | 45 | Connector type 7. Yet another obscure port type. The USB 3 Type B connector with extra pins for power. 46 | 47 | ![USB 3 Type Power-B](https://upload.wikimedia.org/wikipedia/commons/9/9c/USB_3.0_Type-B_Powered.gif) 48 | 49 | ## USB Type C 50 | 51 | There are 3 connector types for USB Type C connectors: Type C - USB 2 only, Type C - with switch, and Type C - without switch. All look like this: 52 | 53 | ![USB Type C](https://upload.wikimedia.org/wikipedia/commons/9/98/USB_Type-C_icon.svg) 54 | 55 | * USB Type C - USB 2 only is connector type 8. Only USB 2 is wired to the USB C port. 56 | * USB Type C - with switch is connector type 9. Both USB 2 and USB 3 are wired to the USB C port, and a switch in the circuitry allows for use of the same two personalities when the connector is flipped. 57 | * USB Type C - without switch is connector type 10. Both USB 2 and USB 3 are wired to the USB C port like when with a switch, but when flipping the connector, different personalities are used. 58 | 59 | ## Internal 60 | 61 | Connector type 255. Used for any internal devices, like card readers, fingerprint scanners, webcams, etc. 62 | 63 | ## Picture Credits 64 | 65 | Pictures sourced from [Wikipedia](https://en.wikipedia.org/wiki/USB_hardware). All credit goes to the authors. 66 | -------------------------------------------------------------------------------- /Windows.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import time 4 | from enum import Enum 5 | 6 | try: 7 | import wmi # pylint: disable=import-error 8 | except Exception: # pylint: disable=broad-except 9 | # Dummy WMI 10 | class wmi: # pylint: disable=invalid-name 11 | class WMI: 12 | @staticmethod 13 | def query(*args, **kwargs): # pylint: disable=unused-argument 14 | return [] 15 | 16 | 17 | from base import BaseUSBMap 18 | from Scripts import shared, usbdump 19 | 20 | 21 | class PnpDeviceProperties(Enum): 22 | ACPI_PATH = "DEVPKEY_Device_BiosDeviceName" 23 | DRIVER_KEY = "DEVPKEY_Device_Driver" 24 | LOCATION_PATHS = "DEVPKEY_Device_LocationPaths" 25 | FRIENDLY_NAME = "DEVPKEY_Device_FriendlyName" 26 | BUS_DEVICE_FUNCTION = "DEVPKEY_Device_LocationInfo" 27 | BUS_REPORTED_NAME = "DEVPKEY_Device_BusReportedDeviceDesc" 28 | BUS_RELATIONS = "DEVPKEY_Device_BusRelations" 29 | INTERFACE = "DEVPKEY_PciDevice_ProgIf" 30 | SERVICE = "DEVPKEY_Device_Service" 31 | 32 | 33 | class WindowsUSBMap(BaseUSBMap): 34 | def __init__(self): 35 | self.usbdump = None 36 | if shared.test_mode: 37 | self.wmi = {} 38 | self.wmi_cache: dict = {p["DEVPKEY_Device_InstanceId"]: p for p in json.load(shared.debug_dump_path.open())["wmitest"] if "duration" not in p} 39 | else: 40 | self.wmi = wmi.WMI() 41 | self.wmi_cache = {} 42 | self.wmi_retries = {} 43 | super().__init__() 44 | 45 | def update_usbdump(self): 46 | self.usbdump = usbdump.get_controllers() 47 | 48 | def get_property_from_wmi(self, instance_id, prop: PnpDeviceProperties): 49 | MAX_TRIES = 2 50 | result = None 51 | if self.wmi_cache.get(instance_id, {}).get(prop.value): 52 | return self.wmi_cache[instance_id][prop.value] 53 | elif self.wmi_retries.get(instance_id, {}).get(prop.value, 0) >= MAX_TRIES: 54 | return None 55 | 56 | try: 57 | result = self.wmi.query(f"SELECT * FROM Win32_PnPEntity WHERE PNPDeviceID = '{instance_id}'")[0].GetDeviceProperties([prop.value])[0][0].Data 58 | except IndexError: 59 | # Race condition between unplug detected in usbdump and WMI 60 | return None 61 | except AttributeError: 62 | if not self.wmi_retries.get(instance_id): 63 | self.wmi_retries[instance_id] = {prop.value: 1} 64 | elif not self.wmi_retries[instance_id].get(prop.value): 65 | self.wmi_retries[instance_id][prop.value] = 1 66 | else: 67 | self.wmi_retries[instance_id][prop.value] += 1 68 | 69 | return None 70 | 71 | if not self.wmi_cache.get(instance_id): 72 | self.wmi_cache[instance_id] = {prop.value: result} 73 | else: 74 | self.wmi_cache[instance_id][prop.value] = result 75 | 76 | return result 77 | 78 | """ def get_property_from_wmi(self, instance_id, prop: PnpDeviceProperties): 79 | value = self.wmi.setdefault(instance_id, {}).get(prop.value) 80 | if value: 81 | return value 82 | else: 83 | value = input(f"Enter value for {instance_id} {prop}: ") 84 | self.wmi[instance_id][prop.value] = value 85 | json.dump(self.wmi, self.wmi_path.open("w"), indent=4, sort_keys=True) 86 | return value """ 87 | 88 | def get_name_from_wmi(self, device): 89 | if not isinstance(device, dict): 90 | return 91 | if device.get("error") or not device["instance_id"]: 92 | return 93 | device["name"] = self.get_property_from_wmi(device["instance_id"], PnpDeviceProperties.BUS_REPORTED_NAME) or device["name"] 94 | for i in device["devices"]: 95 | self.get_name_from_wmi(i) 96 | 97 | def get_controller_class(self, controller): 98 | interface = self.get_property_from_wmi(controller["identifiers"]["instance_id"], PnpDeviceProperties.INTERFACE) 99 | if interface: 100 | return shared.USBControllerTypes(interface) 101 | service = self.get_property_from_wmi(controller["identifiers"]["instance_id"], PnpDeviceProperties.SERVICE) 102 | if not isinstance(service, str): 103 | shared.debug(f"Unknown controller type for interface {interface} and service {service}!") 104 | return shared.USBControllerTypes.Unknown 105 | if service.lower() == "usbxhci": 106 | return shared.USBControllerTypes.XHCI 107 | elif service.lower() == "usbehci": 108 | return shared.USBControllerTypes.EHCI 109 | elif service.lower() == "usbohci": 110 | return shared.USBControllerTypes.OHCI 111 | elif service.lower() == "usbuhci": 112 | return shared.USBControllerTypes.UHCI 113 | else: 114 | shared.debug(f"Unknown controller type for interface {interface} and service {service}!") 115 | return shared.USBControllerTypes.Unknown 116 | 117 | def get_controllers(self): 118 | # self.update_usbdump() 119 | for i in range(10): 120 | try: 121 | # time_it(self.update_usbdump, "USBdump time") 122 | self.update_usbdump() 123 | break 124 | except Exception as e: 125 | if i == 9: 126 | raise 127 | else: 128 | shared.debug(e) 129 | time.sleep(0.05 if shared.debugging else 2) 130 | 131 | controllers = self.usbdump 132 | 133 | for controller in controllers: 134 | controller["name"] = self.get_property_from_wmi(controller["identifiers"]["instance_id"], PnpDeviceProperties.FRIENDLY_NAME) or controller["name"] 135 | controller["class"] = self.get_controller_class(controller) 136 | acpi_path = self.get_property_from_wmi(controller["identifiers"]["instance_id"], PnpDeviceProperties.ACPI_PATH) 137 | if acpi_path: 138 | controller["identifiers"]["acpi_path"] = acpi_path 139 | driver_key = self.get_property_from_wmi(controller["identifiers"]["instance_id"], PnpDeviceProperties.DRIVER_KEY) 140 | if driver_key: 141 | controller["identifiers"]["driver_key"] = driver_key 142 | location_paths = self.get_property_from_wmi(controller["identifiers"]["instance_id"], PnpDeviceProperties.LOCATION_PATHS) 143 | if location_paths: 144 | controller["identifiers"]["location_paths"] = location_paths 145 | # controller["identifiers"]["bdf"] = self.get_property_from_wmi(controller["identifiers"]["instance_id"], PnpDeviceProperties.BUS_DEVICE_FUNCTION) 146 | for port in controller["ports"]: 147 | for device in port["devices"]: 148 | self.get_name_from_wmi(device) 149 | 150 | self.controllers = controllers 151 | if not self.controllers_historical: 152 | self.controllers_historical = copy.deepcopy(self.controllers) 153 | else: 154 | self.merge_controllers(self.controllers_historical, self.controllers) 155 | 156 | def update_devices(self): 157 | self.get_controllers() 158 | 159 | 160 | e = WindowsUSBMap() 161 | -------------------------------------------------------------------------------- /base.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import copy 3 | import json 4 | import platform 5 | import plistlib 6 | import shutil 7 | import subprocess 8 | import textwrap 9 | from enum import Enum 10 | from operator import itemgetter 11 | from pathlib import Path 12 | 13 | from termcolor2 import c as color 14 | 15 | from Scripts import shared, utils 16 | 17 | 18 | 19 | class Colors(Enum): 20 | BLUE = "\u001b[36;1m" 21 | RED = "\u001b[31;1m" 22 | GREEN = "\u001b[32;1m" 23 | RESET = "\u001b[0m" 24 | 25 | 26 | def hexswap(input_hex: str): 27 | hex_pairs = [input_hex[i : i + 2] for i in range(0, len(input_hex), 2)] 28 | hex_rev = hex_pairs[::-1] 29 | hex_str = "".join(["".join(x) for x in hex_rev]) 30 | return hex_str.upper() 31 | 32 | 33 | def read_property(input_value: bytes, places: int): 34 | return binascii.b2a_hex(input_value).decode()[:-places] 35 | 36 | 37 | class BaseUSBMap: 38 | def __init__(self): 39 | self.utils = utils.Utils(f"USBToolBox {shared.VERSION}".strip()) 40 | self.controllers = None 41 | self.json_path = shared.current_dir / Path("usb.json") 42 | self.settings_path = shared.current_dir / Path("settings.json") 43 | 44 | self.settings = ( 45 | json.load(self.settings_path.open()) if self.settings_path.exists() else {"show_friendly_types": True, "use_native": False, "use_legacy_native": False, "add_comments_to_map": True, "auto_bind_companions": True} 46 | ) 47 | self.controllers_historical = json.load(self.json_path.open("r")) if self.json_path.exists() else None 48 | 49 | self.monu() 50 | 51 | @staticmethod 52 | def is_same_controller(controller_1, controller_2): 53 | common_keys: set[str] = set(i for i in controller_1["identifiers"] if controller_1["identifiers"][i]) & set(i for i in controller_2["identifiers"] if controller_2["identifiers"][i]) 54 | if not common_keys: 55 | # No way to tell 56 | return False 57 | for key in common_keys: 58 | if key in ["instance_id", "location_id"]: 59 | # With Thunderbolt controllers, on hotplugs the instance ID will change, but everything else will be the same. 60 | # On macOS, location IDs can change all the time. 61 | continue 62 | elif len(common_keys) == 1 and key in ["pci_revision"]: 63 | # Don't match solely by pci_revision. 64 | return False 65 | elif key == "location_paths": 66 | # Some firmwares have broken ACPI where two or more devices have the same parent and same address, and Windows does not always show all 67 | # Evident with Thunderbolt controllers 68 | # We will be satisified if they have at least 1 in common 69 | if not set(controller_1["identifiers"]["location_paths"]) & set(controller_2["identifiers"]["location_paths"]): 70 | return False 71 | else: 72 | if not (controller_1["identifiers"][key] is not None and controller_2["identifiers"][key] is not None and controller_1["identifiers"][key] == controller_2["identifiers"][key]): 73 | return False 74 | return True 75 | 76 | @staticmethod 77 | def get_controller_from_list(original, controller_list): 78 | for controller in controller_list: 79 | if BaseUSBMap.is_same_controller(original, controller): 80 | return controller 81 | return None 82 | 83 | @staticmethod 84 | def merge_properties(old, new): 85 | if not new: 86 | return old 87 | if not old: 88 | return new 89 | if isinstance(old, list): 90 | retval = list(old) 91 | retval.extend(set(new) - set(old)) 92 | return retval 93 | elif isinstance(old, dict): 94 | retval = dict(old) 95 | for key in new: 96 | retval[key] = BaseUSBMap.merge_properties(retval.get(key), new[key]) 97 | return retval 98 | else: 99 | return new 100 | 101 | @staticmethod 102 | def merge_controllers(base: list, new: list): 103 | for controller in new: 104 | base_controller = BaseUSBMap.get_controller_from_list(controller, base) 105 | if not base_controller: 106 | base.append(controller) 107 | # Don't need to merge properties because there's no base controller 108 | continue 109 | 110 | for key in set(controller.keys()) - set(["ports"]): # Leave merging ports to merge_ports 111 | base_controller[key] = BaseUSBMap.merge_properties(base_controller.get(key), controller[key]) 112 | 113 | BaseUSBMap.merge_ports(base, new) 114 | 115 | @staticmethod 116 | def merge_ports(base: list, new: list): 117 | for controller in new: 118 | base_controller = BaseUSBMap.get_controller_from_list(controller, base) 119 | for port in controller["ports"]: 120 | base_port = ([p for p in base_controller["ports"] if p["index"] == port["index"]] or [None])[0] 121 | if not base_port: 122 | base_controller["ports"].append(copy.deepcopy(port)) 123 | # Don't need to merge properties because there's no base port 124 | continue 125 | 126 | for key in set(port.keys()) - set(["devices"]): # Leave merging devices to merge_devices 127 | base_port[key] = BaseUSBMap.merge_properties(base_port.get(key), port[key]) 128 | base_controller["ports"].sort(key=itemgetter("index")) 129 | BaseUSBMap.merge_devices(base, new) 130 | 131 | @staticmethod 132 | def recursive_merge_devices(base: list, new: list): 133 | for i in new: 134 | if not i or i in base: 135 | continue 136 | elif isinstance(i, str): 137 | base.append(i) 138 | elif i.get("error"): 139 | continue 140 | elif i["name"] not in [hub.get("name") for hub in base if isinstance(hub, dict)]: 141 | base.append(i) 142 | else: 143 | old_hub = [hub for hub in base if isinstance(hub, dict) and hub.get("name") == i["name"]][0] 144 | BaseUSBMap.recursive_merge_devices(old_hub["devices"], i["devices"]) 145 | for i in list(base): 146 | if not i or i.get("error"): 147 | base.remove(i) 148 | 149 | @staticmethod 150 | def merge_devices(base: list, new: list): 151 | for controller in base: 152 | new_controller = BaseUSBMap.get_controller_from_list(controller, new) 153 | if new_controller: 154 | for port in controller["ports"]: 155 | BaseUSBMap.recursive_merge_devices(port["devices"], new_controller["ports"][controller["ports"].index(port)]["devices"]) 156 | 157 | def get_controllers(self): 158 | raise NotImplementedError 159 | 160 | def update_devices(self): 161 | raise NotImplementedError 162 | 163 | def controller_to_str(self, controller): 164 | return f"{controller['name']} | {shared.USBControllerTypes(controller['class'])}" 165 | 166 | def port_to_str(self, port): 167 | if port["type"] is not None: 168 | port_type = shared.USBPhysicalPortTypes(port["type"]) if self.settings["show_friendly_types"] else shared.USBPhysicalPortTypes(port["type"]).value 169 | elif port["guessed"] is not None: 170 | port_type = (str(shared.USBPhysicalPortTypes(port["guessed"])) if self.settings["show_friendly_types"] else str(shared.USBPhysicalPortTypes(port["guessed"]).value)) + " (guessed)" 171 | else: 172 | port_type = "Unknown" 173 | 174 | return f"{port['name']} | {shared.USBDeviceSpeeds(port['class'])} | " + (str(port_type) if self.settings["show_friendly_types"] else f"Type {port_type}") 175 | 176 | def print_controllers(self, controllers, colored=False): 177 | if not controllers: 178 | print("Empty.") 179 | return 180 | for controller in controllers: 181 | if colored: 182 | print(color(self.controller_to_str(controller) + f" | {len(controller['ports'])} ports")) 183 | else: 184 | print(self.controller_to_str(controller) + f" | {len(controller['ports'])} ports") 185 | for port in controller["ports"]: 186 | if not colored: 187 | print(" " + self.port_to_str(port)) 188 | elif port["devices"]: 189 | print(" " + color(self.port_to_str(port)).green.bold) 190 | elif ( 191 | self.get_controller_from_list(controller, self.controllers_historical) 192 | and [i for i in self.get_controller_from_list(controller, self.controllers_historical)["ports"] if i["index"] == port["index"]][0]["devices"] 193 | ): 194 | print(" " + color(self.port_to_str(port)).cyan.bold) 195 | else: 196 | print(" " + self.port_to_str(port)) 197 | 198 | if port["comment"]: 199 | print(" " + port["comment"]) 200 | for device in port["devices"]: 201 | self.print_devices(device) 202 | 203 | def print_devices(self, device, indentation=" "): 204 | if not device: 205 | device = "Enumerating..." 206 | if isinstance(device, str): 207 | print(f"{indentation}- {device}") 208 | elif device.get("error", False): 209 | print(f"{indentation}- {device['error'] if isinstance(device['error'], str) else 'Device connected to port errored.'} Please unplug or connect a different device.") 210 | else: 211 | print(f"{indentation}- {device['name'].strip()} - operating at {shared.USBDeviceSpeeds(device['speed'])}") 212 | for i in device["devices"]: 213 | self.print_devices(i, indentation + " ") 214 | 215 | def discover_ports(self): 216 | self.utils.head("Port Discovery") 217 | print() 218 | dont_refresh = False 219 | if not self.controllers: 220 | print("\nGetting controllers...") 221 | self.get_controllers() 222 | dont_refresh = True 223 | while True: 224 | # os.system("cls" if os.name == "nt" else "clear") 225 | self.utils.head("Port Discovery") 226 | print() 227 | if dont_refresh: 228 | dont_refresh = False 229 | else: 230 | self.update_devices() 231 | 232 | self.print_controllers(self.controllers, colored=True) 233 | 234 | self.dump_historical() 235 | print("\nB. Back\n") 236 | do_quit = self.utils.grab("Waiting 5 seconds: ", timeout=5) 237 | if str(do_quit).lower() == "b": 238 | break 239 | 240 | def print_historical(self): 241 | utils.TUIMenu("Print Historical (DEBUG)", "Select an option: ", in_between=lambda: self.print_controllers(self.controllers_historical), loop=True).start() 242 | 243 | def dump_historical(self): 244 | if self.controllers_historical: 245 | json.dump(self.controllers_historical, self.json_path.open("w"), indent=4, sort_keys=True) 246 | elif self.controllers_historical == []: 247 | self.remove_historical() 248 | 249 | def remove_historical(self): 250 | if self.controllers_historical or self.controllers_historical == []: 251 | self.controllers = None 252 | self.controllers_historical = None 253 | self.json_path.unlink() 254 | 255 | def dump_settings(self): 256 | json.dump(self.settings, self.settings_path.open("w"), indent=4, sort_keys=True) 257 | 258 | def on_quit(self): 259 | self.dump_historical() 260 | 261 | def print_types(self): 262 | in_between = [f"{i}: {i.value}" for i in shared.USBPhysicalPortTypes] + [ 263 | "", 264 | textwrap.dedent( 265 | """\ 266 | The difference between connector types 9 and 10 is if you reverse the plug and the devices are connected to the same ports as before, they have a switch (type 9). 267 | If not, and they are connected to different ports, they do not have a switch (type 10).""" 268 | ), 269 | "", 270 | "For more information and pictures, go to https://github.com/USBToolBox/tool/blob/master/TYPES.md.", 271 | ] 272 | utils.TUIMenu("USB Types", "Select an option: ", in_between=in_between).start() 273 | 274 | def get_companion_port(self, port): 275 | if not port.get("companion_info"): 276 | return None 277 | companion_info = port["companion_info"] 278 | if not companion_info["hub"] or not companion_info["port"]: 279 | return None 280 | hub = [i for i in self.controllers_historical if i.get("hub_name") == companion_info["hub"]] 281 | if hub: 282 | port = [i for i in hub[0]["ports"] if i["index"] == companion_info["port"]] 283 | if port: 284 | return port[0] 285 | return None 286 | 287 | def select_ports(self): 288 | if not self.controllers_historical: 289 | utils.TUIMenu("Select Ports and Build Kext", "Select an option: ", in_between=["No ports! Use the discovery mode."], loop=True).start() 290 | return 291 | 292 | selection_index = 1 293 | by_port = [] 294 | for controller in self.controllers_historical: 295 | controller["selected_count"] = 0 296 | for port in controller["ports"]: 297 | if "selected" not in port: 298 | port["selected"] = bool(port["devices"]) 299 | port["selected"] = port["selected"] or (bool(self.get_companion_port(port)["devices"]) if self.get_companion_port(port) else False) 300 | controller["selected_count"] += 1 if port["selected"] else 0 301 | port["selection_index"] = selection_index 302 | selection_index += 1 303 | by_port.append(port) 304 | 305 | while True: 306 | self.dump_historical() 307 | for controller in self.controllers_historical: 308 | controller["selected_count"] = sum(1 if port["selected"] else 0 for port in controller["ports"]) 309 | 310 | utils.header("Select Ports and Build Kext") 311 | print() 312 | for controller in self.controllers_historical: 313 | port_count_str = f"{controller['selected_count']}/{len(controller['ports'])}" 314 | port_count_str = color(port_count_str).red if controller["selected_count"] > 15 else color(port_count_str).green 315 | print(self.controller_to_str(controller) + f" | {port_count_str} ports") 316 | for port in controller["ports"]: 317 | port_info = f"[{'#' if port['selected'] else ' '}] {port['selection_index']}.{(len(str(selection_index)) - len(str(port['selection_index'])) + 1) * ' ' }" + self.port_to_str(port) 318 | companion = self.get_companion_port(port) 319 | if companion: 320 | port_info += f" | Companion to {companion['selection_index']}" 321 | if port["selected"]: 322 | print(color(port_info).green.bold) 323 | else: 324 | print(port_info) 325 | if port["comment"]: 326 | print( 327 | len(f"[{'#' if port['selected'] else ' '}] {port['selection_index']}.{(len(str(selection_index)) - len(str(port['selection_index'])) + 1) * ' ' }") * " " 328 | + color(port["comment"]).blue.bold 329 | ) 330 | for device in port["devices"]: 331 | self.print_devices(device, indentation=" " + len(str(selection_index)) * " " * 2) 332 | print() 333 | 334 | print(f"Binding companions is currently {color('on').green if self.settings['auto_bind_companions'] else color('off').red}.\n") 335 | 336 | output_kext = None 337 | if self.settings["use_native"] and self.settings["use_legacy_native"]: 338 | output_kext = "USBMapLegacy.kext" 339 | elif self.settings["use_native"]: 340 | output_kext = "USBMap.kext" 341 | else: 342 | output_kext = "UTBMap.kext (requires USBToolBox.kext)" 343 | 344 | print( 345 | textwrap.dedent( 346 | f"""\ 347 | K. Build {output_kext} 348 | A. Select All 349 | N. Select None 350 | P. Enable All Populated Ports 351 | D. Disable All Empty Ports 352 | T. Show Types 353 | 354 | B. Back 355 | 356 | - Select ports to toggle with comma-delimited lists (eg. 1,2,3,4,5) 357 | - Change types using this formula T:1,2,3,4,5:t where t is the type 358 | - Set custom names using this formula C:1:Name - Name = None to clear""" 359 | ) 360 | ) 361 | 362 | output = input("Select an option: ") 363 | if not output: 364 | continue 365 | elif output.upper() == "B": 366 | break 367 | elif output.upper() == "K": 368 | if not self.validate_selections(): 369 | continue 370 | self.build_kext() 371 | continue 372 | elif output.upper() in ("N", "A"): 373 | for port in by_port: 374 | port["selected"] = output.upper() == "A" 375 | elif output.upper() == "P": 376 | for port in by_port: 377 | if port["devices"] or (self.get_companion_port(port)["devices"] if self.get_companion_port(port) else False): 378 | port["selected"] = True 379 | elif output.upper() == "D": 380 | for port in by_port: 381 | if not port["devices"] and not (self.get_companion_port(port)["devices"] if self.get_companion_port(port) else False): 382 | port["selected"] = False 383 | elif output.upper() == "T": 384 | self.print_types() 385 | continue 386 | elif output[0].upper() == "T": 387 | # We should have a type 388 | if len(output.split(":")) != 3: 389 | continue 390 | try: 391 | port_nums, port_type = output.split(":")[1:] 392 | port_nums = port_nums.replace(" ", "").split(",") 393 | port_type = shared.USBPhysicalPortTypes(int(port_type)) 394 | 395 | for port_num in list(port_nums): 396 | if port_num not in port_nums: 397 | continue 398 | 399 | port_num = int(port_num) - 1 400 | 401 | if port_num not in range(len(by_port)): 402 | continue 403 | 404 | companion = self.get_companion_port(by_port[port_num]) 405 | if self.settings["auto_bind_companions"] and companion: 406 | companion["type"] = port_type 407 | if str(companion["selection_index"]) in port_nums: 408 | port_nums.remove(str(companion["selection_index"])) 409 | by_port[port_num]["type"] = port_type 410 | except ValueError: 411 | continue 412 | elif output[0].upper() == "C": 413 | # We should have a new name 414 | if len(output.split(":")) < 2: 415 | continue 416 | try: 417 | port_nums = output.split(":")[1].replace(" ", "").split(",") 418 | port_comment = output.split(":", 2)[2:] 419 | 420 | for port_num in list(port_nums): 421 | if port_num not in port_nums: 422 | continue 423 | 424 | port_num = int(port_num) - 1 425 | 426 | if port_num not in range(len(by_port)): 427 | continue 428 | 429 | by_port[port_num]["comment"] = port_comment[0] if port_comment else None 430 | except ValueError: 431 | continue 432 | else: 433 | try: 434 | port_nums = output.replace(" ", "").split(",") 435 | 436 | for port_num in list(port_nums): 437 | if port_num not in port_nums: 438 | continue 439 | 440 | port_num = int(port_num) - 1 441 | 442 | if port_num not in range(len(by_port)): 443 | continue 444 | 445 | companion = self.get_companion_port(by_port[port_num]) 446 | if self.settings["auto_bind_companions"] and companion: 447 | companion["selected"] = not by_port[port_num]["selected"] 448 | if str(companion["selection_index"]) in port_nums: 449 | port_nums.remove(str(companion["selection_index"])) 450 | by_port[port_num]["selected"] = not by_port[port_num]["selected"] 451 | except ValueError: 452 | continue 453 | 454 | def print_errors(self, errors): 455 | if not errors: 456 | return True 457 | utils.TUIMenu("Selection Validation", "Select an option: ", in_between=errors, loop=True).start() 458 | return False 459 | 460 | def validate_selections(self): 461 | errors = [] 462 | if not any(any(p["selected"] for p in c["ports"]) for c in self.controllers_historical): 463 | utils.TUIMenu("Selection Validation", "Select an option: ", in_between=["No ports are selected! Select some ports."], loop=True).start() 464 | return False 465 | 466 | for controller in self.controllers_historical: 467 | for port in controller["ports"]: 468 | if not port["selected"]: 469 | continue 470 | if port["type"] is None and port["guessed"] is None: 471 | errors.append(f"Port {port['selection_index']} is missing a connector type!") 472 | 473 | return self.print_errors(errors) 474 | 475 | def check_unique(self, identifier, predicate, controller): 476 | if predicate(controller): 477 | values = [identifier(c) for c in self.controllers_historical if predicate(c)] 478 | return values.count(identifier(controller)) == 1 479 | else: 480 | return False 481 | 482 | def choose_matching_key(self, controller): 483 | if "bus_number" in controller["identifiers"]: 484 | # M1 Macs 485 | return {"IOPropertyMatch": {"bus-number": binascii.a2b_hex(hexswap(hex(controller["identifiers"]["bus_number"])[2:].zfill(8)))}} 486 | 487 | elif not self.settings["use_native"] and self.check_unique(lambda c: c["identifiers"]["acpi_path"].rpartition(".")[2], lambda c: "acpi_path" in c["identifiers"], controller): 488 | # Unique ACPI name 489 | # Disable if using native because we don't know if it'll conflict 490 | # TODO: Check this maybe? 491 | shared.debug(f"Using ACPI path: {controller['identifiers']['acpi_path']}") 492 | return {"IONameMatch": controller["identifiers"]["acpi_path"].rpartition(".")[2]} 493 | 494 | elif "bdf" in controller["identifiers"]: 495 | # Use bus-device-function 496 | return {"IOPropertyMatch": {"pcidebug": ":".join([str(i) for i in controller["identifiers"]["bdf"]])}} 497 | 498 | elif self.check_unique(lambda c: c["identifiers"]["path"], lambda c: "path" in c["identifiers"], controller): 499 | # Use IORegistry path 500 | return {"IOPathMatch": controller["identifiers"]["path"]} 501 | 502 | elif self.check_unique(lambda c: c["identifiers"]["pci_id"], lambda c: "pci_id" in c["identifiers"], controller): 503 | # Use PCI ID 504 | pci_id: list[str] = controller["identifiers"]["pci_id"] 505 | return {"IOPCIPrimaryMatch": f"0x{pci_id[1]}{pci_id[0]}"} | ({"IOPCISecondaryMatch": f"0x{pci_id[3]}{pci_id[2]}"} if len(pci_id) > 2 else {}) 506 | 507 | else: 508 | raise RuntimeError("No matching key available") 509 | 510 | def build_kext(self): 511 | empty_controllers = [c for c in self.controllers_historical if not any(p["selected"] for p in c["ports"])] 512 | response = None 513 | if empty_controllers: 514 | empty_menu = utils.TUIMenu( 515 | "Selection Validation", 516 | "Select an option: ", 517 | in_between=["The following controllers have no enabled ports:", ""] 518 | + [controller["name"] for controller in empty_controllers] 519 | + ["Select whether to ignore these controllers and exclude them from the map, or disable all ports on these controllers."], 520 | add_quit=False, 521 | return_number=True, 522 | ) 523 | empty_menu.add_menu_option("Ignore", key="I") 524 | empty_menu.add_menu_option("Disable", key="D") 525 | response = empty_menu.start() 526 | 527 | model_identifier = None 528 | if self.settings["use_native"]: 529 | if platform.system() == "Darwin": 530 | model_identifier = plistlib.loads(subprocess.run("system_profiler -detailLevel mini -xml SPHardwareDataType".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.strip())[ 531 | 0 532 | ]["_items"][0]["machine_model"] 533 | else: 534 | model_menu = utils.TUIOnlyPrint( 535 | "Enter Model Identifier", 536 | "Enter the model identifier: ", 537 | [ 538 | "You are seeing this as you have selected to use native classes. Model identifier autodetection is unavailable as you are not on macOS.", 539 | "Please enter the model identifier of the target system below. You can find it in System Information or with 'system_profiler -detailLevel mini SPHardwareDataType'.", 540 | ], 541 | ).start() 542 | model_identifier = model_menu.strip() 543 | 544 | ignore = response == "I" 545 | 546 | template = plistlib.load((shared.resource_dir / Path("Info.plist")).open("rb")) 547 | 548 | menu = utils.TUIMenu("Building USBMap", "Select an option: ") 549 | menu.head() 550 | print("Generating Info.plist...") 551 | for controller in self.controllers_historical: 552 | if not any(i["selected"] for i in controller["ports"]) and ignore: 553 | continue 554 | 555 | # FIXME: ensure unique 556 | if controller["identifiers"].get("acpi_path"): 557 | if self.check_unique(lambda c: c["identifiers"]["acpi_path"].rpartition(".")[2], lambda c: "acpi_path" in c["identifiers"], controller): 558 | personality_name: str = controller["identifiers"]["acpi_path"].rpartition(".")[2] 559 | else: 560 | personality_name: str = controller["identifiers"]["acpi_path"][1:] # Strip leading \ 561 | elif controller["identifiers"].get("bdf"): 562 | personality_name: str = ":".join([str(i) for i in controller["identifiers"]["bdf"]]) 563 | else: 564 | personality_name: str = controller["name"] 565 | 566 | if self.settings["use_native"]: 567 | personality = { 568 | "CFBundleIdentifier": "com.apple.driver." + ("AppleUSBMergeNub" if self.settings["use_legacy_native"] else "AppleUSBHostMergeProperties"), 569 | "IOClass": ("AppleUSBMergeNub" if self.settings["use_legacy_native"] else "AppleUSBHostMergeProperties"), 570 | "IOProviderClass": "AppleUSBHostController", 571 | "IOParentMatch": self.choose_matching_key(controller), 572 | "model": model_identifier, 573 | } 574 | 575 | else: 576 | personality = { 577 | "CFBundleIdentifier": "com.dhinakg.USBToolBox.kext", 578 | "IOClass": "USBToolBox", 579 | "IOProviderClass": "IOPCIDevice", 580 | "IOMatchCategory": "USBToolBox", 581 | } | self.choose_matching_key( 582 | controller 583 | ) # type: ignore 584 | 585 | personality["IOProviderMergeProperties"] = {"ports": {}, "port-count": None} 586 | 587 | port_name_index = {} 588 | highest_index = 0 589 | 590 | for port in controller["ports"]: 591 | if not port["selected"]: 592 | continue 593 | 594 | if port["index"] > highest_index: 595 | highest_index = port["index"] 596 | 597 | if controller["class"] == shared.USBControllerTypes.XHCI and port["class"] == shared.USBDeviceSpeeds.SuperSpeed: 598 | prefix = "SS" 599 | elif controller["class"] == shared.USBControllerTypes.XHCI and port["class"] == shared.USBDeviceSpeeds.HighSpeed: 600 | prefix = "HS" 601 | else: 602 | prefix = "PRT" 603 | 604 | port_index = port_name_index.setdefault(prefix, 1) 605 | port_name = prefix + str(port_index).zfill(4 - len(prefix)) 606 | port_name_index[prefix] += 1 607 | 608 | personality["IOProviderMergeProperties"]["ports"][port_name] = { 609 | "port": binascii.a2b_hex(hexswap(hex(port["index"])[2:].zfill(8))), 610 | "UsbConnector": port["type"] or port["guessed"], 611 | } 612 | 613 | if self.settings["add_comments_to_map"] and port["comment"]: 614 | personality["IOProviderMergeProperties"]["ports"][port_name]["#comment"] = port["comment"] 615 | 616 | personality["IOProviderMergeProperties"]["port-count"] = binascii.a2b_hex(hexswap(hex(highest_index)[2:].zfill(8))) 617 | 618 | template["IOKitPersonalities"][personality_name] = personality 619 | 620 | if not self.settings["use_native"]: 621 | template["OSBundleLibraries"] = {"com.dhinakg.USBToolBox.kext": "1.0.0"} 622 | 623 | output_kext = None 624 | if self.settings["use_native"] and self.settings["use_legacy_native"]: 625 | output_kext = "USBMapLegacy.kext" 626 | elif self.settings["use_native"]: 627 | output_kext = "USBMap.kext" 628 | else: 629 | output_kext = "UTBMap.kext" 630 | 631 | write_path = shared.current_dir / Path(output_kext) 632 | 633 | if write_path.exists(): 634 | print("Removing existing kext...") 635 | shutil.rmtree(write_path) 636 | 637 | print("Writing kext and Info.plist...") 638 | (write_path / Path("Contents")).mkdir(parents=True) 639 | plistlib.dump(template, (write_path / Path("Contents/Info.plist")).open("wb"), sort_keys=True) 640 | print(f"Done. Saved to {write_path.resolve()}.\n") 641 | menu.print_options() 642 | 643 | menu.select() 644 | return True 645 | 646 | def change_settings(self): 647 | def functionify(func): 648 | return lambda *args, **kwargs: lambda: func(*args, **kwargs) 649 | 650 | @functionify 651 | def color_status(name, variable): 652 | return f"{name}: {color('Enabled').green if self.settings[variable] else color('Disabled').red}" 653 | 654 | @functionify 655 | def toggle_setting(variable): 656 | self.settings[variable] = not self.settings[variable] 657 | 658 | def combination(name, variable): 659 | return color_status(name, variable), toggle_setting(variable) 660 | 661 | menu = utils.TUIMenu("Change Settings", "Toggle a setting: ", loop=True) 662 | for i in [ 663 | ["T", *combination("Show Friendly Types", "show_friendly_types"), ["Show friendly types (ie. 'USB 3 Type A') instead of numbers."]], 664 | ["N", *combination("Use Native Classes", "use_native"), ["Use native Apple classes (AppleUSBHostMergeProperties) instead of the USBToolBox kext."]], 665 | ["L", *combination("Use Legacy Native Classes (requires Use Native Classes)", "use_legacy_native"), ["Use AppleUSBMergeNub instead of AppleUSBHostMergeProperties, for legacy macOS."]], 666 | ["A", *combination("Add Comments to Map", "add_comments_to_map"), ["Add port comments inside the map."]], 667 | [ 668 | "C", 669 | *combination("Bind Companions", "auto_bind_companions"), 670 | ["Tie companion ports together. If one companion is enabled/disable/port type changed, the other companion will also be affected."], 671 | ], 672 | ]: 673 | menu.add_menu_option(name=i[1], function=i[2], key=i[0], description=i[3] if len(i) == 4 else None) 674 | 675 | menu.start() 676 | self.dump_settings() 677 | 678 | def monu(self): 679 | response = None 680 | while not (response and response == utils.TUIMenu.EXIT_MENU): 681 | in_between = [("Saved Data: {}" + Colors.RESET.value).format(Colors.GREEN.value + "Loaded" if self.json_path.exists() else (Colors.RED.value + "None"))] 682 | 683 | menu = utils.TUIMenu(f"USBToolBox {shared.VERSION}", "Select an option: ", in_between=in_between, top_level=True) 684 | 685 | menu_options = [ 686 | # ["H", "Print Historical", self.print_historical], 687 | ["D", "Discover Ports", self.discover_ports], 688 | ["S", "Select Ports and Build Kext", self.select_ports], 689 | ["C", "Change Settings", self.change_settings], 690 | ] 691 | if self.json_path.exists(): 692 | menu_options.insert(0, ["P", "Delete Saved USB Data", self.remove_historical]) 693 | for i in menu_options: 694 | menu.add_menu_option(i[1], None, i[2], i[0]) 695 | 696 | response = menu.start() 697 | self.on_quit() 698 | self.utils.custom_quit() 699 | -------------------------------------------------------------------------------- /debug_dump.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | import sys 4 | import time 5 | import tkinter as tk 6 | import tkinter.filedialog as filedialog 7 | from enum import Enum 8 | from pathlib import Path 9 | 10 | import win32com.client 11 | import wmi 12 | 13 | from Scripts import shared 14 | 15 | c = wmi.WMI() 16 | 17 | 18 | class PnpDeviceProperties(Enum): 19 | ACPI_PATH = "DEVPKEY_Device_BiosDeviceName" 20 | DRIVER_KEY = "DEVPKEY_Device_Driver" 21 | LOCATION_PATHS = "DEVPKEY_Device_LocationPaths" 22 | FRIENDLY_NAME = "DEVPKEY_Device_FriendlyName" 23 | BUS_DEVICE_FUNCTION = "DEVPKEY_Device_LocationInfo" 24 | BUS_REPORTED_NAME = "DEVPKEY_Device_BusReportedDeviceDesc" 25 | BUS_RELATIONS = "DEVPKEY_Device_BusRelations" 26 | 27 | 28 | def get_property_from_wmi(instance_id, prop: PnpDeviceProperties): 29 | try: 30 | return c.query(f"SELECT * FROM Win32_PnPEntity WHERE PNPDeviceID = '{instance_id}'")[0].GetDeviceProperties([prop.value])[0][0].Data 31 | except Exception: 32 | return None 33 | 34 | 35 | def build_dict(instance): 36 | pnp = c.query(f"SELECT * FROM Win32_PnPEntity WHERE PNPDeviceID = '{instance}'")[0].GetDeviceProperties()[0] 37 | d = {} 38 | for i in pnp: 39 | if isinstance(i.Data, win32com.client.CDispatch): 40 | # Probably useless 41 | d[i.KeyName] = "Removed for garbage" 42 | else: 43 | d[i.KeyName] = i.Data 44 | return d 45 | 46 | 47 | controllers = [] 48 | 49 | all_devices = {} 50 | start = time.time() 51 | 52 | 53 | def recurse_bus(instance_id): 54 | device = build_dict(instance_id) 55 | all_devices[instance_id] = device 56 | 57 | device["devices"] = {} 58 | for f in device.get(PnpDeviceProperties.BUS_RELATIONS.value, []): 59 | if f.startswith("USB"): 60 | device["devices"][f] = recurse_bus(f) 61 | 62 | return device 63 | 64 | 65 | for e in c.Win32_USBController(): 66 | controller = build_dict(e.PNPDeviceID) 67 | all_devices[e.PNPDeviceID] = dict(controller) 68 | 69 | if controller.get(PnpDeviceProperties.BUS_RELATIONS.value, None): 70 | controller["root_hub"] = recurse_bus(controller[PnpDeviceProperties.BUS_RELATIONS.value][0]) 71 | 72 | controllers.append(controller) 73 | end = time.time() 74 | wmi_time = end - start 75 | 76 | 77 | usbdump_path = Path("resources/usbdump.exe") 78 | 79 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): 80 | usbdump_path = Path(sys._MEIPASS) / usbdump_path 81 | 82 | start = time.time() 83 | output = subprocess.run(usbdump_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.decode() 84 | end = time.time() 85 | usbdump_time = end - start 86 | 87 | try: 88 | usbdump = json.loads(output) 89 | except json.JSONDecodeError: 90 | usbdump = {"error": "usbdump.exe returned an invalid JSON", "raw": output} 91 | 92 | temp_tk_root = tk.Tk() 93 | temp_tk_root.wm_withdraw() 94 | save_path = filedialog.asksaveasfilename(title="Save debugging information", defaultextension=".json", filetypes=[("json", "*.json")]) 95 | temp_tk_root.destroy() 96 | 97 | if not save_path: 98 | sys.exit(1) 99 | else: 100 | save_path = Path(save_path) 101 | 102 | json.dump({"info": {"version": shared.VERSION, "build": shared.BUILD, "wmi_time": wmi_time, "usbdump_time": usbdump_time}, "wmitest": controllers, "usbdump": usbdump}, save_path.open("w"), sort_keys=True) 103 | input(f"Please upload {save_path}.\nPress [Enter] to exit") 104 | -------------------------------------------------------------------------------- /macOS.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import copy 3 | from enum import Enum 4 | from operator import itemgetter 5 | 6 | from Scripts import iokit, shared 7 | from base import BaseUSBMap 8 | 9 | # from gui import * 10 | 11 | 12 | class IOEntryProperties(Enum): 13 | NAME = "IORegistryEntryName" 14 | CHILDREN = "IORegistryEntryChildren" 15 | CLASS = "IOClass" 16 | OBJECT_CLASS = "IOObjectClass" 17 | ADDRESS = "IOChildIndex" 18 | 19 | 20 | def hexswap(input_hex: str) -> str: 21 | hex_pairs = [input_hex[i : i + 2] for i in range(0, len(input_hex), 2)] 22 | hex_rev = hex_pairs[::-1] 23 | hex_str = "".join(["".join(x) for x in hex_rev]) 24 | return hex_str.upper() 25 | 26 | 27 | def read_property(input_value: bytes, places: int) -> str: 28 | return binascii.hexlify(input_value).decode()[:-places] 29 | 30 | 31 | class macOSUSBMap(BaseUSBMap): 32 | @staticmethod 33 | def port_class_to_type(speed): 34 | if "AppleUSB30XHCIPort" in speed: 35 | return shared.USBDeviceSpeeds.SuperSpeed 36 | elif set(["AppleUSB20XHCIPort", "AppleUSBEHCIPort"]) & set(speed): 37 | return shared.USBDeviceSpeeds.HighSpeed 38 | elif set(["AppleUSBOHCIPort", "AppleUSBUHCIPort"]) & set(speed): 39 | return shared.USBDeviceSpeeds.FullSpeed 40 | else: 41 | shared.debug(f"Unknown port type for {speed}!") 42 | return shared.USBDeviceSpeeds.Unknown 43 | 44 | @staticmethod 45 | def controller_class_to_type(parent_props, controller_props, inheritance): 46 | # Check class code 47 | if "class-code" in parent_props: 48 | return shared.USBControllerTypes(parent_props["class-code"][0]) 49 | # Check class type 50 | elif "AppleUSBXHCI" in inheritance: 51 | return shared.USBControllerTypes.XHCI 52 | elif "AppleUSBEHCI" in inheritance: 53 | return shared.USBControllerTypes.EHCI 54 | elif "AppleUSBOHCI" in inheritance: 55 | return shared.USBControllerTypes.OHCI 56 | elif "AppleUSBUHCI" in inheritance: 57 | return shared.USBControllerTypes.UHCI 58 | else: 59 | shared.debug(f"Unknown controller type for class code {read_property(parent_props['class-code'], 2) if 'class-code' in parent_props else 'none'}, inheritance {inheritance}!") 60 | return shared.USBControllerTypes.Unknown 61 | 62 | def get_controllers(self): 63 | controllers = [] 64 | 65 | err, controller_iterator = iokit.IOServiceGetMatchingServices(iokit.kIOMasterPortDefault, iokit.IOServiceMatching("AppleUSBHostController".encode()), None) 66 | for controller_instance in iokit.ioiterator_to_list(controller_iterator): 67 | controller_properties: dict = iokit.corefoundation_to_native(iokit.IORegistryEntryCreateCFProperties(controller_instance, None, iokit.kCFAllocatorDefault, iokit.kNilOptions)[1]) # type: ignore 68 | 69 | err, parent_device = iokit.IORegistryEntryGetParentEntry(controller_instance, "IOService".encode(), None) 70 | parent_properties: dict = iokit.corefoundation_to_native(iokit.IORegistryEntryCreateCFProperties(parent_device, None, iokit.kCFAllocatorDefault, iokit.kNilOptions)[1]) # type: ignore 71 | 72 | controller = { 73 | "name": iokit.io_name_t_to_str(iokit.IORegistryEntryGetName(parent_device, None)[1]), 74 | # "class": macOSUSBMap.port_class_to_type(iokit.get_class_inheritance(controller_instance)), 75 | "identifiers": {"location_id": controller_properties["locationID"], "path": iokit.IORegistryEntryCopyPath(controller_instance, "IOService".encode())}, 76 | "ports": [], 77 | } 78 | if set(["vendor-id", "device-id"]) & set(parent_properties.keys()): 79 | controller["identifiers"]["pci_id"] = [hexswap(read_property(parent_properties[i], 4)).lower() for i in ["vendor-id", "device-id"]] 80 | 81 | if set(["subsystem-vendor-id", "subsystem-id"]) & set(parent_properties.keys()): 82 | controller["identifiers"]["pci_id"] += [hexswap(read_property(parent_properties[i], 4)).lower() for i in ["subsystem-vendor-id", "subsystem-id"]] 83 | 84 | if "revision-id" in parent_properties: 85 | controller["identifiers"]["pci_revision"] = int(hexswap(read_property(parent_properties.get("revision-id", b""), 6)), 16) 86 | 87 | if "acpi-path" in parent_properties: 88 | controller["identifiers"]["acpi_path"] = "\\" + ".".join([i.split("@")[0] for i in parent_properties["acpi-path"].split("/")[1:]]) 89 | 90 | if "pcidebug" in parent_properties: 91 | controller["identifiers"]["bdf"] = [int(i) for i in parent_properties["pcidebug"].split(":", 3)[:3]] 92 | 93 | if "bus-number" in parent_properties: 94 | # TODO: Properly figure out max value 95 | controller["identifiers"]["bus_number"] = int(hexswap(read_property(parent_properties["bus-number"], 6)), 16) 96 | 97 | controller["class"] = self.controller_class_to_type(parent_properties, controller_properties, iokit.get_class_inheritance(controller_instance)) 98 | 99 | err, port_iterator = iokit.IORegistryEntryGetChildIterator(controller_instance, "IOService".encode(), None) 100 | for port in iokit.ioiterator_to_list(port_iterator): 101 | port_properties: dict = iokit.corefoundation_to_native(iokit.IORegistryEntryCreateCFProperties(port, None, iokit.kCFAllocatorDefault, iokit.kNilOptions)[1]) # type: ignore 102 | controller["ports"].append( 103 | { 104 | "name": iokit.io_name_t_to_str(iokit.IORegistryEntryGetName(port, None)[1]), 105 | "comment": None, 106 | "index": int(read_property(port_properties["port"], 6), 16), 107 | "class": macOSUSBMap.port_class_to_type(iokit.get_class_inheritance(port)), 108 | "type": None, 109 | "guessed": shared.USBPhysicalPortTypes.USB3TypeC_WithSwitch 110 | if set(iokit.get_class_inheritance(port)) & set(["AppleUSB20XHCITypeCPort", "AppleUSB30XHCITypeCPort"]) 111 | else port_properties.get("UsbConnector"), 112 | "location_id": port_properties["locationID"], 113 | "devices": [], 114 | } 115 | ) 116 | iokit.IOObjectRelease(port) 117 | controllers.append(controller) 118 | 119 | iokit.IOObjectRelease(controller_instance) 120 | iokit.IOObjectRelease(parent_device) 121 | self.controllers = controllers 122 | if not self.controllers_historical: 123 | self.controllers_historical = copy.deepcopy(self.controllers) 124 | else: 125 | self.merge_controllers(self.controllers_historical, self.controllers) 126 | self.update_devices() 127 | 128 | def recurse_devices(self, iterator): 129 | props = [] 130 | iokit.IORegistryIteratorEnterEntry(iterator) 131 | device = iokit.IOIteratorNext(iterator) 132 | while device: 133 | props.append( 134 | { 135 | "name": iokit.io_name_t_to_str(iokit.IORegistryEntryGetName(device, None)[1]), 136 | "port": iokit.IORegistryEntryCreateCFProperty(device, "PortNum", iokit.kCFAllocatorDefault, iokit.kNilOptions), 137 | "location_id": iokit.IORegistryEntryCreateCFProperty(device, "locationID", iokit.kCFAllocatorDefault, iokit.kNilOptions), 138 | "speed": shared.USBDeviceSpeeds(iokit.IORegistryEntryCreateCFProperty(device, "Device Speed", iokit.kCFAllocatorDefault, iokit.kNilOptions)), # type: ignore 139 | "devices": self.recurse_devices(iterator), 140 | } 141 | ) 142 | iokit.IOObjectRelease(device) 143 | device = iokit.IOIteratorNext(iterator) 144 | iokit.IORegistryIteratorExitEntry(iterator) 145 | props.sort(key=itemgetter("name")) 146 | return props 147 | 148 | def update_devices(self): 149 | # Reset devices 150 | for controller in self.controllers: 151 | for port in controller["ports"]: 152 | port["devices"] = [] 153 | 154 | err, usb_plane_iterator = iokit.IORegistryCreateIterator(iokit.kIOMasterPortDefault, "IOUSB".encode(), 0, None) 155 | controller_instance = iokit.IOIteratorNext(usb_plane_iterator) 156 | while controller_instance: 157 | location_id = iokit.corefoundation_to_native(iokit.IORegistryEntryCreateCFProperty(controller_instance, "locationID", iokit.kCFAllocatorDefault, iokit.kNilOptions)) 158 | 159 | controller = [i for i in self.controllers if i["identifiers"]["location_id"] == location_id][0] 160 | # This is gonna be a controller 161 | 162 | devices = self.recurse_devices(usb_plane_iterator) 163 | 164 | for port in controller["ports"]: 165 | port["devices"] = [i for i in devices if i["port"] == port["index"] or i["location_id"] == port["location_id"]] 166 | 167 | iokit.IOObjectRelease(controller_instance) 168 | controller_instance = iokit.IOIteratorNext(usb_plane_iterator) 169 | iokit.IOObjectRelease(usb_plane_iterator) 170 | 171 | self.merge_devices(self.controllers_historical, self.controllers) 172 | 173 | 174 | e = macOSUSBMap() 175 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Base requirements 2 | ansiescapes @ git+https://github.com/embedded-dev/ansiescapes.git 3 | termcolor2 4 | 5 | # Windows requirements 6 | wmi; platform_system == "Windows" 7 | 8 | # macOS requirements 9 | pyobjc; platform_system == "Darwin" 10 | 11 | # Debugging 12 | # debugpy 13 | -------------------------------------------------------------------------------- /resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleGetInfoString 8 | v1.1 9 | CFBundleIdentifier 10 | com.dhinakg.USBToolBox.map 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | UTBMap 15 | CFBundlePackageType 16 | KEXT 17 | CFBundleShortVersionString 18 | 1.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.1 23 | IOKitPersonalities 24 | 25 | 26 | OSBundleRequired 27 | Root 28 | 29 | 30 | -------------------------------------------------------------------------------- /resources/usbdump.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/USBToolBox/tool/0c6823a2c643a4488888938451194723dc9ae29a/resources/usbdump.exe -------------------------------------------------------------------------------- /spec/Windows.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.append(str(Path.cwd())) 7 | 8 | from spec.insert_version import write_version 9 | write_version() 10 | 11 | block_cipher = None 12 | 13 | 14 | a = Analysis(['../Windows.py'], 15 | pathex=['Scripts'], 16 | binaries=[], 17 | datas=[('../Scripts', 'Scripts'), ('../resources', 'resources')], 18 | hiddenimports=['msvcrt', 'win32com', 'win32api', 'wmi'], 19 | hookspath=[], 20 | runtime_hooks=[], 21 | excludes=[], 22 | win_no_prefer_redirects=False, 23 | win_private_assemblies=False, 24 | cipher=block_cipher, 25 | noarchive=False) 26 | pyz = PYZ(a.pure, a.zipped_data, 27 | cipher=block_cipher) 28 | exe = EXE(pyz, 29 | a.scripts, 30 | a.binaries, 31 | a.zipfiles, 32 | a.datas, 33 | [], 34 | name='Windows', 35 | debug=False, 36 | bootloader_ignore_signals=False, 37 | strip=False, 38 | upx=True, 39 | upx_exclude=[], 40 | runtime_tmpdir=None, 41 | console=True) 42 | -------------------------------------------------------------------------------- /spec/Windows_dir.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.append(str(Path.cwd())) 7 | 8 | from spec.insert_version import write_version 9 | write_version() 10 | 11 | block_cipher = None 12 | 13 | 14 | a = Analysis(['../Windows.py'], 15 | pathex=['Scripts'], 16 | binaries=[], 17 | datas=[('../Scripts', 'Scripts'), ('../resources', 'resources')], 18 | hiddenimports=['msvcrt', 'win32com', 'win32api', 'wmi'], 19 | hookspath=[], 20 | runtime_hooks=[], 21 | excludes=[], 22 | win_no_prefer_redirects=False, 23 | win_private_assemblies=False, 24 | cipher=block_cipher, 25 | noarchive=False) 26 | pyz = PYZ(a.pure, a.zipped_data, 27 | cipher=block_cipher) 28 | exe = EXE(pyz, 29 | a.scripts, 30 | [], 31 | exclude_binaries=True, 32 | name='Windows', 33 | debug=False, 34 | bootloader_ignore_signals=False, 35 | strip=False, 36 | upx=True, 37 | console=True ) 38 | coll = COLLECT(exe, 39 | a.binaries, 40 | a.zipfiles, 41 | a.datas, 42 | strip=False, 43 | upx=True, 44 | upx_exclude=[], 45 | name='Windows') 46 | -------------------------------------------------------------------------------- /spec/debug_dump.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.append(str(Path.cwd())) 7 | 8 | from spec.insert_version import write_version 9 | write_version() 10 | 11 | block_cipher = None 12 | 13 | 14 | a = Analysis(['../debug_dump.py'], 15 | pathex=[], 16 | binaries=[], 17 | datas=[('../resources', 'resources')], 18 | hiddenimports=[], 19 | hookspath=[], 20 | runtime_hooks=[], 21 | excludes=[], 22 | win_no_prefer_redirects=False, 23 | win_private_assemblies=False, 24 | cipher=block_cipher, 25 | noarchive=False) 26 | pyz = PYZ(a.pure, a.zipped_data, 27 | cipher=block_cipher) 28 | exe = EXE(pyz, 29 | a.scripts, 30 | a.binaries, 31 | a.zipfiles, 32 | a.datas, 33 | [], 34 | name='debug_dump', 35 | debug=False, 36 | bootloader_ignore_signals=False, 37 | strip=False, 38 | upx=True, 39 | upx_exclude=[], 40 | runtime_tmpdir=None, 41 | console=True ) 42 | -------------------------------------------------------------------------------- /spec/debug_dump_dir.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.append(str(Path.cwd())) 7 | 8 | from spec.insert_version import write_version 9 | write_version() 10 | 11 | block_cipher = None 12 | 13 | 14 | a = Analysis(['../debug_dump.py'], 15 | pathex=[], 16 | binaries=[], 17 | datas=[('../resources', 'resources')], 18 | hiddenimports=[], 19 | hookspath=[], 20 | runtime_hooks=[], 21 | excludes=[], 22 | win_no_prefer_redirects=False, 23 | win_private_assemblies=False, 24 | cipher=block_cipher, 25 | noarchive=False) 26 | pyz = PYZ(a.pure, a.zipped_data, 27 | cipher=block_cipher) 28 | exe = EXE(pyz, 29 | a.scripts, 30 | [], 31 | exclude_binaries=True, 32 | name='debug_dump', 33 | debug=False, 34 | bootloader_ignore_signals=False, 35 | strip=False, 36 | upx=True, 37 | console=True ) 38 | coll = COLLECT(exe, 39 | a.binaries, 40 | a.zipfiles, 41 | a.datas, 42 | strip=False, 43 | upx=True, 44 | upx_exclude=[], 45 | name='debug_dump') 46 | -------------------------------------------------------------------------------- /spec/insert_version.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from pathlib import Path 3 | 4 | 5 | def write_version(): 6 | try: 7 | result = subprocess.run("git describe --tags --always".split(), stdout=subprocess.PIPE) 8 | BUILD = result.stdout.decode().strip() 9 | except: 10 | BUILD = None 11 | 12 | Path("Scripts/_build.py").write_text(f"BUILD = {repr(BUILD)}") 13 | -------------------------------------------------------------------------------- /spec/macOS.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | sys.path.append(str(Path.cwd())) 7 | 8 | from spec.insert_version import write_version 9 | write_version() 10 | 11 | block_cipher = None 12 | 13 | 14 | a = Analysis(['../macOS.py'], 15 | pathex=['Scripts'], 16 | binaries=[], 17 | datas=[('../Scripts', 'Scripts'), ('../resources', 'resources')], 18 | hiddenimports=[], 19 | hookspath=[], 20 | runtime_hooks=[], 21 | excludes=[], 22 | win_no_prefer_redirects=False, 23 | win_private_assemblies=False, 24 | cipher=block_cipher, 25 | noarchive=False) 26 | pyz = PYZ(a.pure, a.zipped_data, 27 | cipher=block_cipher) 28 | exe = EXE(pyz, 29 | a.scripts, 30 | a.binaries, 31 | a.zipfiles, 32 | a.datas, 33 | [], 34 | name='macOS', 35 | debug=False, 36 | bootloader_ignore_signals=False, 37 | strip=False, 38 | upx=True, 39 | upx_exclude=[], 40 | runtime_tmpdir=None, 41 | console=True ) 42 | --------------------------------------------------------------------------------