├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── construct_editor ├── __init__.py ├── core │ ├── __init__.py │ ├── callbacks.py │ ├── commands.py │ ├── construct_editor.py │ ├── context_menu.py │ ├── custom.py │ ├── entries.py │ ├── model.py │ └── preprocessor.py ├── gallery │ ├── __init__.py │ ├── example_cmd_resp.py │ ├── example_ipstack.py │ ├── example_pe32coff.py │ ├── test_aligned.py │ ├── test_array.py │ ├── test_bits_swapped_bitwise.py │ ├── test_bitwise.py │ ├── test_bytes_greedybytes.py │ ├── test_checksum.py │ ├── test_compressed.py │ ├── test_computed.py │ ├── test_const.py │ ├── test_dataclass_bit_struct.py │ ├── test_dataclass_struct.py │ ├── test_enum.py │ ├── test_fixedsized.py │ ├── test_flag.py │ ├── test_flagsenum.py │ ├── test_focusedseq.py │ ├── test_greedyrange.py │ ├── test_ifthenelse.py │ ├── test_ifthenelse_nested_switch.py │ ├── test_nullstripped.py │ ├── test_nullterminated.py │ ├── test_padded.py │ ├── test_padded_string.py │ ├── test_pass.py │ ├── test_pointer_peek_seek_tell.py │ ├── test_renamed.py │ ├── test_select.py │ ├── test_select_complex.py │ ├── test_stringencodded.py │ ├── test_switch.py │ ├── test_switch_dataclass.py │ ├── test_tenum.py │ ├── test_tflagsenum.py │ └── test_timestamp.py ├── main.py ├── py.typed ├── version.py └── wx_widgets │ ├── __init__.py │ ├── wx_construct_editor.py │ ├── wx_construct_hex_editor.py │ ├── wx_context_menu.py │ ├── wx_exception_dialog.py │ ├── wx_hex_editor.py │ ├── wx_obj_view.py │ └── wx_python_code_editor.py ├── doc ├── example.png ├── preview.gif ├── screenshot_scripts │ ├── _create_all.py │ ├── _helper.py │ ├── bitwise.py │ └── example.py └── screenshots │ ├── bitwise_win32.png │ └── example_win32.png ├── pyrightconfig.json ├── requirements.txt └── setup.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.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 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Intellij, pyCharm, etc. 132 | .idea/ 133 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": false 14 | }, 15 | { 16 | "name": "Python: main.py", 17 | "type": "python", 18 | "request": "launch", 19 | "program": "${workspaceFolder}/construct_editor/main.py", 20 | "console": "integratedTerminal", 21 | "justMyCode": false 22 | }, 23 | { 24 | "name": "Debug Tests", 25 | "type": "python", 26 | "request": "test", 27 | "console": "integratedTerminal", 28 | "justMyCode": false 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Python Settings //////////////////////////////////////////////////////// 3 | "python.pythonPath": ".venv\\Scripts\\python.exe", 4 | "python.languageServer": "Pylance", 5 | // 6 | // Python Testing (pytest) //////////////////////////////////////////////// 7 | "python.testing.unittestEnabled": false, 8 | "python.testing.pytestEnabled": true, 9 | // 10 | // Python Formatting ////////////////////////////////////////////////////// 11 | "python.formatting.provider": "black", 12 | // 13 | // Python Sort Imports //////////////////////////////////////////////////// 14 | "python.sortImports.args": [ 15 | "--profile=black", 16 | ], 17 | // 18 | // Python Static Analysis (Pylance) /////////////////////////////////////// 19 | "python.analysis.autoImportCompletions": false, 20 | "python.analysis.typeCheckingMode": "basic", 21 | "python.analysis.diagnosticMode": "workspace", 22 | "python.analysis.diagnosticSeverityOverrides": { 23 | "reportPrivateUsage": "information", 24 | "reportUntypedNamedTuple": "information", 25 | }, 26 | // 27 | // Misc /////////////////////////////////////////////////////////////////// 28 | "files.exclude": { 29 | ".venv/": true, 30 | "**/__pycache__": true, 31 | "*.egg-info": true, 32 | ".pytest_cache": true 33 | } 34 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.5] - 2023-07-24 4 | - updated construct-typing dependency to v0.6.* 5 | 6 | ------------------------------------------------------------------------------- 7 | 8 | ## [0.1.4] - 2023-06-28 9 | Enhanced ConstructEditor: 10 | - Fixed wrong enum flags name in Python 3.11 11 | 12 | ------------------------------------------------------------------------------- 13 | ## [0.1.3] - 2023-06-01 14 | Enhanced ConstructEditor: 15 | - Fixed wrong enum name in Python 3.11 (#30) 16 | 17 | ------------------------------------------------------------------------------- 18 | ## [0.1.2] - 2023-05-09 19 | - updated construct-typing dependency to v0.5.6 20 | 21 | ------------------------------------------------------------------------------- 22 | ## [0.1.1] - 2023-01-09 23 | Enhanced ConstructEditor: 24 | - `cs.Bytes`, `cs.GreedyBytes`, `cs.Array`, `cs.GreedyRange` now use the length of the object instead of evaluating the length using the saved context. The problem was, that in some cases the context is dyamically created and modified while parsing. So it is not save to use it afterwarts. 25 | - `cs.Struct._subcons` and `cs.FocusedSeq._subcons` are now correctly updated and accessable 26 | - updated construct-typing dependency to v0.5.5 27 | 28 | ------------------------------------------------------------------------------- 29 | ## [0.1.0] - 2023-01-03 30 | Complete refactoring of the code, so that core components of the construct-editor are seperated vom GUI components. That makes it theoretically possible to add multiple GUI frameworks in the future. Besides this the following notable enhancements are implemented: 31 | 32 | Enhanced ConstructEditor: 33 | - Any keypress of an printable key will start editing an item. No ENTER or double click is reqired any more. 34 | - Protected entries (starting with _) are not visible in list view if "hide protected" is activated. (#13) 35 | - Implemented checkbox for `cs.Flag` 36 | - Fixed bug with PaddedString (#14) 37 | - Added module "construct_editor.core.custom" for easier addition of custom constructs. 38 | - Show arrays appropriately (use .[number]. instead of .number.) (#18) 39 | - Show full tooltip of the "name" column, which when fields are too long is shown with ellipses. (#18) 40 | - Implemented "Copy" / Ctrl+C (#18) 41 | - Added "Copy path to clipboard" button in the context menu (#18) 42 | - Fixed a bug, when a struct has multiple times the same `Enum` construct. Then the metadata (eg. byte position) of the last parsed enum value is used for all enum values. 43 | - Added exception dialog to show the complete parse/build exception. 44 | 45 | Enhanced HexEditor: 46 | - fix crash when selecting or extending selection before the beginning of the hex editor using the shift LEFT and UP arrow keys (#20) 47 | - Add DELETE key to remove a single byte (#20) 48 | - Add INSERT key to add a single byte (#20) 49 | - Add BACK and DELETE key in hex cell editor (#20) 50 | - Add basic arrow keys in hex cell editor doing the same as Escape (#20) 51 | - Optimize paste from clipboard (#17) 52 | 53 | ------------------------------------------------------------------------------- 54 | ## [0.0.19] - 2022-09-07 55 | Enhanced ConstructHexEditor: 56 | - Fixed a bug which leads to an "Fatal Python error" as of wxPython 4.2. 57 | 58 | ------------------------------------------------------------------------------- 59 | ## [0.0.18] - 2022-09-07 60 | General: 61 | - Package is now also compatible with Python 3.9 and 3.10 62 | 63 | Enhanced ConstructHexEditor: 64 | - Fixed a bug which leads to an exception as of wxPython 4.2. 65 | 66 | ------------------------------------------------------------------------------- 67 | ## [0.0.17] - 2022-08-03 68 | Enhanced ConstructEditor: 69 | - Fixed a bug in conditional constructs (IfThenElse/Switch/FocusedSeq/Select) 70 | 71 | ------------------------------------------------------------------------------- 72 | ## [0.0.16] - 2022-05-11 73 | Enhanced ConstructEditor: 74 | - Added support for 75 | - `cs.NullStripped` 76 | - `cs.NullTerminated` 77 | - `cs.StringEncoded` 78 | - `cs.BitsSwapped` 79 | - `cs.Const` 80 | - `cs.FocusedSeq` 81 | - `cs.Select` 82 | - Fixed issue: When the users has opened an DVC Editor and clicks somewhere else, the Object-Editor is now closed correcty. 83 | - Fixed issue: Removed recursion bug inside conditional constructs (`cs.IfThenElse`, `cs.Switch`, ...). 84 | 85 | Enhanced HexEditor: 86 | - Added thousands separator for byte/bit positions. 87 | - Display `x Bits` insted of `x Bytes` if we are inside a `cs.Bitwise`. 88 | - Fixed issue: When 0 or a multiple of 16 Bytes are shown in the hex editor, then there is no new line for adding data. This is now fixed, and an new line is added. 89 | 90 | ------------------------------------------------------------------------------- 91 | ## [0.0.15] - 2022-01-20 92 | Enhanced ConstructHexEditor: 93 | - added support for `cs.Default`: when right clicking an object with default value the option "Set to default" is availabe 94 | - fixed issue: when sub-hex-panels are created (eg. for bitwiese data), the data is not visible in the root-hex-panel when selecting an item. 95 | - fixed issue: the GUI can completely crash, when the building/parsing takes its time and the user slowly double clicks somewhere else in the DataViewControl 96 | 97 | ------------------------------------------------------------------------------- 98 | ## [0.0.14] - 2021-07-29 99 | Enhanced ConstructHexEditor: 100 | - fixed issue: When the another binary data was set, then the old HexEditors were still displayed. 101 | 102 | ------------------------------------------------------------------------------- 103 | ## [0.0.13] - 2021-07-29 104 | Enhanced ConstructHexEditor: 105 | - fixed stream issues: When selecting the root element, not all data was marked in the HexEditor. 106 | 107 | ------------------------------------------------------------------------------- 108 | ## [0.0.12] - 2021-07-25 109 | Enhanced ConstructEditor: 110 | - added `cs.Compressed`, `cs.Prefixed` 111 | - show Infos about the construct object, when hovering over the type in the DVC 112 | - changed "->" to "." as path seperator 113 | 114 | Enhanced HexEditor: 115 | - added read_only mode 116 | 117 | Enhanced ConstructHexEditor: 118 | - Show multiple HexEditor's when nested streams are used. This is a usefull feature, if the construct switches from byte- to bit-level to see the single bits. Or when using `cs.Tunnel` for `cs.Compressed` or encryption. So you can see also the uncompressed/decrypted data. 119 | 120 | ------------------------------------------------------------------------------- 121 | ## [0.0.11] - 2021-07-20 122 | Enhanced ConstructEditor: 123 | - added `cs.Checksum` 124 | 125 | ------------------------------------------------------------------------------- 126 | ## [0.0.10] - 2021-07-16 127 | Enhanced ConstructEditor: 128 | - the enter key (in the number block) can be used to finish editing a construct entry 129 | - added `cs.FixedSized` #8 130 | - changed `cs.Timestamp` a little bit #10 131 | - fixed wrong Byte-Position in constructs with nested streams #9 132 | 133 | ------------------------------------------------------------------------------- 134 | ## [0.0.9] - 2021-06-27 135 | Enhanced ConstructEditor: 136 | - multiple speed optimizations for large Arrays 137 | - added support for `cs.Flag` 138 | - replaces "Expand/Collapse all" with "Expand/Collapse children" in the Context menu, which are now only available for `cs.Struct` and `cs.Array` 139 | 140 | ------------------------------------------------------------------------------- 141 | ## [0.0.8] - 2021-06-17 142 | Enhanced ConstructEditor: 143 | - fixing bug, when root construct is a GreedyRange and the "List View" is used 144 | - name of root construct is added to the path 145 | 146 | ------------------------------------------------------------------------------- 147 | ## [0.0.7] - 2021-06-16 148 | Enhanced ConstructEditor: 149 | - added support for `cs.GreedyBytes` 150 | - `cs.Bytes` and `cs.GreedyBytes` are now changeable in the ConstructEditor 151 | - when an object in the ConstructEditor is changed and the binary data is recalculated, then the binary data is parsed again, to recalculate all values that may depend on the changed object (eg. for `cs.Peek`) 152 | - fixed dependency error. wxPython>=4.1.1 is needed. see #2 153 | - `cs.GreedyRange` now also supports "List View" 154 | - some other small bugfixes 155 | 156 | ------------------------------------------------------------------------------- 157 | ## [0.0.6] - 2021-05-24 158 | - changed dependencies to `construct==2.10.67` and `construct-typing==0.5.0` 159 | - changed class names according to `construct-typing==0.5.0` 160 | 161 | ------------------------------------------------------------------------------- 162 | ## [0.0.5] - 2021-05-19 163 | Enhanced ConstructEditor: 164 | - added `cs.Padded`, `cs.Padding` and `cs.Aligned` 165 | - added "ASCII View" Button to ContextMenu for `cs.Bytes` 166 | - fixed bug, when the root construct has a name 167 | - fixed bug, when an exception was thrown, while trying to edit a read-only field 168 | 169 | ------------------------------------------------------------------------------- 170 | ## [0.0.4] - 2021-05-03 171 | Enhanced ConstructEditor: 172 | - edit entrys directly in the DataViewCtrl 173 | - show docs directly in the DataViewCtrl as Tooltip 174 | - show byte position, offset and the path to the selected object in a StatusBar 175 | - added undo/redo 176 | - added a ListView for `cs.Array` 177 | - added shortcuts for copy/paste/expand/collapse 178 | - added the possibility to add custom `cs.Adapter` constructs to the GUI 179 | 180 | Enhanced ConstructHexEditor: 181 | - HexEditor Panel can be collapsed to make the ConstructEditor larger 182 | 183 | ------------------------------------------------------------------------------- 184 | ## [0.0.3] - 2021-04-14 185 | reduce wxPython dependency to >=4.1.0 186 | 187 | ------------------------------------------------------------------------------- 188 | ## [0.0.2] - 2021-04-11 189 | Enhanced HexEditor: 190 | - corrected selection of a byte sequence 191 | - added cut/copy/paste of byte sequences 192 | - added undo/redo 193 | - added status bar to show the size and the current selection 194 | 195 | ------------------------------------------------------------------------------- 196 | ## [0.0.1] - 2021-04-05 197 | Initial Version -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 timrid 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 | # Construct Editor 2 | This package provides a GUI (based on wxPython) for 'construct', which is a powerful declarative and symmetrical parser and builder for binary data. It can either be used standalone or embedded as a widget in another application. 3 | 4 | 5 | ![Preview](https://raw.githubusercontent.com/timrid/construct-editor/main/doc/preview.gif) 6 | 7 | 8 | Features: 9 | - show documentation as tooltip 10 | - different editors for: 11 | - Integer values 12 | - Enum values 13 | - FlagsEnum values 14 | - DateTime values 15 | - undo/redo in HexEditor and in ConstructEditor 16 | - extensible for custom constructs 17 | 18 | ## Installation 19 | The preferred way to installation is via PyPI: 20 | ``` 21 | pip install construct-editor 22 | ``` 23 | 24 | ## Getting started (Standalone) 25 | To start the standalone version, just execute the following in the command line: 26 | ``` 27 | construct-editor 28 | ``` 29 | 30 | ## Getting started (as Widgets) 31 | This is a simple example 32 | ```python 33 | import wx 34 | import construct as cs 35 | from construct_editor.wx_widgets import WxConstructHexEditor 36 | 37 | constr = cs.Struct( 38 | "a" / cs.Int16sb, 39 | "b" / cs.Int16sb, 40 | ) 41 | b = bytes([0x12, 0x34, 0x56, 0x78]) 42 | 43 | app = wx.App(False) 44 | frame = wx.Frame(None, title="Construct Hex Editor", size=(1000, 200)) 45 | editor_panel = WxConstructHexEditor(frame, construct=constr, binary=b) 46 | editor_panel.construct_editor.expand_all() 47 | frame.Show(True) 48 | app.MainLoop() 49 | ``` 50 | 51 | This snipped generates a GUI like this: 52 | 53 | ![Screenshot of the example](https://raw.githubusercontent.com/timrid/construct-editor/main/doc/example.png) 54 | 55 | 56 | ## Widgets 57 | ### ConstructHexEditor 58 | This is the main widget ot this library. It offers a look at the raw binary data and also at the parsed structure. 59 | It offers a way to modify the raw binary data, which is then automaticly converted to the structed view. It also supports to modify the structed data and build the binary data from it. 60 | 61 | 62 | ### ConstructEditor 63 | This is just the right side of the `ConstructHexEditor`, but can be used also used as standalone widget. It provides: 64 | - Viewing the structure of a construct (without binary data) 65 | - Parsing binary data according to the construct 66 | 67 | 68 | ### HexEditor 69 | Just the left side of the `ConstructHexEditor`, but can be used also used as standalone widget. It provides: 70 | - Viewing Bytes in a Hexadecimal form 71 | - Callbacks when some value changed 72 | - Changeable format via `TableFormat` 73 | -------------------------------------------------------------------------------- /construct_editor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrid/construct-editor/b4c63dcea1a057cbcc7106b2d58c8bb4d8503e3b/construct_editor/__init__.py -------------------------------------------------------------------------------- /construct_editor/core/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /construct_editor/core/callbacks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Callable, Generic, TypeVar 3 | 4 | from typing_extensions import ParamSpec 5 | 6 | T = TypeVar("T") 7 | P = ParamSpec("P") 8 | 9 | 10 | class CallbackList(Generic[P]): 11 | def __init__(self): 12 | self._callbacks = [] 13 | 14 | def append(self, callback: Callable[P, None]): 15 | """ 16 | Add new callback function to the list (ignroe duplicates) 17 | """ 18 | if callback not in self._callbacks: 19 | self._callbacks.append(callback) 20 | 21 | def remove(self, callback: Callable[P, None]): 22 | """ 23 | Remove callback function from the list. 24 | """ 25 | self._callbacks.remove(callback) 26 | 27 | def clear(self): 28 | """ 29 | Clear the complete list. 30 | """ 31 | self._callbacks.clear() 32 | 33 | def fire(self, *args: P.args, **kwargs: P.kwargs): 34 | """ 35 | Call all callback functions, with the given parameters. 36 | """ 37 | for callback in self._callbacks: 38 | callback(*args, **kwargs) 39 | -------------------------------------------------------------------------------- /construct_editor/core/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import abc 3 | import typing as t 4 | 5 | 6 | class Command: 7 | def __init__(self, can_undo: bool, name: str) -> None: 8 | self.can_undo: bool = can_undo 9 | self.name: str = name 10 | 11 | @abc.abstractmethod 12 | def do(self) -> bool: 13 | ... 14 | 15 | @abc.abstractmethod 16 | def undo(self) -> bool: 17 | ... 18 | 19 | 20 | class CommandProcessor: 21 | def __init__(self, max_commands: int) -> None: 22 | self._max_commands = max_commands 23 | 24 | self._history: t.List[Command] = [] 25 | self._current_command_idx: t.Optional[int] = None 26 | 27 | def can_undo(self) -> bool: 28 | """ 29 | Returns true if the currently-active command can be undone, false 30 | otherwise. 31 | """ 32 | current_command = self.get_current_command() 33 | if current_command is None: 34 | return False 35 | else: 36 | return current_command.can_undo 37 | 38 | def can_redo(self) -> bool: 39 | """ 40 | Returns true if the currently-active command can be redone, false 41 | otherwise. 42 | """ 43 | next_command = self.get_next_command() 44 | if next_command is None: 45 | return False 46 | else: 47 | return True 48 | 49 | def redo(self) -> bool: 50 | """ 51 | Executes (redoes) the current command (the command that has just been 52 | undone if any). 53 | """ 54 | next_command = self.get_next_command() 55 | 56 | # no command to redo in the history 57 | if next_command is None: 58 | return False 59 | 60 | if next_command.do() is False: 61 | return False 62 | 63 | self._increment_current_command() 64 | 65 | return True 66 | 67 | def undo(self) -> bool: 68 | """ 69 | Undoes the last command executed. 70 | """ 71 | current_command = self.get_current_command() 72 | 73 | # no command available 74 | if current_command is None: 75 | return False 76 | 77 | # command cant be undone 78 | if not current_command.can_undo: 79 | return False 80 | 81 | # error on undo 82 | if current_command.undo() is False: 83 | return False 84 | 85 | # set current command to previous command 86 | self._decrement_current_command() 87 | 88 | return True 89 | 90 | def submit(self, command: Command) -> None: 91 | """ 92 | Submits a new command to the command processor. 93 | 94 | The command processor calls Command.do to execute the command; if it 95 | succeeds, the command is stored in the history list, and the associated 96 | edit menu (if any) updated appropriately. If it fails, the command is 97 | deleted immediately. 98 | """ 99 | command.do() 100 | self.store(command) 101 | 102 | def store(self, command: Command) -> None: 103 | """ 104 | Just store the command without executing it. 105 | 106 | Any command that has been undone will be chopped off the history list. 107 | """ 108 | # We must chop off the current 'branch', so that 109 | # we're at the end of the command list. 110 | if self._current_command_idx is None: 111 | self.clear_commands() 112 | else: 113 | self._history = self._history[: self._current_command_idx + 1] 114 | 115 | # Limit history length. Remove fist commands from history 116 | # if an overflow occures 117 | if len(self._history) >= self._max_commands: 118 | if self._current_command_idx is None: 119 | raise ValueError("history and current_command_idx are out of sync") 120 | self._history.pop(0) 121 | self._current_command_idx = self._current_command_idx - 1 122 | 123 | # append command to history 124 | self._current_command_idx = len(self._history) 125 | self._history.append(command) 126 | 127 | def clear_commands(self): 128 | """ 129 | Deletes all commands in the list and sets the current command pointer to None. 130 | """ 131 | self._history.clear() 132 | self._current_command_idx = None 133 | 134 | def get_current_command(self) -> t.Optional[Command]: 135 | """ 136 | Returns the current command. 137 | """ 138 | if self._current_command_idx is None: 139 | return None 140 | 141 | return self._history[self._current_command_idx] 142 | 143 | def get_next_command(self) -> t.Optional[Command]: 144 | """ 145 | Returns the next command. 146 | """ 147 | if self._current_command_idx is None: 148 | next_command_idx = 0 149 | else: 150 | next_command_idx = self._current_command_idx + 1 151 | 152 | if next_command_idx >= len(self._history): 153 | return None 154 | 155 | return self._history[next_command_idx] 156 | 157 | def _decrement_current_command(self) -> None: 158 | if self._current_command_idx is None: 159 | return 160 | if self._current_command_idx > 0: 161 | self._current_command_idx -= 1 162 | else: 163 | self._current_command_idx = None 164 | 165 | def _increment_current_command(self) -> None: 166 | if self._current_command_idx is None: 167 | next_command_idx = 0 168 | else: 169 | next_command_idx = self._current_command_idx + 1 170 | 171 | if next_command_idx >= len(self._history): 172 | return 173 | 174 | self._current_command_idx = next_command_idx 175 | -------------------------------------------------------------------------------- /construct_editor/core/construct_editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import abc 3 | import typing as t 4 | 5 | import construct as cs 6 | 7 | import construct_editor.core.entries as entries 8 | from construct_editor.core.callbacks import CallbackList 9 | from construct_editor.core.model import ConstructEditorColumn, ConstructEditorModel 10 | from construct_editor.core.preprocessor import include_metadata 11 | 12 | 13 | class ConstructEditor: 14 | def __init__(self, construct: cs.Construct, model: ConstructEditorModel): 15 | self._model = model 16 | 17 | self.change_construct(construct) 18 | 19 | self.on_entry_selected: CallbackList[ 20 | ["entries.EntryConstruct"] 21 | ] = CallbackList() 22 | self.on_root_obj_changed: CallbackList[[t.Any]] = CallbackList() 23 | 24 | @abc.abstractmethod 25 | def reload(self): 26 | """ 27 | Reload the ConstructEditor, while remaining expaned elements and selection. 28 | 29 | This has to be implemented by the derived class. 30 | """ 31 | 32 | @abc.abstractmethod 33 | def show_parse_error_message(self, msg: t.Optional[str], ex: t.Optional[Exception]): 34 | """ 35 | Show an parse error message to the user. 36 | 37 | This has to be implemented by the derived class. 38 | """ 39 | 40 | @abc.abstractmethod 41 | def show_build_error_message(self, msg: t.Optional[str], ex: t.Optional[Exception]): 42 | """ 43 | Show an build error message to the user. 44 | 45 | This has to be implemented by the derived class. 46 | """ 47 | 48 | @abc.abstractmethod 49 | def show_status(self, path_info: str, bytes_info: str): 50 | """ 51 | Show an status to the user. 52 | 53 | This has to be implemented by the derived class. 54 | """ 55 | 56 | @abc.abstractmethod 57 | def get_selected_entry(self) -> "entries.EntryConstruct": 58 | """ 59 | Get the currently selected entry (or None if nothing is selected). 60 | 61 | This has to be implemented by the derived class. 62 | """ 63 | 64 | @abc.abstractmethod 65 | def select_entry(self, entry: "entries.EntryConstruct") -> None: 66 | """ 67 | Select an entry programmatically. 68 | 69 | This has to be implemented by the derived class. 70 | """ 71 | 72 | @abc.abstractmethod 73 | def _put_to_clipboard(self, txt: str): 74 | """ 75 | Put text to the clipboard. 76 | 77 | This has to be implemented by the derived class. 78 | """ 79 | 80 | @abc.abstractmethod 81 | def _get_from_clipboard(self): 82 | """ 83 | Get text from the clipboard. 84 | 85 | This has to be implemented by the derived class. 86 | """ 87 | 88 | def copy_entry_value_to_clipboard(self, entry: "entries.EntryConstruct"): 89 | """ 90 | Copy the value of the entry to the clipboard. 91 | """ 92 | copy_txt = entry.obj_str 93 | self._put_to_clipboard(copy_txt) 94 | 95 | def copy_entry_path_to_clipboard(self, entry: "entries.EntryConstruct"): 96 | """ 97 | Copy the path of the entry to the clipboard. 98 | """ 99 | copy_txt = entries.create_path_str(entry.path) 100 | self._put_to_clipboard(copy_txt) 101 | 102 | def paste_entry_value_from_clipboard(self, entry: "entries.EntryConstruct"): 103 | """ 104 | Paste the value of the entry from the clipboard. 105 | """ 106 | txt = self._get_from_clipboard() 107 | if txt is None: 108 | return 109 | 110 | # TODO: This does not work correctly, because the clipboard only saves 111 | # strings. So here is a string to entry.obj conversation needed, which is 112 | # not so easy. 113 | # self.model.set_value(txt, entry, ConstructEditorColumn.Value) 114 | # self.on_root_obj_changed.fire(self.root_obj) 115 | 116 | def change_construct(self, constr: cs.Construct) -> None: 117 | """ 118 | Change the construct format, that is used for building/parsing. 119 | """ 120 | # reset error messages 121 | self.show_build_error_message(None, None) 122 | self.show_parse_error_message(None, None) 123 | 124 | # add root name, is none is available 125 | if constr.name is None: 126 | constr = "root" / constr 127 | 128 | # modify the copied construct, so that each item also includes metadata for the GUI 129 | self._construct = include_metadata(constr) 130 | 131 | # create entry from the construct 132 | self._model.root_entry = entries.create_entry_from_construct( 133 | self._model, None, self._construct, None, "" 134 | ) 135 | 136 | self._model.list_viewed_entries.clear() 137 | 138 | def change_hide_protected(self, hide_protected: bool) -> None: 139 | """ 140 | Show/hide protected entries. 141 | A protected member starts with an undescore (_) 142 | """ 143 | self._model.hide_protected = hide_protected 144 | self.reload() 145 | 146 | def parse(self, binary: bytes, **contextkw: t.Any): 147 | """ 148 | Parse binary data to struct. 149 | """ 150 | try: 151 | self._model.root_obj = self._construct.parse(binary, **contextkw) 152 | self.show_parse_error_message(None, None) 153 | except Exception as e: 154 | self.show_parse_error_message( 155 | f"Error while parsing binary data: {type(e).__name__}\n{str(e)}", e 156 | ) 157 | self._model.root_obj = None 158 | 159 | # clear all commands, when new data is set from external 160 | self._model.command_processor.clear_commands() 161 | self.reload() 162 | 163 | def build(self, **contextkw: t.Any) -> bytes: 164 | """ 165 | Build binary data from struct. 166 | """ 167 | try: 168 | binary = self._construct.build(self._model.root_obj, **contextkw) 169 | self.show_build_error_message(None, None) 170 | except Exception as e: 171 | self.show_build_error_message( 172 | f"Error while building binary data: {type(e).__name__}\n{str(e)}", e 173 | ) 174 | raise e 175 | 176 | # Parse the build binary, so that constructs that parses from nothing 177 | # are shown correctly (eg. cs.Peek, cs.Pointer). 178 | self.parse(binary, **contextkw) 179 | 180 | return binary 181 | 182 | @abc.abstractmethod 183 | def expand_entry(self, entry: "entries.EntryConstruct"): 184 | """ 185 | Expand an entry. 186 | 187 | This has to be implemented by the derived class. 188 | """ 189 | 190 | def expand_children(self, entry: "entries.EntryConstruct"): 191 | """ 192 | Expand all children of an entry recursively including the entry itself. 193 | """ 194 | self.expand_entry(entry) 195 | 196 | subentries = entry.subentries 197 | if subentries is not None: 198 | for sub_entry in subentries: 199 | self.expand_children(sub_entry) 200 | 201 | def expand_all(self): 202 | """ 203 | Expand all entries. 204 | """ 205 | if self._model.root_entry is not None: 206 | self.expand_children(self._model.root_entry) 207 | 208 | def expand_level(self, level: int): 209 | """ 210 | Expand all Entries to Level ... (0=root level) 211 | """ 212 | 213 | def dvc_expand(entry: "entries.EntryConstruct", current_level: int): 214 | subentries = entry.subentries 215 | if subentries is None: 216 | return 217 | 218 | self.expand_entry(entry) 219 | 220 | if current_level < level: 221 | for sub_entry in subentries: 222 | dvc_expand(sub_entry, current_level + 1) 223 | 224 | if self._model.root_entry: 225 | dvc_expand(self._model.root_entry, 1) 226 | 227 | @abc.abstractmethod 228 | def collapse_entry(self, entry: "entries.EntryConstruct"): 229 | """ 230 | Collapse an entry. 231 | 232 | This has to be implemented by the derived class. 233 | """ 234 | 235 | def collapse_children(self, entry: "entries.EntryConstruct"): 236 | """ 237 | Collapse all children of an entry recursively including the entry itself. 238 | """ 239 | subentries = entry.subentries 240 | if subentries is not None: 241 | for sub_entry in subentries: 242 | self.collapse_children(sub_entry) 243 | 244 | self.collapse_entry(entry) 245 | 246 | def collapse_all(self): 247 | """ 248 | Collapse all entries. 249 | """ 250 | if self._model.root_entry: 251 | self.collapse_children(self._model.root_entry) 252 | 253 | def restore_expansion_from_model(self, entry: "entries.EntryConstruct"): 254 | """ 255 | Restore the expansion state from the model recursively. 256 | 257 | While reloading the view in some frameworks (eg. wxPython) the expansion 258 | state of the entries get lost. Because auf this, the expansion state is 259 | saved in the model data itself an with this method the expansion state 260 | of the model can be restored. 261 | """ 262 | visible_entry = entry.get_visible_row_entry() 263 | if visible_entry is None: 264 | return 265 | 266 | if visible_entry.row_expanded is True: 267 | self.expand_entry(visible_entry) 268 | else: 269 | self.collapse_entry(visible_entry) 270 | 271 | subentries = self._model.get_children(entry) 272 | if len(subentries) == 0: 273 | return 274 | 275 | for subentry in subentries: 276 | self.restore_expansion_from_model(subentry) 277 | 278 | def enable_list_view(self, entry: "entries.EntryConstruct"): 279 | """ 280 | Enable the list view for an entry. 281 | """ 282 | if self.is_list_view_enabled(entry): 283 | return 284 | 285 | self._model.list_viewed_entries.append(entry) 286 | self.reload() 287 | 288 | # collapse all subentries without the entry itself, 289 | # so that the list can be seen better. 290 | self.collapse_children(entry) 291 | self.expand_entry(entry) 292 | 293 | def disable_list_view(self, entry: "entries.EntryConstruct"): 294 | """ 295 | Disable the list view for an entry. 296 | """ 297 | if not self.is_list_view_enabled(entry): 298 | return 299 | 300 | self._model.list_viewed_entries.remove(entry) 301 | self.reload() 302 | 303 | def is_list_view_enabled(self, entry: "entries.EntryConstruct") -> bool: 304 | """ 305 | Check if an entry is shown in a list view. 306 | """ 307 | if entry in self._model.list_viewed_entries: 308 | return True 309 | return False 310 | 311 | @property 312 | def construct(self) -> cs.Construct: 313 | """ 314 | Construct that is used for displaying. 315 | """ 316 | return self._construct 317 | 318 | @construct.setter 319 | def construct(self, constr: cs.Construct): 320 | self.change_construct(constr) 321 | 322 | @property 323 | def hide_protected(self) -> bool: 324 | """ 325 | Hide protected members. 326 | A protected member starts with an undescore (_) 327 | """ 328 | return self._model.hide_protected 329 | 330 | @hide_protected.setter 331 | def hide_protected(self, hide_protected: bool): 332 | self.change_hide_protected(hide_protected) 333 | 334 | @property 335 | def root_obj(self) -> t.Any: 336 | """ 337 | Root object that is displayed 338 | """ 339 | return self._model.root_obj 340 | 341 | @property 342 | def model(self) -> ConstructEditorModel: 343 | """ 344 | Model with the displayed data. 345 | """ 346 | return self._model 347 | 348 | # Internals ############################################################### 349 | def _get_list_viewed_column_count(self): 350 | """ 351 | Get the count of all list viewed columns. 352 | """ 353 | column_count = 0 354 | for list_viewed_entry in self._model.list_viewed_entries: 355 | if list_viewed_entry.subentries is None: 356 | continue 357 | for subentry in list_viewed_entry.subentries: 358 | flat_list = self._model.create_flat_subentry_list(subentry) 359 | column_count = max(column_count, len(flat_list)) 360 | return column_count 361 | 362 | def _get_list_viewed_column_names( 363 | self, selected_entry: "entries.EntryConstruct" 364 | ) -> t.List[str]: 365 | """ 366 | Get the names of all list viewed columns. 367 | 368 | The selected entry is used to get the column names. If there are more 369 | columns than subentries in the selected entry or no selected entry is 370 | passed, the column number is used as label. 371 | """ 372 | column_names: t.List[str] = [] 373 | flat_list = self._model.create_flat_subentry_list(selected_entry) 374 | for entry in flat_list: 375 | column_path = entry.path 376 | column_path = column_path[ 377 | len(selected_entry.path) : 378 | ] # remove the path from the selected_entry 379 | column_names.append(entries.create_path_str(column_path)) 380 | return column_names 381 | 382 | def _refresh_status_bar(self, entry: t.Optional["entries.EntryConstruct"]) -> None: 383 | if entry is None: 384 | self.show_status("", "") 385 | return 386 | 387 | path_info = entries.create_path_str(entry.path) 388 | bytes_info = "" 389 | stream_infos = entry.get_stream_infos() 390 | 391 | # Show byte range only when no nested streams are used 392 | if len(stream_infos) == 1: 393 | byte_range = stream_infos[0].byte_range 394 | start = byte_range[0] 395 | end = byte_range[1] - 1 396 | size = end - start + 1 397 | if size > 0: 398 | bytes_info = f"Bytes: {start:n}-{end:n} ({size:n})" 399 | self.show_status(path_info, bytes_info) 400 | -------------------------------------------------------------------------------- /construct_editor/core/context_menu.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import abc 3 | import dataclasses 4 | import typing as t 5 | 6 | import construct_editor.core.construct_editor as construct_editor 7 | import construct_editor.core.entries as entries 8 | from construct_editor.core.model import ConstructEditorModel, IntegerFormat 9 | 10 | COPY_LABEL = "Copy" 11 | PASTE_LABEL = "Paste" 12 | UNDO_LABEL = "Undo" 13 | REDO_LABEL = "Redo" 14 | 15 | INTFORMAT_DEC_LABEL = "Dec" 16 | INTFORMAT_HEX_LABEL = "Hex" 17 | 18 | # ##################################################################################################################### 19 | # Context Menu ######################################################################################################## 20 | # ##################################################################################################################### 21 | @dataclasses.dataclass 22 | class SeparatorMenuItem: 23 | pass 24 | 25 | 26 | @dataclasses.dataclass 27 | class ButtonMenuItem: 28 | label: str 29 | shortcut: t.Optional[str] 30 | enabled: bool 31 | callback: t.Callable[[], None] 32 | 33 | 34 | @dataclasses.dataclass 35 | class CheckboxMenuItem: 36 | label: str 37 | shortcut: t.Optional[str] 38 | enabled: bool 39 | checked: bool 40 | callback: t.Callable[[bool], None] 41 | 42 | 43 | @dataclasses.dataclass 44 | class RadioGroupMenuItems: 45 | labels: t.List[str] 46 | checked_label: str 47 | callback: t.Callable[[str], None] 48 | 49 | 50 | @dataclasses.dataclass 51 | class SubmenuItem: 52 | label: str 53 | subitems: t.List["MenuItem"] 54 | 55 | 56 | MenuItem = t.Union[ 57 | ButtonMenuItem, 58 | SeparatorMenuItem, 59 | CheckboxMenuItem, 60 | RadioGroupMenuItems, 61 | SubmenuItem, 62 | ] 63 | 64 | 65 | class ContextMenu: 66 | def __init__( 67 | self, 68 | parent: "construct_editor.ConstructEditor", 69 | model: "ConstructEditorModel", 70 | entry: t.Optional["entries.EntryConstruct"], 71 | ): 72 | self.parent = parent 73 | self.model = model 74 | self.entry = entry 75 | 76 | self._init_default_menu() 77 | 78 | def _init_default_menu(self): 79 | self._init_copy_paste() 80 | self.add_menu_item(SeparatorMenuItem()) 81 | self._init_undo_redo() 82 | self.add_menu_item(SeparatorMenuItem()) 83 | self._init_hide_protected() 84 | self.add_menu_item(SeparatorMenuItem()) 85 | self._init_intformat() 86 | if len(self.model.list_viewed_entries) > 0: 87 | self.add_menu_item(SeparatorMenuItem()) 88 | self._init_list_viewed_entries() 89 | 90 | if self.entry is not None: 91 | self.entry.modify_context_menu(self) 92 | 93 | def _init_copy_paste(self): 94 | self.add_menu_item( 95 | ButtonMenuItem( 96 | COPY_LABEL, 97 | "Ctrl+C", 98 | True, 99 | self.on_copy_value_to_clipboard, 100 | ) 101 | ) 102 | self.add_menu_item( 103 | ButtonMenuItem( 104 | "Copy path to clipboard", 105 | "", 106 | True, 107 | self.on_copy_path_to_clipboard, 108 | ) 109 | ) 110 | self.add_menu_item( 111 | ButtonMenuItem( 112 | PASTE_LABEL, 113 | "Ctrl+V", 114 | False, 115 | self.on_paste, 116 | ) 117 | ) 118 | 119 | def _init_undo_redo(self): 120 | self.add_menu_item( 121 | ButtonMenuItem( 122 | UNDO_LABEL, 123 | "Ctrl+Z", 124 | self.model.command_processor.can_undo(), 125 | self.on_undo, 126 | ) 127 | ) 128 | self.add_menu_item( 129 | ButtonMenuItem( 130 | REDO_LABEL, 131 | "Ctrl+Y", 132 | self.model.command_processor.can_redo(), 133 | self.on_redo, 134 | ) 135 | ) 136 | 137 | def _init_hide_protected(self): 138 | self.add_menu_item( 139 | CheckboxMenuItem( 140 | "Hide Protected", 141 | None, 142 | True, 143 | self.parent.hide_protected, 144 | self.on_hide_protected, 145 | ) 146 | ) 147 | 148 | def _init_intformat(self): 149 | if self.model.integer_format is IntegerFormat.Hex: 150 | checked_label = INTFORMAT_HEX_LABEL 151 | else: 152 | checked_label = INTFORMAT_DEC_LABEL 153 | self.add_menu_item( 154 | RadioGroupMenuItems( 155 | [INTFORMAT_DEC_LABEL, INTFORMAT_HEX_LABEL], 156 | checked_label, 157 | self.on_intformat, 158 | ) 159 | ) 160 | 161 | def _init_list_viewed_entries(self): 162 | submenu = SubmenuItem("List Viewed Items", []) 163 | for e in self.model.list_viewed_entries: 164 | 165 | def on_remove_list_viewed_item(checked: bool): 166 | self.parent.disable_list_view(e) 167 | 168 | label = entries.create_path_str(e.path) 169 | submenu.subitems.append( 170 | CheckboxMenuItem( 171 | label, 172 | None, 173 | True, 174 | True, 175 | on_remove_list_viewed_item, 176 | ) 177 | ) 178 | self.add_menu_item(submenu) 179 | 180 | @abc.abstractmethod 181 | def add_menu_item(self, item: MenuItem): 182 | """ 183 | Add an menu item to the context menu. 184 | 185 | This has to be implemented by the derived class. 186 | """ 187 | 188 | def on_copy_value_to_clipboard(self): 189 | if self.entry is None: 190 | return 191 | self.parent.copy_entry_value_to_clipboard(self.entry) 192 | 193 | def on_copy_path_to_clipboard(self): 194 | if self.entry is None: 195 | return 196 | self.parent.copy_entry_path_to_clipboard(self.entry) 197 | 198 | def on_paste(self): 199 | if self.entry is None: 200 | return 201 | self.parent.paste_entry_value_from_clipboard(self.entry) 202 | 203 | def on_undo(self): 204 | self.model.command_processor.undo() 205 | 206 | def on_redo(self): 207 | self.model.command_processor.redo() 208 | 209 | def on_hide_protected(self, checked: bool): 210 | self.parent.change_hide_protected(checked) 211 | self.parent.reload() 212 | 213 | def on_intformat(self, label: str): 214 | if label == INTFORMAT_DEC_LABEL: 215 | self.model.integer_format = IntegerFormat.Dec 216 | else: 217 | self.model.integer_format = IntegerFormat.Hex 218 | self.parent.reload() 219 | -------------------------------------------------------------------------------- /construct_editor/core/custom.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import typing as t 3 | 4 | import construct as cs 5 | 6 | import construct_editor.core.entries as entries 7 | import construct_editor.core.model as model 8 | import construct_editor.core.preprocessor as preprocessor 9 | 10 | 11 | def add_custom_transparent_subconstruct( 12 | subconstruct: t.Type["cs.Subconstruct[t.Any,t.Any,t.Any, t.Any]"], 13 | ): 14 | """ 15 | Add compatibility of an custom `cs.Subconstruct` to the construct-editor. 16 | """ 17 | preprocessor.custom_subconstructs.append(subconstruct) 18 | entries.construct_entry_mapping[subconstruct] = entries.EntryTransparentSubcon 19 | 20 | 21 | def add_custom_tunnel( 22 | tunnel: t.Type["cs.Tunnel[t.Any, t.Any]"], 23 | type_str: str, 24 | ): 25 | """ 26 | Add compatibility of an custom `cs.Tunnel` to the construct-editor. 27 | """ 28 | 29 | class EntryTunnel(entries.EntrySubconstruct): 30 | def __init__( 31 | self, 32 | model: "model.ConstructEditorModel", 33 | parent: t.Optional["entries.EntryConstruct"], 34 | construct: "cs.Compressed[t.Any, t.Any]", 35 | name: entries.NameType, 36 | docs: str, 37 | ): 38 | super().__init__(model, parent, construct, name, docs) 39 | 40 | @property 41 | def typ_str(self) -> str: 42 | return f"{type_str}[{self.subentry.typ_str}]" 43 | 44 | entries.construct_entry_mapping[tunnel] = EntryTunnel 45 | 46 | 47 | class AdapterObjEditorType(enum.Enum): 48 | Default = enum.auto() 49 | Integer = enum.auto() 50 | String = enum.auto() 51 | 52 | 53 | def add_custom_adapter( 54 | adapter: t.Union[ 55 | t.Type["cs.Adapter[t.Any,t.Any,t.Any, t.Any]"], # for cs.Adapter 56 | "cs.Adapter[t.Any,t.Any,t.Any, t.Any]", # for cs.ExprAdapter 57 | ], 58 | type_str: str, 59 | obj_editor_type: AdapterObjEditorType, 60 | ): 61 | """ 62 | Add compatibility of an custom `cs.Adapter` or `cs.ExprAdapter` to the construct-editor. 63 | """ 64 | 65 | class EntryAdapter(entries.EntryConstruct): 66 | def __init__( 67 | self, 68 | model: "model.ConstructEditorModel", 69 | parent: t.Optional["entries.EntryConstruct"], 70 | construct: "cs.Subconstruct[t.Any, t.Any, t.Any, t.Any]", 71 | name: entries.NameType, 72 | docs: str, 73 | ): 74 | super().__init__(model, parent, construct, name, docs) 75 | 76 | @property 77 | def typ_str(self) -> str: 78 | return type_str 79 | 80 | @property 81 | def obj_str(self) -> t.Any: 82 | return str(self.obj) 83 | 84 | @property 85 | def obj_view_settings(self) -> entries.ObjViewSettings: 86 | if obj_editor_type == AdapterObjEditorType.Integer: 87 | return entries.ObjViewSettings_Integer(self) 88 | elif obj_editor_type == AdapterObjEditorType.String: 89 | return entries.ObjViewSettings_String(self) 90 | else: 91 | return entries.ObjViewSettings_Default(self) 92 | 93 | entries.construct_entry_mapping[adapter] = EntryAdapter 94 | -------------------------------------------------------------------------------- /construct_editor/core/model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import abc 3 | import enum 4 | import typing as t 5 | 6 | import construct_editor.core.entries as entries 7 | from construct_editor.core.commands import Command, CommandProcessor 8 | from construct_editor.core.preprocessor import add_gui_metadata, get_gui_metadata 9 | 10 | 11 | class IntegerFormat(enum.Enum): 12 | Dec = enum.auto() 13 | Hex = enum.auto() 14 | 15 | 16 | class ConstructEditorColumn(enum.IntEnum): 17 | Name = 0 18 | Type = 1 19 | Value = 2 20 | 21 | 22 | class ChangeValueCmd(Command): 23 | def __init__( 24 | self, entry: "entries.EntryConstruct", old_value: t.Any, new_value: t.Any 25 | ) -> None: 26 | super().__init__(True, f"Value '{entry.path[-1]}' changed") 27 | self.entry = entry 28 | self.old_value = old_value 29 | self.new_value = new_value 30 | 31 | def do(self) -> None: 32 | self.entry.obj = self.new_value 33 | self.entry.model.on_value_changed(self.entry) 34 | 35 | def undo(self) -> None: 36 | self.entry.obj = self.old_value 37 | self.entry.model.on_value_changed(self.entry) 38 | 39 | 40 | class ConstructEditorModel: 41 | """ 42 | This model acts as a bridge between the DataViewCtrl and the dataclasses. 43 | This model provides these data columns: 44 | 0. Name: string 45 | 1. Type: string 46 | 2. Value: string 47 | """ 48 | 49 | def __init__(self): 50 | self.root_entry: t.Optional["entries.EntryConstruct"] = None 51 | self.root_obj: t.Optional[t.Any] = None 52 | 53 | # Modelwide flag, if hidden entries should be shown (hidden means starting with an underscore) 54 | self.hide_protected = True 55 | 56 | # Modelwide format of integer values 57 | self.integer_format = IntegerFormat.Dec 58 | 59 | # List with all entries that have the list view enabled 60 | self.list_viewed_entries: t.List["entries.EntryConstruct"] = [] 61 | 62 | self.command_processor = CommandProcessor(max_commands=10) 63 | 64 | @abc.abstractmethod 65 | def on_value_changed(self, entry: "entries.EntryConstruct"): 66 | """Implement this in the derived class""" 67 | ... 68 | 69 | def get_children( 70 | self, entry: t.Optional["entries.EntryConstruct"] 71 | ) -> t.List["entries.EntryConstruct"]: 72 | """ 73 | Get all children of an entry 74 | """ 75 | # no root entry set 76 | if self.root_entry is None: 77 | return [] 78 | 79 | # root entry is requested 80 | if entry is None: 81 | self.root_entry.visible_row = True 82 | return [self.root_entry] 83 | 84 | if entry.subentries is None: 85 | return [] 86 | 87 | children = [] 88 | for subentry in entry.subentries: 89 | name = subentry.name 90 | 91 | if (self.hide_protected == True) and (name.startswith("_") or name == ""): 92 | subentry.visible_row = False 93 | continue 94 | 95 | children.append(subentry) 96 | subentry.visible_row = True 97 | return children 98 | 99 | def is_container(self, entry: "entries.EntryConstruct") -> bool: 100 | """ 101 | Check if an entry is a container (contains children) 102 | """ 103 | return entry.subentries is not None 104 | 105 | def get_parent( 106 | self, entry: t.Optional["entries.EntryConstruct"] 107 | ) -> t.Optional["entries.EntryConstruct"]: 108 | """ 109 | Get the parent of an entry 110 | """ 111 | # root entry has no parent 112 | if entry is None: 113 | return None 114 | 115 | # get the visible row entry of this entry 116 | visible_row_entry = entry.get_visible_row_entry() 117 | if visible_row_entry is None: 118 | return None 119 | 120 | # get the parent of the visible row entry 121 | parent = visible_row_entry.parent 122 | if parent is None: 123 | return None 124 | 125 | # get the visible row entry of the parent 126 | return parent.get_visible_row_entry() 127 | 128 | def get_value(self, entry: "entries.EntryConstruct", column: int): 129 | """ 130 | Return the value to be displayed for this entry in a specific column. 131 | """ 132 | if column == ConstructEditorColumn.Name: 133 | return entry.name 134 | if column == ConstructEditorColumn.Type: 135 | return entry.typ_str 136 | if column == ConstructEditorColumn.Value: 137 | return entry 138 | 139 | # other columns are unused except for list_viewed_entries 140 | if (entry.parent is None) or (entry.parent not in self.list_viewed_entries): 141 | return "" 142 | 143 | # flatten the hierarchical structure to a list 144 | column = column - len(ConstructEditorColumn) 145 | flat_subentry_list: t.List["entries.EntryConstruct"] = [] 146 | flat_subentry_list = self.create_flat_subentry_list(entry) 147 | if len(flat_subentry_list) > column: 148 | return flat_subentry_list[column].obj_str 149 | else: 150 | return "" 151 | 152 | def set_value( 153 | self, new_value: t.Any, entry: "entries.EntryConstruct", column: int 154 | ) -> None: 155 | """ 156 | Set the value of an entry. 157 | """ 158 | if column != ConstructEditorColumn.Value: 159 | raise ValueError(f"{column=} cannot be modified") 160 | 161 | # get the current object 162 | current_value = entry.obj 163 | 164 | # link the metadata from the current object to the new one 165 | metadata = get_gui_metadata(current_value) 166 | if metadata is not None: 167 | new_value = add_gui_metadata(new_value, metadata) 168 | 169 | cmd = ChangeValueCmd(entry, current_value, new_value) 170 | self.command_processor.submit(cmd) 171 | 172 | def create_flat_subentry_list( 173 | self, entry: "entries.EntryConstruct" 174 | ) -> t.List["entries.EntryConstruct"]: 175 | """ 176 | Create a flat list with all subentires, recursively. 177 | """ 178 | flat_subentry_list: t.List["entries.EntryConstruct"] = [] 179 | 180 | childs = self.get_children(entry) 181 | 182 | if len(childs) == 0: 183 | # append this entry only when no childs/subentires are available 184 | flat_subentry_list.append(entry) 185 | return flat_subentry_list 186 | 187 | # add all childs/subentries recursivly 188 | for child in childs: 189 | child_flat_subentry_list = self.create_flat_subentry_list(child) 190 | flat_subentry_list.extend(child_flat_subentry_list) 191 | return flat_subentry_list 192 | -------------------------------------------------------------------------------- /construct_editor/core/preprocessor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import copy 3 | import enum 4 | import io 5 | import typing as t 6 | 7 | import construct as cs 8 | import construct_typed as cst 9 | import wrapt 10 | 11 | 12 | class GuiMetaData(t.TypedDict): 13 | byte_range: t.Tuple[int, int] 14 | construct: cs.Construct 15 | context: "cs.Context" 16 | stream: io.BytesIO 17 | child_gui_metadata: t.Optional["GuiMetaData"] 18 | 19 | 20 | class IntWithGuiMetadata(int): 21 | pass 22 | 23 | 24 | class FloatWithGuiMetadata(float): 25 | pass 26 | 27 | 28 | class BytesWithGuiMetadata(bytes): 29 | pass 30 | 31 | 32 | class BytearrayWithGuiMetadata(bytearray): 33 | pass 34 | 35 | 36 | class StrWithGuiMetadata(str): 37 | pass 38 | 39 | 40 | class NoneWithGuiMetadata: 41 | pass 42 | 43 | 44 | class ObjProxyWithGuiMetaData(wrapt.ObjectProxy): 45 | __slots__ = "__construct_editor_metadata__" 46 | 47 | def __init__(self, wrapped: t.Any, gui_metadata: GuiMetaData): 48 | super(ObjProxyWithGuiMetaData, self).__init__(wrapped) 49 | wrapt.ObjectProxy.__setattr__( 50 | self, "__construct_editor_metadata__", gui_metadata 51 | ) 52 | 53 | 54 | def get_gui_metadata(obj: t.Any) -> t.Optional[GuiMetaData]: 55 | """Get the GUI metadata if they are available""" 56 | try: 57 | return getattr(obj, "__construct_editor_metadata__") 58 | except Exception: 59 | return None 60 | 61 | 62 | def add_gui_metadata(obj: t.Any, gui_metadata: GuiMetaData) -> t.Any: 63 | """ 64 | Append the private field "__construct_editor_metadata__" to an object 65 | """ 66 | obj_type = type(obj) 67 | if isinstance(obj, enum.Enum): 68 | obj = ObjProxyWithGuiMetaData(obj, gui_metadata) 69 | elif (obj_type is int) or (obj_type is bool): 70 | obj = IntWithGuiMetadata(obj) 71 | obj.__construct_editor_metadata__ = gui_metadata 72 | elif obj_type is float: 73 | obj = FloatWithGuiMetadata(obj) 74 | obj.__construct_editor_metadata__ = gui_metadata 75 | elif obj_type is bytes: 76 | obj = BytesWithGuiMetadata(obj) 77 | obj.__construct_editor_metadata__ = gui_metadata 78 | elif obj_type is bytearray: 79 | obj = BytearrayWithGuiMetadata(obj) 80 | obj.__construct_editor_metadata__ = gui_metadata 81 | elif obj_type is str: 82 | obj = StrWithGuiMetadata(obj) 83 | obj.__construct_editor_metadata__ = gui_metadata 84 | elif obj is None: 85 | obj = NoneWithGuiMetadata() 86 | obj.__construct_editor_metadata__ = gui_metadata 87 | else: 88 | try: 89 | obj.__construct_editor_metadata__ = gui_metadata # type: ignore 90 | except AttributeError: 91 | raise ValueError(f"add_gui_metadata dont work with type of {type(obj)}") 92 | return obj 93 | 94 | 95 | class IncludeGuiMetaData(cs.Subconstruct): 96 | """Include GUI metadata to the parsed object""" 97 | 98 | def __init__(self, subcon, bitwise: bool): 99 | super().__init__(subcon) # type: ignore 100 | self.bitwise = bitwise 101 | 102 | def _parse(self, stream, context, path): 103 | offset_start = cs.stream_tell(stream, path) 104 | obj = self.subcon._parsereport(stream, context, path) # type: ignore 105 | offset_end = cs.stream_tell(stream, path) 106 | 107 | # Maybe the obj has already gui_metadata. Read it 108 | # out and save it in the parent gui_metadata object. 109 | child_gui_metadata = get_gui_metadata(obj) 110 | 111 | if self.bitwise is True: 112 | stream._construct_bitstream_flag = True 113 | 114 | gui_metadata = GuiMetaData( 115 | byte_range=(offset_start, offset_end), 116 | construct=self.subcon, 117 | context=context, 118 | stream=stream, 119 | child_gui_metadata=child_gui_metadata, 120 | ) 121 | 122 | return add_gui_metadata(obj, gui_metadata) 123 | 124 | def _build(self, obj, stream, context, path): 125 | buildret = self.subcon._build(obj, stream, context, path) # type: ignore 126 | return obj 127 | 128 | # passthrought attribute access 129 | def __getattr__(self, name): 130 | return getattr(self.subcon, name) 131 | 132 | # ############################################################################# 133 | def include_metadata( 134 | constr: "cs.Construct[t.Any, t.Any]", bitwise: bool = False 135 | ) -> "cs.Construct[t.Any, t.Any]": 136 | """ 137 | Surrond all named entries of a construct with offsets, so that 138 | we know the offset in the byte-stream and the length 139 | """ 140 | 141 | # Simple Constructs ####################################################### 142 | if isinstance( 143 | constr, 144 | ( 145 | cs.BytesInteger, 146 | cs.BitsInteger, 147 | cs.Bytes, 148 | cs.FormatField, 149 | cs.BytesInteger, 150 | cs.BitsInteger, 151 | cs.Computed, 152 | cs.Check, 153 | cs.StopIf, 154 | cst.TEnum, 155 | cs.Enum, 156 | cs.FlagsEnum, 157 | cs.TimestampAdapter, 158 | cs.Seek, 159 | ), 160 | ): 161 | return IncludeGuiMetaData(constr, bitwise) 162 | 163 | # Bitwiese Construct ###################################################### 164 | elif ( 165 | isinstance(constr, (cs.Restreamed)) 166 | and (constr.decoder is cs.bytes2bits) 167 | and (constr.encoder is cs.bits2bytes) 168 | ) or ( 169 | isinstance(constr, (cs.Transformed)) 170 | and (constr.decodefunc is cs.bytes2bits) 171 | and (constr.encodefunc is cs.bits2bytes) 172 | ): 173 | constr = copy.copy(constr) # constr is modified, so we have to make a copy 174 | constr.subcon = include_metadata(constr.subcon, bitwise=True) 175 | return IncludeGuiMetaData(constr, bitwise) 176 | 177 | # Subconstructs ########################################################### 178 | elif isinstance( 179 | constr, 180 | ( 181 | cs.Const, 182 | cs.Rebuild, 183 | cs.Default, 184 | cs.Padded, 185 | cs.Aligned, 186 | cs.Pointer, 187 | cs.Peek, 188 | cst.DataclassStruct, 189 | cs.Array, 190 | cs.GreedyRange, 191 | cs.Restreamed, 192 | cs.Transformed, 193 | cs.Tunnel, 194 | cs.Prefixed, 195 | cs.FixedSized, 196 | cs.NullStripped, 197 | cs.NullTerminated, 198 | *custom_subconstructs, 199 | ), 200 | ): 201 | constr = copy.copy(constr) # constr is modified, so we have to make a copy 202 | constr.subcon = include_metadata(constr.subcon, bitwise) # type: ignore 203 | return IncludeGuiMetaData(constr, bitwise) 204 | 205 | # Struct ################################################################## 206 | elif isinstance(constr, cs.Struct): 207 | constr = copy.copy(constr) # constr is modified, so we have to make a copy 208 | new_subcons = [] 209 | for subcon in constr.subcons: 210 | new_subcons.append(include_metadata(subcon, bitwise)) 211 | constr.subcons = new_subcons 212 | constr._subcons = cs.Container((sc.name,sc) for sc in constr.subcons if sc.name) 213 | return IncludeGuiMetaData(constr, bitwise) 214 | 215 | # FocusedSeq ############################################################## 216 | elif isinstance(constr, cs.FocusedSeq): 217 | constr = copy.copy(constr) # constr is modified, so we have to make a copy 218 | new_subcons = [] 219 | for subcon in constr.subcons: 220 | new_subcons.append(include_metadata(subcon, bitwise)) 221 | constr.subcons = new_subcons 222 | constr._subcons = cs.Container((sc.name,sc) for sc in constr.subcons if sc.name) 223 | return IncludeGuiMetaData(constr, bitwise) 224 | 225 | # Select ################################################################## 226 | elif isinstance(constr, cs.Select): 227 | constr = copy.copy(constr) # constr is modified, so we have to make a copy 228 | new_subcons = [] 229 | for subcon in constr.subcons: 230 | new_subcons.append(include_metadata(subcon, bitwise)) 231 | constr.subcons = new_subcons 232 | return IncludeGuiMetaData(constr, bitwise) 233 | 234 | # IfThenElse ############################################################## 235 | elif isinstance(constr, cs.IfThenElse): 236 | constr = copy.copy(constr) # constr is modified, so we have to make a copy 237 | constr.thensubcon = include_metadata(constr.thensubcon, bitwise) 238 | constr.elsesubcon = include_metadata(constr.elsesubcon, bitwise) 239 | return IncludeGuiMetaData(constr, bitwise) 240 | 241 | # Switch ################################################################## 242 | elif isinstance(constr, cs.Switch): 243 | constr = copy.copy(constr) # constr is modified, so we have to make a copy 244 | new_cases = {} 245 | for key, subcon in constr.cases.items(): 246 | new_cases[key] = include_metadata(subcon, bitwise) 247 | constr.cases = new_cases 248 | if constr.default is not None: 249 | constr.default = include_metadata(constr.default, bitwise) 250 | return IncludeGuiMetaData(constr, bitwise) 251 | 252 | # Checksum ################################################################# 253 | elif isinstance(constr, cs.Checksum): 254 | constr = copy.copy(constr) # constr is modified, so we have to make a copy 255 | constr.checksumfield = include_metadata(constr.checksumfield, bitwise) 256 | return IncludeGuiMetaData(constr, bitwise) 257 | 258 | # Renamed ################################################################# 259 | elif isinstance(constr, cs.Renamed): 260 | constr = copy.copy(constr) # constr is modified, so we have to make a copy 261 | constr.subcon = include_metadata(constr.subcon, bitwise) # type: ignore 262 | return constr 263 | 264 | # Misc #################################################################### 265 | elif isinstance( 266 | constr, 267 | ( 268 | cs.ExprAdapter, 269 | cs.Adapter, 270 | type(cs.GreedyBytes), 271 | type(cs.VarInt), 272 | type(cs.ZigZag), 273 | type(cs.Flag), 274 | type(cs.Index), 275 | type(cs.Error), 276 | type(cs.Pickled), 277 | type(cs.Numpy), 278 | type(cs.Tell), 279 | type(cs.Pass), 280 | type(cs.Terminated), 281 | ), 282 | ): 283 | return IncludeGuiMetaData(constr, bitwise) 284 | 285 | # TODO: 286 | # # Grouping: 287 | # - Sequence 288 | # - Union 289 | # - LazyStruct 290 | 291 | # # Grouping lists: 292 | # - Array 293 | # - GreedyRange 294 | # - RepeatUntil 295 | 296 | # # Special: 297 | # - Pointer 298 | # - RawCopy 299 | # - Restreamed 300 | # - Transformed 301 | # - RestreamData 302 | raise ValueError(f"construct of type '{constr}' is not supported") 303 | 304 | 305 | custom_subconstructs: t.List[t.Type[cs.Subconstruct]] = [] 306 | -------------------------------------------------------------------------------- /construct_editor/gallery/__init__.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import construct as cs 3 | import typing as t 4 | 5 | 6 | @dataclasses.dataclass 7 | class GalleryItem: 8 | construct: "cs.Construct[t.Any, t.Any]" 9 | contextkw: t.Dict[str, t.Any] = dataclasses.field(default_factory=dict) 10 | example_binarys: t.Dict[str, bytes] = dataclasses.field(default_factory=dict) 11 | -------------------------------------------------------------------------------- /construct_editor/gallery/example_cmd_resp.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | import copy 7 | 8 | 9 | class DefaultSizedError(cs.ConstructError): 10 | pass 11 | 12 | 13 | class DefaultSized(cs.Subconstruct): 14 | r""" 15 | Returns a size when calling sizeof of GreedyBytes. Parsing and building is not changed. 16 | 17 | :param subcon: Construct instance 18 | :param default_size: size that should be returned 19 | 20 | :raises DefaultSizedError: anouter GreedyBytes than GreedyBytes is passed 21 | 22 | Example:: 23 | 24 | >>> d = DefaultSized(GreedyBytes) 25 | >>> d.sizeof() 26 | 0 27 | """ 28 | 29 | def __init__(self, subcon, default_size=0): 30 | if subcon is not cs.GreedyBytes: 31 | raise DefaultSizedError("DefaultSized only works with cs.GreedyBytes") 32 | super().__init__(subcon) # type: ignore 33 | self.default_size = default_size 34 | 35 | def _sizeof(self, context, path): 36 | return 0 37 | 38 | 39 | class CmdCode(cst.EnumBase): 40 | Command1 = 0 41 | Command2 = 1 42 | 43 | 44 | @dataclasses.dataclass 45 | class RespData_Command1(cst.DataclassMixin): 46 | d1: int = cst.csfield(cs.Int8ul) 47 | d2: int = cst.csfield(cs.Int8ul) 48 | 49 | 50 | @dataclasses.dataclass 51 | class RespData_Command2(cst.DataclassMixin): 52 | c1: int = cst.csfield(cs.Int16ul) 53 | c2: int = cst.csfield(cs.Int16ul) 54 | 55 | 56 | @dataclasses.dataclass 57 | class RespData_Error(cst.DataclassMixin): 58 | e1: int = cst.csfield(cs.Int32ul) 59 | e2: int = cst.csfield(cs.Int32ul) 60 | 61 | 62 | resp_data_formats: t.Dict[CmdCode, cst.Construct[t.Any, t.Any]] = { 63 | CmdCode.Command1: cst.DataclassStruct(RespData_Command1), 64 | CmdCode.Command2: cst.DataclassStruct(RespData_Command2), 65 | } 66 | 67 | RespDataType = t.Union[ 68 | bytes, 69 | RespData_Command1, 70 | RespData_Command2, 71 | ] 72 | 73 | 74 | class StatusCode(cst.EnumBase): 75 | OK = 0 76 | Error1 = 1 77 | Error2 = 2 78 | 79 | 80 | status_code_format = cst.TEnum(cs.Int32ul, StatusCode) 81 | 82 | 83 | def _get_cmd_code(ctx: cst.Context): 84 | contextkw = ctx._root._ 85 | if "cmd_code" not in contextkw: 86 | # if no cmd_code is passed, fallback to GreedyBytes 87 | return None 88 | 89 | return contextkw.cmd_code 90 | 91 | 92 | def _is_status_code_ok(ctx: cst.Context): 93 | if ctx._sizing is True: 94 | # while sizing pretend an ok status_code 95 | return True 96 | 97 | return ctx.status_code == StatusCode.OK 98 | 99 | 100 | @dataclasses.dataclass 101 | class Response(cst.DataclassMixin): 102 | status_code: StatusCode = cst.csfield(status_code_format) 103 | data: RespDataType = cst.csfield( 104 | cs.Select( 105 | cs.IfThenElse( 106 | condfunc=_is_status_code_ok, 107 | thensubcon=cs.Switch( 108 | keyfunc=_get_cmd_code, # select the response format based on the cmd_code 109 | cases=resp_data_formats, 110 | default=cs.StopIf(True), 111 | ), 112 | elsesubcon=cst.DataclassStruct(RespData_Error), 113 | ), 114 | DefaultSized(cs.GreedyBytes), 115 | ) 116 | ) 117 | 118 | 119 | constr = cst.DataclassStruct(Response) 120 | 121 | gallery_item = GalleryItem( 122 | construct=constr, 123 | example_binarys={ 124 | "Zeros": bytes(20), 125 | "1": bytes([1, 1, 2, 1, 2]), 126 | }, 127 | contextkw={"cmd_code": CmdCode.Command1}, 128 | ) 129 | 130 | # ###################################################################################### 131 | # ################## Adding new constructs to construct-editor ######################### 132 | # ###################################################################################### 133 | import construct_editor.core.custom as custom 134 | 135 | 136 | custom.add_custom_transparent_subconstruct(DefaultSized) 137 | -------------------------------------------------------------------------------- /construct_editor/gallery/example_pe32coff.py: -------------------------------------------------------------------------------- 1 | from construct import * # type: ignore 2 | from . import GalleryItem 3 | 4 | docs = """ 5 | PE/COFF format as used on Windows to store EXE DLL SYS files. This includes 64-bit and .NET code. 6 | Microsoft specifications: 7 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms680547(v=vs.85).aspx 8 | https://msdn.microsoft.com/en-us/library/ms809762.aspx 9 | Format tutorial breakdown at: 10 | http://blog.dkbza.org/ 11 | https://drive.google.com/file/d/0B3_wGJkuWLytQmc2di0wajB1Xzg/view 12 | https://drive.google.com/file/d/0B3_wGJkuWLytbnIxY1J5WUs4MEk/view 13 | Authored by Arkadiusz Bulski, under same license. 14 | """ 15 | 16 | msdosheader = Struct( 17 | "signature" / Const(b"MZ"), 18 | "lfanew" / Pointer(0x3c, Int16ul), 19 | ) 20 | 21 | coffheader = Struct( 22 | "signature" / Const(b"PE\x00\x00"), 23 | "machine" / Enum(Int16ul, 24 | UNKNOWN = 0x0, 25 | AM33 = 0x1d3, 26 | AMD64 = 0x8664, 27 | ARM = 0x1c0, 28 | ARM64 = 0xaa64, 29 | ARMNT = 0x1c4, 30 | EBC = 0xebc, 31 | I386 = 0x14c, 32 | IA64 = 0x200, 33 | M32R = 0x9041, 34 | MIPS16 = 0x266, 35 | MIPSFPU = 0x366, 36 | MIPSFPU16 = 0x466, 37 | POWERPC = 0x1f0, 38 | POWERPCFP = 0x1f1, 39 | R4000 = 0x166, 40 | RISCV32 = 0x5032, 41 | RISCV64 = 0x5064, 42 | RISCV128 = 0x5128, 43 | SH3 = 0x1a2, 44 | SH3DSP = 0x1a3, 45 | SH4 = 0x1a6, 46 | SH5 = 0x1a8, 47 | THUMB = 0x1c2, 48 | WCEMIPSV2 = 0x169, 49 | ), 50 | "sections_count" / Int16ul, 51 | "created" / Timestamp(Int32ul, 1., 1970), 52 | "symbol_pointer" / Int32ul, #deprecated 53 | "symbol_count" / Int32ul, #deprecated 54 | "optionalheader_size" / Int16ul, 55 | "characteristics" / FlagsEnum(Int16ul, 56 | RELOCS_STRIPPED = 0x0001, 57 | EXECUTABLE_IMAGE = 0x0002, 58 | LINE_NUMS_STRIPPED = 0x0004, #deprecated 59 | LOCAL_SYMS_STRIPPED = 0x0008, #deprecated 60 | AGGRESSIVE_WS_TRIM = 0x0010, #deprecated 61 | LARGE_ADDRESS_AWARE = 0x0020, 62 | RESERVED = 0x0040, #reserved 63 | BYTES_REVERSED_LO = 0x0080, #deprecated 64 | MACHINE_32BIT = 0x0100, 65 | DEBUG_STRIPPED = 0x0200, 66 | REMOVABLE_RUN_FROM_SWAP = 0x0400, 67 | SYSTEM = 0x1000, 68 | DLL = 0x2000, 69 | UNIPROCESSOR_ONLY = 0x4000, 70 | BIG_ENDIAN_MACHINE = 0x8000, #deprecated 71 | ), 72 | ) 73 | 74 | plusfield = IfThenElse(this.signature == "PE32plus", Int64ul, Int32ul) 75 | 76 | entriesnames = { 77 | 0 : 'export_table', 78 | 1 : 'import_table', 79 | 2 : 'resource_table', 80 | 3 : 'exception_table', 81 | 4 : 'certificate_table', 82 | 5 : 'base_relocation_table', 83 | 6 : 'debug', 84 | 7 : 'architecture', 85 | 8 : 'global_ptr', 86 | 9 : 'tls_table', 87 | 10 : 'load_config_table', 88 | 11 : 'bound_import', 89 | 12 : 'import_address_table', 90 | 13 : 'delay_import_descriptor', 91 | 14 : 'clr_runtime_header', 92 | 15 : 'reserved', 93 | } 94 | 95 | datadirectory = Struct( 96 | "name" / Computed(lambda this: entriesnames[this._._index]), 97 | "virtualaddress" / Int32ul, 98 | "size" / Int32ul, 99 | ) 100 | 101 | optionalheader = Struct( 102 | "signature" / Enum(Int16ul, 103 | PE32 = 0x10b, 104 | PE32plus = 0x20b, 105 | ROMIMAGE = 0x107, 106 | ), 107 | "linker_version" / Int8ul[2], 108 | "size_code" / Int32ul, 109 | "size_initialized_data" / Int32ul, 110 | "size_uninitialized_data" / Int32ul, 111 | "entrypoint" / Int32ul, 112 | "base_code" / Int32ul, 113 | "base_data" / If(this.signature == "PE32", Int32ul), 114 | "image_base" / plusfield, 115 | "section_alignment" / Int32ul, 116 | "file_alignment" / Int32ul, 117 | "os_version" / Int16ul[2], 118 | "image_version" / Int16ul[2], 119 | "subsystem_version" / Int16ul[2], 120 | "win32versionvalue" / Int32ul, #deprecated 121 | "image_size" / Int32ul, 122 | "headers_size" / Int32ul, 123 | "checksum" / Int32ul, 124 | "subsystem" / Enum(Int16ul, 125 | UNKNOWN = 0, 126 | NATIVE = 1, 127 | WINDOWS_GUI = 2, 128 | WINDOWS_CUI = 3, 129 | OS2_CUI = 5, 130 | POSIX_CUI = 7, 131 | WINDOWS_NATIVE = 8, 132 | WINDOWS_CE_GUI = 9, 133 | EFI_APPLICATION = 10, 134 | EFI_BOOT_SERVICE_DRIVER = 11, 135 | EFI_RUNTIME_DRIVER = 12, 136 | EFI_ROM = 13, 137 | XBOX = 14, 138 | WINDOWS_BOOT_APPLICATION = 16, 139 | ), 140 | "dll_characteristics" / FlagsEnum(Int16ul, 141 | HIGH_ENTROPY_VA = 0x0020, 142 | DYNAMIC_BASE = 0x0040, 143 | FORCE_INTEGRITY = 0x0080, 144 | NX_COMPAT = 0x0100, 145 | NO_ISOLATION = 0x0200, 146 | NO_SEH = 0x0400, 147 | NO_BIND = 0x0800, 148 | APPCONTAINER = 0x1000, 149 | WDM_DRIVER = 0x2000, 150 | GUARD_CF = 0x4000, 151 | TERMINAL_SERVER_AWARE = 0x8000, 152 | ), 153 | "stack_reserve" / plusfield, 154 | "stack_commit" / plusfield, 155 | "heap_reserve" / plusfield, 156 | "heap_commit" / plusfield, 157 | "loader_flags" / Int32ul, #reserved 158 | "datadirectories_count" / Int32ul, 159 | "datadirectories" / Array(this.datadirectories_count, 160 | datadirectory), 161 | ) 162 | 163 | section = Struct( 164 | "name" / PaddedString(8, "utf8"), 165 | "virtual_size" / Int32ul, 166 | "virtual_address" / Int32ul, 167 | "rawdata_size" / Int32ul, 168 | "rawdata_pointer" / Int32ul, 169 | "relocations_pointer" / Int32ul, 170 | "linenumbers_pointer" / Int32ul, 171 | "relocations_count" / Int16ul, 172 | "linenumbers_count" / Int16ul, 173 | "characteristics" / FlagsEnum(Int32ul, 174 | TYPE_REG = 0x00000000, 175 | TYPE_DSECT = 0x00000001, 176 | TYPE_NOLOAD = 0x00000002, 177 | TYPE_GROUP = 0x00000004, 178 | TYPE_NO_PAD = 0x00000008, 179 | TYPE_COPY = 0x00000010, 180 | CNT_CODE = 0x00000020, 181 | CNT_INITIALIZED_DATA = 0x00000040, 182 | CNT_UNINITIALIZED_DATA = 0x00000080, 183 | LNK_OTHER = 0x00000100, 184 | LNK_INFO = 0x00000200, 185 | TYPE_OVER = 0x00000400, 186 | LNK_REMOVE = 0x00000800, 187 | LNK_COMDAT = 0x00001000, 188 | MEM_FARDATA = 0x00008000, 189 | MEM_PURGEABLE = 0x00020000, 190 | MEM_16BIT = 0x00020000, 191 | MEM_LOCKED = 0x00040000, 192 | MEM_PRELOAD = 0x00080000, 193 | ALIGN_1BYTES = 0x00100000, 194 | ALIGN_2BYTES = 0x00200000, 195 | ALIGN_4BYTES = 0x00300000, 196 | ALIGN_8BYTES = 0x00400000, 197 | ALIGN_16BYTES = 0x00500000, 198 | ALIGN_32BYTES = 0x00600000, 199 | ALIGN_64BYTES = 0x00700000, 200 | ALIGN_128BYTES = 0x00800000, 201 | ALIGN_256BYTES = 0x00900000, 202 | ALIGN_512BYTES = 0x00A00000, 203 | ALIGN_1024BYTES = 0x00B00000, 204 | ALIGN_2048BYTES = 0x00C00000, 205 | ALIGN_4096BYTES = 0x00D00000, 206 | ALIGN_8192BYTES = 0x00E00000, 207 | LNK_NRELOC_OVFL = 0x01000000, 208 | MEM_DISCARDABLE = 0x02000000, 209 | MEM_NOT_CACHED = 0x04000000, 210 | MEM_NOT_PAGED = 0x08000000, 211 | MEM_SHARED = 0x10000000, 212 | MEM_EXECUTE = 0x20000000, 213 | MEM_READ = 0x40000000, 214 | MEM_WRITE = 0x80000000, 215 | ), 216 | "rawdata" / Pointer(this.rawdata_pointer, 217 | Bytes(lambda this: this.rawdata_size if this.rawdata_pointer else 0)), 218 | "relocations" / Pointer(this.relocations_pointer, 219 | Array(this.relocations_count, Struct( 220 | "virtualaddress" / Int32ul, 221 | "symboltable_index" / Int32ul, 222 | "type" / Int16ul * "complicated platform-dependant Enum", 223 | )) 224 | ), 225 | "linenumbers" / Pointer(this.linenumbers_pointer, 226 | Array(this.linenumbers_count, Struct( 227 | "_type" / Int32ul, 228 | "_linenumber" / Int16ul, 229 | "is_symboltableindex" / Computed(this._linenumber == 0), 230 | "is_linenumber" / Computed(this._linenumber > 0), 231 | "symboltableindex" / If(this.is_symboltableindex, Computed(this._type)), 232 | "linenumber" / If(this.is_linenumber, Computed(this._linenumber)), 233 | "virtualaddress" / If(this.is_linenumber, Computed(this._type)), 234 | )) 235 | ), 236 | ) 237 | 238 | pe32file = docs * Struct( 239 | "msdosheader" / msdosheader, 240 | Seek(this.msdosheader.lfanew), 241 | "coffheader" / coffheader, 242 | "optionalheader" / If(this.coffheader.optionalheader_size > 0, optionalheader), 243 | "sections_count" / Computed(this.coffheader.sections_count), 244 | "sections" / Array(this.sections_count, section), 245 | ) 246 | 247 | gallery_item = GalleryItem( 248 | construct=pe32file, 249 | ) 250 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_aligned.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | 4 | 5 | constr = cs.Struct( 6 | "before" / cs.Int8ub, 7 | "aligned_16" / cs.Aligned(5, cs.Bytes(3)), 8 | "aligned_len" / cs.Int8ub, 9 | "aligned" / cs.Aligned(5, cs.Bytes(cs.this.aligned_len)), 10 | "after" / cs.Int8ub, 11 | ) 12 | 13 | 14 | gallery_item = GalleryItem( 15 | construct=constr, 16 | example_binarys={ 17 | "1": bytes([0xF0, 0, 1, 2, 3, 4, 6, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xFF]), 18 | "Zeros": bytes(8), 19 | }, 20 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_array.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | @dataclasses.dataclass 9 | class ArrayTest(cst.DataclassMixin): 10 | simple_static: t.List[int] = cst.csfield(cs.Array(5, cs.Int8ul)) 11 | 12 | simple_dynamic_len: int = cst.csfield( 13 | cs.Int32ul, doc="Length of the simple dynamic array" 14 | ) 15 | simple_dynamic: t.List[int] = cst.csfield( 16 | cs.Array(cs.this.simple_dynamic_len, cs.Int8ul) 17 | ) 18 | 19 | @dataclasses.dataclass 20 | class Entry(cst.DataclassMixin): 21 | id: int = cst.csfield(cs.Int8ul) 22 | width: int = cst.csfield(cs.Int8ul) 23 | height: int = cst.csfield(cs.Int8ul) 24 | 25 | struct_static: t.List[Entry] = cst.csfield(cs.Array(3, cst.DataclassStruct(Entry))) 26 | 27 | struct_dynamic_len: int = cst.csfield( 28 | cs.Int8ul, doc="Length of the struct dynamic array" 29 | ) 30 | struct_dynamic: t.List[Entry] = cst.csfield( 31 | cs.Array(cs.this.struct_dynamic_len, cst.DataclassStruct(Entry)) 32 | ) 33 | 34 | 35 | constr = cst.DataclassStruct(ArrayTest) 36 | 37 | gallery_item = GalleryItem( 38 | construct=constr, 39 | example_binarys={ 40 | "Zeros": bytes(5 + 4 + 3 * 3 + 1), 41 | "1": ( 42 | bytes([1, 2, 3, 4, 5, 4, 0, 0, 0, 1, 2, 3, 4]) 43 | + bytes([0xA0, 0xA1, 0xA2, 0xB0, 0xB1, 0xB2, 0xC0, 0xC1, 0xC2]) 44 | + bytes([2, 0xA0, 0xA1, 0xA2, 0xB0, 0xB1, 0xB2]) 45 | ), 46 | "2": ( 47 | bytes([1, 2, 3, 4, 5, 8, 0, 0, 0, 7, 6, 5, 4, 3, 2, 1, 0]) 48 | + bytes([0xA0, 0xA1, 0xA2, 0xB0, 0xB1, 0xB2, 0xC0, 0xC1, 0xC2]) 49 | + bytes([3, 0xA0, 0xA1, 0xA2, 0xB0, 0xB1, 0xB2, 0xC0, 0xC1, 0xC2]) 50 | ), 51 | "Huge": ( 52 | bytes([1, 2, 3, 4, 5, 0x20, 0x4E, 0, 0] + ([0]*20000)) 53 | + bytes([0xA0, 0xA1, 0xA2, 0xB0, 0xB1, 0xB2, 0xC0, 0xC1, 0xC2]) 54 | + bytes([3, 0xA0, 0xA1, 0xA2, 0xB0, 0xB1, 0xB2, 0xC0, 0xC1, 0xC2]) 55 | ), 56 | }, 57 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_bits_swapped_bitwise.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | constr = cs.BitsSwapped(cs.Bitwise(cs.GreedyRange(cs.Bit))) 9 | 10 | gallery_item = GalleryItem( 11 | construct=constr, 12 | example_binarys={ 13 | "1": bytes([1, 2, 3, 4]), 14 | "Zeros": bytes(0), 15 | "Huge": bytes(10000), 16 | }, 17 | ) 18 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_bitwise.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | constr = cs.Bitwise(cs.GreedyRange(cs.Bit)) 9 | 10 | gallery_item = GalleryItem( 11 | construct=constr, 12 | example_binarys={ 13 | "1": bytes([1, 2, 3, 4]), 14 | "Zeros": bytes(0), 15 | "Huge": bytes(10000), 16 | }, 17 | ) 18 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_bytes_greedybytes.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | 4 | 5 | constr = cs.Struct( 6 | "len" / cs.Int8ul, 7 | "bytes_fix" / cs.Bytes(5), 8 | "bytes_lambda" / cs.Bytes(lambda ctx: 2), 9 | "bytes_this" / cs.Bytes(cs.this.len), 10 | "greedybytes" / cs.GreedyBytes, 11 | ) 12 | 13 | 14 | gallery_item = GalleryItem( 15 | construct=constr, 16 | example_binarys={ 17 | "Zeros": bytes([3]) + bytes(15), 18 | "1": b"\x03123456789ABCDEF", 19 | }, 20 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_checksum.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import construct as cs 3 | from . import GalleryItem 4 | 5 | 6 | constr = cs.Struct( 7 | "checksum_start" / cs.Tell, 8 | "fields" / cs.Struct( 9 | cs.Padding(1000), 10 | ), 11 | "checksum_end" / cs.Tell, 12 | "checksum" / cs.Checksum(cs.Bytes(64), 13 | lambda data: hashlib.sha512(data).digest(), 14 | lambda ctx: ctx._io.getvalue()[ctx.checksum_start:ctx.checksum_end]), # type: ignore 15 | ) 16 | 17 | 18 | gallery_item = GalleryItem( 19 | construct=constr, 20 | example_binarys={ 21 | "1": constr.build(dict(fields=dict(value={}))), 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_compressed.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | 4 | 5 | constr = cs.Struct( 6 | "compressed_bits" 7 | / cs.Prefixed( 8 | cs.VarInt, 9 | cs.Compressed( 10 | cs.BitStruct( 11 | "value1" / cs.BitsInteger(7), 12 | "value2" / cs.BitsInteger(3), 13 | "value3" / cs.BitsInteger(6), 14 | ), 15 | "zlib", 16 | ), 17 | ), 18 | "compressed_bytes" 19 | / cs.Prefixed( 20 | cs.VarInt, 21 | cs.Compressed( 22 | cs.Struct( 23 | "value1" / cs.Int8ul, 24 | "value2" / cs.Int16ul, 25 | "value3" / cs.Int8ul, 26 | "bits" 27 | / cs.BitStruct( 28 | "bits1" / cs.BitsInteger(7), 29 | "bits2" / cs.BitsInteger(3), 30 | "bits3" / cs.BitsInteger(6), 31 | ), 32 | "swapped_bits" 33 | / cs.ByteSwapped( 34 | cs.BitStruct( 35 | "bits1" / cs.BitsInteger(7), 36 | "bits2" / cs.BitsInteger(3), 37 | "bits3" / cs.BitsInteger(6), 38 | ) 39 | ), 40 | ), 41 | "zlib", 42 | ), 43 | ), 44 | ) 45 | 46 | # b = constr.build( 47 | # dict( 48 | # compressed_bits=dict(value1=0, value2=1, value3=3), 49 | # compressed_bytes=dict(value1=0, value2=1, value3=3, bits=dict(bits1=0, bits2=1, bits3=3),swapped_bits=dict(bits1=0, bits2=1, bits3=3),), 50 | # ), 51 | # ) 52 | # print(b.hex()) 53 | 54 | gallery_item = GalleryItem( 55 | construct=constr, 56 | example_binarys={ 57 | "1": bytes.fromhex("0a789c637006000045004410789c63606460667076660000016d008b"), 58 | }, 59 | ) 60 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_computed.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | @dataclasses.dataclass 9 | class ComputedTest(cst.DataclassMixin): 10 | type_int: int = cst.csfield(cs.Computed(lambda ctx: 50)) 11 | type_float: float = cst.csfield(cs.Computed(lambda ctx: 80.0)) 12 | type_bool: bool = cst.csfield(cs.Computed(lambda ctx: True)) 13 | type_bytes: bytes = cst.csfield(cs.Computed(lambda ctx: bytes([0x00, 0xAB]))) 14 | type_bytearray: bytearray = cst.csfield( 15 | cs.Computed(lambda ctx: bytearray([0x00, 0xAB, 0xFF])) 16 | ) 17 | 18 | 19 | constr = cst.DataclassStruct(ComputedTest) 20 | 21 | gallery_item = GalleryItem( 22 | construct=constr, 23 | example_binarys={"Zeros": bytes([])}, 24 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_const.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | class CarBrand(cst.EnumBase): 9 | Porsche = 0 10 | Audi = 4 11 | VW = 7 12 | 13 | 14 | @dataclasses.dataclass 15 | class Car(cst.DataclassMixin): 16 | const_brand: CarBrand = cst.csfield( 17 | cs.Const(CarBrand.Audi, cst.TEnum(cs.Int8ul, CarBrand)) 18 | ) 19 | const_int: int = cst.csfield(cs.Const(15, cs.Int8ul)) 20 | const_bytes: bytes = cst.csfield(cs.Const(b"1234")) 21 | 22 | 23 | constr = cst.DataclassStruct(Car) 24 | 25 | gallery_item = GalleryItem( 26 | construct=constr, 27 | example_binarys={ 28 | "1": b"\x04\x0f1234", 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_dataclass_bit_struct.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | from . import GalleryItem 5 | 6 | 7 | @dataclasses.dataclass 8 | class TBitsStructTest(cst.DataclassMixin): 9 | @dataclasses.dataclass 10 | class Nested(cst.DataclassMixin): 11 | test_bit: int = cst.csfield(cs.Bit) 12 | test_nibble: int = cst.csfield(cs.Nibble) 13 | test_bits_1: int = cst.csfield(cs.BitsInteger(3)) 14 | test_bits_2: int = cst.csfield(cs.BitsInteger(6)) 15 | test_bits_3: int = cst.csfield(cs.BitsInteger(2)) 16 | 17 | nested: Nested = cst.csfield(cs.ByteSwapped(cst.DataclassBitStruct(Nested))) 18 | 19 | nested_reverse: Nested = cst.csfield(cst.DataclassBitStruct(Nested, reverse=True)) 20 | 21 | 22 | constr = cst.DataclassStruct(TBitsStructTest) 23 | 24 | gallery_item = GalleryItem( 25 | construct=constr, 26 | example_binarys={ 27 | "Zeros": bytes(constr.sizeof()), 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_dataclass_struct.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | @dataclasses.dataclass 9 | class DataclassStructTest(cst.DataclassMixin): 10 | width: int = cst.csfield(cs.Int8sb, doc="Das hier ist die Dokumentation von 'width'") 11 | height: int = cst.csfield(cs.Int8sb, doc="Und hier von 'height") 12 | update: int = cst.csfield(cs.Int8sb) 13 | 14 | @dataclasses.dataclass 15 | class Nested(cst.DataclassMixin): 16 | nested_width: int = cst.csfield(cs.Int16sb) 17 | nested_height: int = cst.csfield(cs.Int24ub) 18 | nested_bytes: bytes = cst.csfield(cs.Bytes(2)) 19 | nested_array: t.List[int] = cst.csfield(cs.Array(2, cs.Int8sb)) 20 | 21 | nested: Nested = cst.csfield((cst.DataclassStruct(Nested))) 22 | 23 | 24 | constr = cst.DataclassStruct(DataclassStructTest) 25 | 26 | gallery_item = GalleryItem( 27 | construct=constr, 28 | example_binarys={ 29 | "Zeros": bytes(constr.sizeof()), 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_enum.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | constr = cs.Struct( 9 | "brand" / cs.Enum(cs.Int8ul, Porsche=0, Audi=4, VW=7), 10 | "wheels" / cs.Int8ul, 11 | "color" / cs.Enum(cs.Int8ul, Red=1, Green=10, Blue=11, Black=12), 12 | "long_list" 13 | / cs.Enum( 14 | cs.Int8ul, 15 | Entry0=0, 16 | Entry1=1, 17 | Entry2=2, 18 | Entry3=3, 19 | Entry4=4, 20 | Entry5=5, 21 | Entry6=6, 22 | Entry7=7, 23 | Entry8=8, 24 | Entry9=9, 25 | Entry10=10, 26 | Entry11=11, 27 | Entry12=12, 28 | Entry13=13, 29 | Entry14=14, 30 | Entry15=15, 31 | Entry16=16, 32 | Entry17=17, 33 | Entry18=18, 34 | Entry19=19, 35 | Entry20=20, 36 | Entry21=21, 37 | Entry22=22, 38 | Entry23=23, 39 | Entry24=24, 40 | Entry25=25, 41 | Entry26=26, 42 | Entry27=27, 43 | Entry28=28, 44 | Entry29=29, 45 | Entry30=30, 46 | Entry31=31, 47 | Entry32=32, 48 | Entry33=33, 49 | Entry34=34, 50 | Entry35=35, 51 | Entry36=36, 52 | Entry37=37, 53 | Entry38=38, 54 | Entry39=39, 55 | Entry40=40, 56 | Entry41=41, 57 | Entry42=42, 58 | Entry43=43, 59 | Entry44=44, 60 | Entry45=45, 61 | Entry46=46, 62 | Entry47=47, 63 | Entry48=48, 64 | Entry49=49, 65 | Entry50=50, 66 | ), 67 | ) 68 | 69 | 70 | gallery_item = GalleryItem( 71 | construct=constr, 72 | example_binarys={ 73 | "3": bytes([7, 2, 1, 7]), 74 | "2": bytes([4, 4, 13, 6]), 75 | "1": bytes([4, 4, 12, 5]), 76 | "Zeros": bytes(constr.sizeof()), 77 | }, 78 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_fixedsized.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | 4 | 5 | constr = cs.Struct( 6 | "choice" / cs.Int32ul, 7 | "fixedsized" 8 | / cs.FixedSized( 9 | 5, 10 | cs.Struct( 11 | switch=cs.Switch( 12 | cs.this._.choice, 13 | cases={ 14 | 1: cs.Int8ul, 15 | 2: cs.Int16ul, 16 | }, 17 | default=cs.Pass, 18 | ), 19 | bytes=cs.GreedyBytes, 20 | ), 21 | ), 22 | # "fixedsized" / cs.FixedSized(5, cs.GreedyBytes), 23 | "bytes2" / cs.Bytes(5), 24 | ) 25 | 26 | 27 | gallery_item = GalleryItem( 28 | construct=constr, 29 | example_binarys={ 30 | "1": bytes([1, 0, 0, 0, 0, 1, 2, 3, 4, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5]), 31 | "2": bytes([2, 0, 0, 0, 0, 1, 2, 3, 4, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5]), 32 | "Zeros": bytes(15), 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_flag.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | 4 | 5 | constr = cs.Struct( 6 | "flag0" / cs.Flag, 7 | "flag1" / cs.Flag, 8 | 9 | "bit_struct" / cs.BitStruct( 10 | "bit_flag0" / cs.Flag, 11 | "bit_flag1" / cs.Flag, 12 | "bit_flag2" / cs.Flag, 13 | "bit_flag3" / cs.Flag, 14 | cs.Padding(4) 15 | ) 16 | ) 17 | 18 | 19 | gallery_item = GalleryItem( 20 | construct=constr, 21 | example_binarys={ 22 | "1": bytes([0x01, 0x02, 0x40]), 23 | "Zeros": bytes(3), 24 | }, 25 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_flagsenum.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | constr = cs.Struct( 9 | "permissions" / cs.FlagsEnum(cs.Int8ul, R=4, W=2, X=1), 10 | "days" 11 | / cs.FlagsEnum( 12 | cs.Int8ul, 13 | Monday=(1 << 0), 14 | Tuesday=(1 << 1), 15 | Wednesday=(1 << 2), 16 | Thursday=(1 << 3), 17 | Friday=(1 << 4), 18 | Saturday=(1 << 5), 19 | Sunday=(1 << 6), 20 | ), 21 | "long_list" 22 | / cs.FlagsEnum( 23 | cs.Int64ul, 24 | Entry0=(1 << 0), 25 | Entry1=(1 << 1), 26 | Entry2=(1 << 2), 27 | Entry3=(1 << 3), 28 | Entry4=(1 << 4), 29 | Entry5=(1 << 5), 30 | Entry6=(1 << 6), 31 | Entry7=(1 << 7), 32 | Entry8=(1 << 8), 33 | Entry9=(1 << 9), 34 | Entry10=(1 << 10), 35 | Entry11=(1 << 11), 36 | Entry12=(1 << 12), 37 | Entry13=(1 << 13), 38 | Entry14=(1 << 14), 39 | Entry15=(1 << 15), 40 | Entry16=(1 << 16), 41 | Entry17=(1 << 17), 42 | Entry18=(1 << 18), 43 | Entry19=(1 << 19), 44 | Entry20=(1 << 20), 45 | Entry21=(1 << 21), 46 | Entry22=(1 << 22), 47 | Entry23=(1 << 23), 48 | Entry24=(1 << 24), 49 | Entry25=(1 << 25), 50 | Entry26=(1 << 26), 51 | Entry27=(1 << 27), 52 | Entry28=(1 << 28), 53 | Entry29=(1 << 29), 54 | Entry30=(1 << 30), 55 | Entry31=(1 << 31), 56 | Entry32=(1 << 32), 57 | Entry33=(1 << 33), 58 | Entry34=(1 << 34), 59 | Entry35=(1 << 35), 60 | Entry36=(1 << 36), 61 | Entry37=(1 << 37), 62 | Entry38=(1 << 38), 63 | Entry39=(1 << 39), 64 | Entry40=(1 << 40), 65 | Entry41=(1 << 41), 66 | Entry42=(1 << 42), 67 | Entry43=(1 << 43), 68 | Entry44=(1 << 44), 69 | Entry45=(1 << 45), 70 | Entry46=(1 << 46), 71 | Entry47=(1 << 47), 72 | Entry48=(1 << 48), 73 | Entry49=(1 << 49), 74 | Entry50=(1 << 50), 75 | ), 76 | ) 77 | 78 | gallery_item = GalleryItem( 79 | construct=constr, 80 | example_binarys={ 81 | "2": constr.build( 82 | { 83 | "permissions": constr.permissions.R | constr.permissions.W, 84 | "days": constr.days.Monday | constr.days.Sunday, 85 | "long_list": constr.long_list.Entry49 | constr.long_list.Entry50, 86 | } 87 | ), 88 | "1": constr.build( 89 | { 90 | "permissions": constr.permissions.R, 91 | "days": constr.days.Monday, 92 | "long_list": constr.long_list.Entry0, 93 | } 94 | ), 95 | "Zeros": constr.build( 96 | { 97 | "permissions": 0, 98 | "days": 0, 99 | "long_list": 0, 100 | } 101 | ), 102 | }, 103 | ) 104 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_focusedseq.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | 3 | import construct as cs 4 | import construct_typed as cst 5 | 6 | from . import GalleryItem 7 | 8 | 9 | @dataclasses.dataclass 10 | class Image(cst.DataclassMixin): 11 | width: int = cst.csfield(cs.Int8sb) 12 | height: int = cst.csfield(cs.Int8sb) 13 | pixels: bytes = cst.csfield(cs.Bytes(4)) 14 | 15 | 16 | constr = cs.FocusedSeq( 17 | "image", 18 | "const" / cs.Const(b"MZ"), 19 | "image" / cst.DataclassStruct(Image), 20 | "const2" / cs.Const(b"MZ"), 21 | ) 22 | 23 | 24 | gallery_item = GalleryItem( 25 | construct=constr, 26 | example_binarys={ 27 | "1": b"MZ\x10\x20\x00\x00\x00\x00MZ", 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_greedyrange.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | @dataclasses.dataclass 9 | class Entry(cst.DataclassMixin): 10 | id: int = cst.csfield(cs.Int8sb) 11 | width: int = cst.csfield(cs.Int8sb) 12 | height: int = cst.csfield(cs.Int8sb) 13 | _protected: int = cst.csfield(cs.Int8sb) 14 | 15 | 16 | constr = cs.GreedyRange(cst.DataclassStruct(Entry)) 17 | 18 | gallery_item = GalleryItem( 19 | construct=constr, 20 | example_binarys={ 21 | "5": bytes([1, 10, 10, 1, 2, 10, 10, 2, 3, 18, 18, 3, 4, 10, 10, 4, 5, 20, 20, 5]), 22 | "1": bytes([1, 10, 10, 1]), 23 | "Zeros": bytes([]), 24 | }, 25 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_ifthenelse.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | @dataclasses.dataclass 9 | class IfThenElse(cst.DataclassMixin): 10 | @dataclasses.dataclass 11 | class Then(cst.DataclassMixin): 12 | then_1: int = cst.csfield(cs.Int16sb) 13 | then_2: int = cst.csfield(cs.Int16sb) 14 | 15 | @dataclasses.dataclass 16 | class Else(cst.DataclassMixin): 17 | else_1: int = cst.csfield(cs.Int8sb) 18 | else_2: int = cst.csfield(cs.Int8sb) 19 | else_3: int = cst.csfield(cs.Int8sb) 20 | else_4: int = cst.csfield(cs.Int8sb) 21 | 22 | choice: int = cst.csfield(cs.Int8ub) 23 | if_then_else: t.Union[Then, Else] = cst.csfield( 24 | cs.IfThenElse( 25 | cs.this.choice == 0, cst.DataclassStruct(Then), cst.DataclassStruct(Else) 26 | ) 27 | ) 28 | 29 | 30 | constr = cst.DataclassStruct(IfThenElse) 31 | 32 | gallery_item = GalleryItem( 33 | construct=constr, 34 | example_binarys={ 35 | "Zeros": bytes([0, 0, 0, 0, 0]), 36 | "1": bytes([0, 1, 2, 1, 2]), 37 | }, 38 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_ifthenelse_nested_switch.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | @dataclasses.dataclass 9 | class IfThenElse(cst.DataclassMixin): 10 | @dataclasses.dataclass 11 | class Then(cst.DataclassMixin): 12 | then_1: int = cst.csfield(cs.Int16sb) 13 | then_2: int = cst.csfield(cs.Int16sb) 14 | 15 | @dataclasses.dataclass 16 | class Else(cst.DataclassMixin): 17 | else_1: int = cst.csfield(cs.Int8sb) 18 | else_2: int = cst.csfield(cs.Int8sb) 19 | else_3: int = cst.csfield(cs.Int8sb) 20 | else_4: int = cst.csfield(cs.Int8sb) 21 | 22 | choice: int = cst.csfield(cs.Int8ub) 23 | if_then_else: t.Union[Then, Else, bytes] = cst.csfield( 24 | cs.IfThenElse( 25 | cs.this.choice == 0, 26 | cs.Switch( 27 | 1, 28 | cases={1: cst.DataclassStruct(Then)}, 29 | default=cs.GreedyBytes, 30 | ), 31 | cst.DataclassStruct(Else), 32 | ) 33 | ) 34 | 35 | 36 | constr = cst.DataclassStruct(IfThenElse) 37 | 38 | gallery_item = GalleryItem( 39 | construct=constr, 40 | example_binarys={ 41 | "Zeros": bytes([0, 0, 0, 0, 0]), 42 | "1": bytes([1, 1, 2, 1, 2]), 43 | }, 44 | ) 45 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_nullstripped.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | 4 | 5 | constr = cs.Struct( 6 | "null_stripped" / cs.NullStripped(cs.GreedyBytes, pad=b"\x00"), 7 | ) 8 | 9 | 10 | gallery_item = GalleryItem( 11 | construct=constr, 12 | example_binarys={ 13 | "1": b"Hallo Welt!\x00\x00\x00", 14 | "Zeros": bytes(15), 15 | }, 16 | ) 17 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_nullterminated.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | 4 | 5 | constr = cs.Struct( 6 | "null_terminated" / cs.NullTerminated(cs.Int16ul, term=b"\x00"), 7 | "remaining" / cs.GreedyBytes, 8 | ) 9 | 10 | 11 | gallery_item = GalleryItem( 12 | construct=constr, 13 | example_binarys={ 14 | "1": bytes([10, 20, 0, 0xFF, 0xFF]), 15 | "Zeros": bytes(15), 16 | }, 17 | ) 18 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_padded.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | 4 | 5 | constr = cs.Struct( 6 | "padded" / cs.Padded(5, cs.Bytes(3)), 7 | "padding" / cs.Padding(5), 8 | ) 9 | 10 | 11 | gallery_item = GalleryItem( 12 | construct=constr, 13 | example_binarys={ 14 | "1": bytes([0, 1, 2, 3, 4, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5]), 15 | "Zeros": bytes(10), 16 | }, 17 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_padded_string.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | @dataclasses.dataclass 9 | class DataclassStructTest(cst.DataclassMixin): 10 | value: str = cst.csfield( 11 | cs.PaddedString(10, "ascii"), 12 | ) 13 | 14 | 15 | constr = cst.DataclassStruct(DataclassStructTest) 16 | 17 | gallery_item = GalleryItem( 18 | construct=constr, 19 | example_binarys={ 20 | "0": b"Hello".ljust(10, b"\x00"), 21 | "Zeros": bytes(constr.sizeof()), 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_pass.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | 4 | 5 | constr = cs.Struct( 6 | "value1" / cs.Int8sb, 7 | "pass" / cs.Pass, 8 | "value2" / cs.Int8sb, 9 | ) 10 | 11 | 12 | gallery_item = GalleryItem( 13 | construct=constr, 14 | example_binarys={ 15 | "Zeros": bytes(2), 16 | "1": b"12", 17 | }, 18 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_pointer_peek_seek_tell.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | constr = cs.Struct( 9 | "signature" / cs.Bytes(23), 10 | "data_start_pos" / cs.Tell, 11 | "data_peek" / cs.Peek(cs.Array(5, cs.Int24ub)), 12 | cs.Seek(15, 1), 13 | "pos_after_seek" / cs.Tell, 14 | "data_pointer" 15 | / cs.Pointer(lambda ctx: ctx.data_start_pos, cs.Array(5, cs.Int24ub)), 16 | ) 17 | 18 | 19 | gallery_item = GalleryItem( 20 | construct=constr, 21 | example_binarys={ 22 | "Zeros": bytes(23 + 15), 23 | "1": b"TestPointerPeekSeekTell0123456789abcde", 24 | }, 25 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_renamed.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | @dataclasses.dataclass 9 | class RenamedTest(cst.DataclassMixin): 10 | doc: int = cst.csfield(cs.Int8sb, doc="This is a one line documentation") 11 | doc_multiline: int = cst.csfield( 12 | cs.Int8sb, 13 | doc=""" 14 | This is a multiline documentation. 15 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. 16 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 17 | 18 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 19 | """, 20 | ) 21 | doc2: int = cst.csfield(cs.Int8sb * "This is a one line documentation") 22 | doc2_multiline: int = cst.csfield( 23 | cs.Int8sb 24 | * """ 25 | This is a multiline documentation. 26 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. 27 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 28 | 29 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 30 | """ 31 | ) 32 | 33 | 34 | constr = "renamed_test" / cst.DataclassStruct(RenamedTest) 35 | 36 | gallery_item = GalleryItem( 37 | construct=constr, 38 | example_binarys={ 39 | "Zeros": bytes(constr.sizeof()), 40 | }, 41 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_select.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | 3 | import construct as cs 4 | import construct_typed as cst 5 | 6 | from . import GalleryItem 7 | 8 | 9 | @dataclasses.dataclass 10 | class BigImage(cst.DataclassMixin): 11 | big_width: int = cst.csfield(cs.Int8sb) 12 | big_height: int = cst.csfield(cs.Int8sb) 13 | big_pixels: bytes = cst.csfield(cs.Bytes(10)) 14 | 15 | 16 | @dataclasses.dataclass 17 | class SmallImage(cst.DataclassMixin): 18 | small_width: int = cst.csfield(cs.Int8sb) 19 | small_height: int = cst.csfield(cs.Int8sb) 20 | small_pixels: bytes = cst.csfield(cs.Bytes(4)) 21 | 22 | 23 | constr = cs.Select( 24 | cst.DataclassStruct(BigImage), 25 | cst.DataclassStruct(SmallImage), 26 | ) 27 | 28 | gallery_item = GalleryItem( 29 | construct=constr, 30 | example_binarys={ 31 | "Big": b"\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 32 | "Small": b"\x01\x08\x00\x00\x00\x00", 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_select_complex.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import typing as t 3 | 4 | import construct as cs 5 | import construct_typed as cst 6 | 7 | from . import GalleryItem 8 | 9 | 10 | @dataclasses.dataclass 11 | class BigImage(cst.DataclassMixin): 12 | big_width: int = cst.csfield(cs.Int8sb) 13 | big_height: int = cst.csfield(cs.Int8sb) 14 | big_pixels: bytes = cst.csfield(cs.Bytes(10)) 15 | 16 | 17 | @dataclasses.dataclass 18 | class SmallImage(cst.DataclassMixin): 19 | small_width: int = cst.csfield(cs.Int8sb) 20 | small_height: int = cst.csfield(cs.Int8sb) 21 | small_pixels: bytes = cst.csfield(cs.Bytes(4)) 22 | 23 | 24 | @dataclasses.dataclass 25 | class Image(cst.DataclassMixin): 26 | is_big: int = cst.csfield(cs.Int8ub) 27 | data: t.Union[BigImage, SmallImage] = cst.csfield( 28 | cs.Select( 29 | cs.IfThenElse( 30 | condfunc=cs.this.is_big == 1, 31 | thensubcon=cs.Switch( 32 | 1, 33 | cases={ 34 | 1: cst.DataclassStruct(BigImage), 35 | 2: cst.DataclassStruct(BigImage), 36 | 3: cst.DataclassStruct(BigImage), 37 | 4: cst.DataclassStruct(BigImage), 38 | }, 39 | default=cs.StopIf(True), 40 | ), 41 | elsesubcon=cst.DataclassStruct(SmallImage), 42 | ), 43 | cs.GreedyBytes, 44 | ) 45 | ) 46 | 47 | 48 | constr = cst.DataclassStruct(Image) 49 | 50 | 51 | gallery_item = GalleryItem( 52 | construct=constr, 53 | example_binarys={ 54 | "Big": b"\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 55 | "Small": b"\x00\x01\x08\x00\x00\x00\x00", 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_stringencodded.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | from . import GalleryItem 3 | from typing import Dict, Any 4 | 5 | ENCODDINGS = dict( 6 | ASCII="ascii", 7 | UTF8="utf8", 8 | UTF16="utf16", 9 | MANDARIN="gb2312", 10 | ARABIC="iso8859_6", 11 | RUSSIAN="iso8859_5", 12 | JAPANESE="shift_jis", 13 | PORTUGUESE="cp860", 14 | ) 15 | 16 | ENCODDINGS_NUMBER = dict([(key,number,) for number,(key,_) in enumerate(ENCODDINGS.items())]) 17 | 18 | def text_helper(encodding :str, text: str) -> bytes: 19 | return ENCODDINGS_NUMBER[encodding].to_bytes(1, "little") + text.encode(ENCODDINGS[encodding]) 20 | 21 | def generate_all_string_encodded() -> Dict[str, Any]: 22 | return dict([(key,cs.StringEncoded(cs.GreedyBytes, value),) for key,value in ENCODDINGS.items()]) 23 | 24 | 25 | constr = cs.Struct( 26 | "encodding" / cs.Enum(cs.Int8ub, **ENCODDINGS_NUMBER), 27 | "string" 28 | / cs.Switch( 29 | cs.this.encodding, 30 | cases=generate_all_string_encodded(), 31 | ), 32 | ) 33 | 34 | 35 | gallery_item = GalleryItem( 36 | construct=constr, 37 | example_binarys={ 38 | "English": text_helper("ASCII", "hello world"), 39 | "Mandarin" : text_helper("MANDARIN", "你好世界"), 40 | "HINDI" : text_helper("UTF8", "नमस्ते दुनिया"), 41 | "SPANISH" : text_helper("ASCII", "Hola Mundo"), 42 | "FRENCH" : text_helper("ASCII", "Bonjour le monde"), 43 | "ARABIC" : text_helper("ARABIC", "مرحبا بالعالم"), 44 | "RUSSIAN" : text_helper("RUSSIAN", "Привет мир"), 45 | "PORTUGUESE" : text_helper("PORTUGUESE", "Olá Mundo"), 46 | "INDONESIAN" : text_helper("ASCII", "Halo Dunia"), 47 | "JAPANESE" : text_helper("JAPANESE", "こんにちは世界"), 48 | "emoji": text_helper("UTF8", "🙋🏼🌎"), 49 | "Zeros": bytes(8), 50 | }, 51 | ) 52 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_switch.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | constr = cs.Struct( 9 | "choice" 10 | / cs.Enum( 11 | cs.Int8ub, 12 | USE_CHOICE1=1, 13 | USE_CHOICE2=2, 14 | ), 15 | "switch" 16 | / cs.Switch( 17 | cs.this.choice, 18 | cases={ 19 | "USE_CHOICE1": cs.Default( 20 | cs.Struct( 21 | "case1_1" / cs.Int16sb, 22 | "case1_2" / cs.Int16sb, 23 | ), 24 | dict(case1_1=0, case1_2=0), 25 | ), 26 | "USE_CHOICE2": cs.Default( 27 | cs.Struct( 28 | "case2_1" / cs.Int8sb, 29 | "case2_2" / cs.Int8sb, 30 | "case2_3" / cs.Int8sb, 31 | "case2_4" / cs.Int8sb, 32 | ), 33 | dict(case2_1=0, case2_2=0, case2_3=0, case2_4=0), 34 | ), 35 | }, 36 | default=cs.Default( 37 | cs.Struct( 38 | "case_default_1" / cs.Int32sb, 39 | ), 40 | dict(case_default_1=0), 41 | ), 42 | ), 43 | "switch_without_default" 44 | / cs.Switch( 45 | cs.this.choice, 46 | cases={ 47 | "USE_CHOICE1": cs.Default( 48 | cs.Struct( 49 | "case1_1" / cs.Int16sb, 50 | "case1_2" / cs.Int16sb, 51 | ), 52 | dict(case1_1=0, case1_2=0), 53 | ), 54 | "USE_CHOICE2": cs.Default( 55 | cs.Struct( 56 | "case2_1" / cs.Int8sb, 57 | "case2_2" / cs.Int8sb, 58 | "case2_3" / cs.Int8sb, 59 | "case2_4" / cs.Int8sb, 60 | ), 61 | dict(case2_1=0, case2_2=0, case2_3=0, case2_4=0), 62 | ), 63 | }, 64 | default=cs.Default(cs.Pass, None), 65 | ), 66 | ) 67 | 68 | 69 | gallery_item = GalleryItem( 70 | construct=constr, 71 | example_binarys={ 72 | "Default": bytes([0, 0, 0, 0, 0]), 73 | "Case 1": bytes([1, 1, 2, 1, 2, 5, 6, 7, 8]), 74 | "Case 2": bytes([2, 1, 2, 1, 2, 5, 6, 7, 8]), 75 | }, 76 | ) 77 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_switch_dataclass.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | class Choice(cst.EnumBase): 9 | USE_CHOICE1 = 1 10 | USE_CHOICE2 = 2 11 | 12 | 13 | @dataclasses.dataclass 14 | class SwitchTest(cst.DataclassMixin): 15 | @dataclasses.dataclass 16 | class Case1(cst.DataclassMixin): 17 | case1_1: int = cst.csfield(cs.Int16sb) 18 | case1_2: int = cst.csfield(cs.Int16sb) 19 | 20 | @classmethod 21 | def get_default(cls): 22 | return cls(0, 0) 23 | 24 | @dataclasses.dataclass 25 | class Case2(cst.DataclassMixin): 26 | case2_1: int = cst.csfield(cs.Int8sb) 27 | case2_2: int = cst.csfield(cs.Int8sb) 28 | case2_3: int = cst.csfield(cs.Int8sb) 29 | case2_4: int = cst.csfield(cs.Int8sb) 30 | 31 | @classmethod 32 | def get_default(cls): 33 | return cls(0, 0, 0, 0) 34 | 35 | @dataclasses.dataclass 36 | class CaseDefault(cst.DataclassMixin): 37 | case_default_1: int = cst.csfield(cs.Int32sb) 38 | 39 | @classmethod 40 | def get_default(cls): 41 | return cls(0) 42 | 43 | choice: int = cst.csfield(cst.TEnum(cs.Int8ub, Choice)) 44 | switch: t.Union[Case1, Case2, CaseDefault] = cst.csfield( 45 | cs.Switch( 46 | cs.this.choice, 47 | cases={ 48 | 1: cs.Default(cst.DataclassStruct(Case1), Case1.get_default()), 49 | 2: cs.Default(cst.DataclassStruct(Case2), Case2.get_default()), 50 | }, 51 | default=cs.Default( 52 | cst.DataclassStruct(CaseDefault), CaseDefault.get_default() 53 | ), 54 | ) 55 | ) 56 | 57 | switch_without_default: t.Union[Case1, Case2, None] = cst.csfield( 58 | cs.Switch( 59 | cs.this.choice, 60 | cases={ 61 | 1: cs.Default(cst.DataclassStruct(Case1), Case1.get_default()), 62 | 2: cs.Default(cst.DataclassStruct(Case2), Case2.get_default()), 63 | }, 64 | default=cs.Default(cs.Pass, None), 65 | ) 66 | ) 67 | 68 | 69 | constr = cst.DataclassStruct(SwitchTest) 70 | 71 | gallery_item = GalleryItem( 72 | construct=constr, 73 | example_binarys={ 74 | "Default": bytes([0, 0, 0, 0, 0]), 75 | "Case 1": bytes([1, 1, 2, 1, 2, 5, 6, 7, 8]), 76 | "Case 2": bytes([2, 1, 2, 1, 2, 5, 6, 7, 8]), 77 | }, 78 | ) 79 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_tenum.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | class CarBrand(cst.EnumBase): 9 | Porsche = 0 10 | Audi = 4 11 | VW = 7 12 | 13 | 14 | class CarColor(cst.EnumBase): 15 | Red = 1 16 | Green = 10 17 | Blue = 11 18 | Black = 12 19 | 20 | 21 | class LongList(cst.EnumBase): 22 | Entry0 = 0 23 | Entry1 = 1 24 | Entry2 = 2 25 | Entry3 = 3 26 | Entry4 = 4 27 | Entry5 = 5 28 | Entry6 = 6 29 | Entry7 = 7 30 | Entry8 = 8 31 | Entry9 = 9 32 | Entry10 = 10 33 | Entry11 = 11 34 | Entry12 = 12 35 | Entry13 = 13 36 | Entry14 = 14 37 | Entry15 = 15 38 | Entry16 = 16 39 | Entry17 = 17 40 | Entry18 = 18 41 | Entry19 = 19 42 | Entry20 = 20 43 | Entry21 = 21 44 | Entry22 = 22 45 | Entry23 = 23 46 | Entry24 = 24 47 | Entry25 = 25 48 | Entry26 = 26 49 | Entry27 = 27 50 | Entry28 = 28 51 | Entry29 = 29 52 | Entry30 = 30 53 | Entry31 = 31 54 | Entry32 = 32 55 | Entry33 = 33 56 | Entry34 = 34 57 | Entry35 = 35 58 | Entry36 = 36 59 | Entry37 = 37 60 | Entry38 = 38 61 | Entry39 = 39 62 | Entry40 = 40 63 | Entry41 = 41 64 | Entry42 = 42 65 | Entry43 = 43 66 | Entry44 = 44 67 | Entry45 = 45 68 | Entry46 = 46 69 | Entry47 = 47 70 | Entry48 = 48 71 | Entry49 = 49 72 | Entry50 = 50 73 | 74 | 75 | @dataclasses.dataclass 76 | class Car(cst.DataclassMixin): 77 | brand: CarBrand = cst.csfield(cst.TEnum(cs.Int8ul, CarBrand)) 78 | wheels: int = cst.csfield(cs.Int8ul) 79 | color: CarColor = cst.csfield(cst.TEnum(cs.Int8ul, CarColor)) 80 | long_list: LongList = cst.csfield(cst.TEnum(cs.Int8ul, LongList)) 81 | 82 | 83 | constr = cst.DataclassStruct(Car) 84 | 85 | gallery_item = GalleryItem( 86 | construct=constr, 87 | example_binarys={ 88 | "3": bytes([7, 2, 1, 7]), 89 | "2": bytes([4, 4, 13, 6]), 90 | "1": bytes([4, 4, 12, 5]), 91 | "Zeros": bytes(constr.sizeof()), 92 | }, 93 | ) -------------------------------------------------------------------------------- /construct_editor/gallery/test_tflagsenum.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | from . import GalleryItem 6 | 7 | 8 | class Permission(cst.FlagsEnumBase): 9 | R = 4 10 | W = 2 11 | X = 1 12 | RWX = 7 13 | 14 | 15 | class Day(cst.FlagsEnumBase): 16 | Monday = 1 << 0 17 | Tuesday = 1 << 1 18 | Wednesday = 1 << 2 19 | Thursday = 1 << 3 20 | Friday = 1 << 4 21 | Saturday = 1 << 5 22 | Sunday = 1 << 6 23 | 24 | 25 | class LongList(cst.FlagsEnumBase): 26 | Entry0 = 1 << 0 27 | Entry1 = 1 << 1 28 | Entry2 = 1 << 2 29 | Entry3 = 1 << 3 30 | Entry4 = 1 << 4 31 | Entry5 = 1 << 5 32 | Entry6 = 1 << 6 33 | Entry7 = 1 << 7 34 | Entry8 = 1 << 8 35 | Entry9 = 1 << 9 36 | Entry10 = 1 << 10 37 | Entry11 = 1 << 11 38 | Entry12 = 1 << 12 39 | Entry13 = 1 << 13 40 | Entry14 = 1 << 14 41 | Entry15 = 1 << 15 42 | Entry16 = 1 << 16 43 | Entry17 = 1 << 17 44 | Entry18 = 1 << 18 45 | Entry19 = 1 << 19 46 | Entry20 = 1 << 20 47 | Entry21 = 1 << 21 48 | Entry22 = 1 << 22 49 | Entry23 = 1 << 23 50 | Entry24 = 1 << 24 51 | Entry25 = 1 << 25 52 | Entry26 = 1 << 26 53 | Entry27 = 1 << 27 54 | Entry28 = 1 << 28 55 | Entry29 = 1 << 29 56 | Entry30 = 1 << 30 57 | Entry31 = 1 << 31 58 | Entry32 = 1 << 32 59 | Entry33 = 1 << 33 60 | Entry34 = 1 << 34 61 | Entry35 = 1 << 35 62 | Entry36 = 1 << 36 63 | Entry37 = 1 << 37 64 | Entry38 = 1 << 38 65 | Entry39 = 1 << 39 66 | Entry40 = 1 << 40 67 | Entry41 = 1 << 41 68 | Entry42 = 1 << 42 69 | Entry43 = 1 << 43 70 | Entry44 = 1 << 44 71 | Entry45 = 1 << 45 72 | Entry46 = 1 << 46 73 | Entry47 = 1 << 47 74 | Entry48 = 1 << 48 75 | Entry49 = 1 << 49 76 | Entry50 = 1 << 50 77 | 78 | 79 | @dataclasses.dataclass 80 | class FlagsEnumTest(cst.DataclassMixin): 81 | permissions: Permission = cst.csfield(cst.TFlagsEnum(cs.Int8ul, Permission)) 82 | days: Day = cst.csfield(cst.TFlagsEnum(cs.Int8ul, Day)) 83 | long_list: LongList = cst.csfield(cst.TFlagsEnum(cs.Int64ul, LongList)) 84 | days2: Day = cst.csfield(cst.TFlagsEnum(cs.Int8ul, Day)) 85 | 86 | 87 | constr = cst.DataclassStruct(FlagsEnumTest) 88 | 89 | gallery_item = GalleryItem( 90 | construct=constr, 91 | example_binarys={ 92 | "2": constr.build( 93 | FlagsEnumTest( 94 | permissions=Permission.R | Permission.W, 95 | days=Day.Monday | Day.Sunday, 96 | long_list=LongList.Entry49 | LongList.Entry50, 97 | days2=Day.Monday | Day.Sunday, 98 | ) 99 | ), 100 | "1": constr.build( 101 | FlagsEnumTest( 102 | permissions=Permission.R, 103 | days=Day.Monday, 104 | long_list=LongList.Entry0, 105 | days2=Day.Monday, 106 | ) 107 | ), 108 | "Zeros": constr.build( 109 | FlagsEnumTest( 110 | permissions=Permission(0), 111 | days=Day(0), 112 | long_list=LongList(0), 113 | days2=Day(0), 114 | ) 115 | ), 116 | }, 117 | ) 118 | -------------------------------------------------------------------------------- /construct_editor/gallery/test_timestamp.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | import construct_typed as cst 3 | import dataclasses 4 | import typing as t 5 | import arrow 6 | from . import GalleryItem 7 | 8 | 9 | @dataclasses.dataclass 10 | class Timestamps(cst.DataclassMixin): 11 | time_int8: arrow.Arrow = cst.csfield( 12 | cs.Timestamp(cs.Int8ul, unit=1, epoch=arrow.Arrow(2020, 1, 1)) 13 | ) 14 | time_int16: arrow.Arrow = cst.csfield( 15 | cs.Timestamp(cs.Int16ul, unit=1, epoch=arrow.Arrow(2019, 1, 1)) 16 | ) 17 | time_int32: arrow.Arrow = cst.csfield( 18 | cs.Timestamp(cs.Int32ul, unit=1, epoch=arrow.Arrow(2080, 1, 1)) 19 | ) 20 | 21 | 22 | constr = cst.DataclassStruct(Timestamps) 23 | 24 | gallery_item = GalleryItem( 25 | construct=constr, 26 | example_binarys={ 27 | "Zeros": bytes(constr.sizeof()), 28 | "1": bytes([4, 4, 12, 4, 4, 4, 4]), 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /construct_editor/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import typing as t 3 | from pathlib import Path 4 | from types import TracebackType 5 | 6 | import wx 7 | from wx.lib.embeddedimage import PyEmbeddedImage 8 | 9 | import construct_editor.gallery.example_cmd_resp 10 | import construct_editor.gallery.example_ipstack 11 | import construct_editor.gallery.example_pe32coff 12 | import construct_editor.gallery.test_aligned 13 | import construct_editor.gallery.test_array 14 | import construct_editor.gallery.test_bits_swapped_bitwise 15 | import construct_editor.gallery.test_bitwise 16 | import construct_editor.gallery.test_bytes_greedybytes 17 | import construct_editor.gallery.test_checksum 18 | import construct_editor.gallery.test_compressed 19 | import construct_editor.gallery.test_computed 20 | import construct_editor.gallery.test_const 21 | import construct_editor.gallery.test_dataclass_bit_struct 22 | import construct_editor.gallery.test_dataclass_struct 23 | import construct_editor.gallery.test_enum 24 | import construct_editor.gallery.test_fixedsized 25 | import construct_editor.gallery.test_flag 26 | import construct_editor.gallery.test_flagsenum 27 | import construct_editor.gallery.test_focusedseq 28 | import construct_editor.gallery.test_greedyrange 29 | import construct_editor.gallery.test_ifthenelse 30 | import construct_editor.gallery.test_ifthenelse_nested_switch 31 | import construct_editor.gallery.test_nullstripped 32 | import construct_editor.gallery.test_nullterminated 33 | import construct_editor.gallery.test_padded 34 | import construct_editor.gallery.test_padded_string 35 | import construct_editor.gallery.test_pass 36 | import construct_editor.gallery.test_pointer_peek_seek_tell 37 | import construct_editor.gallery.test_renamed 38 | import construct_editor.gallery.test_select 39 | import construct_editor.gallery.test_select_complex 40 | import construct_editor.gallery.test_stringencodded 41 | import construct_editor.gallery.test_switch 42 | import construct_editor.gallery.test_switch_dataclass 43 | import construct_editor.gallery.test_tenum 44 | import construct_editor.gallery.test_tflagsenum 45 | import construct_editor.gallery.test_timestamp 46 | from construct_editor.wx_widgets import WxConstructHexEditor 47 | from construct_editor.wx_widgets.wx_exception_dialog import ( 48 | ExceptionInfo, 49 | WxExceptionDialog, 50 | ) 51 | 52 | 53 | class ConstructGalleryFrame(wx.Frame): 54 | def __init__(self, *args, **kwargs): 55 | super().__init__(*args, **kwargs) 56 | 57 | self.SetTitle("Construct Gallery") 58 | self.SetSize(1600, 1000) 59 | self.SetIcon(icon.GetIcon()) 60 | self.Center() 61 | 62 | # show uncatched exceptions in a dialog... 63 | sys.excepthook = self.on_uncaught_exception 64 | 65 | self.main_panel = ConstructGallery(self) 66 | 67 | self.status_bar: wx.StatusBar = self.CreateStatusBar() 68 | 69 | def on_uncaught_exception( 70 | self, etype: t.Type[BaseException], value: BaseException, trace: TracebackType 71 | ): 72 | """ 73 | Handler for all unhandled exceptions. 74 | 75 | :param `etype`: the exception type (`SyntaxError`, `ZeroDivisionError`, etc...); 76 | :type `etype`: `Exception` 77 | :param string `value`: the exception error message; 78 | :param string `trace`: the traceback header, if any (otherwise, it prints the 79 | standard Python header: ``Traceback (most recent call last)``. 80 | """ 81 | dial = WxExceptionDialog( 82 | None, "Uncaught Exception...", ExceptionInfo(etype, value, trace) 83 | ) 84 | dial.ShowModal() 85 | dial.Destroy() 86 | 87 | 88 | class ConstructGallery(wx.Panel): 89 | def __init__(self, parent: ConstructGalleryFrame): 90 | super().__init__(parent) 91 | 92 | # Define all galleries ############################################ 93 | self.construct_gallery = { 94 | "################ EXAMPLES ################": None, 95 | "Example: pe32coff": construct_editor.gallery.example_pe32coff.gallery_item, 96 | "Example: ipstack": construct_editor.gallery.example_ipstack.gallery_item, 97 | "Example: Cmd/Resp": construct_editor.gallery.example_cmd_resp.gallery_item, 98 | "################ TESTS ####################": None, 99 | # "## bytes and bits ################": None, 100 | "Test: Bytes/GreedyBytes": construct_editor.gallery.test_bytes_greedybytes.gallery_item, 101 | # "## integers and floats ###########": None, 102 | # "Test: FormatField (TODO)": None, 103 | # "Test: BytesInteger (TODO)": None, 104 | # "Test: BitsInteger (TODO)": None, 105 | "Test: Bitwiese": construct_editor.gallery.test_bitwise.gallery_item, 106 | "Test: BitsSwapped/Bitwiese": construct_editor.gallery.test_bits_swapped_bitwise.gallery_item, 107 | "## strings #######################": None, 108 | "Test: StringEncoded": construct_editor.gallery.test_stringencodded.gallery_item, 109 | "Test: PaddedString": construct_editor.gallery.test_padded_string.gallery_item, 110 | "## mappings ######################": None, 111 | "Test: Flag": construct_editor.gallery.test_flag.gallery_item, 112 | "Test: Enum": construct_editor.gallery.test_enum.gallery_item, 113 | "Test: FlagsEnum": construct_editor.gallery.test_flagsenum.gallery_item, 114 | "Test: TEnum": construct_editor.gallery.test_tenum.gallery_item, 115 | "Test: TFlagsEnum": construct_editor.gallery.test_tflagsenum.gallery_item, 116 | # "Test: Mapping (TODO)": None, 117 | "## structures and sequences ######": None, 118 | # "Test: Struct (TODO)": None, 119 | # "Test: Sequence (TODO)": None, 120 | "Test: DataclassStruct": construct_editor.gallery.test_dataclass_struct.gallery_item, 121 | "Test: DataclassBitStruct": construct_editor.gallery.test_dataclass_bit_struct.gallery_item, 122 | "## arrays ranges and repeaters ######": None, 123 | "Test: Array": construct_editor.gallery.test_array.gallery_item, 124 | "Test: GreedyRange": construct_editor.gallery.test_greedyrange.gallery_item, 125 | # "Test: RepeatUntil (TODO)": None, 126 | "## specials ##########################": None, 127 | "Test: Renamed": construct_editor.gallery.test_renamed.gallery_item, 128 | "## miscellaneous ##########################": None, 129 | "Test: Const": construct_editor.gallery.test_const.gallery_item, 130 | "Test: Computed": construct_editor.gallery.test_computed.gallery_item, 131 | # "Test: Index (TODO)": None, 132 | # "Test: Rebuild (TODO)": None, 133 | # "Test: Default (TODO)": None, 134 | # "Test: Check (TODO)": None, 135 | # "Test: Error (TODO)": None, 136 | "Test: FocusedSeq": construct_editor.gallery.test_focusedseq.gallery_item, 137 | # "Test: Pickled (TODO)": None, 138 | # "Test: Numpy (TODO)": None, 139 | # "Test: NamedTuple (TODO)": None, 140 | "Test: TimestampAdapter": construct_editor.gallery.test_timestamp.gallery_item, 141 | # "Test: Hex (TODO)": None, 142 | # "Test: HexDump (TODO)": None, 143 | "## conditional ##########################": None, 144 | # "Test: Union (TODO)": None, 145 | "Test: Select": construct_editor.gallery.test_select.gallery_item, 146 | "Test: Select (Complex)": construct_editor.gallery.test_select_complex.gallery_item, 147 | "Test: IfThenElse": construct_editor.gallery.test_ifthenelse.gallery_item, 148 | "Test: IfThenElse (Nested Switch)": construct_editor.gallery.test_ifthenelse_nested_switch.gallery_item, 149 | "Test: Switch": construct_editor.gallery.test_switch.gallery_item, 150 | "Test: Switch (Dataclass)": construct_editor.gallery.test_switch_dataclass.gallery_item, 151 | # "Test: StopIf (TODO)": None, 152 | "## alignment and padding ##########################": None, 153 | "Test: Padded": construct_editor.gallery.test_padded.gallery_item, 154 | "Test: Aligned": construct_editor.gallery.test_aligned.gallery_item, 155 | "## stream manipulation ##########################": None, 156 | "Test: Pointer/Peek/Seek/Tell": construct_editor.gallery.test_pointer_peek_seek_tell.gallery_item, 157 | "Test: Pass": construct_editor.gallery.test_pass.gallery_item, 158 | # "Test: Terminated (TODO)": None, 159 | "## tunneling and byte/bit swapping ##########################": None, 160 | # "Test: RawCopy (TODO)": None, 161 | # "Test: Prefixed (TODO)": None, 162 | "Test: FixedSized": construct_editor.gallery.test_fixedsized.gallery_item, 163 | "Test: NullTerminated": construct_editor.gallery.test_nullterminated.gallery_item, 164 | "Test: NullStripped": construct_editor.gallery.test_nullstripped.gallery_item, 165 | # "Test: RestreamData (TODO)": None, 166 | # "Test: Transformed (TODO)": None, 167 | # "Test: Restreamed (TODO)": None, 168 | # "Test: ProcessXor (TODO)": None, 169 | # "Test: ProcessRotateLeft (TODO)": None, 170 | "Test: Checksum": construct_editor.gallery.test_checksum.gallery_item, 171 | "Test: Compressed": construct_editor.gallery.test_compressed.gallery_item, 172 | # "Test: CompressedLZ4 (TODO)": None, 173 | # "Test: Rebuffered (TODO)": None, 174 | # "## lazy equivalents ##########################": None, 175 | # "Test: Lazy (TODO)": None, 176 | # "Test: LazyStruct (TODO)": None, 177 | # "Test: LazyArray (TODO)": None, 178 | # "Test: LazyBound (TODO)": None, 179 | # "## adapters and validators ##########################": None, 180 | # "Test: ExprAdapter (TODO)": None, 181 | # "Test: ExprSymmetricAdapter (TODO)": None, 182 | # "Test: ExprValidator (TODO)": None, 183 | # "Test: Slicing (TODO)": None, 184 | # "Test: Indexing (TODO)": None, 185 | } 186 | self.gallery_selection = 1 187 | default_gallery = list(self.construct_gallery.keys())[self.gallery_selection] 188 | default_gallery_item = self.construct_gallery[default_gallery] 189 | 190 | # Define GUI elements ############################################# 191 | self.sizer = wx.BoxSizer(wx.HORIZONTAL) 192 | vsizer = wx.BoxSizer(wx.VERTICAL) 193 | 194 | # gallery selctor 195 | self.gallery_selector_lbx = wx.ListBox( 196 | self, 197 | wx.ID_ANY, 198 | wx.DefaultPosition, 199 | wx.DefaultSize, 200 | list(self.construct_gallery.keys()), 201 | 0, 202 | name="gallery_selector", 203 | ) 204 | self.gallery_selector_lbx.SetStringSelection(default_gallery) 205 | vsizer.Add(self.gallery_selector_lbx, 1, wx.ALL, 1) 206 | 207 | # reload btn 208 | self.reload_btn = wx.Button( 209 | self, wx.ID_ANY, "Reload", wx.DefaultPosition, wx.DefaultSize, 0 210 | ) 211 | vsizer.Add(self.reload_btn, 0, wx.ALL | wx.EXPAND, 1) 212 | 213 | # line 214 | vsizer.Add(wx.StaticLine(self), 0, wx.TOP | wx.BOTTOM | wx.EXPAND, 5) 215 | 216 | # clear binary 217 | self.clear_binary_btn = wx.Button( 218 | self, wx.ID_ANY, "Clear Binary", wx.DefaultPosition, wx.DefaultSize, 0 219 | ) 220 | vsizer.Add(self.clear_binary_btn, 0, wx.ALL | wx.EXPAND, 1) 221 | 222 | # example selctor 223 | self.example_selector_lbx = wx.ListBox( 224 | self, 225 | wx.ID_ANY, 226 | wx.DefaultPosition, 227 | wx.Size(-1, 100), 228 | list(default_gallery_item.example_binarys.keys()), 229 | 0, 230 | name="gallery_selector", 231 | ) 232 | if len(default_gallery_item.example_binarys) > 0: 233 | self.example_selector_lbx.SetStringSelection( 234 | list(default_gallery_item.example_binarys.keys())[0] 235 | ) 236 | vsizer.Add(self.example_selector_lbx, 0, wx.ALL | wx.EXPAND, 1) 237 | 238 | # load binary from file 239 | self.load_binary_file_btn = wx.Button( 240 | self, 241 | wx.ID_ANY, 242 | "Load Binary from File", 243 | wx.DefaultPosition, 244 | wx.DefaultSize, 245 | 0, 246 | ) 247 | vsizer.Add(self.load_binary_file_btn, 0, wx.ALL | wx.EXPAND, 1) 248 | 249 | self.sizer.Add(vsizer, 0, wx.ALL | wx.EXPAND, 0) 250 | 251 | self.sizer.Add( 252 | wx.StaticLine(self, style=wx.LI_VERTICAL), 253 | 0, 254 | wx.LEFT | wx.RIGHT | wx.EXPAND, 255 | 5, 256 | ) 257 | 258 | # construct hex editor 259 | self.construct_hex_editor = WxConstructHexEditor( 260 | self, 261 | construct=default_gallery_item.construct, 262 | contextkw=default_gallery_item.contextkw, 263 | ) 264 | # self.construct_hex_editor.construct_editor.expand_all() 265 | self.sizer.Add(self.construct_hex_editor, 1, wx.ALL | wx.EXPAND, 0) 266 | 267 | self.SetSizer(self.sizer) 268 | 269 | # Connect Events ################################################## 270 | self.gallery_selector_lbx.Bind( 271 | wx.EVT_LISTBOX, self.on_gallery_selection_changed 272 | ) 273 | self.reload_btn.Bind(wx.EVT_BUTTON, self.on_gallery_selection_changed) 274 | self.clear_binary_btn.Bind(wx.EVT_BUTTON, self.on_clear_binary_clicked) 275 | self.example_selector_lbx.Bind( 276 | wx.EVT_LISTBOX, self.on_example_selection_changed 277 | ) 278 | 279 | self.load_binary_file_btn.Bind(wx.EVT_BUTTON, self.on_load_binary_file_clicked) 280 | 281 | # Emulate Selection Click 282 | self.on_gallery_selection_changed(None) 283 | 284 | def on_gallery_selection_changed(self, event): 285 | selection = self.gallery_selector_lbx.GetStringSelection() 286 | gallery_item = self.construct_gallery[selection] 287 | if gallery_item is None: 288 | self.gallery_selector_lbx.SetSelection( 289 | self.gallery_selection 290 | ) # restore old selection 291 | return 292 | 293 | # save currently shown selection 294 | self.gallery_selection = self.gallery_selector_lbx.GetSelection() 295 | 296 | self.example_selector_lbx.Clear() 297 | if len(gallery_item.example_binarys) > 0: 298 | self.example_selector_lbx.InsertItems( 299 | list(gallery_item.example_binarys.keys()), 0 300 | ) 301 | self.example_selector_lbx.SetStringSelection( 302 | list(gallery_item.example_binarys.keys())[0] 303 | ) 304 | 305 | example = self.example_selector_lbx.GetStringSelection() 306 | example_binary = self.construct_gallery[selection].example_binarys[example] 307 | else: 308 | example_binary = bytes(0) 309 | 310 | self.Freeze() 311 | self.construct_hex_editor.change_construct(gallery_item.construct) 312 | self.construct_hex_editor.change_contextkw(gallery_item.contextkw) 313 | self.construct_hex_editor.binary = example_binary 314 | self.construct_hex_editor.construct_editor.expand_all() 315 | self.Thaw() 316 | 317 | def on_clear_binary_clicked(self, event): 318 | self.example_selector_lbx.SetSelection(wx.NOT_FOUND) 319 | self.construct_hex_editor.binary = bytes() 320 | self.construct_hex_editor.construct_editor.expand_all() 321 | 322 | def on_example_selection_changed(self, event): 323 | selection = self.gallery_selector_lbx.GetStringSelection() 324 | example = self.example_selector_lbx.GetStringSelection() 325 | example_binary = self.construct_gallery[selection].example_binarys[example] 326 | 327 | # Set example binary 328 | self.construct_hex_editor.binary = example_binary 329 | self.construct_hex_editor.construct_editor.expand_all() 330 | 331 | def on_load_binary_file_clicked(self, event): 332 | with wx.FileDialog( 333 | self, 334 | "Open binary file", 335 | wildcard="binary files (*.*)|*.*", 336 | style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST, 337 | ) as fileDialog: 338 | 339 | if fileDialog.ShowModal() == wx.ID_CANCEL: 340 | return # the user changed their mind 341 | 342 | # Proceed loading the file chosen by the user 343 | pathname = Path(fileDialog.GetPath()) 344 | with open(pathname, "rb") as file: 345 | self.construct_hex_editor.binary = file.read() 346 | self.construct_hex_editor.refresh() 347 | 348 | 349 | icon = PyEmbeddedImage( 350 | b"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBj" 351 | b"SFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+g" 352 | b"vaeTAAAAB3RJTUUH5QMVEwoXzWaNiAAACetJREFUWMPFl3tQVNcdxz/37oNlF3mJoAS0pviq" 353 | b"gqiMxRCjkGhSajMTw0wYJyEgiWmijulfRqfEvGge2nSsto5TnFjrMyaTxqmP0WiqEDWIFDS8" 354 | b"Nmp4s6zswr5Z9t57+geyFSHT6V89M3fOueeec37f3+P7+50L/+cmjTdZ9t6OpwOBwcUCKUKS" 355 | b"JCQJhLh/hRg9EqPnxb3JB3tN0zDo9T2mCOPn5ds2t40L4Nebtv41I31OUd5jjxATM4GRE4UA" 356 | b"gQiPRw4WCISmoaoamqahaiqaOtyr6r2xqqCqGoHBAB1dNi5X13kiI/RPfPzB29W6+4W/+9Gu" 357 | b"rCmTE//0xGM/Z/tHH1L3r1p+teqX1NfVUf7eu0xOSmR+RgaBgJ+3tm2jsvISK1c8Qd/du5S/" 358 | b"9y6qqrD00UdxDfTzwfu/w+N2k5e7HIvFzK4/7qSq8hKPZC9mVtr0iIvfVM9JTXnokHw/AEVR" 359 | b"5i1ZvJAdO3awYcMG5s+fz6effkp2djYrV67EarUihGDPnj3k5+ezZs0adu3aRVpaGs8//zz1" 360 | b"9fUIIUhOTmbdunXU1taiaRrnz5/HbDazdetWdu/ezUPJk5kyefK8yn+enzYKgNFo1JnNkbjd" 361 | b"bqZOncrMmTNpbGxEr9cTFxeHuBcIra2tzJs3jxkzZtDc3Iwsy0ycOBEATdMQQoTXq6pKa2sr" 362 | b"M2bMYMKECfh8PhRFIdIUYfD7fZP09wOQJQkhBJGRkaiqit1uJyYmBiHEqCchIYGBgQF8Pt+Y" 363 | b"76qqomlaGIiiKMTHxzMwMMDQ0BCKoowoIgGmUQBGAm7t2rVs3rwZr9dLWVkZN27c4MCBA3i9" 364 | b"XhYtWkRxcTHl5eUIISguLqazs5OdO3ditVo5ffo0CxcOu7GpqYkTJ06wbNky3njjDWpqasjL" 365 | b"y0On+0/ojWLB+7//c+kvViyrSJ87G4/Hg9/vZ9KkSQghsNvtCCEwGo1ER0fj9/txu90kJCQg" 366 | b"hKCvr49QKARATEwMTqeTYDCIEIKYmBgCgQAOh4MJEyYgSRJHvzjt//CdrU/LYw0wLOzrr78O" 367 | b"vzudTqqrqwmFQkRHRyOE4MKFCzQ2NiLEML/r6uq4fv16WFh9fT12u524uDiCwSC1tbX09vZi" 368 | b"NBpHNJcAxrhAALdu3eLYsWN4vV6ee+45Wltb+fLLL+no6GDdunUcOXIEq9WKLMu4XC50Oh3n" 369 | b"zp0jNTUVm83G8uXLuXbtGu3t7Wzfvh2/38/Nmzepra1l165do+TJo4UPJ5rs7Gzy8vLCgZSZ" 370 | b"mclTTz0VDrTKykoKCwspKiri9OnTVFVVUVBQQHFxMWfPnmXy5MkUFhYihECSJOLj4ykqKhrW" 371 | b"XJJGxYD+QfXFPbOPUO7+8f000+l0aJrG0NAQQggMBgNAOA6GU7iELMvIsoxOp0OSJAwGA6qq" 372 | b"/giAeyyw2+20trZiNBpxuVwoisLt27dxOBw4HA5ycnI4deoUg4ODZGVlkZqaypkzZ5gyZQpZ" 373 | b"WVmEQiGam5vp7+/HbreTmJhIQ0MDHo8Hm80WzhnjukAIgcvlIj4+HovFQltbG16vF71eT2Ji" 374 | b"Irdv36agoICJEycSGxvLM888Q15eHunp6SiKQklJCaqq0t3dTW5uLk1NTQDcuXOH/Px8Ghsb" 375 | b"h10gjUPD8u27S1fk5lRkzJszyvSapoUTzINJ6cEmSVK4f/C7pmkAeDwe/nLg2OAHb29dNdoC" 376 | b"9yh18uRJcnNzOXPmDJqmUVNTw5o1a1i7di29vb3U1NTw7LPPUlFRAUBXVxcvvvgipaWlfP/9" 377 | b"97hcLkpKSigtLaW+vp5QKMRLL73EK6+8QlVVFbIsI93TXf8gAFVVycnJob29nYGBAVRV5ZNP" 378 | b"PmHLli24XC4OHjzIpk2bWL9+PdevX0eSJI4fP05RURGZmZmUl5eTlZXFqlWrWL16NS+//DIF" 379 | b"BQVkZ2fz2muvUVhYyJ49e8aPAU0Mm7qzsxOn08ndu3fp6enB4XBgMpmIioqio6ODUCiEXq8P" 380 | b"W6yvr4/U1FSSkpLo6enB6XSSmpqK2WzG4/HQ29tLSkpKeM8IU8ZaQBOo9/wEMDQ0hNfrJTo6" 381 | b"mq6uLmw2G5MmTSIUCoX9Kcsy06dPx2azERUVRXJyMtOnT6e7uxuHw4HFYiEtLY3m5mYCgUCY" 382 | b"sqqmhsYCQKCpKvX19Vy8eBFJkkhJSaGoqIi9e/eiqiqvv/46ra2tHDhwAJ/Px9mzZ3nhhRco" 383 | b"KyvD5/Oxfv16MjIy2LJlC+fOnePVV19l6dKlfPXVV2zcuJHS0lJs9j5hbbF2A2IUC7aVf1ya" 384 | b"k72wIi7aEjaTTqfDZDKFs5fFYsFgMCCEQK/XYzQaMZlMAASDQSIjI8NM+K6xhQG3Z7g8qyp+" 385 | b"vx8kCYfDyTtvvbm3u7N9z5gg1FQVj8fDkSNHSEpKYvXq1QSDQQ4ePEhsbCwlJSUoisK+ffsw" 386 | b"GAxs3LiR3t5eDh06RGZmJitWrECSJL6tqWNKUiLzfjaLYDCI2+2mv7+fYDDIP06esna03blo" 387 | b"NlvaxqVhRUUFGRkZuFwurly5wv79+3n44YeJiIjgxIkTHD58mNjYWKZNm8a+ffswmUykpKRw" 388 | b"8uRJgsEg1deuEz0hip9MS0HTNILBIF6vl6GhIb759rr36MH9f9PpdI1+v29gTCYcuQnNnj2b" 389 | b"RYsWcePGDdra2li4cCFLliyhtraWhoYGli5dyvLly7l69SqxsbEsWLAAIQQtLS1cqa5hzqw0" 390 | b"FEXB7/fT39+Pz+fjh7ZOdftHHx72+7xXzZao5nGLkaZp4QISCATQ6/Xo9cPL/H5/2O8jh48U" 391 | b"IYDBwUF6enpwu92EQiGCwSBOpxOv10tP713tt2Vln3W1/3DGbLbUeNyu4BgAI3e5nJwcjh49" 392 | b"SkdHB4WFhcydO5f9+/fj8XhYtWoV8fHxVFRUoNfrefLJJ3E4HBw/fhyr1UpLSwtCiPCNyePx" 393 | b"0NXTq5W9+daJlsabX0SYTJf9ft/AuHkgpCiEFIX8/Hyam5uRJInZs2eHfaxpGunp6ZjNZhIS" 394 | b"EvD5fCxYsACdTsfKlSt5/PHHuXXrFm6/HZfLhcfj4dYP7UpZWdln1qbvvjCZIqsGBwO998sc" 395 | b"BUCSpLaOLhvTpz7EnDlzwjQUQjBz5kwMBkOYgmlpaRgMBvR6PbIss3jxYiRJwmg0Yutz4Xa7" 396 | b"uXDpsvvjHdsP9XS1n42IMF0eHAzYHyxeowAY9LpLDc236+Nio+fPnfVTZFlG0zQURUHTNEKh" 397 | b"UPhyMRInsiyHLx8And29IiY2ju1/2P3d3z8/dngw4K82W6Jq/T7vAOO0Mf+GG36zNf5O6533" 398 | b"IyIiH42KslgQQh61Thq9/f5XVVWV7u5Oe0N9baXd1vWtLOuaLFEWq8ftHuJH2rh/x0uW5uqa" 399 | b"Gm4kDzgdkwAj/1sTsiy7jcaIrsHBgPu/Lf4340NlvbmvI1QAAAAldEVYdGRhdGU6Y3JlYXRl" 400 | b"ADIwMjEtMDMtMjFUMTk6MTA6MjIrMDA6MDDMGKfHAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIx" 401 | b"LTAzLTIxVDE5OjEwOjIyKzAwOjAwvUUfewAAAABJRU5ErkJggg==" 402 | ) 403 | 404 | 405 | def main(): 406 | if sys.platform == "win32": 407 | # Windows Icon fix: https://stackoverflow.com/a/1552105 408 | import ctypes 409 | 410 | myappid = "timrid.construct_hex_editor" 411 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) 412 | 413 | inspect = False 414 | if inspect is True: 415 | import wx.lib.mixins.inspection as wit 416 | 417 | app = wit.InspectableApp() 418 | else: 419 | wit = None 420 | app = wx.App(False) 421 | 422 | frame = ConstructGalleryFrame(None) 423 | frame.Show(True) 424 | if wit is not None: 425 | wit.InspectionTool().Show() 426 | app.MainLoop() 427 | 428 | 429 | if __name__ == "__main__": 430 | main() 431 | -------------------------------------------------------------------------------- /construct_editor/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrid/construct-editor/b4c63dcea1a057cbcc7106b2d58c8bb4d8503e3b/construct_editor/py.typed -------------------------------------------------------------------------------- /construct_editor/version.py: -------------------------------------------------------------------------------- 1 | version = (0, 1, 5) 2 | version_string = ".".join(map(str, version)) 3 | -------------------------------------------------------------------------------- /construct_editor/wx_widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from construct_editor.wx_widgets.wx_construct_editor import WxConstructEditor 2 | from construct_editor.wx_widgets.wx_construct_hex_editor import WxConstructHexEditor 3 | from construct_editor.wx_widgets.wx_hex_editor import WxHexEditor 4 | from construct_editor.wx_widgets.wx_python_code_editor import WxPythonCodeEditor 5 | 6 | __all__ = [ 7 | "WxConstructEditor", 8 | "WxConstructHexEditor", 9 | "WxHexEditor", 10 | "WxPythonCodeEditor", 11 | ] 12 | -------------------------------------------------------------------------------- /construct_editor/wx_widgets/wx_construct_hex_editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import typing as t 3 | 4 | import construct as cs 5 | import wx 6 | 7 | from construct_editor.core.entries import EntryConstruct, StreamInfo 8 | from construct_editor.core.model import ConstructEditorModel 9 | from construct_editor.wx_widgets.wx_construct_editor import WxConstructEditor 10 | from construct_editor.wx_widgets.wx_hex_editor import HexEditorFormat, WxHexEditor 11 | 12 | 13 | class HexEditorPanel(wx.SplitterWindow): 14 | def __init__( 15 | self, parent, name: str, read_only: bool = False, bitwiese: bool = False 16 | ): 17 | super().__init__(parent, style=wx.SP_LIVE_UPDATE) 18 | self.SetSashGravity(0.5) 19 | 20 | panel = wx.Panel(self) 21 | vsizer = wx.BoxSizer(wx.VERTICAL) 22 | 23 | # Create Name if available 24 | if name != "": 25 | line = wx.StaticLine(panel, style=wx.LI_HORIZONTAL) 26 | vsizer.Add(line, 0, wx.EXPAND) 27 | self._name_txt = wx.StaticText(panel, wx.ID_ANY) 28 | self._name_txt.SetFont( 29 | wx.Font( 30 | 10, 31 | wx.FONTFAMILY_DEFAULT, 32 | wx.FONTSTYLE_NORMAL, 33 | wx.FONTWEIGHT_BOLD, 34 | underline=True, 35 | ) 36 | ) 37 | vsizer.Add(self._name_txt, 0, wx.ALIGN_CENTER | wx.ALL, 5) 38 | self._name_txt.SetLabelText(name) 39 | 40 | # Create HexEditor 41 | self.hex_editor: WxHexEditor = WxHexEditor( 42 | panel, 43 | b"", 44 | HexEditorFormat(width=16), 45 | read_only=read_only, 46 | bitwiese=bitwiese, 47 | ) 48 | vsizer.Add(self.hex_editor, 1) 49 | panel.SetSizer(vsizer) 50 | 51 | self.Initialize(panel) 52 | 53 | self.sub_panel: t.Optional["HexEditorPanel"] = None 54 | 55 | def clear_sub_panels(self): 56 | """Clears all sub-panels recursivly""" 57 | if self.sub_panel is not None: 58 | self.Unsplit(self.sub_panel) 59 | self.sub_panel.Destroy() 60 | self.sub_panel = None 61 | 62 | def create_sub_panel(self, name: str, bitwise: bool) -> "HexEditorPanel": 63 | """Create a new sub-panel""" 64 | if self.sub_panel is None: 65 | self.sub_panel = HexEditorPanel( 66 | self, name, read_only=True, bitwiese=bitwise 67 | ) 68 | self.SplitHorizontally(self.GetWindow1(), self.sub_panel) 69 | return self.sub_panel 70 | else: 71 | raise RuntimeError("sub-panel already created") 72 | 73 | 74 | class WxConstructHexEditor(wx.Panel): 75 | def __init__( 76 | self, 77 | parent, 78 | construct: cs.Construct, 79 | contextkw: dict = {}, 80 | binary: bytes = b"", 81 | ): 82 | super().__init__(parent) 83 | 84 | self._contextkw = contextkw 85 | 86 | hsizer = wx.BoxSizer(wx.HORIZONTAL) 87 | self._init_gui_hex_editor_splitter(hsizer, binary) 88 | self._init_gui_hex_visibility(hsizer) 89 | self._init_gui_construct_editor(hsizer, construct) 90 | 91 | self._converting = False 92 | self._hex_editor_visible = True 93 | 94 | # show data in construct editor 95 | self.refresh() 96 | 97 | self.SetSizer(hsizer) 98 | self.Layout() 99 | 100 | def _init_gui_hex_editor_splitter(self, hsizer: wx.BoxSizer, binary: bytes): 101 | # Create Splitter for showing one or multiple HexEditors 102 | self.hex_panel: HexEditorPanel = HexEditorPanel(self, "") 103 | hsizer.Add(self.hex_panel, 0, wx.EXPAND, 0) 104 | 105 | # Init Root HexEditor 106 | self.hex_panel.hex_editor.binary = binary 107 | self.hex_panel.hex_editor.on_binary_changed.append( 108 | lambda _: self._convert_binary_to_struct() 109 | ) 110 | 111 | def _init_gui_hex_visibility(self, hsizer: wx.BoxSizer): 112 | self.toggle_hex_visibility_btn = wx.Button( 113 | self, wx.ID_ANY, "«", size=wx.Size(12, -1) 114 | ) 115 | hsizer.Add(self.toggle_hex_visibility_btn, 0, wx.EXPAND | wx.ALL, 0) 116 | self.toggle_hex_visibility_btn.Bind( 117 | wx.EVT_BUTTON, lambda evt: self.toggle_hex_visibility() 118 | ) 119 | 120 | def _init_gui_construct_editor(self, hsizer: wx.BoxSizer, construct: cs.Construct): 121 | self.construct_editor: WxConstructEditor = WxConstructEditor( 122 | self, 123 | construct, 124 | ) 125 | hsizer.Add(self.construct_editor, 1, wx.EXPAND, 0) 126 | self.construct_editor.on_root_obj_changed.append( 127 | lambda _: self._convert_struct_to_binary() 128 | ) 129 | self.construct_editor.on_entry_selected.append(self._on_entry_selected) 130 | 131 | def refresh(self): 132 | """Refresh the content of the construct view""" 133 | self.Freeze() 134 | self.hex_panel.hex_editor.refresh() 135 | self._convert_binary_to_struct() 136 | self.Thaw() 137 | 138 | def toggle_hex_visibility(self): 139 | """Toggle the visibility of the HexEditor""" 140 | if self._hex_editor_visible: 141 | self.hex_panel.HideWithEffect(wx.SHOW_EFFECT_ROLL_TO_LEFT) 142 | self.toggle_hex_visibility_btn.SetLabelText("»") 143 | self._hex_editor_visible = False 144 | else: 145 | self.hex_panel.ShowWithEffect(wx.SHOW_EFFECT_ROLL_TO_RIGHT) 146 | self.toggle_hex_visibility_btn.SetLabelText("«") 147 | self._hex_editor_visible = True 148 | self.Freeze() 149 | self.Layout() 150 | self.Refresh() 151 | self.Thaw() 152 | 153 | def change_construct(self, constr: cs.Construct) -> None: 154 | """ 155 | Change the construct format, that is used for building/parsing. 156 | """ 157 | self.construct_editor.change_construct(constr) 158 | 159 | def change_contextkw(self, contextkw: dict): 160 | """ 161 | Change the contextkw used for building/parsing. 162 | """ 163 | self._contextkw = contextkw 164 | 165 | def change_binary(self, binary: bytes): 166 | """ 167 | Change the binary data, that should be displayed. 168 | """ 169 | self.hex_panel.clear_sub_panels() 170 | self.hex_panel.hex_editor.binary = binary 171 | 172 | @property 173 | def construct(self) -> cs.Construct: 174 | """ 175 | Construct that is used for displaying. 176 | """ 177 | return self.construct_editor.construct 178 | 179 | @construct.setter 180 | def construct(self, constr: cs.Construct): 181 | self.construct_editor.construct = constr 182 | 183 | @property 184 | def contextkw(self) -> dict: 185 | """ 186 | Context used for building/parsing. 187 | """ 188 | return self._contextkw 189 | 190 | @contextkw.setter 191 | def contextkw(self, contextkw: dict): 192 | self.change_contextkw(contextkw) 193 | 194 | @property 195 | def binary(self) -> bytes: 196 | """ 197 | Binary data, that should be displayed. 198 | """ 199 | return self.hex_panel.hex_editor.binary 200 | 201 | @binary.setter 202 | def binary(self, binary: bytes): 203 | self.change_binary(binary) 204 | 205 | @property 206 | def hide_protected(self) -> bool: 207 | """ 208 | Hide protected members. 209 | A protected member starts with an undescore (_) 210 | """ 211 | return self.construct_editor.hide_protected 212 | 213 | @hide_protected.setter 214 | def hide_protected(self, hide_protected: bool): 215 | self.construct_editor.hide_protected = hide_protected 216 | 217 | @property 218 | def root_obj(self) -> t.Any: 219 | """ 220 | Root object that is displayed 221 | """ 222 | return self.construct_editor.root_obj 223 | 224 | @property 225 | def model(self) -> ConstructEditorModel: 226 | """ 227 | Model with the displayed data. 228 | """ 229 | return self.construct_editor.model 230 | 231 | # Internals ############################################################### 232 | def _convert_binary_to_struct(self): 233 | """Convert binary to construct object""" 234 | if self._converting: 235 | return 236 | try: 237 | self._converting = True 238 | self.Freeze() 239 | self.construct_editor.parse( 240 | self.hex_panel.hex_editor.binary, **self._contextkw 241 | ) 242 | finally: 243 | self.Thaw() 244 | self._converting = False 245 | 246 | def _convert_struct_to_binary(self): 247 | """Convert construct object to binary""" 248 | try: 249 | self._converting = True 250 | self.Freeze() 251 | binary = self.construct_editor.build(**self._contextkw) 252 | self.hex_panel.hex_editor.binary = binary 253 | self._on_entry_selected(self.construct_editor.get_selected_entry()) 254 | except Exception: 255 | pass # ignore errors, because they are already shown in the gui 256 | finally: 257 | self.Thaw() 258 | self._converting = False 259 | 260 | def _on_entry_selected(self, entry: t.Optional[EntryConstruct]): 261 | try: 262 | self.Freeze() 263 | self.hex_panel.clear_sub_panels() 264 | if entry is not None: 265 | stream_infos = entry.get_stream_infos() 266 | self._show_stream_infos(stream_infos) 267 | finally: 268 | self.Thaw() 269 | 270 | def _show_stream_infos(self, stream_infos: t.List[StreamInfo]): 271 | hex_pnl = self.hex_panel 272 | panel_stream_mapping: t.List[t.Tuple[HexEditorPanel, StreamInfo]] = [] 273 | 274 | # Create all Sub-Panels 275 | for idx, stream_info in enumerate(stream_infos): 276 | if idx != 0: # dont create Sub-Panel for the root stream 277 | hex_pnl = hex_pnl.create_sub_panel( 278 | stream_info.path_str, stream_info.bitstream 279 | ) 280 | hex_pnl.hex_editor.binary = stream_info.stream.getvalue() 281 | 282 | panel_stream_mapping.append((hex_pnl, stream_info)) 283 | 284 | # Mark to correct bytes in the stream. 285 | # Can only be made when alls sub-panels are created. Otherwise "scroll_to_idx" 286 | # does not work properly because the size of the HexEditorPanel may change. 287 | for hex_pnl, stream_info in panel_stream_mapping: 288 | start = stream_info.byte_range[0] 289 | end = stream_info.byte_range[1] 290 | 291 | # Show the byte range in the corresponding HexEditor 292 | hex_pnl.hex_editor.colorise(start, end, refresh=False) 293 | hex_pnl.hex_editor.scroll_to_idx(end - 1, refresh=False) 294 | hex_pnl.hex_editor.scroll_to_idx(start, refresh=False) 295 | hex_pnl.hex_editor.refresh() 296 | -------------------------------------------------------------------------------- /construct_editor/wx_widgets/wx_context_menu.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import typing as t 3 | 4 | import wx 5 | 6 | import construct_editor.core.construct_editor as construct_editor 7 | import construct_editor.core.entries as entries 8 | from construct_editor.core.context_menu import ( 9 | COPY_LABEL, 10 | PASTE_LABEL, 11 | REDO_LABEL, 12 | UNDO_LABEL, 13 | ButtonMenuItem, 14 | CheckboxMenuItem, 15 | ContextMenu, 16 | MenuItem, 17 | RadioGroupMenuItems, 18 | SeparatorMenuItem, 19 | SubmenuItem, 20 | ) 21 | from construct_editor.core.model import ConstructEditorModel 22 | 23 | LABEL_TO_ID_MAPPING = { 24 | COPY_LABEL: wx.ID_COPY, 25 | PASTE_LABEL: wx.ID_PASTE, 26 | UNDO_LABEL: wx.ID_UNDO, 27 | REDO_LABEL: wx.ID_REDO, 28 | } 29 | 30 | 31 | class WxContextMenu(wx.Menu, ContextMenu): 32 | def __init__( 33 | self, 34 | parent: "construct_editor.ConstructEditor", 35 | model: "ConstructEditorModel", 36 | entry: t.Optional["entries.EntryConstruct"], 37 | ): 38 | wx.Menu.__init__(self) 39 | ContextMenu.__init__(self, parent, model, entry) 40 | 41 | def add_menu_item(self, item: MenuItem): 42 | """ 43 | Add an menu item to the context menu. 44 | """ 45 | self._add_menu_item(self, item) 46 | 47 | @classmethod 48 | def _add_menu_item(cls, menu: wx.Menu, item: MenuItem): 49 | if isinstance(item, SeparatorMenuItem): 50 | cls._add_separator_item(menu, item) 51 | elif isinstance(item, ButtonMenuItem): 52 | cls._add_button_item(menu, item) 53 | elif isinstance(item, CheckboxMenuItem): 54 | cls._add_checkbox_item(menu, item) 55 | elif isinstance(item, RadioGroupMenuItems): 56 | cls._add_radio_group_item(menu, item) 57 | elif isinstance(item, SubmenuItem): 58 | cls._add_submenu_item(menu, item) 59 | else: 60 | raise ValueError(f"menu item unsupported ({item})") 61 | 62 | @classmethod 63 | def _add_separator_item(cls, menu: wx.Menu, item: SeparatorMenuItem): 64 | menu.AppendSeparator() 65 | 66 | @classmethod 67 | def _add_button_item(cls, menu: wx.Menu, item: ButtonMenuItem): 68 | item_id = LABEL_TO_ID_MAPPING.get(item.label, wx.ID_ANY) 69 | label = item.label 70 | if item.shortcut is not None: 71 | label += "\t" + item.shortcut 72 | 73 | def button_event(event: wx.CommandEvent): 74 | item.callback() 75 | 76 | mi: wx.MenuItem = menu.Append(item_id, label) 77 | menu.Bind(wx.EVT_MENU, button_event, id=mi.Id) 78 | mi.Enable(item.enabled) 79 | 80 | @classmethod 81 | def _add_checkbox_item(cls, menu: wx.Menu, item: CheckboxMenuItem): 82 | label = item.label 83 | if item.shortcut is not None: 84 | label += "\t" + item.shortcut 85 | 86 | def checkbox_event(event: wx.CommandEvent): 87 | item.callback(event.IsChecked()) 88 | 89 | mi: wx.MenuItem = menu.AppendCheckItem(wx.ID_ANY, label) 90 | menu.Bind(wx.EVT_MENU, checkbox_event, id=mi.Id) 91 | mi.Check(item.checked) 92 | mi.Enable(item.enabled) 93 | 94 | @classmethod 95 | def _add_radio_group_item(cls, menu: wx.Menu, item: RadioGroupMenuItems): 96 | def radio_group_event(event: wx.CommandEvent): 97 | item.callback(menu.GetLabel(event.GetId())) 98 | 99 | for label in item.labels: 100 | mi: wx.MenuItem = menu.AppendRadioItem(wx.ID_ANY, label) 101 | menu.Bind(wx.EVT_MENU, radio_group_event, id=mi.Id) 102 | if label == item.checked_label: 103 | mi.Check(True) 104 | 105 | @classmethod 106 | def _add_submenu_item(cls, menu: wx.Menu, item: SubmenuItem): 107 | submenu = wx.Menu() 108 | for subitem in item.subitems: 109 | cls._add_menu_item(submenu, subitem) 110 | menu.AppendSubMenu(submenu, item.label) 111 | -------------------------------------------------------------------------------- /construct_editor/wx_widgets/wx_exception_dialog.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import traceback 3 | import typing as t 4 | from types import TracebackType 5 | 6 | import wx 7 | 8 | 9 | @dataclasses.dataclass 10 | class ExceptionInfo: 11 | etype: t.Type[BaseException] 12 | value: BaseException 13 | trace: t.Optional[TracebackType] 14 | 15 | 16 | class WxExceptionDialog(wx.Dialog): 17 | def __init__( 18 | self, parent, title: str, exception: t.Union[ExceptionInfo, BaseException] 19 | ): 20 | wx.Dialog.__init__( 21 | self, 22 | parent, 23 | id=wx.ID_ANY, 24 | title=title, 25 | pos=wx.DefaultPosition, 26 | size=wx.Size(800, 600), 27 | style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, 28 | ) 29 | 30 | self._init_gui() 31 | 32 | if isinstance(exception, ExceptionInfo): 33 | exception_info = exception 34 | else: 35 | exception_info = ExceptionInfo( 36 | type(exception), exception, exception.__traceback__ 37 | ) 38 | 39 | self.exception_txt.SetValue( 40 | "".join( 41 | traceback.format_exception_only( 42 | exception_info.etype, exception_info.value 43 | ) 44 | ) 45 | ) 46 | 47 | if exception_info.trace is None: 48 | self.traceback_txt.SetValue("") 49 | else: 50 | self.traceback_txt.SetValue( 51 | "".join(traceback.format_tb(exception_info.trace)) 52 | ) 53 | 54 | def _init_gui(self): 55 | self.SetSizeHints(wx.DefaultSize, wx.DefaultSize) 56 | 57 | sizer = wx.BoxSizer(wx.VERTICAL) 58 | 59 | self.ok_btn = wx.Button( 60 | self, wx.ID_ANY, "OK", wx.DefaultPosition, wx.DefaultSize, 0 61 | ) 62 | sizer.Add(self.ok_btn, 0, wx.ALL | wx.EXPAND, 5) 63 | 64 | self.exception_txt = wx.TextCtrl( 65 | self, 66 | wx.ID_ANY, 67 | wx.EmptyString, 68 | wx.DefaultPosition, 69 | wx.Size(-1, -1), 70 | wx.TE_MULTILINE | wx.TE_READONLY, 71 | ) 72 | sizer.Add(self.exception_txt, 1, wx.ALL | wx.EXPAND, 5) 73 | 74 | self.traceback_txt = wx.TextCtrl( 75 | self, 76 | wx.ID_ANY, 77 | wx.EmptyString, 78 | wx.DefaultPosition, 79 | wx.Size(-1, -1), 80 | wx.TE_MULTILINE | wx.TE_READONLY, 81 | ) 82 | sizer.Add(self.traceback_txt, 2, wx.ALL | wx.EXPAND, 5) 83 | 84 | self.SetSizer(sizer) 85 | self.Layout() 86 | 87 | self.Centre(wx.BOTH) 88 | 89 | # Connect Events 90 | self.ok_btn.Bind(wx.EVT_BUTTON, self.on_ok_clicked) 91 | 92 | def on_ok_clicked(self, event): 93 | self.Close() 94 | -------------------------------------------------------------------------------- /construct_editor/wx_widgets/wx_obj_view.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | import arrow 4 | import wx 5 | import wx.adv 6 | import wx.dataview as dv 7 | 8 | import construct_editor.wx_widgets.wx_construct_editor as wx_construct_editor 9 | from construct_editor.core.entries import ( 10 | EntryFlagsEnum, 11 | EntryTFlagsEnum, 12 | FlagsEnumItem, 13 | ObjViewSettings, 14 | ObjViewSettings_Bytes, 15 | ObjViewSettings_Default, 16 | ObjViewSettings_Enum, 17 | ObjViewSettings_Flag, 18 | ObjViewSettings_FlagsEnum, 19 | ObjViewSettings_Integer, 20 | ObjViewSettings_String, 21 | ObjViewSettings_Timestamp, 22 | int_to_str, 23 | str_to_bytes, 24 | str_to_int, 25 | ) 26 | 27 | 28 | # ##################################################################################################################### 29 | # Value Editors 30 | # ##################################################################################################################### 31 | class WxObjEditor_Default(wx.TextCtrl): 32 | def __init__(self, parent, settings: ObjViewSettings): 33 | self.entry = settings.entry 34 | 35 | super(wx.TextCtrl, self).__init__( 36 | parent, 37 | wx.ID_ANY, 38 | self.entry.obj_str, 39 | wx.DefaultPosition, 40 | wx.Size(-1, -1), 41 | style=wx.TE_READONLY, 42 | ) 43 | 44 | def get_new_obj(self) -> t.Any: 45 | return self.entry.obj 46 | 47 | 48 | class WxObjEditor_String(wx.TextCtrl): 49 | def __init__(self, parent, settings: ObjViewSettings_String): 50 | self.entry = settings.entry 51 | 52 | super(wx.TextCtrl, self).__init__( 53 | parent, 54 | wx.ID_ANY, 55 | self.entry.obj_str, 56 | style=wx.TE_PROCESS_ENTER, 57 | ) 58 | 59 | self.SelectAll() 60 | 61 | def get_new_obj(self) -> t.Any: 62 | val_str: str = self.GetValue() 63 | return val_str 64 | 65 | 66 | class WxObjEditor_Integer(wx.TextCtrl): 67 | def __init__(self, parent, settings: ObjViewSettings_Integer): 68 | self.entry = settings.entry 69 | 70 | super(wx.TextCtrl, self).__init__( 71 | parent, 72 | wx.ID_ANY, 73 | self.entry.obj_str, 74 | style=wx.TE_PROCESS_ENTER, 75 | ) 76 | 77 | self.SelectAll() 78 | 79 | def get_new_obj(self) -> t.Any: 80 | val_str: str = self.GetValue() 81 | 82 | try: 83 | # convert string to integer 84 | new_obj = str_to_int(val_str) 85 | except Exception: 86 | new_obj = val_str # this will probably result in a building error 87 | 88 | return new_obj 89 | 90 | 91 | class WxObjEditor_Bytes(wx.TextCtrl): 92 | def __init__(self, parent, settings: ObjViewSettings_Bytes): 93 | self.entry = settings.entry 94 | 95 | super(wx.TextCtrl, self).__init__( 96 | parent, 97 | wx.ID_ANY, 98 | settings.entry.obj_str, 99 | style=wx.TE_PROCESS_ENTER, 100 | ) 101 | 102 | self.SelectAll() 103 | 104 | def get_new_obj(self) -> t.Any: 105 | val_str: str = self.GetValue() 106 | 107 | try: 108 | # convert string to bytes 109 | new_obj = str_to_bytes(val_str) 110 | except Exception: 111 | new_obj = val_str # this will probably result in a building error 112 | 113 | return new_obj 114 | 115 | 116 | class WxObjEditor_Enum(wx.ComboBox): 117 | def __init__(self, parent, settings: ObjViewSettings_Enum): 118 | self.entry = settings.entry 119 | 120 | super(wx.ComboBox, self).__init__( 121 | parent, 122 | style=wx.CB_DROPDOWN | wx.TE_PROCESS_ENTER, 123 | ) 124 | 125 | items = self.entry.get_enum_items() 126 | for pos, item in enumerate(items): 127 | self.Insert( 128 | item=f"{int_to_str(self.entry.model.integer_format, item.value)} ({item.name})", 129 | pos=pos, 130 | clientData=item, 131 | ) 132 | item = self.entry.get_enum_item_from_obj() 133 | sel_item_str = ( 134 | f"{int_to_str(self.entry.model.integer_format, item.value)} ({item.name})" 135 | ) 136 | self.SetStringSelection(sel_item_str) 137 | self.SetValue(sel_item_str) # show even if it is not in the list 138 | 139 | def get_new_obj(self) -> t.Any: 140 | val_str: str = self.GetValue() 141 | if len(val_str) == 0: 142 | val_str = "0" 143 | 144 | val_str = val_str.split()[0] 145 | new_obj = self.entry.conv_str_to_obj(val_str) 146 | return new_obj 147 | 148 | 149 | class FlagsEnumComboPopup(wx.ComboPopup): 150 | def __init__( 151 | self, 152 | combo_ctrl: wx.ComboCtrl, 153 | entry: t.Union["EntryTFlagsEnum", "EntryFlagsEnum"], 154 | ): 155 | super().__init__() 156 | self.combo_ctrl = combo_ctrl 157 | self.entry = entry 158 | self.clbx: wx.CheckListBox 159 | 160 | def on_motion(self, evt): 161 | item = self.clbx.HitTest(evt.GetPosition()) 162 | if item != wx.NOT_FOUND: 163 | # only select if not selected prevents flickering 164 | if not self.clbx.IsSelected(item): 165 | self.clbx.Select(item) 166 | 167 | def on_left_down(self, evt): 168 | item = self.clbx.HitTest(evt.GetPosition()) 169 | if item != wx.NOT_FOUND: 170 | # select the new item in the gui 171 | items = list(self.clbx.GetCheckedItems()) 172 | if item in items: 173 | items.remove(item) 174 | else: 175 | items.append(item) 176 | self.clbx.SetCheckedItems(items) 177 | 178 | # refresh shown string 179 | self.combo_ctrl.SetValue(self.GetStringValue()) 180 | 181 | def Create(self, parent): 182 | self.clbx = wx.CheckListBox(parent) 183 | self.clbx.Bind(wx.EVT_MOTION, self.on_motion) 184 | self.clbx.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) 185 | return True 186 | 187 | # Return the widget that is to be used for the popup 188 | def GetControl(self): 189 | return self.clbx 190 | 191 | # Return final size of popup. Called on every popup, just prior to OnPopup. 192 | # minWidth = preferred minimum width for window 193 | # prefHeight = preferred height. Only applies if > 0, 194 | # maxHeight = max height for window, as limited by screen size 195 | # and should only be rounded down, if necessary. 196 | def GetAdjustedSize(self, minWidth, prefHeight, maxHeight): 197 | row_height = self.clbx.GetCharHeight() + 2 198 | row_count = self.clbx.GetCount() 199 | prefHeight = min(row_height * row_count + 4, prefHeight) 200 | return wx.ComboPopup.GetAdjustedSize(self, minWidth, prefHeight, maxHeight) 201 | 202 | def get_flagsenum_items(self) -> t.List["FlagsEnumItem"]: 203 | # read all flagsenum items and modify checked status 204 | flagsenum_items: t.List[FlagsEnumItem] = [] 205 | for item in range(self.clbx.GetCount()): 206 | flagsenum_item: FlagsEnumItem = self.clbx.GetClientData(item) 207 | flagsenum_item.checked = self.clbx.IsChecked(item) 208 | flagsenum_items.append(flagsenum_item) 209 | return flagsenum_items 210 | 211 | def GetStringValue(self): 212 | flagsenum_items = self.get_flagsenum_items() 213 | temp_obj = self.entry.conv_flagsenum_items_to_obj(flagsenum_items) 214 | return self.entry.conv_obj_to_str(temp_obj) 215 | 216 | 217 | class WxObjEditor_FlagsEnum(wx.ComboCtrl): 218 | def __init__(self, parent, settings: ObjViewSettings_FlagsEnum): 219 | self.entry = settings.entry 220 | 221 | super(wx.ComboCtrl, self).__init__( 222 | parent, 223 | style=wx.CB_READONLY, 224 | ) 225 | 226 | self.popup_ctrl = FlagsEnumComboPopup(self, self.entry) 227 | self.SetPopupControl(self.popup_ctrl) 228 | 229 | # Initialize CheckListBox 230 | items = self.entry.get_flagsenum_items_from_obj() 231 | for pos, item in enumerate(items): 232 | self.popup_ctrl.clbx.Insert( 233 | item=f"{int_to_str(self.entry.model.integer_format, item.value)} ({item.name})", 234 | pos=pos, 235 | clientData=item, 236 | ) 237 | self.popup_ctrl.clbx.Check(pos, item.checked) 238 | 239 | self.SetValue(self.popup_ctrl.GetStringValue()) 240 | 241 | def get_new_obj(self) -> t.Any: 242 | flagsenum_items = self.popup_ctrl.get_flagsenum_items() 243 | new_obj = self.entry.conv_flagsenum_items_to_obj(flagsenum_items) 244 | return new_obj 245 | 246 | 247 | class WxObjEditor_Timestamp(wx.Panel): 248 | def __init__(self, parent, settings: ObjViewSettings_Timestamp): 249 | self.entry = settings.entry 250 | 251 | super(wx.Panel, self).__init__(parent) 252 | self.parent = parent 253 | 254 | # Test if the obj of the entry is available 255 | if self.entry.obj is None: 256 | return 257 | if not isinstance(self.entry.obj, arrow.Arrow): 258 | return 259 | 260 | # Obj 261 | hsizer = wx.BoxSizer(wx.HORIZONTAL) 262 | dt = self.entry.obj.datetime 263 | wx_datetime = wx.DateTime( 264 | day=dt.day, 265 | month=dt.month - 1, # in wx.adc.DatePickerCtrl the month start with 0 266 | year=dt.year, 267 | hour=dt.hour, 268 | minute=dt.minute, 269 | second=dt.second, 270 | millisec=dt.microsecond // 1000, 271 | ) 272 | 273 | self.date_picker = wx.adv.DatePickerCtrl( 274 | self, 275 | wx.ID_ANY, 276 | wx_datetime, 277 | wx.DefaultPosition, 278 | wx.DefaultSize, 279 | wx.adv.DP_DROPDOWN | wx.adv.DP_SHOWCENTURY, 280 | ) 281 | hsizer.Add(self.date_picker, 0, wx.LEFT, 0) 282 | 283 | self.time_picker = wx.adv.TimePickerCtrl( 284 | self, 285 | wx.ID_ANY, 286 | wx_datetime, 287 | wx.DefaultPosition, 288 | wx.DefaultSize, 289 | wx.adv.TP_DEFAULT, 290 | ) 291 | hsizer.Add(self.time_picker, 0, wx.LEFT, 5) 292 | 293 | self.obj_txtctrl = wx.TextCtrl( 294 | self, 295 | wx.ID_ANY, 296 | self.entry.obj_str, 297 | wx.DefaultPosition, 298 | wx.DefaultSize, 299 | style=wx.TE_READONLY, 300 | ) 301 | hsizer.Add(self.obj_txtctrl, 1, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) 302 | 303 | self.date_picker.Bind(wx.EVT_KILL_FOCUS, self._on_kill_focus) 304 | self.time_picker.Bind(wx.EVT_KILL_FOCUS, self._on_kill_focus) 305 | self.obj_txtctrl.Bind(wx.EVT_KILL_FOCUS, self._on_kill_focus) 306 | 307 | self.SetSizer(hsizer) 308 | self.Layout() 309 | 310 | def get_new_obj(self) -> t.Any: 311 | date: wx.DateTime = self.date_picker.GetValue() 312 | time: wx.DateTime = self.time_picker.GetValue() 313 | new_obj = arrow.Arrow( 314 | year=date.year, 315 | month=date.month + 1, # in wx.adc.DatePickerCtrl the month start with 0 316 | day=date.day, 317 | hour=time.hour, 318 | minute=time.minute, 319 | second=time.second, 320 | ) 321 | return new_obj 322 | 323 | def _on_kill_focus(self, event): 324 | # The kill focus event is not propagated from the childs to the panel. So we have to do it manually. 325 | # If this is not done, the dvc editor is not closed correctly, when the focus is lost. 326 | evt_handler: wx.EvtHandler = self.GetEventHandler() 327 | evt_handler.ProcessEvent(event) 328 | 329 | 330 | WxObjEditor = t.Union[ 331 | WxObjEditor_Default, 332 | WxObjEditor_String, 333 | WxObjEditor_Integer, 334 | WxObjEditor_Bytes, 335 | WxObjEditor_Enum, 336 | WxObjEditor_FlagsEnum, 337 | WxObjEditor_Timestamp, 338 | ] 339 | 340 | 341 | # ##################################################################################################################### 342 | # Value Editor Factory 343 | # ##################################################################################################################### 344 | def create_obj_editor(parent, settings: ObjViewSettings) -> WxObjEditor: 345 | if isinstance(settings, ObjViewSettings_String): 346 | return WxObjEditor_String(parent, settings) 347 | elif isinstance(settings, ObjViewSettings_Integer): 348 | return WxObjEditor_Integer(parent, settings) 349 | elif isinstance(settings, ObjViewSettings_Bytes): 350 | return WxObjEditor_Bytes(parent, settings) 351 | elif isinstance(settings, ObjViewSettings_Enum): 352 | return WxObjEditor_Enum(parent, settings) 353 | elif isinstance(settings, ObjViewSettings_FlagsEnum): 354 | return WxObjEditor_FlagsEnum(parent, settings) 355 | elif isinstance(settings, ObjViewSettings_Timestamp): 356 | return WxObjEditor_Timestamp(parent, settings) 357 | else: 358 | return WxObjEditor_Default(parent, settings) 359 | 360 | 361 | # ##################################################################################################################### 362 | # Obj Renderer Helper 363 | # ##################################################################################################################### 364 | class WxObjRendererHelper_Default: 365 | def __init__(self, settings: ObjViewSettings): 366 | self.entry = settings.entry 367 | 368 | def get_size( 369 | self, 370 | renderer: "wx_construct_editor.ObjectRenderer", 371 | ) -> wx.Size: 372 | # Return the size needed to display the value. The renderer 373 | # has a helper function we can use for measuring text that is 374 | # aware of any custom attributes that may have been set for 375 | # this item. 376 | obj_str = self.entry.obj_str if self.entry else "" 377 | size = renderer.GetTextExtent(obj_str) 378 | size += (2, 2) 379 | return size 380 | 381 | def render( 382 | self, 383 | renderer: "wx_construct_editor.ObjectRenderer", 384 | rect: wx.Rect, 385 | dc: wx.DC, 386 | state, 387 | ) -> bool: 388 | # And then finish up with this helper function that draws the 389 | # text for us, dealing with alignment, font and color 390 | # attributes, etc. 391 | obj_str = self.entry.obj_str if self.entry else "" 392 | renderer.RenderText(obj_str, 0, rect, dc, state) 393 | return True 394 | 395 | def get_mode(self): 396 | return dv.DATAVIEW_CELL_EDITABLE 397 | 398 | def activate_cell( 399 | self, 400 | renderer: "wx_construct_editor.ObjectRenderer", 401 | rect: wx.Rect, 402 | model: dv.DataViewModel, 403 | item: dv.DataViewItem, 404 | col: int, 405 | mouse_event: t.Optional[wx.MouseEvent], 406 | ): 407 | return False 408 | 409 | 410 | class WxObjRendererHelper_Flag(WxObjRendererHelper_Default): 411 | def __init__(self, settings: ObjViewSettings_Flag): 412 | self.entry = settings.entry 413 | 414 | def get_size( 415 | self, 416 | renderer: "wx_construct_editor.ObjectRenderer", 417 | ) -> wx.Size: 418 | native_renderer: wx.RendererNative = wx.RendererNative.Get() 419 | win: wx.Window = renderer.GetView() 420 | 421 | size = native_renderer.GetCheckBoxSize(win) 422 | return size 423 | 424 | def render( 425 | self, 426 | renderer: "wx_construct_editor.ObjectRenderer", 427 | rect: wx.Rect, 428 | dc: wx.DC, 429 | state, 430 | ) -> bool: 431 | native_renderer: wx.RendererNative = wx.RendererNative.Get() 432 | win: wx.Window = renderer.GetView() 433 | 434 | flags = 0 435 | if bool(self.entry.obj) is True: 436 | flags = wx.CONTROL_CHECKED 437 | native_renderer.DrawCheckBox(win, dc, rect, flags) 438 | return True 439 | 440 | def get_mode(self): 441 | return dv.DATAVIEW_CELL_ACTIVATABLE 442 | 443 | def activate_cell( 444 | self, 445 | renderer: "wx_construct_editor.ObjectRenderer", 446 | rect: wx.Rect, 447 | model: dv.DataViewModel, 448 | item: dv.DataViewItem, 449 | col: int, 450 | mouse_event: t.Optional[wx.MouseEvent], 451 | ): 452 | # see wxWidgets: wxDataViewToggleRenderer::WXActivateCell 453 | 454 | if mouse_event is not None: 455 | # Only react to clicks directly on the checkbox, not elsewhere in 456 | # the same cell. 457 | size = self.get_size(renderer) 458 | column: dv.DataViewColumn = renderer.GetOwner() 459 | 460 | if column.GetAlignment() == wx.ALIGN_LEFT: 461 | # i dont know why, but without the offset on windows the 462 | # clickable area is shifted 463 | mouse_event.SetX(mouse_event.GetX() - 3) 464 | 465 | if not wx.Rect(size).Contains(mouse_event.GetPosition()): 466 | return False 467 | 468 | new_value = not bool(self.entry.obj) 469 | model.ChangeValue(wx_construct_editor.ValueFromEditorCtrl(new_value), item, col) 470 | return True 471 | 472 | 473 | WxObjRendererHelper = t.Union[ 474 | WxObjRendererHelper_Default, 475 | WxObjRendererHelper_Flag, 476 | ] 477 | 478 | 479 | def create_obj_renderer_helper(settings: ObjViewSettings) -> WxObjRendererHelper: 480 | if isinstance(settings, ObjViewSettings_Flag): 481 | return WxObjRendererHelper_Flag(settings) 482 | else: 483 | return WxObjRendererHelper_Default(settings) 484 | -------------------------------------------------------------------------------- /construct_editor/wx_widgets/wx_python_code_editor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import keyword 4 | 5 | import sys 6 | import wx 7 | import wx.stc as stc 8 | 9 | # import images 10 | 11 | # ---------------------------------------------------------------------- 12 | 13 | demoText = """\ 14 | ## This version of the editor has been set up to edit Python source 15 | ## code. Here is a copy of wxPython/demo/Main.py to play with. 16 | 17 | 18 | """ 19 | 20 | # ---------------------------------------------------------------------- 21 | 22 | 23 | faces = { 24 | "times": "Times New Roman", 25 | "mono": "Courier New", 26 | # "helv": "Arial", 27 | "helv": "Consolas", 28 | "other": "Comic Sans MS", 29 | "size": 10, 30 | "size2": 8, 31 | } 32 | 33 | 34 | # ---------------------------------------------------------------------- 35 | 36 | 37 | class PythonSTC(stc.StyledTextCtrl): 38 | def __init__(self, parent, ID, text="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=0): 39 | stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style) 40 | 41 | self.CmdKeyAssign(ord("B"), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN) 42 | self.CmdKeyAssign(ord("N"), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT) 43 | 44 | self.SetLexer(stc.STC_LEX_PYTHON) 45 | self.SetKeyWords(0, " ".join(keyword.kwlist)) 46 | 47 | self.SetProperty("fold", "1") 48 | self.SetProperty("tab.timmy.whinge.level", "1") 49 | self.SetMargins(0, 0) 50 | 51 | self.SetViewWhiteSpace(False) 52 | # self.SetBufferedDraw(False) 53 | # self.SetViewEOL(True) 54 | # self.SetEOLMode(stc.STC_EOL_CRLF) 55 | # self.SetUseAntiAliasing(True) 56 | 57 | self.SetEdgeMode(stc.STC_EDGE_BACKGROUND) 58 | self.SetEdgeColumn(78) 59 | 60 | # Setup a margin to hold fold markers 61 | # self.SetFoldFlags(16) ### WHAT IS THIS VALUE? WHAT ARE THE OTHER FLAGS? DOES IT MATTER? 62 | self.SetMarginType(2, stc.STC_MARGIN_SYMBOL) 63 | self.SetMarginMask(2, stc.STC_MASK_FOLDERS) 64 | self.SetMarginSensitive(2, True) 65 | self.SetMarginWidth(2, 12) 66 | 67 | # Fold Style: Like a flattened tree control using square headers 68 | self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_BOXMINUS, "white", "#808080") 69 | self.MarkerDefine(stc.STC_MARKNUM_FOLDER, stc.STC_MARK_BOXPLUS, "white", "#808080") 70 | self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_VLINE, "white", "#808080") 71 | self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_LCORNER, "white", "#808080") 72 | self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND, stc.STC_MARK_BOXPLUSCONNECTED, "white", "#808080") 73 | self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_BOXMINUSCONNECTED, "white", "#808080") 74 | self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_TCORNER, "white", "#808080") 75 | 76 | self.Bind(stc.EVT_STC_UPDATEUI, self.OnUpdateUI) 77 | self.Bind(stc.EVT_STC_MARGINCLICK, self.OnMarginClick) 78 | self.Bind(wx.EVT_KEY_DOWN, self.OnKeyPressed) 79 | 80 | # Make some styles, The lexer defines what each style is used for, we 81 | # just have to define what each style looks like. This set is adapted from 82 | # Scintilla sample property files. 83 | 84 | # Global default styles for all languages 85 | self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "face:%(helv)s,size:%(size)d" % faces) 86 | self.StyleClearAll() # Reset all to be like the default 87 | 88 | # Global default styles for all languages 89 | self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "face:%(helv)s,size:%(size)d" % faces) 90 | self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:#C0C0C0,face:%(helv)s,size:%(size2)d" % faces) 91 | self.StyleSetSpec(stc.STC_STYLE_CONTROLCHAR, "face:%(other)s" % faces) 92 | self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, "fore:#FFFFFF,back:#0000FF,bold") 93 | self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, "fore:#000000,back:#FF0000,bold") 94 | 95 | # Python styles 96 | # Default 97 | self.StyleSetSpec(stc.STC_P_DEFAULT, "fore:#000000,face:%(helv)s,size:%(size)d" % faces) 98 | # Comments 99 | self.StyleSetSpec(stc.STC_P_COMMENTLINE, "fore:#007F00,face:%(helv)s,size:%(size)d" % faces) 100 | # Number 101 | self.StyleSetSpec(stc.STC_P_NUMBER, "fore:#007F7F,size:%(size)d" % faces) 102 | # String 103 | self.StyleSetSpec(stc.STC_P_STRING, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces) 104 | # Single quoted string 105 | self.StyleSetSpec(stc.STC_P_CHARACTER, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces) 106 | # Keyword 107 | self.StyleSetSpec(stc.STC_P_WORD, "fore:#00007F,bold,size:%(size)d" % faces) 108 | # Triple quotes 109 | self.StyleSetSpec(stc.STC_P_TRIPLE, "fore:#7F0000,size:%(size)d" % faces) 110 | # Triple double quotes 111 | self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, "fore:#7F0000,size:%(size)d" % faces) 112 | # Class name definition 113 | self.StyleSetSpec(stc.STC_P_CLASSNAME, "fore:#0000FF,bold,underline,size:%(size)d" % faces) 114 | # Function or method name definition 115 | self.StyleSetSpec(stc.STC_P_DEFNAME, "fore:#007F7F,bold,size:%(size)d" % faces) 116 | # Operators 117 | self.StyleSetSpec(stc.STC_P_OPERATOR, "bold,size:%(size)d" % faces) 118 | # Identifiers 119 | self.StyleSetSpec(stc.STC_P_IDENTIFIER, "fore:#000000,face:%(helv)s,size:%(size)d" % faces) 120 | # Comment-blocks 121 | self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, "fore:#7F7F7F,size:%(size)d" % faces) 122 | # End of line where string is not closed 123 | self.StyleSetSpec(stc.STC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eol,size:%(size)d" % faces) 124 | 125 | self.SetCaretForeground("BLUE") 126 | 127 | # register some images for use in the AutoComplete box. 128 | # self.RegisterImage(1, images.Smiles.GetBitmap()) 129 | self.RegisterImage(2, wx.ArtProvider.GetBitmap(wx.ART_NEW, size=wx.Size(16, 16))) 130 | self.RegisterImage(3, wx.ArtProvider.GetBitmap(wx.ART_COPY, size=wx.Size(16, 16))) 131 | 132 | self.SetText(text) 133 | 134 | def OnKeyPressed(self, event): 135 | if self.CallTipActive(): 136 | self.CallTipCancel() 137 | key = event.GetKeyCode() 138 | 139 | if key == 32 and event.ControlDown(): 140 | pos = self.GetCurrentPos() 141 | 142 | # Tips 143 | if event.ShiftDown(): 144 | self.CallTipSetBackground("yellow") 145 | self.CallTipShow( 146 | pos, 147 | "lots of of text: blah, blah, blah\n\n" 148 | "show some suff, maybe parameters..\n\n" 149 | "fubar(param1, param2)", 150 | ) 151 | # Code completion 152 | else: 153 | # lst = [] 154 | # for x in range(50000): 155 | # lst.append('%05d' % x) 156 | # st = " ".join(lst) 157 | # print(len(st)) 158 | # self.AutoCompShow(0, st) 159 | 160 | kw = list(keyword.kwlist) 161 | kw.append("zzzzzz?2") 162 | kw.append("aaaaa?2") 163 | kw.append("__init__?3") 164 | kw.append("zzaaaaa?2") 165 | kw.append("zzbaaaa?2") 166 | kw.append("this_is_a_longer_value") 167 | # kw.append("this_is_a_much_much_much_much_much_much_much_longer_value") 168 | 169 | kw.sort() # Python sorts are case sensitive 170 | self.AutoCompSetIgnoreCase(False) # so this needs to match 171 | 172 | # Images are specified with a appended "?type" 173 | for i in range(len(kw)): 174 | if kw[i] in keyword.kwlist: 175 | kw[i] = kw[i] + "?1" 176 | 177 | self.AutoCompShow(0, " ".join(kw)) 178 | else: 179 | event.Skip() 180 | 181 | def OnUpdateUI(self, evt): 182 | # check for matching braces 183 | braceAtCaret = -1 184 | braceOpposite = -1 185 | charBefore = None 186 | styleBefore = None 187 | caretPos = self.GetCurrentPos() 188 | 189 | if caretPos > 0: 190 | charBefore = self.GetCharAt(caretPos - 1) 191 | styleBefore = self.GetStyleAt(caretPos - 1) 192 | 193 | # check before 194 | if charBefore and chr(charBefore) in "[]{}()" and styleBefore == stc.STC_P_OPERATOR: 195 | braceAtCaret = caretPos - 1 196 | 197 | # check after 198 | if braceAtCaret < 0: 199 | charAfter = self.GetCharAt(caretPos) 200 | styleAfter = self.GetStyleAt(caretPos) 201 | 202 | if charAfter and chr(charAfter) in "[]{}()" and styleAfter == stc.STC_P_OPERATOR: 203 | braceAtCaret = caretPos 204 | 205 | if braceAtCaret >= 0: 206 | braceOpposite = self.BraceMatch(braceAtCaret) 207 | 208 | if braceAtCaret != -1 and braceOpposite == -1: 209 | self.BraceBadLight(braceAtCaret) 210 | else: 211 | self.BraceHighlight(braceAtCaret, braceOpposite) 212 | # pt = self.PointFromPosition(braceOpposite) 213 | # self.Refresh(True, wxRect(pt.x, pt.y, 5,5)) 214 | # print(pt) 215 | # self.Refresh(False) 216 | 217 | def OnMarginClick(self, evt): 218 | # fold and unfold as needed 219 | if evt.GetMargin() == 2: 220 | if evt.GetShift() and evt.GetControl(): 221 | self.FoldAll() 222 | else: 223 | lineClicked = self.LineFromPosition(evt.GetPosition()) 224 | 225 | if self.GetFoldLevel(lineClicked) & stc.STC_FOLDLEVELHEADERFLAG: 226 | if evt.GetShift(): 227 | self.SetFoldExpanded(lineClicked, True) 228 | self.Expand(lineClicked, True, True, 1) 229 | elif evt.GetControl(): 230 | if self.GetFoldExpanded(lineClicked): 231 | self.SetFoldExpanded(lineClicked, False) 232 | self.Expand(lineClicked, False, True, 0) 233 | else: 234 | self.SetFoldExpanded(lineClicked, True) 235 | self.Expand(lineClicked, True, True, 100) 236 | else: 237 | self.ToggleFold(lineClicked) 238 | 239 | def FoldAll(self): 240 | lineCount = self.GetLineCount() 241 | expanding = True 242 | 243 | # find out if we are folding or unfolding 244 | for lineNum in range(lineCount): 245 | if self.GetFoldLevel(lineNum) & stc.STC_FOLDLEVELHEADERFLAG: 246 | expanding = not self.GetFoldExpanded(lineNum) 247 | break 248 | 249 | lineNum = 0 250 | 251 | while lineNum < lineCount: 252 | level = self.GetFoldLevel(lineNum) 253 | if level & stc.STC_FOLDLEVELHEADERFLAG and (level & stc.STC_FOLDLEVELNUMBERMASK) == stc.STC_FOLDLEVELBASE: 254 | 255 | if expanding: 256 | self.SetFoldExpanded(lineNum, True) 257 | lineNum = self.Expand(lineNum, True) 258 | lineNum = lineNum - 1 259 | else: 260 | lastChild = self.GetLastChild(lineNum, -1) 261 | self.SetFoldExpanded(lineNum, False) 262 | 263 | if lastChild > lineNum: 264 | self.HideLines(lineNum + 1, lastChild) 265 | 266 | lineNum = lineNum + 1 267 | 268 | def Expand(self, line, doExpand, force=False, visLevels=0, level=-1): 269 | lastChild = self.GetLastChild(line, level) 270 | line = line + 1 271 | 272 | while line <= lastChild: 273 | if force: 274 | if visLevels > 0: 275 | self.ShowLines(line, line) 276 | else: 277 | self.HideLines(line, line) 278 | else: 279 | if doExpand: 280 | self.ShowLines(line, line) 281 | 282 | if level == -1: 283 | level = self.GetFoldLevel(line) 284 | 285 | if level & stc.STC_FOLDLEVELHEADERFLAG: 286 | if force: 287 | if visLevels > 1: 288 | self.SetFoldExpanded(line, True) 289 | else: 290 | self.SetFoldExpanded(line, False) 291 | 292 | line = self.Expand(line, doExpand, force, visLevels - 1) 293 | 294 | else: 295 | if doExpand and self.GetFoldExpanded(line): 296 | line = self.Expand(line, True, force, visLevels - 1) 297 | else: 298 | line = self.Expand(line, False, force, visLevels - 1) 299 | else: 300 | line = line + 1 301 | 302 | return line 303 | 304 | 305 | def GetCaretPeriod(win=None): 306 | """ 307 | Attempts to identify the correct caret blinkrate to use in the Demo Code panel. 308 | 309 | :pram wx.Window win: a window to pass to wx.SystemSettings.GetMetric. 310 | 311 | :return: a value in milliseconds that indicates the proper period. 312 | :rtype: int 313 | 314 | :raises: ValueError if unable to resolve a proper caret blink rate. 315 | """ 316 | if "--no-caret-blink" in sys.argv: 317 | return 0 318 | 319 | try: 320 | onmsec = wx.SystemSettings.GetMetric(wx.SYS_CARET_ON_MSEC, win) 321 | offmsec = wx.SystemSettings.GetMetric(wx.SYS_CARET_OFF_MSEC, win) 322 | 323 | # check values aren't -1 324 | if -1 in (onmsec, offmsec): 325 | raise ValueError("Unable to determine caret blink rate.") 326 | 327 | # attempt to average. 328 | # (wx systemsettings allows on and off time, but scintilla just takes a single period.) 329 | return int((onmsec + offmsec) / 2.0) 330 | 331 | except AttributeError: 332 | # Issue where wx.SYS_CARET_ON/OFF_MSEC is unavailable. 333 | raise ValueError("Unable to determine caret blink rate.") 334 | 335 | 336 | class WxPythonCodeEditor(PythonSTC): 337 | def __init__(self, parent, ID, text="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=0): 338 | PythonSTC.__init__(self, parent, ID, text, pos, size, style) 339 | self.SetUpEditor() 340 | 341 | # Some methods to make it compatible with how the wxTextCtrl is used 342 | def SetValue(self, value): 343 | # if wx.USE_UNICODE: 344 | # value = value.decode('iso8859_1') 345 | val = self.GetReadOnly() 346 | self.SetReadOnly(False) 347 | self.SetText(value) 348 | self.EmptyUndoBuffer() 349 | self.SetSavePoint() 350 | self.SetReadOnly(val) 351 | 352 | def SetEditable(self, val): 353 | self.SetReadOnly(not val) 354 | 355 | def IsModified(self): 356 | return self.GetModify() 357 | 358 | def Clear(self): 359 | self.ClearAll() 360 | 361 | def SetInsertionPoint(self, pos): 362 | self.SetCurrentPos(pos) 363 | self.SetAnchor(pos) 364 | 365 | def ShowPosition(self, pos): 366 | line = self.LineFromPosition(pos) 367 | # self.EnsureVisible(line) 368 | self.GotoLine(line) 369 | 370 | def GetLastPosition(self): 371 | return self.GetLength() 372 | 373 | def GetPositionFromLine(self, line): 374 | return self.PositionFromLine(line) 375 | 376 | def GetRange(self, start, end): 377 | return self.GetTextRange(start, end) 378 | 379 | def GetSelection(self): 380 | return self.GetAnchor(), self.GetCurrentPos() 381 | 382 | def SetSelection(self, start, end): 383 | self.SetSelectionStart(start) 384 | self.SetSelectionEnd(end) 385 | 386 | def SelectLine(self, line): 387 | start = self.PositionFromLine(line) 388 | end = self.GetLineEndPosition(line) 389 | self.SetSelection(start, end) 390 | 391 | def SetUpEditor(self): 392 | """ 393 | This method carries out the work of setting up the demo editor. 394 | It's separate so as not to clutter up the init code. 395 | """ 396 | import keyword 397 | 398 | self.SetLexer(stc.STC_LEX_PYTHON) 399 | self.SetKeyWords(0, " ".join(keyword.kwlist)) 400 | 401 | # Enable folding 402 | self.SetProperty("fold", "1") 403 | 404 | # Highlight tab/space mixing (shouldn't be any) 405 | self.SetProperty("tab.timmy.whinge.level", "1") 406 | 407 | # Set left and right margins 408 | self.SetMargins(2, 2) 409 | 410 | # Set up the numbers in the margin for margin #1 411 | self.SetMarginType(1, stc.STC_MARGIN_NUMBER) 412 | # Reasonable value for, say, 4-5 digits using a mono font (40 pix) 413 | self.SetMarginWidth(1, 40) 414 | 415 | # Indentation and tab stuff 416 | self.SetIndent(4) # Proscribed indent size for wx 417 | self.SetIndentationGuides(True) # Show indent guides 418 | self.SetBackSpaceUnIndents(True) # Backspace unindents rather than delete 1 space 419 | self.SetTabIndents(True) # Tab key indents 420 | self.SetTabWidth(4) # Proscribed tab size for wx 421 | self.SetUseTabs(False) # Use spaces rather than tabs, or 422 | # TabTimmy will complain! 423 | # White space 424 | self.SetViewWhiteSpace(False) # Don't view white space 425 | 426 | # EOL: Since we are loading/saving ourselves, and the 427 | # strings will always have \n's in them, set the STC to 428 | # edit them that way. 429 | self.SetEOLMode(stc.STC_EOL_LF) 430 | self.SetViewEOL(False) 431 | 432 | # No right-edge mode indicator 433 | self.SetEdgeMode(stc.STC_EDGE_NONE) 434 | 435 | # Setup a margin to hold fold markers 436 | self.SetMarginType(2, stc.STC_MARGIN_SYMBOL) 437 | self.SetMarginMask(2, stc.STC_MASK_FOLDERS) 438 | self.SetMarginSensitive(2, True) 439 | self.SetMarginWidth(2, 12) 440 | 441 | # and now set up the fold markers 442 | self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND, stc.STC_MARK_BOXPLUSCONNECTED, "white", "black") 443 | self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_BOXMINUSCONNECTED, "white", "black") 444 | self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_TCORNER, "white", "black") 445 | self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_LCORNER, "white", "black") 446 | self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_VLINE, "white", "black") 447 | self.MarkerDefine(stc.STC_MARKNUM_FOLDER, stc.STC_MARK_BOXPLUS, "white", "black") 448 | self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_BOXMINUS, "white", "black") 449 | 450 | # Global default style 451 | if wx.Platform == "__WXMSW__": 452 | self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "fore:#000000,back:#FFFFFF,face:Courier New") 453 | elif wx.Platform == "__WXMAC__": 454 | # TODO: if this looks fine on Linux too, remove the Mac-specific case 455 | # and use this whenever OS != MSW. 456 | self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "fore:#000000,back:#FFFFFF,face:Monaco") 457 | else: 458 | defsize = wx.SystemSettings.GetFont(wx.SYS_ANSI_FIXED_FONT).GetPointSize() 459 | self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "fore:#000000,back:#FFFFFF,face:Courier,size:%d" % defsize) 460 | 461 | # Clear styles and revert to default. 462 | self.StyleClearAll() 463 | 464 | # Following style specs only indicate differences from default. 465 | # The rest remains unchanged. 466 | 467 | # Line numbers in margin 468 | self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "fore:#000000,back:#99A9C2") 469 | # Highlighted brace 470 | self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, "fore:#00009D,back:#FFFF00") 471 | # Unmatched brace 472 | self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, "fore:#00009D,back:#FF0000") 473 | # Indentation guide 474 | self.StyleSetSpec(stc.STC_STYLE_INDENTGUIDE, "fore:#CDCDCD") 475 | 476 | # Python styles 477 | self.StyleSetSpec(stc.STC_P_DEFAULT, "fore:#000000") 478 | # Comments 479 | self.StyleSetSpec(stc.STC_P_COMMENTLINE, "fore:#008000,back:#F0FFF0") 480 | self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, "fore:#008000,back:#F0FFF0") 481 | # Numbers 482 | self.StyleSetSpec(stc.STC_P_NUMBER, "fore:#008080") 483 | # Strings and characters 484 | self.StyleSetSpec(stc.STC_P_STRING, "fore:#800080") 485 | self.StyleSetSpec(stc.STC_P_CHARACTER, "fore:#800080") 486 | # Keywords 487 | self.StyleSetSpec(stc.STC_P_WORD, "fore:#000080,bold") 488 | # Triple quotes 489 | self.StyleSetSpec(stc.STC_P_TRIPLE, "fore:#800080,back:#FFFFEA") 490 | self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, "fore:#800080,back:#FFFFEA") 491 | # Class names 492 | self.StyleSetSpec(stc.STC_P_CLASSNAME, "fore:#0000FF,bold") 493 | # Function names 494 | self.StyleSetSpec(stc.STC_P_DEFNAME, "fore:#008080,bold") 495 | # Operators 496 | self.StyleSetSpec(stc.STC_P_OPERATOR, "fore:#800000,bold") 497 | # Identifiers. I leave this as not bold because everything seems 498 | # to be an identifier if it doesn't match the above criterae 499 | self.StyleSetSpec(stc.STC_P_IDENTIFIER, "fore:#000000") 500 | 501 | # Caret color 502 | self.SetCaretForeground("BLUE") 503 | # Selection background 504 | self.SetSelBackground(1, "#66CCFF") 505 | 506 | # Attempt to set caret blink rate. 507 | try: 508 | self.SetCaretPeriod(GetCaretPeriod(self)) 509 | except ValueError: 510 | pass 511 | 512 | self.SetSelBackground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)) 513 | self.SetSelForeground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)) 514 | 515 | def RegisterModifiedEvent(self, eventHandler): 516 | self.Bind(stc.EVT_STC_CHANGE, eventHandler) 517 | -------------------------------------------------------------------------------- /doc/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrid/construct-editor/b4c63dcea1a057cbcc7106b2d58c8bb4d8503e3b/doc/example.png -------------------------------------------------------------------------------- /doc/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrid/construct-editor/b4c63dcea1a057cbcc7106b2d58c8bb4d8503e3b/doc/preview.gif -------------------------------------------------------------------------------- /doc/screenshot_scripts/_create_all.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | 4 | def create_all(): 5 | screenshot_module_names = [ 6 | "doc.screenshot_scripts.bitwise", 7 | "doc.screenshot_scripts.example", 8 | ] 9 | 10 | for module_name in screenshot_module_names: 11 | module = importlib.import_module(module_name) 12 | module.Frame.create_screenshot() 13 | 14 | 15 | if __name__ == "__main__": 16 | create_all() 17 | -------------------------------------------------------------------------------- /doc/screenshot_scripts/_helper.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import sys 3 | 4 | import construct as cs 5 | import wx 6 | 7 | from construct_editor.wx_widgets import WxConstructHexEditor 8 | 9 | 10 | def take_screenshot(win: wx.Window, file_name: str): 11 | """ 12 | Takes a screenshot of the screen at give pos & size (rect). 13 | """ 14 | rect: wx.Rect = win.GetClientRect() 15 | rect.SetPosition(win.ClientToScreen(0, 0)) 16 | # see http://aspn.activestate.com/ASPN/Mail/Message/wxpython-users/3575899 17 | # created by Andrea Gavana 18 | 19 | # adjust widths for Linux (figured out by John Torres 20 | # http://article.gmane.org/gmane.comp.python.wxpython/67327) 21 | if sys.platform == "linux2": 22 | client_x, client_y = win.ClientToScreen((0, 0)) 23 | border_width = client_x - rect.x 24 | title_bar_height = client_y - rect.y 25 | rect.width += border_width * 2 26 | rect.height += title_bar_height + border_width 27 | 28 | # Create a DC for the whole screen area 29 | dcScreen = wx.ScreenDC() 30 | 31 | # Create a Bitmap that will hold the screenshot image later on 32 | # Note that the Bitmap must have a size big enough to hold the screenshot 33 | # -1 means using the current default colour depth 34 | bmp: wx.Bitmap = wx.Bitmap(rect.width, rect.height) 35 | 36 | # Create a memory DC that will be used for actually taking the screenshot 37 | memDC = wx.MemoryDC() 38 | 39 | # Tell the memory DC to use our Bitmap 40 | # all drawing action on the memory DC will go to the Bitmap now 41 | memDC.SelectObject(bmp) 42 | 43 | # Blit (in this case copy) the actual screen on the memory DC 44 | # and thus the Bitmap 45 | memDC.Blit( 46 | 0, # Copy to this X coordinate 47 | 0, # Copy to this Y coordinate 48 | rect.width, # Copy this width 49 | rect.height, # Copy this height 50 | dcScreen, # From where do we copy? 51 | rect.x, # What's the X offset in the original DC? 52 | rect.y, # What's the Y offset in the original DC? 53 | ) 54 | 55 | # Select the Bitmap out of the memory DC by selecting a new 56 | # uninitialized Bitmap 57 | memDC.SelectObject(wx.NullBitmap) 58 | img: wx.Image = bmp.ConvertToImage() 59 | 60 | img.SaveFile(str(file_name), wx.BITMAP_TYPE_PNG) 61 | 62 | 63 | SCREENSHOT_FOLDER = "doc/screenshots/" 64 | 65 | 66 | class ScreenshotFrame(wx.Frame): 67 | screenshot_name: str 68 | 69 | def __init__(self, auto_close: bool): 70 | super().__init__(None) 71 | self.SetTitle("Construct Hex Editor Example") 72 | self.SetSize(1000, 400) 73 | self.Center() 74 | 75 | self.auto_close = auto_close 76 | 77 | self.editor_panel = WxConstructHexEditor(self, construct=cs.Pass, binary=b"") 78 | 79 | self.init_example() 80 | 81 | wx.CallLater(100, self._take_screenshot_and_close) 82 | 83 | def _take_screenshot_and_close(self): 84 | take_screenshot( 85 | self.editor_panel, 86 | SCREENSHOT_FOLDER + f"{self.screenshot_name}_{sys.platform}.png", 87 | ) 88 | if self.auto_close is True: 89 | self.Close() 90 | 91 | @abc.abstractmethod 92 | def init_example(self): 93 | ... 94 | 95 | @classmethod 96 | def create_screenshot(cls, auto_close: bool = True): 97 | app = wx.App(False) 98 | frame = cls(auto_close) 99 | frame.Show(True) 100 | app.MainLoop() 101 | -------------------------------------------------------------------------------- /doc/screenshot_scripts/bitwise.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | 3 | from doc.screenshot_scripts._helper import ScreenshotFrame 4 | 5 | 6 | 7 | class Frame(ScreenshotFrame): 8 | screenshot_name = "bitwise" 9 | 10 | def init_example(self): 11 | # show construct 12 | constr = cs.Bitwise(cs.GreedyRange(cs.Bit)) 13 | b = bytes([0x12]) 14 | 15 | self.editor_panel.change_construct(constr) 16 | self.editor_panel.change_binary(b) 17 | self.editor_panel.construct_editor.expand_all() 18 | 19 | # select item 20 | editor = self.editor_panel.construct_editor 21 | 22 | childs = editor.model.get_children(None) 23 | childs = editor.model.get_children(childs[0]) 24 | editor.select_entry(childs[1]) 25 | 26 | 27 | if __name__ == "__main__": 28 | Frame.create_screenshot(auto_close=False) 29 | -------------------------------------------------------------------------------- /doc/screenshot_scripts/example.py: -------------------------------------------------------------------------------- 1 | import construct as cs 2 | 3 | from doc.screenshot_scripts._helper import ScreenshotFrame 4 | 5 | 6 | class Frame(ScreenshotFrame): 7 | screenshot_name = "example" 8 | 9 | def init_example(self): 10 | # show construct 11 | constr = cs.Struct( 12 | "signature" / cs.Const(b"BMP"), 13 | "width" / cs.Int8ub, 14 | "height" / cs.Int8ub, 15 | "pixels" / cs.Array(cs.this.width * cs.this.height, cs.Byte), 16 | ) 17 | b = b"BMP\x03\x02\x07\x08\t\x0b\x0c\r" 18 | 19 | self.editor_panel.change_construct(constr) 20 | self.editor_panel.change_binary(b) 21 | self.editor_panel.construct_editor.expand_all() 22 | 23 | 24 | if __name__ == "__main__": 25 | Frame.create_screenshot(auto_close=False) 26 | -------------------------------------------------------------------------------- /doc/screenshots/bitwise_win32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrid/construct-editor/b4c63dcea1a057cbcc7106b2d58c8bb4d8503e3b/doc/screenshots/bitwise_win32.png -------------------------------------------------------------------------------- /doc/screenshots/example_win32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrid/construct-editor/b4c63dcea1a057cbcc7106b2d58c8bb4d8503e3b/doc/screenshots/example_win32.png -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | ".venv", 4 | ".venv_38", 5 | ".venv_39", 6 | ".venv_310" 7 | ] 8 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=6.2.0 2 | numpy>=1.20.* 3 | arrow>=1.0.0 4 | ruamel.yaml 5 | cloudpickle 6 | lz4 7 | black 8 | isort 9 | mypy 10 | -e . -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | exec(open("./construct_editor/version.py").read()) 5 | 6 | setup( 7 | name="construct-editor", 8 | version=version_string, # type: ignore 9 | packages=[ 10 | "construct_editor", 11 | "construct_editor.core", 12 | "construct_editor.gallery", 13 | "construct_editor.wx_widgets", 14 | ], 15 | package_data={ 16 | "construct_editor": ["py.typed"], 17 | }, 18 | entry_points={ 19 | "gui_scripts": [ 20 | "construct-editor=construct_editor.main:main" 21 | ] 22 | }, 23 | include_package_data=True, 24 | license="MIT", 25 | description="GUI (based on wxPython) for 'construct', which is a powerful declarative and symmetrical parser and builder for binary data.", 26 | long_description=open("README.md").read(), 27 | long_description_content_type="text/markdown", 28 | platforms=["Windows"], 29 | url="https://github.com/timrid/construct-editor", 30 | author="Tim Riddermann", 31 | python_requires=">=3.8", 32 | install_requires=[ 33 | "construct==2.10.68", 34 | "construct-typing==0.6.*", 35 | "wxPython>=4.1.1", 36 | "arrow>=1.0.0", 37 | "wrapt>=1.14.0", 38 | "typing-extensions>=4.4.0" 39 | ], 40 | keywords=[ 41 | "gui", 42 | "wx", 43 | "wxpython", 44 | "widget", 45 | "binary", 46 | "editor" "construct", 47 | "kaitai", 48 | "declarative", 49 | "data structure", 50 | "struct", 51 | "binary", 52 | "symmetric", 53 | "parser", 54 | "builder", 55 | "parsing", 56 | "building", 57 | "pack", 58 | "unpack", 59 | "packer", 60 | "unpacker", 61 | "bitstring", 62 | "bytestring", 63 | "bitstruct", 64 | ], 65 | classifiers=[ 66 | "Development Status :: 3 - Alpha", 67 | "License :: OSI Approved :: MIT License", 68 | "Programming Language :: Python :: 3", 69 | "Programming Language :: Python :: 3.8", 70 | "Programming Language :: Python :: 3.9", 71 | "Programming Language :: Python :: 3.10", 72 | "Programming Language :: Python :: 3.11", 73 | "Programming Language :: Python :: Implementation :: CPython", 74 | "Typing :: Typed", 75 | ], 76 | ) 77 | --------------------------------------------------------------------------------