├── .clang-format ├── .github ├── Invoke-VisualStudio.ps1 └── workflows │ └── build.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake └── FindDieLibrary.cmake ├── pyproject.toml └── python ├── CMakeLists.txt ├── die └── __init__.py ├── inc └── die.hpp ├── src └── die.cpp └── tests ├── data ├── README.md ├── test.rar └── test.txt ├── test_die.py └── test_regression.py /.clang-format: -------------------------------------------------------------------------------- 1 | # Format Style Options - Created with Clang Power Tools 2 | --- 3 | AccessModifierOffset: -4 4 | AlignAfterOpenBracket: AlwaysBreak 5 | AlignConsecutiveAssignments: true 6 | AlignConsecutiveBitFields: true 7 | AlignConsecutiveMacros: false 8 | AlignEscapedNewlines: Right 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllArgumentsOnNextLine: false 12 | AllowAllConstructorInitializersOnNextLine: false 13 | AllowAllParametersOfDeclarationOnNextLine: false 14 | AllowShortBlocksOnASingleLine: false 15 | AllowShortCaseLabelsOnASingleLine: false 16 | AllowShortLambdasOnASingleLine: Empty 17 | AllowShortEnumsOnASingleLine: false 18 | AllowShortFunctionsOnASingleLine: None 19 | AllowShortIfStatementsOnASingleLine: Never 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterReturnType: All 22 | AlwaysBreakBeforeMultilineStrings: true 23 | AlwaysBreakTemplateDeclarations: Yes 24 | BasedOnStyle: Google 25 | BinPackArguments: false 26 | BinPackParameters: false 27 | BitFieldColonSpacing: Both 28 | BraceWrapping: 29 | AfterCaseLabel: true 30 | AfterClass: true 31 | AfterControlStatement: true 32 | AfterEnum: true 33 | AfterFunction: true 34 | AfterNamespace: true 35 | AfterObjCDeclaration: true 36 | AfterStruct: true 37 | AfterUnion: true 38 | AfterExternBlock: true 39 | BeforeCatch: true 40 | BeforeElse: true 41 | IndentBraces: true 42 | SplitEmptyFunction: true 43 | SplitEmptyRecord: true 44 | SplitEmptyNamespace: true 45 | BeforeLambdaBody: false 46 | BeforeWhile: false 47 | BreakBeforeBinaryOperators: None 48 | BreakBeforeBraces: Allman 49 | BreakInheritanceList: AfterColon 50 | BreakBeforeTernaryOperators: false 51 | BreakConstructorInitializers: AfterColon 52 | ColumnLimit: 120 53 | CommentPragmas: '^ IWYU pragma:' 54 | CompactNamespaces: false 55 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 56 | ConstructorInitializerIndentWidth : 4 57 | ContinuationIndentWidth: 4 58 | Cpp11BracedListStyle: true 59 | DerivePointerAlignment: false 60 | ExperimentalAutoDetectBinPacking: false 61 | FixNamespaceComments: true 62 | ForEachMacros: 63 | [foreach, Q_FOREACH, BOOST_FOREACH] 64 | IndentCaseBlocks: false 65 | IndentCaseLabels: false 66 | IndentExternBlock: NoIndent 67 | IndentGotoLabels: false 68 | IndentPPDirectives: None 69 | IndentWidth: 4 70 | IndentWrappedFunctionNames: false 71 | KeepEmptyLinesAtTheStartOfBlocks: true 72 | Language: Cpp 73 | MaxEmptyLinesToKeep: 2 74 | NamespaceIndentation: None 75 | PenaltyBreakBeforeFirstCallParameter: 1 76 | PenaltyBreakComment: 300 77 | PenaltyBreakFirstLessLess: 120 78 | PenaltyBreakString: 1000 79 | PenaltyExcessCharacter: 1000000 80 | PenaltyReturnTypeOnItsOwnLine: 200 81 | PointerAlignment: Left 82 | ReflowComments: true 83 | SortUsingDeclarations: true 84 | SpaceAfterCStyleCast: false 85 | SpaceAfterLogicalNot: false 86 | SpaceAfterTemplateKeyword: false 87 | SpaceAroundPointerQualifiers: Before 88 | SpaceBeforeAssignmentOperators: true 89 | SpaceBeforeCpp11BracedList: true 90 | SpaceBeforeParens: ControlStatements 91 | SpaceBeforeRangeBasedForLoopColon: true 92 | SpaceBeforeSquareBrackets: false 93 | SpaceInEmptyBlock: false 94 | SpaceInEmptyParentheses: false 95 | SpacesBeforeTrailingComments: 1 96 | SpacesInAngles: false 97 | SpacesInContainerLiterals: false 98 | SpacesInCStyleCastParentheses: false 99 | SpacesInConditionalStatement: true 100 | SpacesInParentheses: false 101 | SpacesInSquareBrackets: false 102 | Standard: Auto 103 | StatementMacros: 104 | ['EXTERN_C', 'PAGED', 'PAGEDX', 'NONPAGED', 'PNPCODE', 'INITCODE', '_At_', '_When_', '_Success_', '_Check_return_', '_Must_inspect_result_', '_IRQL_requires_', '_IRQL_requires_max_', '_IRQL_requires_min_', '_IRQL_saves_', '_IRQL_restores_', '_IRQL_saves_global_', '_IRQL_restores_global_', '_IRQL_raises_', '_IRQL_lowers_', '_Acquires_lock_', '_Releases_lock_', '_Acquires_exclusive_lock_', '_Releases_exclusive_lock_', '_Acquires_shared_lock_', '_Releases_shared_lock_', '_Requires_lock_held_', '_Use_decl_annotations_', '_Guarded_by_', '__drv_preferredFunction', '__drv_allocatesMem', '__drv_freesMem'] 105 | TabWidth: 4 106 | UseCRLF: true 107 | UseTab: Never 108 | ... 109 | -------------------------------------------------------------------------------- /.github/Invoke-VisualStudio.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-CmdScript { 2 | param( 3 | [String] $scriptName 4 | ) 5 | $cmdLine = """$scriptName"" $args & set" 6 | & $env:SystemRoot\system32\cmd.exe /c $cmdLine | 7 | Select-String '^([^=]*)=(.*)$' | ForEach-Object { 8 | $varName = $_.Matches[0].Groups[1].Value 9 | $varValue = $_.Matches[0].Groups[2].Value 10 | Set-Item Env:$varName $varValue 11 | } 12 | } 13 | 14 | Function Invoke-VisualStudio2019x86 15 | { 16 | Invoke-CmdScript "${env:ProgramFiles(x86)}/Microsoft Visual Studio/2019/Professional/VC/Auxiliary\Build/vcvars32.bat" 17 | } 18 | 19 | Function Invoke-VisualStudio2019x64 20 | { 21 | Invoke-CmdScript "${env:ProgramFiles(x86)}/Microsoft Visual Studio/2019/Professional/VC/Auxiliary/Build/vcvars64.bat" 22 | } 23 | 24 | Function Invoke-VisualStudio2022win32 { 25 | Invoke-CmdScript "${env:ProgramFiles}/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars32.bat" 26 | } 27 | 28 | Function Invoke-VisualStudio2022x64 { 29 | Invoke-CmdScript "${env:ProgramFiles}/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat" 30 | } 31 | 32 | Function Invoke-VisualStudio2022arm64 { 33 | Invoke-CmdScript "${env:ProgramFiles}/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvarsamd64_arm64.bat" 34 | } 35 | 36 | Function Invoke-VisualStudio2022arm { 37 | Invoke-CmdScript "${env:ProgramFiles}/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvarsamd64_arm.bat" 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | 4 | on: 5 | workflow_dispatch: 6 | pull_request: 7 | branches: 8 | - main 9 | push: 10 | branches: 11 | - main 12 | tags: 13 | - 'v*' 14 | 15 | env: 16 | VERSION: 0.5.0 17 | 18 | jobs: 19 | bindings: 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | variant: 24 | # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json 25 | - {runner: macos-13, qt-os: mac, arch: x64, py-arch: x64, qt-compiler: clang_64, config: Release, py-version: '3.9' } 26 | - {runner: macos-13, qt-os: mac, arch: x64, py-arch: x64, qt-compiler: clang_64, config: Release, py-version: '3.10' } 27 | - {runner: macos-13, qt-os: mac, arch: x64, py-arch: x64, qt-compiler: clang_64, config: Release, py-version: '3.11' } 28 | - {runner: macos-13, qt-os: mac, arch: x64, py-arch: x64, qt-compiler: clang_64, config: Release, py-version: '3.12' } 29 | - {runner: macos-13, qt-os: mac, arch: x64, py-arch: x64, qt-compiler: clang_64, config: Release, py-version: '3.13' } 30 | - {runner: ubuntu-22.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.9' } 31 | - {runner: ubuntu-22.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.10' } 32 | - {runner: ubuntu-22.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.11' } 33 | - {runner: ubuntu-22.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.12' } 34 | - {runner: ubuntu-22.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.13' } 35 | - {runner: ubuntu-24.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.9' } 36 | - {runner: ubuntu-24.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.10' } 37 | - {runner: ubuntu-24.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.11' } 38 | - {runner: ubuntu-24.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.12' } 39 | - {runner: ubuntu-24.04, qt-os: linux, arch: x64, py-arch: x64, qt-compiler: gcc_64, config: RelWithDebInfo, py-version: '3.13' } 40 | - {runner: ubuntu-24.04-arm, qt-os: linux_arm64, arch: arm64, py-arch: arm64, qt-compiler: gcc_arm64, config: RelWithDebInfo, py-version: '3.13' } 41 | - {runner: windows-2022, qt-os: windows, arch: x64, py-arch: x64, qt-compiler: msvc2019_64, config: RelWithDebInfo, py-version: '3.9' } 42 | - {runner: windows-2022, qt-os: windows, arch: x64, py-arch: x64, qt-compiler: msvc2019_64, config: RelWithDebInfo, py-version: '3.10' } 43 | - {runner: windows-2022, qt-os: windows, arch: x64, py-arch: x64, qt-compiler: msvc2019_64, config: RelWithDebInfo, py-version: '3.11' } 44 | - {runner: windows-2022, qt-os: windows, arch: x64, py-arch: x64, qt-compiler: msvc2019_64, config: RelWithDebInfo, py-version: '3.12' } 45 | - {runner: windows-2022, qt-os: windows, arch: x64, py-arch: x64, qt-compiler: msvc2019_64, config: RelWithDebInfo, py-version: '3.13' } 46 | runs-on: ${{ matrix.variant.runner }} 47 | name: python${{ matrix.variant.py-version }} / ${{ matrix.variant.runner }} / ${{ matrix.variant.config }} 48 | env: 49 | CMAKE_FLAGS: "" 50 | QT_VERSION: 6.7.3 # arm64 support in aqt appears from >= 6.7.0 - and almalinux ships an old libc incompat with >= 6.8.x 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | 55 | - name: Setup Python 56 | uses: actions/setup-python@v5 57 | with: 58 | python-version: ${{ matrix.variant.py-version }} 59 | architecture: ${{ matrix.variant.py-arch }} 60 | 61 | - name: Environment Setup (Windows) 62 | if: startsWith(matrix.variant.runner, 'windows-') 63 | run: | 64 | Import-Module .\.github\Invoke-VisualStudio.ps1 65 | Invoke-VisualStudio2019${{ matrix.variant.arch }} 66 | 67 | - name: Environment Setup (Linux) 68 | if: startsWith(matrix.variant.runner, 'ubuntu-') 69 | run: | 70 | sudo apt-get -y update 71 | sudo apt install -y g++ ninja-build clang 72 | echo CC=clang >> $GITHUB_ENV 73 | echo CXX=clang++ >> $GITHUB_ENV 74 | 75 | - name: Environment Setup (macOS) 76 | if: startsWith(matrix.variant.runner, 'macos-') 77 | run: | 78 | echo CC=clang >> $GITHUB_ENV 79 | echo CXX=clang++ >> $GITHUB_ENV 80 | 81 | - name: Cache Artifacts 82 | id: cache-artifacts 83 | uses: actions/cache@v4 84 | with: 85 | path: build/${{ env.QT_VERSION }} 86 | key: aqt-install-${{ matrix.variant.runner }}-${{ matrix.variant.arch }}-${{ env.QT_VERSION }}-${{ matrix.variant.qt-compiler }} 87 | 88 | - name: Setup Qt (Windows) 89 | if: steps.cache-artifacts.outputs.cache-hit != 'true' && startsWith(matrix.variant.runner, 'windows-') 90 | run: | 91 | python -m pip install aqtinstall --user --upgrade 92 | python -m aqt install-qt -O build/ ${{ matrix.variant.qt-os }} desktop ${{ env.QT_VERSION }} win64_${{ matrix.variant.qt-compiler }} 93 | 94 | - name: Setup Qt (macOS) 95 | if: steps.cache-artifacts.outputs.cache-hit != 'true' && startsWith(matrix.variant.runner, 'macos-') 96 | run: | 97 | python -m pip install aqtinstall --user --upgrade 98 | python -m aqt install-qt -O build/ ${{ matrix.variant.qt-os }} desktop ${{ env.QT_VERSION }} ${{ matrix.variant.qt-compiler }} 99 | 100 | - name: Setup Qt (Linux) 101 | if: steps.cache-artifacts.outputs.cache-hit != 'true' && startsWith(matrix.variant.runner, 'ubuntu-') 102 | run: | 103 | python -m pip install aqtinstall --user --upgrade 104 | python -m aqt install-qt -O build/ ${{ matrix.variant.qt-os }} desktop ${{ env.QT_VERSION }} linux_${{ matrix.variant.qt-compiler }} 105 | 106 | - name: Python installation 107 | run: | 108 | python -m pip install '.[tests]' -U --user 109 | 110 | - name: Test 111 | if: startsWith(matrix.variant.runner, 'macos-') == false 112 | run: | 113 | python -m pytest -v python/tests 114 | 115 | - name: Build wheels (Linux arm64) 116 | if: matrix.variant.runner == 'ubuntu-24.04-arm' && matrix.variant.py-version == '3.13' 117 | run: | 118 | python -m pip uninstall --quiet --yes die-python 119 | rm -fr -- build/cp* 120 | python -m pip install cibuildwheel==2.23.0 121 | python -m cibuildwheel --output-dir wheelhouse --archs aarch64 122 | 123 | - name: Build wheels (Linux x64) 124 | if: matrix.variant.runner == 'ubuntu-24.04' && matrix.variant.py-version == '3.13' 125 | run: | 126 | python -m pip uninstall --quiet --yes die-python 127 | rm -fr -- build/cp* 128 | python -m pip install cibuildwheel==2.23.0 129 | python -m cibuildwheel --output-dir wheelhouse/ --archs x86_64 130 | 131 | - name: Build wheels (Others) 132 | if: startsWith(matrix.variant.runner, 'ubuntu-') == false 133 | run: | 134 | mkdir wheelhouse 135 | python -m pip wheel . --wheel-dir wheel/ --progress-bar on 136 | 137 | - name: Upload artifacts 138 | uses: actions/upload-artifact@v4 139 | with: 140 | name: die-python-py${{ matrix.variant.py-version }}-${{ matrix.variant.runner }}.${{ matrix.variant.config }} 141 | path: | 142 | wheelhouse/die_python-*.whl 143 | wheel/die_python-*.whl 144 | retention-days: 1 145 | 146 | publish: 147 | needs: bindings 148 | strategy: 149 | fail-fast: false 150 | matrix: 151 | variant: 152 | # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json 153 | - {runner: macos-13, config: Release, py-version: '3.9' } 154 | - {runner: macos-13, config: Release, py-version: '3.10' } 155 | - {runner: macos-13, config: Release, py-version: '3.11' } 156 | - {runner: macos-13, config: Release, py-version: '3.12' } 157 | - {runner: macos-13, config: Release, py-version: '3.13' } 158 | # - {runner: ubuntu-24.04, config: RelWithDebInfo, py-version: '3.12' } 159 | - {runner: ubuntu-24.04, config: RelWithDebInfo, py-version: '3.13' } 160 | - {runner: ubuntu-24.04-arm, config: RelWithDebInfo, py-version: '3.13' } 161 | - {runner: windows-2022, config: RelWithDebInfo, py-version: '3.9' } 162 | - {runner: windows-2022, config: RelWithDebInfo, py-version: '3.10' } 163 | - {runner: windows-2022, config: RelWithDebInfo, py-version: '3.11' } 164 | - {runner: windows-2022, config: RelWithDebInfo, py-version: '3.12' } 165 | - {runner: windows-2022, config: RelWithDebInfo, py-version: '3.13' } 166 | runs-on: ubuntu-latest 167 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 168 | name: PyPI upload of ${{ matrix.variant.runner }}/${{ matrix.variant.config }}/py${{ matrix.variant.py-version }} 169 | environment: 170 | name: pypi 171 | url: https://pypi.org/p/die-python 172 | permissions: 173 | id-token: write 174 | steps: 175 | - name: Download Wheel ${{ matrix.variant.runner }}/${{ matrix.variant.py-version }}/${{ matrix.variant.config }} 176 | uses: actions/download-artifact@v4 177 | id: download_wheels 178 | with: 179 | name: die-python-py${{ matrix.variant.py-version }}-${{ matrix.variant.runner }}.${{ matrix.variant.config }} 180 | path: . 181 | - name: Install uv 182 | run: | 183 | curl -LsSf https://astral.sh/uv/install.sh | sh 184 | 185 | - name: Publish package 186 | run: | 187 | uv publish 188 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build* 2 | python/inc/constants.hpp 3 | __pycache__ 4 | .pytest_cache 5 | *.pyc 6 | aqtinstall.log 7 | wheel 8 | wheelhouse 9 | .vscode 10 | .venv 11 | 6.* 12 | /uv.lock -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.26) 2 | 3 | project( 4 | DIE 5 | VERSION 0.5.0 6 | LANGUAGES CXX 7 | DESCRIPTION "DIE Library implementation" 8 | ) 9 | 10 | set(CMAKE_CXX_STANDARD 20) 11 | set(CMAKE_CXX_STANDARD_REQUIRED True) 12 | set(CMAKE_CXX_EXTENSIONS OFF) 13 | set(VCPKG_MANIFEST_MODE OFF) 14 | set(CXX_STANDARD 20) 15 | 16 | set(DIE_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}) 17 | 18 | list(APPEND CMAKE_MODULE_PATH "${DIE_ROOT_DIR}/cmake") 19 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 20 | 21 | add_subdirectory(python) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DetectItEasy-Python 2 | 3 | [![Python 3.8+](https://img.shields.io/pypi/v/die-python.svg)](https://pypi.org/project/die-python/) 4 | [![Downloads](https://static.pepy.tech/badge/die-python)](https://pepy.tech/project/die-python) 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 6 | [![Licence Apache2](https://img.shields.io/badge/License-Apache_2-blue)](https://github.com/elastic/die-python/blob/main/LICENSE) 7 | [![Build](https://github.com/elastic/die-python/actions/workflows/build.yml/badge.svg)](https://github.com/elastic/die-python/actions/workflows/build.yml) 8 | 9 | Native Python 3.8+ bindings for [@horsicq](https://github.com/horsicq/)'s [Detect-It-Easy](https://github.com/horsicq/Detect-It-Easy) 10 | 11 | 12 | ## Install 13 | 14 | ### From PIP 15 | 16 | The easiest and recommended installation is through `pip`. 17 | 18 | ```console 19 | pip install die-python 20 | ``` 21 | 22 | ### Using Git 23 | 24 | ```console 25 | git clone https://github.com/elastic/die-python 26 | cd die-python 27 | ``` 28 | 29 | Install Qt into the `build`. It can be easily installed using [`aqt`](https://github.com/miurahr/aqtinstall) as follow (here with Qt version 6.7.3): 30 | 31 | ```console 32 | python -m pip install aqtinstall --user -U 33 | python -m aqt install-qt -O ./build linux desktop 6.7.3 linux_gcc_64 # linux x64 only 34 | python -m aqt install-qt -O ./build linux_arm64 desktop 6.7.3 linux_gcc_arm64 # linux arm64 only 35 | python -m aqt install-qt -O ./build windows desktop 6.7.3 win64_msvc2019_64 # windows x64 only 36 | python -m aqt install-qt -O ./build mac desktop 6.7.3 clang_64 # mac only 37 | ``` 38 | 39 | Then you can install the package 40 | 41 | ```console 42 | python -m pip install . --user -U 43 | ``` 44 | 45 | 46 | ## Quick start 47 | 48 | ```python 49 | import die, pathlib 50 | 51 | print(die.scan_file("c:/windows/system32/ntdll.dll", die.ScanFlags.DEEP_SCAN)) 52 | 'PE64' 53 | 54 | print(die.scan_file("../upx.exe", die.ScanFlags.RESULT_AS_JSON, str(die.database_path/'db') )) 55 | { 56 | "detects": [ 57 | { 58 | "filetype": "PE64", 59 | "parentfilepart": "Header", 60 | "values": [ 61 | { 62 | "info": "Console64,console", 63 | "name": "GNU linker ld (GNU Binutils)", 64 | "string": "Linker: GNU linker ld (GNU Binutils)(2.28)[Console64,console]", 65 | "type": "Linker", 66 | "version": "2.28" 67 | }, 68 | { 69 | "info": "", 70 | "name": "MinGW", 71 | "string": "Compiler: MinGW", 72 | "type": "Compiler", 73 | "version": "" 74 | }, 75 | { 76 | "info": "NRV,brute", 77 | "name": "UPX", 78 | "string": "Packer: UPX(4.24)[NRV,brute]", 79 | "type": "Packer", 80 | "version": "4.24" 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | 87 | for db in die.databases(): 88 | print(db) 89 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\ACE 90 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\APK\PackageName.1.sg 91 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\APK\SingleJar.3.sg 92 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\APK\_APK.0.sg 93 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\APK\_init 94 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\Archive\_init 95 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\archive-file 96 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\arj 97 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\Binary\Amiga loadable.1.sg 98 | C:\Users\User\AppData\Roaming\Python\Python312\site-packages\die\db\db\Binary\archive.7z.1.sg 99 | [...] 100 | ``` 101 | 102 | ## Licenses 103 | 104 | Released under Apache 2.0 License and integrates the following repositories: 105 | 106 | - [Detect-It-Easy](https://github.com/horsicq/Detect-It-Easy): MIT license 107 | - [die_library](https://github.com/horsicq/die_library): MIT license 108 | - [qt](https://github.com/qt/qt): LGPL license 109 | -------------------------------------------------------------------------------- /cmake/FindDieLibrary.cmake: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | 3 | cmake_path(SET ROOT_DIR NORMALIZE "${CMAKE_CURRENT_LIST_DIR}/..") 4 | 5 | # Only use Qt6 6 | 7 | set(QT_BUILD_VERSION "6.7.3") 8 | 9 | # TODO (calladoum) : here we oversimplify by assuming that compilation HOST and TARGET have same architecture 10 | 11 | if(WIN32) 12 | # python -m aqt install-qt -O build windows desktop ${QT_BUILD_VERSION} win64_msvc2019_64 13 | set(QT_BUILD_COMPILER "msvc2019_64") 14 | 15 | elseif(LINUX) 16 | if (${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "x86_64") 17 | # python -m aqt install-qt -O build linux desktop ${QT_BUILD_VERSION} linux_gcc_64 (x64) 18 | set(QT_BUILD_COMPILER "gcc_64") 19 | endif() 20 | 21 | if (${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "aarch64") 22 | # python -m aqt install-qt -O build linux_arm desktop ${QT_BUILD_VERSION} linux_gcc_arm64 (arm64) 23 | set(QT_BUILD_COMPILER "gcc_arm64") 24 | endif() 25 | 26 | elseif(APPLE) 27 | # python -m aqt install-qt -O build mac desktop ${QT_BUILD_VERSION} clang_64 28 | set(QT_BUILD_COMPILER "macos") 29 | endif() 30 | 31 | if(NOT QT_BUILD_COMPILER) 32 | message(FATAL_ERROR "Invalid Qt compiler setting") 33 | else() 34 | message(STATUS "CMAKE_HOST_SYSTEM_PROCESSOR: ${CMAKE_HOST_SYSTEM_PROCESSOR}") 35 | message(STATUS "QT_BUILD_VERSION: ${QT_BUILD_VERSION}") 36 | message(STATUS "QT_BUILD_COMPILER: ${QT_BUILD_COMPILER}") 37 | endif() 38 | 39 | set(Qt6_CMAKE_ROOT "${ROOT_DIR}/build/${QT_BUILD_VERSION}/${QT_BUILD_COMPILER}/lib/cmake") 40 | set(Qt6_DIR ${Qt6_CMAKE_ROOT}/Qt6) 41 | set(QT_DIR ${Qt6_DIR}) 42 | 43 | message(STATUS "Qt6_CMAKE_ROOT: ${Qt6_CMAKE_ROOT}") 44 | message(STATUS "Qt6_DIR: ${Qt6_DIR}") 45 | 46 | list(INSERT CMAKE_MODULE_PATH 0 47 | ${Qt6_CMAKE_ROOT} 48 | ${Qt6_DIR} 49 | ) 50 | 51 | find_package(Qt6 REQUIRED COMPONENTS Core Qml Concurrent) 52 | 53 | FetchContent_Declare( 54 | DieLibrary 55 | GIT_REPOSITORY "https://github.com/horsicq/die_library" 56 | GIT_TAG 09df9ccafe48a0531987ad1e605402ed79d4c3f6 57 | ) 58 | 59 | set(DIE_BUILD_AS_STATIC ON CACHE INTERNAL "") 60 | FetchContent_MakeAvailable( DieLibrary ) 61 | 62 | message(STATUS "Using DieLibrary in '${dielibrary_SOURCE_DIR}'") 63 | 64 | list(APPEND CMAKE_MODULE_PATH "${dielibrary_SOURCE_DIR}/dep/build_tools/cmake") 65 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2"] 3 | build-backend = "scikit_build_core.build" 4 | 5 | [project] 6 | name = "die_python" 7 | version = "0.5.0" 8 | description = "Python bindings for Detect It Easy (DIE)." 9 | readme = "./README.md" 10 | license.file = "./LICENSE" 11 | requires-python = ">=3.9" 12 | authors = [{ name = "@calladoum-elastic" }] 13 | classifiers = [ 14 | "Development Status :: 4 - Beta", 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: 3.9", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "Programming Language :: Python :: 3.13", 21 | "Natural Language :: English", 22 | ] 23 | dependencies = ["setuptools", "wheel", "nanobind"] 24 | 25 | [project.optional-dependencies] 26 | tests = ["pytest", "black", "beautifulsoup4", "lxml"] 27 | 28 | [project.urls] 29 | Homepage = "https://github.com/elastic/die-python" 30 | 31 | [tool.isort] 32 | profile = "black" 33 | 34 | [tool.scikit-build] 35 | wheel.py-api = "cp313" 36 | minimum-version = "0.4" 37 | build-dir = "build/{wheel_tag}" 38 | cmake.minimum-version = "3.20" 39 | 40 | # Note: VS2022 throws a compiler crash when building nanobind, forcing VS2019 for now 41 | # cmake.args = ["-G", "Visual Studio 16 2019"] 42 | 43 | # Uncomment for debug (+ASAN) 44 | # cmake.verbose = true 45 | # logging.level = "DEBUG" 46 | # cmake.build-type = "Debug" 47 | 48 | [tool.cibuildwheel] 49 | before-build = "dnf install libstdc++ glibc -y && ldconfig" 50 | build = "" 51 | skip = "cp27-* cp35-* cp36-* cp37-* cp38-* pp* *musllinux*" 52 | test-skip = "" 53 | free-threaded-support = false 54 | # use images from https://github.com/pypa/manylinux 55 | archs = ["x86_64", "aarch64"] 56 | # manylinux-x86_64-image = "manylinux_2_28" 57 | # manylinux-aarch64-image = "manylinux_2_28" 58 | manylinux-x86_64-image = "quay.io/pypa/manylinux_2_34_x86_64" 59 | manylinux-aarch64-image = "quay.io/pypa/manylinux_2_34_aarch64" 60 | musllinux-x86_64-image = "musllinux_1_2" 61 | musllinux-aarch64-image = "musllinux_1_2" 62 | -------------------------------------------------------------------------------- /python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project( 2 | die-python 3 | LANGUAGES CXX 4 | VERSION 0.5.0 5 | ) 6 | 7 | find_package(Python 3 8 | REQUIRED COMPONENTS Interpreter Development.Module 9 | OPTIONAL_COMPONENTS Development.SABIModule 10 | ) 11 | 12 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 13 | set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) 14 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") 15 | endif() 16 | 17 | execute_process( 18 | COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir 19 | OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NB_DIR) 20 | list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}") 21 | 22 | find_package(nanobind CONFIG REQUIRED) 23 | find_package(DieLibrary REQUIRED) 24 | 25 | set(DIE_PYTHON_ROOT_DIR ${DIE_ROOT_DIR}/python) 26 | set(DIELIB_BASE_ROOT ${dielibrary_SOURCE_DIR}) 27 | set(DIE_BASE_ROOT "${dielibrary_SOURCE_DIR}/dep/Detect-It-Easy") 28 | 29 | file(STRINGS ${DIELIB_BASE_ROOT}/release_version.txt DIELIB_VERSION) 30 | file(STRINGS ${DIE_BASE_ROOT}/die_version.txt DIE_VERSION) 31 | 32 | nanobind_add_module(_die 33 | ./src/die.cpp 34 | ) 35 | 36 | add_dependencies(_die die) 37 | 38 | target_include_directories( 39 | _die 40 | PRIVATE 41 | ${DIE_PYTHON_ROOT_DIR}/inc 42 | ${DIELIB_BASE_ROOT}/src/include 43 | ) 44 | 45 | if(MSVC) 46 | # Disable MSVC `min` & `max` macros for `die` (breaks Qt) 47 | target_compile_definitions(die PUBLIC NOMINMAX) 48 | endif() 49 | 50 | if(APPLE) 51 | target_link_libraries(_die PRIVATE dl "-framework CoreFoundation") 52 | target_link_options(_die PRIVATE -Wl,-rpath,$ORIGIN/lib) 53 | endif() 54 | 55 | if(LINUX) 56 | target_link_options(_die PRIVATE -Wl,-rpath,$ORIGIN/lib) 57 | endif() 58 | 59 | target_compile_definitions(_die 60 | PRIVATE 61 | DIE_VERSION="${DIE_VERSION}" 62 | DIELIB_VERSION="${DIELIB_VERSION}" 63 | ) 64 | 65 | target_link_libraries(_die PRIVATE $ $) 66 | target_link_libraries(_die PRIVATE Qt6::Core) 67 | target_link_libraries(_die PRIVATE Qt6::Qml) 68 | target_link_libraries(_die PRIVATE Qt6::Concurrent) 69 | target_link_libraries(_die PRIVATE Qt6::Network) 70 | 71 | install(DIRECTORY die DESTINATION .) 72 | install(TARGETS _die DESTINATION die/) 73 | install(TARGETS die DESTINATION die/) 74 | install(DIRECTORY ${DIELIB_BASE_ROOT}/dep/Detect-It-Easy/db DESTINATION die/db) 75 | install(DIRECTORY ${DIELIB_BASE_ROOT}/dep/Detect-It-Easy/db_custom DESTINATION die/db) 76 | 77 | if(LINUX OR APPLE) 78 | install( 79 | DIRECTORY 80 | ${Qt6_DIR}/../../ 81 | DESTINATION 82 | die/lib 83 | FILES_MATCHING 84 | PATTERN "libQt6Core.*" 85 | PATTERN "libQt6Qml.*" 86 | PATTERN "libQt6Concurrent.*" 87 | PATTERN "libQt6Network.*" 88 | PATTERN "libicui18n.*" 89 | PATTERN "libicuuc.*" 90 | PATTERN "libicudata.*" 91 | PATTERN "cmake" EXCLUDE 92 | PATTERN "objects-*" EXCLUDE 93 | PATTERN "pkgconfig" EXCLUDE 94 | ) 95 | else() 96 | install( 97 | DIRECTORY 98 | ${Qt6_DIR}/../../../bin/ 99 | DESTINATION 100 | die/ 101 | FILES_MATCHING 102 | PATTERN "Qt6Core.*" 103 | PATTERN "Qt6Qml.*" 104 | PATTERN "Qt6Concurrent.*" 105 | PATTERN "Qt6Network.*" 106 | ) 107 | endif() -------------------------------------------------------------------------------- /python/die/__init__.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import pathlib 3 | 4 | from typing import Generator, Optional, Union 5 | 6 | from ._die import __version__ # type: ignore 7 | from ._die import DieFlags as _DieFlags # type: ignore 8 | from ._die import ( 9 | ScanFileA as _ScanFileA, # type: ignore 10 | ScanFileExA as _ScanFileExA, # type: ignore 11 | ScanMemoryA as _ScanMemoryA, # type: ignore 12 | ScanMemoryExA as _ScanMemoryExA, # type: ignore 13 | LoadDatabaseA as _LoadDatabaseA, # type: ignore 14 | ) 15 | from ._die import die_version, dielib_version # type: ignore 16 | 17 | version_major, version_minor, version_patch = map(int, __version__.split(".")) 18 | 19 | database_path = pathlib.Path(__path__[0]) / "db" 20 | """Path to the DIE signature database""" 21 | 22 | 23 | class ScanFlags(enum.IntFlag): 24 | ALL_TYPES_SCAN = _DieFlags.AlltypesScan.value 25 | DEEP_SCAN = _DieFlags.Deepscan.value 26 | HEURISTIC_SCAN = _DieFlags.HeuristicScan.value 27 | RECURSIVE_SCAN = _DieFlags.RecursiveScan.value 28 | RESULT_AS_CSV = _DieFlags.ResultAsCsv.value 29 | RESULT_AS_JSON = _DieFlags.ResultAsJson.value 30 | RESULT_AS_TSV = _DieFlags.ResultAsTsv.value 31 | RESULT_AS_XML = _DieFlags.ResultAsXml.value 32 | VERBOSE_FLAG = _DieFlags.Verbose.value 33 | 34 | 35 | def scan_file( 36 | filepath: Union[pathlib.Path, str], flags: ScanFlags, database: Optional[str] = None 37 | ) -> Optional[str]: 38 | """ 39 | Scan the given file against the signature database, if specified 40 | 41 | Arguments: 42 | filepath: Union[pathlib.Path, str] 43 | flags: ScanFlags 44 | database: Optional[str] 45 | 46 | Returns: 47 | Optional[str] 48 | """ 49 | # Check `filepath` 50 | if isinstance(filepath, str): 51 | _fpath = pathlib.Path(filepath) 52 | elif isinstance(filepath, pathlib.Path): 53 | _fpath = filepath 54 | else: 55 | raise TypeError 56 | assert _fpath.exists() 57 | 58 | # Check `database` 59 | if database is None: 60 | res = _ScanFileExA(str(_fpath), flags) 61 | elif isinstance(database, str): 62 | res = _ScanFileA(str(_fpath), flags, database) 63 | else: 64 | raise TypeError 65 | 66 | if not res: 67 | return None 68 | return res.strip() 69 | 70 | 71 | def databases() -> Generator[pathlib.Path, None, None]: 72 | """ 73 | Enumerate all databases 74 | 75 | Returns: 76 | Generator[pathlib.Path, None, None] 77 | """ 78 | 79 | def __enum_db(root: pathlib.Path) -> Generator[pathlib.Path, None, None]: 80 | for child in root.iterdir(): 81 | if child.is_file(): 82 | yield child 83 | if child.is_dir(): 84 | yield from __enum_db(child) 85 | 86 | return __enum_db(database_path) 87 | 88 | 89 | def scan_memory( 90 | memory: Union[bytes, bytearray], flags: ScanFlags, database: Optional[str] = None 91 | ) -> Optional[str]: 92 | """ 93 | Scan the given sequence of bytes against the signature database, if specified 94 | 95 | Arguments: 96 | memory: bytes 97 | flags: ScanFlags 98 | database: Optional[str] 99 | 100 | Returns: 101 | Optional[str] 102 | """ 103 | if not isinstance(memory, bytes) and not isinstance(memory, bytearray): 104 | raise TypeError 105 | 106 | if database is None: 107 | res = _ScanMemoryExA(memory, flags) 108 | elif isinstance(database, str): 109 | res = _ScanMemoryA(memory, flags, database) 110 | else: 111 | raise TypeError 112 | 113 | if not res: 114 | return None 115 | return res.strip() 116 | 117 | 118 | def load_database(database: str) -> int: 119 | """ 120 | Load a database 121 | """ 122 | if not isinstance(database, str): 123 | raise TypeError 124 | 125 | return _LoadDatabaseA(database) 126 | -------------------------------------------------------------------------------- /python/inc/die.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "die.h" 8 | 9 | #ifndef DIELIB_VERSION 10 | #define DIELIB_VERSION "" 11 | #endif // DIELIB_VERSION 12 | 13 | #ifndef DIE_VERSION 14 | #define DIE_VERSION "" 15 | #endif // DIE_VERSION 16 | 17 | #ifdef __cplusplus 18 | namespace DIE 19 | { 20 | enum class DieFlags : uint32_t 21 | { 22 | Deepscan = DIE_DEEPSCAN, 23 | HeuristicScan = DIE_HEURISTICSCAN, 24 | AlltypesScan = DIE_ALLTYPESSCAN, 25 | RecursiveScan = DIE_RECURSIVESCAN, 26 | Verbose = DIE_VERBOSE, 27 | ResultAsXml = DIE_RESULTASXML, 28 | ResultAsJson = DIE_RESULTASJSON, 29 | ResultAsTsv = DIE_RESULTASTSV, 30 | ResultAsCsv = DIE_RESULTASCSV, 31 | }; 32 | 33 | std::optional 34 | ScanFileA(std::string& pszFileName, uint32_t nFlags, std::string& pszDatabase); 35 | 36 | std::optional 37 | ScanFileExA(std::string& pszFileName, uint32_t nFlags); 38 | 39 | std::optional 40 | ScanMemoryA(std::vector& memory, uint32_t nFlags, std::string& pszDatabase); 41 | 42 | std::optional 43 | ScanMemoryExA(std::vector& pszFileName, uint32_t nFlags); 44 | 45 | int32_t 46 | LoadDatabaseA(std::string& pszDatabase); 47 | 48 | 49 | #ifdef _WIN32 50 | int 51 | VB_ScanFile( 52 | std::wstring& pwszFileName, 53 | uint32_t nFlags, 54 | std::wstring& pwszDatabase, 55 | std::wstring& pwszBuffer, 56 | size_t nBufferSize); 57 | #endif // _WIN32 58 | 59 | } // namespace DIE 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /python/src/die.cpp: -------------------------------------------------------------------------------- 1 | #include "die.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace nb = nanobind; 16 | using namespace nb::literals; 17 | 18 | 19 | namespace DIE 20 | { 21 | std::optional 22 | ScanFileA(std::string& pszFileName, uint32_t nFlags, std::string& pszDatabase) 23 | { 24 | auto res = ::DIE_ScanFileA(pszFileName.data(), static_cast(nFlags), pszDatabase.data()); 25 | if ( res == nullptr ) 26 | { 27 | return std::nullopt; 28 | } 29 | 30 | auto const res_str = std::string(res); 31 | ::DIE_FreeMemoryA(res); 32 | return res_str; 33 | } 34 | 35 | std::optional 36 | ScanFileExA(std::string& pszFileName, uint32_t nFlags) 37 | { 38 | auto res = ::DIE_ScanFileExA(pszFileName.data(), static_cast(nFlags)); 39 | if ( res == nullptr ) 40 | { 41 | return std::nullopt; 42 | } 43 | 44 | auto const res_str = std::string(res); 45 | ::DIE_FreeMemoryA(res); 46 | return res_str; 47 | } 48 | 49 | std::optional 50 | ScanMemoryA(std::vector& memory, uint32_t flags, std::string& database) 51 | { 52 | char* pMemory = (char*)memory.data(); 53 | int nMemorySize = static_cast(memory.size()); 54 | int nFlags = static_cast(flags); 55 | char* pszDatabase = database.data(); 56 | auto res = ::DIE_ScanMemoryA(pMemory, nMemorySize, nFlags, pszDatabase); 57 | if ( res == nullptr ) 58 | { 59 | return std::nullopt; 60 | } 61 | 62 | auto const res_str = std::string(res); 63 | ::DIE_FreeMemoryA(res); 64 | return res_str; 65 | } 66 | 67 | std::optional 68 | ScanMemoryExA(std::vector& memory, uint32_t flags) 69 | { 70 | char* pMemory = (char*)memory.data(); 71 | int nMemorySize = static_cast(memory.size()); 72 | int nFlags = static_cast(flags); 73 | auto res = ::DIE_ScanMemoryExA(pMemory, nMemorySize, nFlags); 74 | if ( res == nullptr ) 75 | { 76 | return std::nullopt; 77 | } 78 | 79 | auto const res_str = std::string(res); 80 | ::DIE_FreeMemoryA(res); 81 | return res_str; 82 | } 83 | 84 | int32_t 85 | LoadDatabaseA(std::string& pszDatabase) 86 | { 87 | return ::DIE_LoadDatabaseA(pszDatabase.data()); 88 | } 89 | 90 | #ifdef _WIN32 91 | int 92 | VB_ScanFile( 93 | std::wstring& pwszFileName, 94 | uint32_t nFlags, 95 | std::wstring& pwszDatabase, 96 | std::wstring& pwszBuffer, 97 | uint32_t nBufferSize) 98 | { 99 | return ::DIE_VB_ScanFile( 100 | pwszFileName.data(), 101 | static_cast(nFlags), 102 | pwszDatabase.data(), 103 | pwszBuffer.data(), 104 | static_cast(nBufferSize)); 105 | } 106 | #endif // _WIN32 107 | 108 | } // namespace DIE 109 | 110 | 111 | NB_MODULE(_die, m) 112 | { 113 | nb::enum_(m, "DieFlags") 114 | .value("Deepscan", DIE::DieFlags::Deepscan) 115 | .value("HeuristicScan", DIE::DieFlags::HeuristicScan) 116 | .value("AlltypesScan", DIE::DieFlags::AlltypesScan) 117 | .value("RecursiveScan", DIE::DieFlags::RecursiveScan) 118 | .value("Verbose", DIE::DieFlags::Verbose) 119 | .value("ResultAsXml", DIE::DieFlags::ResultAsXml) 120 | .value("ResultAsJson", DIE::DieFlags::ResultAsJson) 121 | .value("ResultAsTsv", DIE::DieFlags::ResultAsTsv) 122 | .value("ResultAsCsv", DIE::DieFlags::ResultAsCsv) 123 | .export_values(); 124 | 125 | m.doc() = "The native `die` module"; 126 | m.attr("__version__") = "0.5.0"; 127 | m.attr("die_version") = DIE_VERSION; 128 | m.attr("dielib_version") = DIELIB_VERSION; 129 | 130 | m.def("ScanFileA", DIE::ScanFileA, "filename"_a, "flags"_a, "database"_a, "Scan a file against known signatures"); 131 | 132 | m.def("ScanFileExA", DIE::ScanFileExA, "filename"_a, "flags"_a, "Scan a file"); 133 | 134 | m.def( 135 | "ScanMemoryA", 136 | DIE::ScanMemoryA, 137 | "memory"_a, 138 | "flags"_a, 139 | "database"_a, 140 | "Scan sequence of bytes against known signatures"); 141 | 142 | m.def("ScanMemoryExA", DIE::ScanMemoryExA, "memory"_a, "flags"_a, "Scan sequence of bytes"); 143 | 144 | m.def("LoadDatabaseA", DIE::LoadDatabaseA, "database"_a, "Load signature database"); 145 | } 146 | -------------------------------------------------------------------------------- /python/tests/data/README.md: -------------------------------------------------------------------------------- 1 | ## Test data 2 | 3 | Unless exception, files are the result of transforming `./test.txt` into desired test format. 4 | -------------------------------------------------------------------------------- /python/tests/data/test.rar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/die-python/9dfa53715de412df777b25718734b54f20ada551/python/tests/data/test.rar -------------------------------------------------------------------------------- /python/tests/data/test.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /python/tests/test_die.py: -------------------------------------------------------------------------------- 1 | import bs4 2 | import json 3 | import pathlib 4 | import platform 5 | import pytest 6 | 7 | import die 8 | 9 | 10 | def test_constants(): 11 | # version 12 | assert isinstance(die.version_major, int) 13 | assert isinstance(die.version_minor, int) 14 | assert isinstance(die.version_patch, int) 15 | 16 | assert isinstance(die.die_version, str) 17 | assert die.die_version 18 | assert isinstance(die.dielib_version, str) 19 | assert die.dielib_version 20 | 21 | # validate die database 22 | assert isinstance(die.database_path, pathlib.Path) 23 | assert die.database_path.exists() 24 | assert die.database_path.is_dir() 25 | 26 | # validate scan flags 27 | assert die._DieFlags.Deepscan.value == die.ScanFlags.DEEP_SCAN 28 | assert die._DieFlags.HeuristicScan.value == die.ScanFlags.HEURISTIC_SCAN 29 | assert die._DieFlags.AlltypesScan.value == die.ScanFlags.ALL_TYPES_SCAN 30 | assert die._DieFlags.RecursiveScan.value == die.ScanFlags.RECURSIVE_SCAN 31 | assert die._DieFlags.Verbose.value == die.ScanFlags.VERBOSE_FLAG 32 | assert die._DieFlags.ResultAsXml.value == die.ScanFlags.RESULT_AS_XML 33 | assert die._DieFlags.ResultAsJson.value == die.ScanFlags.RESULT_AS_JSON 34 | assert die._DieFlags.ResultAsTsv.value == die.ScanFlags.RESULT_AS_TSV 35 | assert die._DieFlags.ResultAsCsv.value == die.ScanFlags.RESULT_AS_CSV 36 | # validate no new flag was added and not test 37 | assert sorted([x for x in dir(die._DieFlags) if not x.startswith("__")]) == sorted( 38 | [ 39 | "AlltypesScan", 40 | "Deepscan", 41 | "HeuristicScan", 42 | "RecursiveScan", 43 | "ResultAsCsv", 44 | "ResultAsJson", 45 | "ResultAsTsv", 46 | "ResultAsXml", 47 | "Verbose", 48 | ] 49 | ) 50 | 51 | 52 | @pytest.fixture 53 | def target_binary(): 54 | return ( 55 | pathlib.Path("c:/windows/system32/winver.exe") 56 | if platform.system() == "Windows" 57 | else pathlib.Path("/bin/ls") 58 | ) 59 | 60 | 61 | def test_scan_memory(target_binary: pathlib.Path): 62 | raw_data = target_binary.read_bytes() 63 | res = die.scan_memory( 64 | bytearray(raw_data), 65 | die.ScanFlags.DEEP_SCAN, 66 | ) 67 | assert res 68 | assert isinstance(res, str) 69 | 70 | lines = res.splitlines() 71 | assert len(lines) 72 | 73 | if platform.system() == "Windows": 74 | assert lines[0] == "PE64" 75 | elif platform.system() == "Linux": 76 | assert lines[0] == "ELF64" 77 | 78 | 79 | def test_scan_basic(target_binary: pathlib.Path): 80 | res = die.scan_file( 81 | target_binary, 82 | die.ScanFlags.DEEP_SCAN, 83 | ) 84 | assert res 85 | assert isinstance(res, str) 86 | 87 | lines = res.splitlines() 88 | assert len(lines) 89 | 90 | if platform.system() == "Windows": 91 | assert lines[0] == "PE64" 92 | elif platform.system() == "Linux": 93 | assert lines[0] == "ELF64" 94 | 95 | 96 | def test_scan_export_format_json(target_binary: pathlib.Path): 97 | res = die.scan_file( 98 | target_binary, 99 | die.ScanFlags.DEEP_SCAN | die.ScanFlags.RESULT_AS_JSON, 100 | ) 101 | assert res 102 | 103 | js = json.loads(res) 104 | assert len(js["detects"]) 105 | if platform.system() == "Windows": 106 | assert js["detects"][0]["filetype"] == "PE64" 107 | elif platform.system() == "Linux": 108 | assert js["detects"][0]["filetype"] == "ELF64" 109 | 110 | 111 | def test_scan_export_format_xml(target_binary: pathlib.Path) -> None: 112 | res = die.scan_file( 113 | target_binary, 114 | die.ScanFlags.DEEP_SCAN | die.ScanFlags.RESULT_AS_XML, 115 | ) 116 | assert res 117 | xml = bs4.BeautifulSoup(res, "xml") 118 | assert xml.Result 119 | if platform.system() == "Windows": 120 | assert hasattr(xml.Result, "PE64") 121 | assert xml.Result.PE64["filetype"] == "PE64" 122 | elif platform.system() == "Linux": 123 | assert hasattr(xml.Result, "ELF64") 124 | assert xml.Result.ELF64["filetype"] == "ELF64" 125 | 126 | 127 | def test_scan_export_format_csv(target_binary: pathlib.Path): 128 | CSV_DELIMITER = ";" 129 | res = die.scan_file( 130 | target_binary, 131 | die.ScanFlags.DEEP_SCAN | die.ScanFlags.RESULT_AS_CSV, 132 | ) 133 | assert res 134 | assert len(res.splitlines()) == 1 135 | assert len(res.split(CSV_DELIMITER)) == 5 136 | 137 | 138 | def test_scan_export_format_tsv(target_binary: pathlib.Path): 139 | res = die.scan_file( 140 | target_binary, 141 | die.ScanFlags.DEEP_SCAN | die.ScanFlags.RESULT_AS_TSV, 142 | ) 143 | assert res 144 | 145 | lines = res.splitlines() 146 | assert len(lines) 147 | 148 | if platform.system() == "Windows": 149 | assert lines[0] == "PE64" 150 | elif platform.system() == "Linux": 151 | assert lines[0] == "ELF64" 152 | 153 | 154 | def test_basic_databases(): 155 | for db in die.databases(): 156 | assert isinstance(db, pathlib.Path) 157 | assert db.exists() 158 | assert db.is_file() 159 | -------------------------------------------------------------------------------- /python/tests/test_regression.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | 4 | import die 5 | 6 | TESTS_FOLDER = pathlib.Path(__file__).parent.absolute() 7 | DATA_FOLDER = TESTS_FOLDER / "data" 8 | DB_FOLDER = die.database_path / "db" 9 | 10 | 11 | def test_issue_48(): 12 | # issue https://github.com/elastic/die-python/issues/28 13 | # pr https://github.com/elastic/die-python/pull/30 14 | fpath = DATA_FOLDER / "test.rar" 15 | res = die.scan_file(fpath, die.ScanFlags.RESULT_AS_JSON, str(DB_FOLDER)) 16 | assert res 17 | js = json.loads(res) 18 | assert len(js["detects"]) == 1 19 | assert js["detects"][0]["filetype"] == "Binary" 20 | 21 | values = js["detects"][0]["values"] 22 | assert len(values) > 0 23 | value = values[0] 24 | assert value["name"] == "RAR" 25 | assert value["string"].startswith("Archive: RAR(") 26 | --------------------------------------------------------------------------------