Repository: 0vercl0k/kdmp-parser (language: python)

Issue:
pip / setup.py
Explore the possibility to use a `setup.py` and to have `kdmp` available on pip


Discussion / hints:
Any update on this by any chance? I can adjust [the work I did for udmp-parser](https://github.com/0vercl0k/udmp-parser/pull/6) to use nanobind for this project too if this help. LMK
I don't have any short term plan to look at this - I looked at it a little bit right after your work on `udmp-parser` but I had forgotten  that a custom Python extension already existed for `kdmp-parser` and I couldn't get the CMakefile / pip building working easily so I kind of dropped it.

It might not be hard - it was more that I don't know those technologies very well and working with them can be frustrating :-D.

But yeah if you want to give it a shot, knock yourself out!

Cheers
Thanks for the reply. I should be able to prep you something quickly.

Fix patch:
diff --git a/.github/Invoke-VisualStudio.ps1 b/.github/Invoke-VisualStudio.ps1
new file mode 100644
index 0000000..da61850
--- /dev/null
+++ b/.github/Invoke-VisualStudio.ps1
@@ -0,0 +1,24 @@
+Function Invoke-CmdScript {
+    param(
+        [String] $scriptName
+    )
+    $cmdLine = """$scriptName"" $args & set"
+    & $env:SystemRoot\system32\cmd.exe /c $cmdLine |
+    Select-String '^([^=]*)=(.*)$' | ForEach-Object {
+        $varName = $_.Matches[0].Groups[1].Value
+        $varValue = $_.Matches[0].Groups[2].Value
+        Set-Item Env:$varName $varValue
+    }
+}
+
+Function Invoke-VisualStudio2022win32 {
+    Invoke-CmdScript "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars32.bat"
+}
+
+Function Invoke-VisualStudio2022x64 {
+    Invoke-CmdScript "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat"
+}
+
+Function Invoke-VisualStudio2022arm64 {
+    Invoke-CmdScript "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvarsamd64_arm64.bat"
+}
diff --git a/.github/workflows/kdmp-parser.yml b/.github/workflows/kdmp-parser.yml
index c5030ee..2cfa954 100644
--- a/.github/workflows/kdmp-parser.yml
+++ b/.github/workflows/kdmp-parser.yml
@@ -1,94 +1,185 @@
-# Axel '0vercl0k' Souchet - July 26 2020
 name: Builds
 
-on: [push, pull_request]
+on:
+  push:
+  pull_request:
 
 jobs:
-  Ubuntu2004:
+  testdatas:
+    name: Fetch Test Data
+    runs-on: ubuntu-latest
+    steps:
+    - name: Cache Artifacts
+      id: cache-testdatas
+      uses: actions/cache@v3
+      with:
+        key: kdmp-parser-testdatas-cache
+        path: .
+    - if: steps.cache-testdatas.outputs.cache-hit != 'true'
+      run: |
+        sudo apt-get -y update; sudo apt-get install -y unzip;
+        curl https://github.com/0vercl0k/kdmp-parser/releases/download/v0.1/testdatas.zip -O -L
+        unzip testdatas.zip
+    - name: Upload artifacts
+      uses: actions/upload-artifact@v3
+      with:
+        if-no-files-found: error
+        name: kdmp-parser-testdatas-cache
+        path: .
+
+  parser:
+    needs: testdatas
     strategy:
       fail-fast: false
       matrix:
-        # Available versions: https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json
-        # We don't test below 3.6 because f'strings were implemented in 3.6 and we use them in build scripts, etc.
-        pyver: ['3.6', '3.7', '3.8', '3.9', '3.x']
-        compiler: ['clang', 'gcc']
+        variant:
+          - {os: windows-latest, generator: msvc, arch: x64, config: RelWithDebInfo}
+          - {os: windows-latest, generator: ninja, arch: x64, config: RelWithDebInfo}
+          - {os: windows-latest, generator: msvc, arch: win32, config: RelWithDebInfo}
+          # - {os: windows-latest, generator: msvc, arch: arm64, config: RelWithDebInfo}
+          - {os: ubuntu-latest, generator: gcc, arch: x64, config: RelWithDebInfo}
+          - {os: ubuntu-latest, generator: clang, arch: x64, config: RelWithDebInfo}
+          - {os: macos-latest, generator: clang, arch: x64, config: Release}
+    runs-on: ${{ matrix.variant.os }}
+    name: parser / ${{ matrix.variant.os }} / ${{ matrix.variant.generator }} / ${{ matrix.variant.arch }}
+    env:
+      CMAKE_FLAGS: "-DBUILD_PARSER:BOOL=ON -DBUILD_TESTS:BOOL=ON -DBUILD_PYTHON_BINDING:BOOL=OFF"
+      CMAKE_ARCH: ""
+      CTEST_DIR: "~/testdatas"
 
-    name: Ubuntu 20.04 - Py${{ matrix.pyver }} / ${{ matrix.compiler }}
-    runs-on: ubuntu-20.04
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v4
 
-    - name: Setup Python3
-      uses: actions/setup-python@v2
+    - name: Retrieve testdatas
+      uses: actions/download-artifact@v3
       with:
-        python-version: ${{ matrix.pyver }}
-        architecture: 'x64'
+        name: kdmp-parser-testdatas-cache
+        path: .
 
-    - name: Install dependencies
+    - name: Environment Setup (Windows)
+      if: matrix.variant.os == 'windows-latest'
+      run: |
+        Import-Module .\.github\Invoke-VisualStudio.ps1
+        Invoke-VisualStudio2022${{ matrix.variant.arch }}
+        echo "CMAKE_ARCH='-A ${{ matrix.variant.arch }}'" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+
+    - name: Environment Setup (Linux)
+      if: matrix.variant.os == 'ubuntu-latest'
+      run: |
+        sudo apt update
+
+    - name: Build (Linux/GCC)
+      if: matrix.variant.os == 'ubuntu-latest' && matrix.variant.generator == 'gcc'
+      run: |
+        sudo apt install -y g++
+        echo CC=gcc >> $GITHUB_ENV
+        echo CXX=g++ >> $GITHUB_ENV
+
+    - name: Environment Setup (Linux/CLang)
+      if: matrix.variant.os == 'ubuntu-latest' && matrix.variant.generator == 'clang'
       run: |
-        sudo apt-get -y update
-        sudo apt install -y g++-10-multilib g++-10 ninja-build
         sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
+        echo CC=clang >> $GITHUB_ENV
+        echo CXX=clang++ >> $GITHUB_ENV
 
-    - name: Build with gcc
-      if: matrix.compiler == 'gcc'
-      run: python3 builder.py --run-tests
-      env:
-        CC: gcc-10
-        CXX: g++-10
-
-    - name: Build with clang
-      if: matrix.compiler == 'clang'
-      run: python3 builder.py --run-tests
-      env:
-        CC: clang-11
-        CXX: clang++-11
-
-    - name: Upload artifacts for rel/x64
-      uses: actions/upload-artifact@v2
-      with:
-        name: linx64-RelWithDebInfo-py${{ matrix.pyver }}
-        path: bin/linx64-RelWithDebInfo
+    - name: Build
+      run: |
+        mkdir build
+        mkdir artifact
+        cmake -S . -B ./build ${{ env.CMAKE_ARCH }} ${{ env.CMAKE_FLAGS }}
+        cmake --build ./build --verbose --config ${{ matrix.variant.config }}
+        cmake --install ./build --config ${{ matrix.variant.config }} --prefix ./artifact --verbose
+
+    - name: Tests
+      run: |
+        mv *.dmp ./build/src/tests/
+        ctest --progress --extra-verbose --build-config ${{ matrix.variant.config }} -T test --test-dir ./build/src/tests/
 
-    - name: Upload artifacts for dbg/x64
-      uses: actions/upload-artifact@v2
+    - name: Upload artifacts
+      uses: actions/upload-artifact@v3
       with:
-        name: linx64-Debug-py${{ matrix.pyver }}
-        path: bin/linx64-Debug
+        name: parser-${{ matrix.variant.os }}.${{ matrix.variant.generator }}-${{ matrix.variant.arch }}.${{ matrix.variant.config }}-${{ github.sha }}
+        path: artifact/
 
-  Windows:
+  bindings:
+    needs: testdatas
     strategy:
       fail-fast: false
       matrix:
-        pyver: ['3.6', '3.7', '3.8', '3.9', '3.x']
-
-    name: Windows latest - Py${{ matrix.pyver }}
-    runs-on: windows-latest
+        # nanobind does not support Python < 3.8.
+        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
+        variant:
+          - {os: windows-latest, generator: msvc, arch: x64, config: RelWithDebInfo, py-arch: x64}
+          - {os: windows-latest, generator: msvc, arch: win32, config: RelWithDebInfo, py-arch: x86}
+          # - {os: windows-latest, generator: msvc, arch: arm64, config: RelWithDebInfo, py-arch: x64} # Unsupported (see https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json)
+          - {os: ubuntu-latest, generator: gcc, arch: x64, config: RelWithDebInfo, py-arch: x64}
+          - {os: ubuntu-latest, generator: clang, arch: x64, config: RelWithDebInfo, py-arch: x64}
+          - {os: macos-latest, generator: clang, arch: x64, config: Release, py-arch: x64}
+    runs-on: ${{ matrix.variant.os }}
+    name: bindings / ${{ matrix.variant.os }} / ${{ matrix.variant.generator }} / ${{ matrix.python-version }} / ${{ matrix.variant.arch }}
+    env:
+      CMAKE_FLAGS: "-DBUILD_PARSER:BOOL=OFF -DBUILD_TESTS:BOOL=OFF -DBUILD_PYTHON_BINDING:BOOL=ON"
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v4
 
-    - name: Setup Python3
-      uses: actions/setup-python@v2
+    - name: Retrieve testdatas
+      uses: actions/download-artifact@v3
       with:
-        python-version: ${{ matrix.pyver }}
-        architecture: 'x64'
+        name: kdmp-parser-testdatas-cache
+        path: .
 
-    - name: Setup vs prompt
-      uses: ilammy/msvc-dev-cmd@v1
+    - name: Setup Python
+      uses: actions/setup-python@v4
+      with:
+        python-version: ${{ matrix.python-version }}
+        architecture: ${{ matrix.variant.py-arch }}
 
-    - name: Build
-      run: python builder.py --run-tests
+    - name: Environment Setup (Windows)
+      if: matrix.variant.os == 'windows-latest'
+      run: |
+        Import-Module .\.github\Invoke-VisualStudio.ps1
+        Invoke-VisualStudio2022${{ matrix.variant.arch }}
 
-    - name: Upload artifacts for rel/x64
-      uses: actions/upload-artifact@v2
-      with:
-        name: winx64-RelWithDebInfo-py${{ matrix.pyver }}
-        path: bin/winx64-RelWithDebInfo
+    - name: Environment Setup (Linux)
+      if: matrix.variant.os == 'ubuntu-latest'
+      run: |
+        sudo apt-get -y update
+
+    - name: Environment Setup (OSX)
+      if: matrix.variant.os == 'macos-latest'
+      run: |
+        echo
+
+    - name: Environment Setup (Linux/GCC)
+      if: matrix.variant.os == 'ubuntu-latest' && matrix.variant.generator == 'gcc'
+      run: |
+        sudo apt install -y g++
+        echo CC=gcc >> $GITHUB_ENV
+        echo CXX=g++ >> $GITHUB_ENV
+
+    - name: Environment Setup (Linux/CLang)
+      if: matrix.variant.os == 'ubuntu-latest' && matrix.variant.generator == 'clang'
+      run: |
+        sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
+        echo CC=clang >> $GITHUB_ENV
+        echo CXX=clang++ >> $GITHUB_ENV
+
+    - name: Build wheel
+      run: |
+        mkdir wheel
+        pip wheel src/python -w ./wheel
+
+    - name: Python Binding Tests
+      run: |
+        pip install --upgrade pip setuptools wheel
+        pip install -U -r src/python/tests/requirements.txt
+        pip install -U --user src/python
+        pytest -vvv src/python/tests
 
-    - name: Upload artifacts for dbg/x64
-      uses: actions/upload-artifact@v2
+    - name: Upload wheel
+      uses: actions/upload-artifact@v3
       with:
-        name: winx64-Debug
-        path: bin/winx64-Debug
\ No newline at end of file
+        name: wheels
+        path: wheel/*.whl
diff --git a/.gitignore b/.gitignore
index 896f43e..a4b265c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,7 @@
 
 # vs directory
 **/.vs
+**/.vscode
 
 # user files
 *.vcxproj.user
@@ -49,4 +50,18 @@
 bin/
 
 # Build output
-build/
\ No newline at end of file
+build/
+wheel/
+
+# Cache folders
+__pycache__/
+.pytest_cache/
+
+# Test dumps
+*.dmp
+
+# Version files
+src/lib/kdmp-parser-version.h
+
+kdmp-parser-testdatas
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5ee2525..1ceacc0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,24 +2,35 @@
 # CMakeList.txt : CMake project for kdmp-parser, include source and define
 # project specific logic here.
 #
-cmake_minimum_required (VERSION 3.8)
+cmake_minimum_required(VERSION 3.24)
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED True)
 set(CMAKE_POSITION_INDEPENDENT_CODE True)
 
-OPTION(BUILD_TESTS "Build and run tests" OFF)
-
-project(kdmp-parser)
-add_compile_options(
-    $<$<CXX_COMPILER_ID:MSVC>:/W4$<SEMICOLON>/WX>
-    $<$<CXX_COMPILER_ID:GNU>:-Wall$<SEMICOLON>-Wextra$<SEMICOLON>-pedantic$<SEMICOLON>-Werror>
-    $<$<CXX_COMPILER_ID:Clang>:-Wall$<SEMICOLON>-Wextra$<SEMICOLON>-pedantic$<SEMICOLON>-Werror>
+project(
+    kdmp-parser
+    DESCRIPTION "A Cross-Platform C++ parser library for Windows kernel minidumps."
+    HOMEPAGE_URL https://github.com/0vercl0k/kdmp-parser
+    VERSION 0.6.0
 )
 
+set(PROJECT_AUTHOR 0vercl0k)
+set(PROJECT_LICENSE MIT)
+
+option(BUILD_PARSER "Build the parser executable for kdmp-parser" ON)
+option(BUILD_TESTS "Build the test suite for kdmp-parser" ON)
+option(BUILD_PYTHON_BINDING "Build the Python bindings for kdmp-parser" ON)
+
 add_subdirectory(src/lib)
-add_subdirectory(src/parser)
-add_subdirectory(src/testapp)
-add_subdirectory(src/python)
+
+if(BUILD_PARSER)
+    add_subdirectory(src/parser)
+endif(BUILD_PARSER)
+
+if(BUILD_PYTHON_BINDING)
+    add_subdirectory(src/python)
+endif(BUILD_PYTHON_BINDING)
+
 if(BUILD_TESTS)
     add_subdirectory(src/tests)
-endif()
+endif(BUILD_TESTS)
diff --git a/CMakeSettings.json b/CMakeSettings.json
deleted file mode 100644
index c2e72a2..0000000
--- a/CMakeSettings.json
+++ /dev/null
@@ -1,68 +0,0 @@
-﻿{
-  "configurations": [
-    {
-      "name": "x64-Debug",
-      "generator": "Ninja",
-      "configurationType": "Debug",
-      "inheritEnvironments": [ "msvc_x64_x64" ],
-      "buildRoot": "${projectDir}\\build\\x64-Debug",
-      "installRoot": "${projectDir}\\bin\\x64-Debug",
-      "cmakeCommandArgs": "",
-      "buildCommandArgs": "-v",
-      "ctestCommandArgs": "",
-      "variables": []
-    },
-    {
-      "name": "x64-Release",
-      "generator": "Ninja",
-      "configurationType": "RelWithDebInfo",
-      "buildRoot": "${projectDir}\\build\\x64-RelWithDebInfo",
-      "installRoot": "${projectDir}\\bin\\x64-RelWithDebInfo",
-      "cmakeCommandArgs": "",
-      "buildCommandArgs": "-v",
-      "ctestCommandArgs": "",
-      "inheritEnvironments": [ "msvc_x64_x64" ],
-      "variables": []
-    },
-    {
-      "name": "x86-Debug",
-      "generator": "Ninja",
-      "configurationType": "Debug",
-      "buildRoot": "${projectDir}\\build\\x86-Debug",
-      "installRoot": "${projectDir}\\bin\\x86-Debug",
-      "cmakeCommandArgs": "",
-      "buildCommandArgs": "-v",
-      "ctestCommandArgs": "",
-      "inheritEnvironments": [ "msvc_x86" ],
-      "variables": []
-    },
-    {
-      "name": "x86-Release",
-      "generator": "Ninja",
-      "configurationType": "RelWithDebInfo",
-      "buildRoot": "${projectDir}\\build\\x86-RelWithDebInfo",
-      "installRoot": "${projectDir}\\bin\\x86-RelWithDebInfo",
-      "cmakeCommandArgs": "",
-      "buildCommandArgs": "-v",
-      "ctestCommandArgs": "",
-      "inheritEnvironments": [ "msvc_x86" ],
-      "variables": []
-    },
-    {
-      "name": "WSL-Clang-Release",
-      "generator": "Unix Makefiles",
-      "configurationType": "RelWithDebInfo",
-      "buildRoot": "${projectDir}\\build\\wsl-release",
-      "installRoot": "${projectDir}\\bin\\wsl-release",
-      "cmakeExecutable": "/usr/bin/cmake",
-      "cmakeCommandArgs": "",
-      "buildCommandArgs": "",
-      "ctestCommandArgs": "",
-      "inheritEnvironments": [ "linux_clang_x64" ],
-      "wslPath": "${defaultWSLPath}",
-      "addressSanitizerEnabled": true,
-      "addressSanitizerRuntimeFlags": "",
-      "variables": []
-    }
-  ]
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index 42b8c2c..f9e21c2 100644
--- a/README.md
+++ b/README.md
@@ -21,32 +21,25 @@ Special thanks to:
 
 ## Python 3 bindings
 
-The bindings allow you to: read the context, read physical memory and to do virtual memory translations:
+The Python bindings exposes all functions from `kdmp-parser` to a Python module named `kdmp_parser`. To install the bindings:
 
-```py
-from kdmp import Dump, FullDump, BMPDump
-
-dmp = Dump(sys.argv[2])
-assert(dmp.type() == FullDump or dmp.type() == BMPDump)
-
-ctx = dmp.context()
-dtb = ctx['dtb'] & ~0xfff # remove PCID
-
-assert(ctx['rip'] == 0xfffff805108776a0)
-assert(dtb == 0x6d4000)
-
-page = dmp.get_physical_page(0x5000)
-assert(page[0x34:0x38] == b'MSFT')
+ * from PyPI
+```
+python -m pip install kdmp_parser
+```
+ * using `pip` with `git`
+```
+python -m pip install -e "git+https://github.com/hugsy/kdmp-parser.git/#egg=kdmp-parser&subdirectory=src/python"
+```
+ * from cloning this repository
+```
+git clone https://github.com/0vercl0k/kdmp-parser.git
+cd kdmp-parser
+python -m pip install .\src\python\
+```
 
-assert(dmp.virt_translate(0xfffff78000000000) == 0x0000000000c2f000)
-assert(dmp.virt_translate(0xfffff80513370000) == 0x000000003d555000)
 
-assert(dmp.get_virtual_page(0xfffff78000000000) == dmp.get_physical_page(0x0000000000c2f000))
-assert(dmp.get_virtual_page(0xfffff80513370000) == dmp.get_physical_page(0x000000003d555000))
 
-v = 0xfffff80513568000
-assert(dmp.get_virtual_page(v) == dmp.get_physical_page(dmp.virt_translate(v)))
-```
 
 ## Parser
 
@@ -192,4 +185,5 @@ kdmp-parser\build\x64-RelWithDebInfo>cmake --build .
 * Axel '[@0vercl0k](https://twitter.com/0vercl0k)' Souchet
 
 With contributions from:
-  - [@masthoon](https://github.com/masthoon).
+  - [@masthoon](https://github.com/masthoon)
+  - [@hugsy](https://github.com/hugsy)
diff --git a/bin/empty.txt b/bin/empty.txt
deleted file mode 100644
index e69de29..0000000
diff --git a/build/empty.txt b/build/empty.txt
deleted file mode 100644
index e69de29..0000000
diff --git a/builder.py b/builder.py
deleted file mode 100644
index b173274..0000000
--- a/builder.py
+++ /dev/null
@@ -1,124 +0,0 @@
-# Axel '0vercl0k' Souchet - February 23 2020
-import urllib.request
-import os
-import sys
-import zipfile
-import subprocess
-import itertools
-import platform
-import argparse
-
-os_prefix = 'win' if 'Windows' in platform.platform(terse = 1) else 'lin'
-is_windows = os_prefix == 'win'
-is_linux = not is_windows
-is_linux64 = is_linux and platform.architecture()[0] == '64bit'
-
-vsdevprompt = r'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat'
-
-def source_bat(bat_file, arch):
-    '''https://dmerej.info/blog/post/cmake-visual-studio-and-the-command-line/'''
-    result = {}
-    process = subprocess.Popen(
-        f'"{bat_file}" {arch} & set',
-        stdout = subprocess.PIPE,
-        shell = True
-    )
-
-    (out, _) = process.communicate()
-    for line in out.splitlines():
-        line = line.decode()
-        if '=' not in line:
-            continue
-        line = line.strip()
-        key, value = line.split('=', 1)
-        result[key] = value
-
-    return result
-
-def build(arch, configuration, tests_on):
-    # Grab the environment needed for the appropriate arch on Windows.
-    env = source_bat(vsdevprompt, arch) if is_windows else os.environ
-    dir_name = f'{os_prefix}{arch}-{configuration}'
-    build_dir = os.path.join('build', dir_name)
-    if not os.path.isdir(build_dir):
-        os.mkdir(build_dir)
-
-    # We build an absolute path here because otherwise Ninja ends up creating a 'bin' directory
-    # in build\<target>\bin.
-    output_dir = os.path.abspath(os.path.join('bin', dir_name))
-    if not os.path.isdir(output_dir):
-        os.mkdir(output_dir)
-
-    extra_opts = ()
-    if is_linux64 and arch == 'x86':
-        # To support 32b binary on a 64b host on Linux.
-        extra_opts = (
-            f'-DCMAKE_CXX_FLAGS=-m32',
-            f'-DCMAKE_C_FLAGS=-m32'
-        )
-
-    # Allow the user to override the Python version if `pythonLocation` is found
-    # in the environment. This is particularly useful for the CI as `setup-python`
-    # sets this environment variable:
-    # https://github.com/actions/setup-python/blob/main/src/find-python.ts#L117
-    py_root = os.getenv('pythonLocation')
-    if py_root is not None:
-        extra_opts = extra_opts + (
-            f'-DPython3_FIND_STRATEGY=LOCATION',
-            f'-DPython3_ROOT_DIR={py_root}'
-        )
-
-    cmake_config = (
-        'cmake',
-        f'-DCMAKE_RUNTIME_OUTPUT_DIRECTORY={output_dir}',
-        f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={output_dir}',
-        f'-DCMAKE_BUILD_TYPE={configuration}',
-        f'-DBUILD_TESTS={tests_on}'
-    )
-    cmake_config += extra_opts
-    cmake_config += (
-        '-GNinja',
-        os.path.join('..', '..')
-    )
-
-    ret = subprocess.call(cmake_config, cwd = build_dir, env = env)
-
-    if ret != 0: return ret
-
-    ret = subprocess.call((
-        'cmake',
-        '--build',
-        '.'
-    ), cwd = build_dir, env = env)
-
-    return ret
-
-def main():
-    parser = argparse.ArgumentParser('Build and run test')
-    parser.add_argument('--run-tests', action = 'store_true', default = False)
-    parser.add_argument('--configuration', action = 'append', choices = ('Debug', 'RelWithDebInfo'))
-    parser.add_argument('--arch', action = 'append', choices = ('x64', 'x86'))
-    args = parser.parse_args()
-
-    if args.configuration is None:
-        args.configuration = ('Debug', 'RelWithDebInfo')
-
-    if args.arch is None:
-        args.arch = ('x64', 'x86')
-
-    matrix = tuple(itertools.product(
-        args.arch,
-        args.configuration
-    ))
-
-    tests_on = 'ON' if args.run_tests else 'OFF'
-    for arch, configuration in matrix:
-        if build(arch, configuration, tests_on) != 0:
-            print(f'{arch}/{configuration} build failed, bailing.')
-            return 1
-
-    print('All good!')
-    return 0
-
-if __name__ == '__main__':
-    sys.exit(main())
\ No newline at end of file
diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt
index 8f534e9..29177bf 100644
--- a/src/lib/CMakeLists.txt
+++ b/src/lib/CMakeLists.txt
@@ -1,3 +1,4 @@
 # Axel '0vercl0k' Souchet - April 18 2020
 add_library(kdmp-parser INTERFACE)
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/kdmp-parser-version.h.in ${CMAKE_CURRENT_SOURCE_DIR}/kdmp-parser-version.h)
 target_include_directories(kdmp-parser INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
diff --git a/src/lib/kdmp-parser-structs.h b/src/lib/kdmp-parser-structs.h
index ac8a362..45cbcfe 100644
--- a/src/lib/kdmp-parser-structs.h
+++ b/src/lib/kdmp-parser-structs.h
@@ -2,6 +2,7 @@
 #pragma once
 
 #include "platform.h"
+#include <array>
 #include <cinttypes>
 #include <cstddef>
 #include <cstdint>
@@ -86,12 +87,12 @@ static void DisplayField(const uint32_t Prefix, const char *FieldName,
       printf(": Full Dump.\n");
       return;
     }
+
     case DumpType_t::BMPDump: {
       printf(": BMP Dump.\n");
       return;
     }
     }
-    printf(": Unknown.\n");
   } else {
 
     //
@@ -188,7 +189,7 @@ struct BMP_HEADER64 {
   // 'FirstPage': [0x20, ['unsigned long long']],
   //
 
-  uint8_t Padding0[0x20 - (0x4 + sizeof(ValidDump))];
+  std::array<uint8_t, 0x20 - (0x4 + sizeof(ValidDump))> Padding0;
 
   //
   // The offset of the first page in the file.
@@ -209,7 +210,7 @@ struct BMP_HEADER64 {
 
   uint64_t Pages;
 
-  uint8_t Bitmap[1];
+  std::array<uint8_t, 1> Bitmap;
 
   bool LooksGood() const {
 
@@ -341,7 +342,7 @@ struct CONTEXT {
   uint16_t Reserved3;
   uint32_t MxCsr2;
   uint32_t MxCsr_Mask;
-  uint128_t FloatRegisters[8];
+  std::array<uint128_t, 8> FloatRegisters;
   uint128_t Xmm0;
   uint128_t Xmm1;
   uint128_t Xmm2;
@@ -363,7 +364,7 @@ struct CONTEXT {
   // Vector registers.
   //
 
-  uint128_t VectorRegister[26];
+  std::array<uint128_t, 26> VectorRegister;
   uint64_t VectorControl;
 
   //
@@ -525,7 +526,7 @@ struct EXCEPTION_RECORD64 {
   uint64_t ExceptionAddress;
   uint32_t NumberParameters;
   uint32_t __unusedAlignment;
-  uint64_t ExceptionInformation[15];
+  std::array<uint64_t, 15> ExceptionInformation;
 
   void Show(const uint32_t Prefix = 0) const {
     DISPLAY_HEADER("KDMP_PARSER_EXCEPTION_RECORD64");
@@ -556,8 +557,8 @@ static_assert(sizeof(EXCEPTION_RECORD64) == 0x98,
               "KDMP_PARSER_EXCEPTION_RECORD64's size looks wrong.");
 
 struct HEADER64 {
-  static const uint32_t ExpectedSignature = 0x45474150; // 'EGAP'
-  static const uint32_t ExpectedValidDump = 0x34365544; // '46UD'
+  static constexpr uint32_t ExpectedSignature = 0x45474150; // 'EGAP'
+  static constexpr uint32_t ExpectedValidDump = 0x34365544; // '46UD'
 
   uint32_t Signature;
   uint32_t ValidDump;
@@ -577,8 +578,8 @@ struct HEADER64 {
   // 'BugCheckCodeParameter' : [0x40, ['array', 4, ['unsigned long long']]],
   //
 
-  uint8_t Padding0[0x40 - (0x38 + sizeof(BugCheckCode))];
-  uint64_t BugCheckCodeParameter[4];
+  std::array<uint8_t, 0x40 - (0x38 + sizeof(BugCheckCode))> Padding0;
+  std::array<uint64_t, 4> BugCheckCodeParameter;
 
   //
   // According to rekall there's a gap here:
@@ -586,7 +587,7 @@ struct HEADER64 {
   // 'KdDebuggerDataBlock' : [0x80, ['unsigned long long']],
   //
 
-  uint8_t Padding1[0x80 - (0x40 + sizeof(BugCheckCodeParameter))];
+  std::array<uint8_t, 0x80 - (0x40 + sizeof(BugCheckCodeParameter))> Padding1;
   uint64_t KdDebuggerDataBlock;
   PHYSMEM_DESC PhysicalMemoryBlockBuffer;
 
@@ -596,7 +597,8 @@ struct HEADER64 {
   // 'ContextRecord' : [0x348, ['array', 3000, ['unsigned char']]],
   //
 
-  uint8_t Padding2[0x348 - (0x88 + sizeof(PhysicalMemoryBlockBuffer))];
+  std::array<uint8_t, 0x348 - (0x88 + sizeof(PhysicalMemoryBlockBuffer))>
+      Padding2;
   CONTEXT ContextRecord;
 
   //
@@ -605,7 +607,7 @@ struct HEADER64 {
   // 'Exception' : [0xf00, ['_EXCEPTION_RECORD64']],
   //
 
-  uint8_t Padding3[0xf00 - (0x348 + sizeof(ContextRecord))];
+  std::array<uint8_t, 0xf00 - (0x348 + sizeof(ContextRecord))> Padding3;
   EXCEPTION_RECORD64 Exception;
   DumpType_t DumpType;
 
@@ -614,10 +616,10 @@ struct HEADER64 {
   // 'DumpType' : [0xf98, ['unsigned long']],
   // 'RequiredDumpSpace' : [0xfa0, ['unsigned long long']],
   //
-  uint8_t Padding4[0xfa0 - (0xf98 + sizeof(DumpType))];
+  std::array<uint8_t, 0xfa0 - (0xf98 + sizeof(DumpType))> Padding4;
   uint64_t RequiredDumpSpace;
   uint64_t SystemTime;
-  uint8_t Comment[128];
+  std::array<uint8_t, 128> Comment;
   uint64_t SystemUpTime;
   uint32_t MiniDumpFields;
   uint32_t SecondaryDataState;
@@ -626,8 +628,8 @@ struct HEADER64 {
   uint32_t WriterStatus;
   uint8_t Unused1;
   uint8_t KdSecondaryVersion;
-  uint8_t Unused[2];
-  uint8_t _reserved0[4016];
+  std::array<uint8_t, 2> Unused;
+  std::array<uint8_t, 4016> _reserved0;
   BMP_HEADER64 BmpHeader;
 
   bool LooksGood() const {
@@ -650,16 +652,24 @@ struct HEADER64 {
     // Make sure it's a dump type we know how to handle.
     //
 
-    if (DumpType == DumpType_t::FullDump) {
+    switch (DumpType) {
+    case DumpType_t::FullDump:
       if (!PhysicalMemoryBlockBuffer.LooksGood()) {
         printf("The PhysicalMemoryBlockBuffer looks wrong.\n");
         return false;
       }
-    } else if (DumpType == DumpType_t::BMPDump) {
+      break;
+
+    case DumpType_t::BMPDump:
       if (!BmpHeader.LooksGood()) {
         printf("The BmpHeader looks wrong.\n");
         return false;
       }
+      break;
+
+    default:
+      printf("Unknown Type %#x.\n", uint32_t(DumpType));
+      return false;
     }
 
     //
diff --git a/src/lib/kdmp-parser-version.h.in b/src/lib/kdmp-parser-version.h.in
new file mode 100644
index 0000000..dbe320b
--- /dev/null
+++ b/src/lib/kdmp-parser-version.h.in
@@ -0,0 +1,11 @@
+#pragma once
+
+// clang-format off
+#define KDMPPARSER_PROJECT_NAME "@PROJECT_NAME@"
+#define KDMPPARSER_PROJECT_AUTHOR "@PROJECT_AUTHOR@"
+#define KDMPPARSER_PROJECT_LICENSE "@PROJECT_LICENSE@"
+#define KDMPPARSER_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
+#define KDMPPARSER_VERSION_MINOR @PROJECT_VERSION_MINOR@
+#define KDMPPARSER_VERSION_PATCH @PROJECT_VERSION_PATCH@
+#define KDMPPARSER_VERSION_RELEASE "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@"
+// clang-format on
diff --git a/src/lib/kdmp-parser.h b/src/lib/kdmp-parser.h
index 20cb239..3591aef 100644
--- a/src/lib/kdmp-parser.h
+++ b/src/lib/kdmp-parser.h
@@ -3,18 +3,24 @@
 
 #include "filemap.h"
 #include "kdmp-parser-structs.h"
+#include "kdmp-parser-version.h"
+
+#include <array>
 #include <cstdint>
 #include <cstdio>
+#include <filesystem>
 #include <optional>
+#include <string>
 #include <unordered_map>
 
 namespace kdmpparser {
 
+using Page_t = std::array<uint8_t, kdmpparser::Page::Size>;
 using Physmem_t = std::unordered_map<uint64_t, const uint8_t *>;
 
 struct BugCheckParameters_t {
   uint32_t BugCheckCode;
-  uint64_t BugCheckCodeParameter[4];
+  std::array<uint64_t, 4> BugCheckCodeParameter;
 };
 
 class KernelDumpParser {
@@ -35,7 +41,7 @@ class KernelDumpParser {
   // File path to the crash-dump.
   //
 
-  const char *PathFile_ = nullptr;
+  std::filesystem::path PathFile_;
 
   //
   // Mapping between physical addresses / page data.
@@ -54,7 +60,11 @@ class KernelDumpParser {
     // Copy the path file.
     //
 
-    PathFile_ = PathFile;
+    PathFile_ = std::filesystem::path(PathFile);
+    if (!std::filesystem::exists(PathFile_)) {
+      printf("Invalid file: %s.\n", (char *)PathFile_.string().c_str());
+      return false;
+    }
 
     //
     // Map a view of the file.
@@ -78,16 +88,25 @@ class KernelDumpParser {
     // Retrieve the physical memory according to the type of dump we have.
     //
 
-    if (DmpHdr_->DumpType == DumpType_t::FullDump) {
+    switch (DmpHdr_->DumpType) {
+    case DumpType_t::FullDump: {
       if (!BuildPhysmemFullDump()) {
         printf("BuildPhysmemFullDump failed.\n");
         return false;
       }
-    } else if (DmpHdr_->DumpType == DumpType_t::BMPDump) {
+      break;
+    }
+    case DumpType_t::BMPDump: {
       if (!BuildPhysmemBMPDump()) {
         printf("BuildPhysmemBMPDump failed.\n");
         return false;
       }
+      break;
+    }
+    default: {
+      printf("Invalid type\n");
+      return false;
+    }
     }
 
     return true;
@@ -97,13 +116,13 @@ class KernelDumpParser {
   // Give the Context record to the user.
   //
 
-  constexpr const CONTEXT *GetContext() const {
+  constexpr const CONTEXT &GetContext() const {
 
     //
     // Give the user a view of the context record.
     //
 
-    return &DmpHdr_->ContextRecord;
+    return DmpHdr_->ContextRecord;
   }
 
   //
@@ -122,6 +141,12 @@ class KernelDumpParser {
          DmpHdr_->BugCheckCodeParameter[2], DmpHdr_->BugCheckCodeParameter[3]}};
   }
 
+  //
+  // Get the path of dump.
+  //
+
+  const std::filesystem::path &GetDumpPath() const { return PathFile_; }
+
   //
   // Get the type of dump.
   //
@@ -428,21 +453,21 @@ class KernelDumpParser {
       for (uint64_t PageIdx = 0; PageIdx < PageCount; PageIdx++) {
 
         //
-        // Compute the current PFN as well as the actual physical address of the
-        // page.
+        // Compute the current PFN as well as the actual physical address of
+        // the page.
         //
 
         const uint64_t Pfn = BasePage + PageIdx;
         const uint64_t Pa = Pfn * Page::Size;
 
         //
-        // Now one thing to understand is that the Runs structure allows to skip
-        // for holes in memory. Instead of, padding them with empty spaces to
-        // conserve a 1:1 mapping between physical address and file offset, the
-        // Run gives you the base Pfn. This means that we don't have a 1:1
-        // mapping between file offset and physical addresses so we need to keep
-        // track of where the Run starts in memory and then we can simply access
-        // our pages one after the other.
+        // Now one thing to understand is that the Runs structure allows to
+        // skip for holes in memory. Instead of, padding them with empty
+        // spaces to conserve a 1:1 mapping between physical address and file
+        // offset, the Run gives you the base Pfn. This means that we don't
+        // have a 1:1 mapping between file offset and physical addresses so we
+        // need to keep track of where the Run starts in memory and then we
+        // can simply access our pages one after the other.
         //
         // If this is not clear enough, here is a small example:
         //  Run[0]
@@ -457,12 +482,12 @@ class KernelDumpParser {
         // Now if we want to get the file offset of those pages we start at
         // Run0:
         //   Run0 starts at file offset 0x2000 so Page0 is at file offset
-        //   0x2000, Page1 is at file offset 0x3000. Run1 starts at file offset
-        //   0x2000+(2*0x1000) so Page3 is at file offset
+        //   0x2000, Page1 is at file offset 0x3000. Run1 starts at file
+        //   offset 0x2000+(2*0x1000) so Page3 is at file offset
         //   0x2000+(2*0x1000)+0x1000.
         //
-        // That is the reason why the computation below is RunBase + (PageIdx *
-        // 0x1000) instead of RunBase + (Pfn * 0x1000).
+        // That is the reason why the computation below is RunBase + (PageIdx
+        // * 0x1000) instead of RunBase + (Pfn * 0x1000).
 
         const uint8_t *PageBase = RunBase + (PageIdx * Page::Size);
 
@@ -490,7 +515,7 @@ class KernelDumpParser {
   bool BuildPhysmemBMPDump() {
     const uint8_t *Page = (uint8_t *)DmpHdr_ + DmpHdr_->BmpHeader.FirstPage;
     const uint64_t BitmapSize = DmpHdr_->BmpHeader.Pages / 8;
-    const uint8_t *Bitmap = DmpHdr_->BmpHeader.Bitmap;
+    const uint8_t *Bitmap = DmpHdr_->BmpHeader.Bitmap.data();
 
     //
     // Walk the bitmap byte per byte.
@@ -556,6 +581,14 @@ class KernelDumpParser {
   // Map a view of the file in memory.
   //
 
-  bool MapFile() { return FileMap_.MapFile(PathFile_); }
+  bool MapFile() { return FileMap_.MapFile(PathFile_.string().c_str()); }
 };
+
+struct Version_t {
+  static inline const uint16_t Major = KDMPPARSER_VERSION_MAJOR;
+  static inline const uint16_t Minor = KDMPPARSER_VERSION_MINOR;
+  static inline const uint16_t Patch = KDMPPARSER_VERSION_PATCH;
+  static inline const std::string Release = KDMPPARSER_VERSION_RELEASE;
+};
+
 } // namespace kdmpparser
diff --git a/src/lib/platform.h b/src/lib/platform.h
index a858d2d..67595c7 100644
--- a/src/lib/platform.h
+++ b/src/lib/platform.h
@@ -6,7 +6,7 @@
 #elif defined(__amd64__) || defined(_M_X64)
 #define ARCH_X64
 #else
-#error Platform not supported.
+#error Architecture not supported.
 #endif
 
 #if defined(WIN32) || defined(WIN64) || defined(_WIN32) || defined(_WIN64)
@@ -19,18 +19,13 @@
 #elif defined ARCH_X64
 #define WINDOWS_X64
 #endif
+
 #elif defined(linux) || defined(__linux) || defined(__FreeBSD__) ||            \
     defined(__FreeBSD_kernel__) || defined(__MACH__)
 #define LINUX
 
 #if defined(linux) || defined(__linux)
 #define SYSTEM_PLATFORM "Linux"
-#else
-#error An error occured
-#endif
-
-#define EXIT_FAILURE 1
-#define EXIT_SUCCESS 0
 
 #if defined ARCH_X86
 #define LINUX_X86
@@ -38,6 +33,22 @@
 #define LINUX_X64
 #endif
 
+#elif defined(__APPLE__)
+
+#define SYSTEM_PLATFORM "OSX"
+#if defined ARCH_X86
+#define OSX_X86
+#elif defined ARCH_X64
+#define OSX_X64
+#endif
+
+#else
+#error An error occured
+#endif
+
+#define EXIT_FAILURE 1
+#define EXIT_SUCCESS 0
+
 #else
 #error Platform not supported.
 #endif
diff --git a/src/parser/CMakeLists.txt b/src/parser/CMakeLists.txt
index c72ad51..fd062bd 100644
--- a/src/parser/CMakeLists.txt
+++ b/src/parser/CMakeLists.txt
@@ -1,3 +1,15 @@
 # Axel '0vercl0k' Souchet - April 18 2020
 link_libraries(kdmp-parser)
 add_executable(parser parser.cc)
+target_compile_options(
+    parser
+    PUBLIC
+    $<$<CXX_COMPILER_ID:MSVC>:/W4$<SEMICOLON>/WX>
+    $<$<CXX_COMPILER_ID:GNU>:-Wall$<SEMICOLON>-Wextra$<SEMICOLON>-pedantic$<SEMICOLON>-Werror>
+    $<$<CXX_COMPILER_ID:Clang>:-Wall$<SEMICOLON>-Wextra$<SEMICOLON>-pedantic$<SEMICOLON>-Werror>
+)
+install(TARGETS parser DESTINATION .)
+
+if(MSVC)
+    install(FILES $<TARGET_PDB_FILE:parser> DESTINATION . OPTIONAL)
+endif(MSVC)
diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt
index c33d938..f31064d 100644
--- a/src/python/CMakeLists.txt
+++ b/src/python/CMakeLists.txt
@@ -1,33 +1,63 @@
-# Mastho - 2020
-find_package(Python3 COMPONENTS Interpreter Development)
+#
+# This file is part of kdmp-parser project
+#
+# Released under MIT License, by 0vercl0k - 2023
+#
+# With contributions from:
+# * masthoon - (github.com/masthoon)
+# * hugsy - (github.com/hugsy)
+#
+cmake_minimum_required(VERSION 3.20)
 
-message("Python3_Interpreter_FOUND:${Python3_Interpreter_FOUND}")
-message("Python3_Development_FOUND:${Python3_Development_FOUND}")
-message("Python3_LIBRARY_DIRS:${Python3_LIBRARY_DIRS}")
+project(
+    kdmp-parser-python
+    DESCRIPTION "A Cross-Platform C++ parser library for Windows kernel minidumps."
+    HOMEPAGE_URL https://github.com/0vercl0k/kdmp-parser
+    VERSION 0.6.0
+)
+set(PROJECT_AUTHOR 0vercl0k)
+set(PROJECT_LICENSE MIT)
+set(CMAKE_CXX_STANDARD 20)
 
-# Python on Windows Debug build only load $_d.pyd and require python_d.exe
-if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND WIN32)
-    set(PYMODULE kdmp_d CACHE INTERNAL "")
-else()
-    set(PYMODULE kdmp CACHE INTERNAL "")
-endif()
+find_package(Python 3
+    REQUIRED COMPONENTS Interpreter Development.Module
+    OPTIONAL_COMPONENTS Development.SABIModule
+)
 
-# Require Python3 interpreter and development libs
-if(NOT Python3_Interpreter_FOUND OR (NOT Python3_Development_FOUND) OR (CMAKE_BUILD_TYPE STREQUAL "Debug" AND WIN32 AND NOT Python3_LIBRARY_DEBUG))
-    message(WARNING "Python3 development library not found")
-    set(PYMODULE "" CACHE INTERNAL "")
-    return()
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+    set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
+    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
 endif()
 
-add_library(${PYMODULE} SHARED python-kdmp.cc)
-target_include_directories(${PYMODULE} PRIVATE ${Python3_INCLUDE_DIRS})
-target_link_directories(${PYMODULE} PRIVATE ${Python3_LIBRARY_DIRS} kdmp-parser)
-target_link_libraries(${PYMODULE} PRIVATE kdmp-parser)
+set(KDMPPARSER_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..")
+set(KDMPPARSER_INCLUDE_DIR "${KDMPPARSER_ROOT}/src/lib")
+configure_file(${KDMPPARSER_INCLUDE_DIR}/kdmp-parser-version.h.in ${KDMPPARSER_INCLUDE_DIR}/kdmp-parser-version.h)
+
+execute_process(
+    COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
+    OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NB_DIR)
+list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}")
+
+find_package(nanobind CONFIG REQUIRED)
 
-# On Windows Python load $.pyd :: On Linux remove lib$ prefix
-if(WIN32)
-    set_target_properties(${PYMODULE} PROPERTIES SUFFIX ".pyd")
+nanobind_add_module(_kdmp_parser NB_STATIC src/kdmp_parser.cc)
+
+if(BUILD_PYTHON_PACKAGE)
+    #
+    # Those directives are only used when creating a standalone `kdmp_parser` python package
+    # Usual case is when build with `pip install src/python`
+    #
+    target_include_directories(_kdmp_parser PRIVATE ${KDMPPARSER_INCLUDE_DIR})
+    install(DIRECTORY kdmp_parser DESTINATION .)
+    install(TARGETS _kdmp_parser LIBRARY DESTINATION kdmp_parser)
 else()
-    set_target_properties(${PYMODULE} PROPERTIES PREFIX "")
-endif()
+    #
+    # This is the general case, when built from the root cmakefile
+    #
+    target_include_directories(_kdmp_parser PRIVATE $<TARGET_PROPERTY:kdmp-parser,INTERFACE_INCLUDE_DIRECTORIES>)
+    install(TARGETS _kdmp_parser DESTINATION bindings/python)
 
+    if(MSVC)
+        install(FILES $<TARGET_PDB_FILE:_kdmp_parser> DESTINATION . OPTIONAL)
+    endif(MSVC)
+endif()
diff --git a/src/python/README.md b/src/python/README.md
new file mode 100644
index 0000000..879c6ab
--- /dev/null
+++ b/src/python/README.md
@@ -0,0 +1,82 @@
+# Python building for `kdmp-parser`
+
+[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Licence MIT](https://img.shields.io/packagist/l/doctrine/orm.svg?maxAge=2592000?style=plastic)](https://github.com/0vercl0k/kdmp-parser/blob/master/LICENSE)
+
+
+This C++ library parses Windows kernel [full](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/complete-memory-dump) dumps (`.dump /f` in WinDbg) as well as [BMP](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/active-memory-dump) dumps (`.dump /ka` in WinDbg).
+
+![parser](pics/parser.jpg)
+
+The library supports loading 64-bit dumps and provides read access to things like:
+
+- The context record,
+- The exception record,
+- The bugcheck parameters,
+- The physical memory.
+
+The Python bindings were authored by [hugsy](https://github.com/hugsy) & [masthoon](https://github.com/masthoon). Refer to the [project page on Github](https://github.com/0vercl0k/kdmp-parser) for documentation, issues and pull requests.
+
+## Installing from PyPI
+
+The easiest way is simply to:
+
+```bash
+pip install kdmp_parser
+```
+
+## Examples
+
+### Get context, print the program counter
+
+```python
+import kdmp_parser
+dmp = kdmp_parser.KernelDumpParser("full.dmp")
+assert dmp.type == kdmp_parser.DumpType.FullDump
+ctx = dmp.context
+print(f"Dump RIP={ctx.Rip:#x}")
+```
+
+### Read a virtual memory page at address pointed by RAX
+
+```python
+import kdmp_parser
+dmp = kdmp_parser.KernelDumpParser("full.dmp")
+dmp.read_virtual_page(ctx.Rax)
+```
+
+### Explore the physical memory
+
+```python
+import kdmp_parser
+dmp = kdmp_parser.KernelDumpParser("full.dmp")
+pml4 = dmp.directory_table_base
+print(f"{pml=:#x}")
+dmp.read_physical_page(pml4)
+```
+
+### Translate a virtual address into a physical address
+
+```python
+import kdmp_parser
+dmp = kdmp_parser.KernelDumpParser("full.dmp")
+VA = dmp.Rip
+PA = dmp.translate_virtual(VA)
+print(f"{VA=:#x} -> {PA=:#x}")
+```
+
+## Build
+
+Run the following after installing [CMake](https://cmake.org/) and [Python](https://python.org/) 3.8+ / `pip`:
+
+```bash
+pip install -r src/python/requirements.txt
+pip install src/python
+```
+
+# Authors
+
+* Axel '[@0vercl0k](https://twitter.com/0vercl0k)' Souchet
+
+# Contributors
+
+[ ![contributors-img](https://contrib.rocks/image?repo=0vercl0k/kdmp-parser) ](https://github.com/0vercl0k/kdmp-parser/graphs/contributors)
\ No newline at end of file
diff --git a/src/python/kdmp_parser/__init__.py b/src/python/kdmp_parser/__init__.py
new file mode 100644
index 0000000..12f9964
--- /dev/null
+++ b/src/python/kdmp_parser/__init__.py
@@ -0,0 +1,109 @@
+"""
+Root module of `kdmp_parser` Python package.
+"""
+
+import enum
+import pathlib
+from typing import Optional, Union
+
+#
+# `_kdmp_parser` is the C++ module. It contains the port of all C++ classes/enums/etc. in their
+# original form. The Python package part provides more Pythonic APIs.
+#
+from ._kdmp_parser import (  # type: ignore
+    version,
+    DumpType_t as _DumpType_t,
+    KernelDumpParser as _KernelDumpParser,
+)
+
+from .page import (
+    PageIterator as _PageIterator,
+)
+
+
+class DumpType(enum.IntEnum):
+    FullDump = _DumpType_t.FullDump
+    KernelDump = _DumpType_t.KernelDump
+    BMPDump = _DumpType_t.BMPDump
+
+
+class KernelDumpParser:
+    def __init__(self, path: Union[str, pathlib.Path]):
+        """Parse a kernel dump file
+
+        Args:
+            path (pathlib.Path|str): Path to the kernel dump file
+        """
+        if isinstance(path, str):
+            path = pathlib.Path(path)
+
+        if not isinstance(path, pathlib.Path):
+            raise TypeError
+
+        if not path.exists():
+            raise ValueError
+
+        self.__dump = _KernelDumpParser()
+        if not self.__dump.Parse(str(path.absolute())):
+            raise RuntimeError(f"Invalid kernel dump file: {path}")
+
+        self.filepath = path
+        self.context = self.__dump.GetContext()
+        self.directory_table_base = self.__dump.GetDirectoryTableBase() & ~0xFFF
+        self.type = DumpType(self.__dump.GetDumpType())
+        self.pages = _PageIterator(self.__dump)
+        return
+
+    def __repr__(self) -> str:
+        return f"KernelDumpParser({self.filepath}, {self.type})"
+
+    def read_physical_page(self, physical_address: int) -> Optional[bytearray]:
+        """Read a physical page from the memory dump
+
+        Args:
+            physical_address (int): The physical address to read. Note that no alignment
+            of this parameter is assumed.
+
+        Returns:
+            Optional[bytearray]: The bytes in the page if found, None otherwise
+        """
+        raw_page = self.__dump.GetPhysicalPage(physical_address)
+        if not raw_page:
+            return None
+
+        return bytearray(raw_page)
+
+    def read_virtual_page(
+        self, virtual_address: int, directory_table_base: Optional[int] = 0
+    ) -> Optional[bytearray]:
+        """Read a virtual page from the memory dump
+
+        Args:
+            virtual_address (int): _description_
+            directory_table_base (Optional[int]): if given, corresponds to the DirectoryTableBase value
+
+        Returns:
+            Optional[bytearray]: The bytes in the page if found, None otherwise
+        """
+        raw_page = self.__dump.GetVirtualPage(virtual_address, directory_table_base)
+        if not raw_page:
+            return None
+
+        return bytearray(raw_page)
+
+    def translate_virtual(
+        self, virtual_address: int, directory_table_base: Optional[int] = 0
+    ) -> Optional[int]:
+        """Translate a virtual address to physical. A directory table base can be optionally
+        provided
+
+        Args:
+            virtual_address (int): _description_
+            directory_table_base (Optional[int]): if given, corresponds to the DirectoryTableBase
+            value
+
+        Returns:
+            Optional[int]: If found, return the physical address to the virtual address. None
+            otherwise
+        """
+        return self.__dump.VirtTranslate(virtual_address, directory_table_base)
diff --git a/src/python/kdmp_parser/page.py b/src/python/kdmp_parser/page.py
new file mode 100644
index 0000000..6a388af
--- /dev/null
+++ b/src/python/kdmp_parser/page.py
@@ -0,0 +1,40 @@
+from typing import Generator
+from ._kdmp_parser import (  # type: ignore
+    PageSize as size,
+    PageAlign as align,
+    PageOffset as offset,
+    KernelDumpParser as _KernelDumpParser,
+)
+
+VALID_PAGE_SIZES = (0x1000, 0x20_0000, 0x4000_0000)
+
+
+class PageIterator:
+    """Access a dump pages"""
+
+    def __init__(self, dump: _KernelDumpParser) -> None:
+        self.__dump = dump
+
+    def __getitem__(self, physical_address: int) -> bytearray:
+        raw_page = self.__dump.GetPhysicalPage(physical_address)
+        if not raw_page:
+            raise IndexError
+        return bytearray(raw_page)
+
+    def __iter__(self) -> Generator[int, None, None]:
+        return self.keys()
+
+    def __len__(self) -> int:
+        return len(list(self.keys()))
+
+    def keys(self) -> Generator[int, None, None]:
+        for page_addr in self.__dump.GetPhysmem():
+            yield page_addr
+
+    def values(self) -> Generator[bytearray, None, None]:
+        for page_addr in self.__dump.GetPhysmem():
+            yield self[page_addr]
+
+    def items(self) -> Generator["tuple[int, bytearray]", None, None]:
+        for page_addr in self.__dump.GetPhysmem():
+            yield page_addr, self[page_addr]
diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml
new file mode 100644
index 0000000..11e9a9f
--- /dev/null
+++ b/src/python/pyproject.toml
@@ -0,0 +1,44 @@
+[build-system]
+requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2"]
+build-backend = "scikit_build_core.build"
+
+[project]
+name = "kdmp-parser"
+version = "0.6.0"
+description = "A Cross-Platform C++ parser library for Windows kernel minidumps."
+readme = "README.md"
+requires-python = ">=3.8"
+authors = [{ name = "0vercl0k", email = "0vercl0k@not-your-biz.net" }]
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "License :: OSI Approved :: MIT License",
+    "Programming Language :: Python :: 3",
+    "Topic :: Software Development :: Assemblers",
+    "Natural Language :: English",
+]
+dependencies = []
+
+[project.urls]
+Homepage = "https://github.com/0vercl0k/kdmp-parser"
+
+[tool.isort]
+profile = "black"
+
+[tool.scikit-build]
+wheel.py-api = "cp312"
+minimum-version = "0.4"
+build-dir = "build/{wheel_tag}"
+cmake.minimum-version = "3.20"
+cmake.args = [
+    "-DBUILD_PARSER:BOOL=OFF",
+    "-DBUILD_TESTS:BOOL=OFF",
+    "-DBUILD_PYTHON_PACKAGE:BOOL=ON",
+]
+
+[tool.cibuildwheel]
+build-verbosity = 1
+test-command = "pytest {project}/tests"
+test-requires = "pytest"
+
+[tool.cibuildwheel.macos.environment]
+MACOSX_DEPLOYMENT_TARGET = "10.14"
diff --git a/src/python/python-kdmp.cc b/src/python/python-kdmp.cc
deleted file mode 100644
index 4dfd843..0000000
--- a/src/python/python-kdmp.cc
+++ /dev/null
@@ -1,325 +0,0 @@
-// Mastho - 2020
-// Axel '0vercl0k' Souchet - December 27 2020
-#include "python-kdmp.h"
-
-//
-// Python Dump instance creation (allocate and initialize kernel dump object).
-//   >>> Dump(filepath)
-//
-
-PyObject *NewDumpParser(PyTypeObject *Type, PyObject *Args, PyObject *) {
-
-  //
-  // Allocate and zero PythonDumpParser.
-  //
-
-  auto *Self = PyObject_New(PythonDumpParser, Type);
-  Self->DumpParser = nullptr;
-
-  //
-  // Parse Python argument (expect a string i.e. the file path of the dump).
-  //    *PyErr_Format returns nullptr to raise the exception*
-  //
-
-  char *DumpPath = nullptr;
-  if (!PyArg_ParseTuple(Args, "s", &DumpPath)) {
-    DeleteDumpParser((PyObject *)Self);
-    return PyErr_Format(PyExc_TypeError, "Dump() expected a string");
-  }
-
-  //
-  // Initialize the internal KernelDumpParser and validate the dump file.
-  //
-
-  Self->DumpParser = new kdmpparser::KernelDumpParser();
-  if (!Self->DumpParser->Parse(DumpPath)) {
-    DeleteDumpParser((PyObject *)Self);
-    return PyErr_Format(PyExc_ValueError, "Dump() invalid path");
-  }
-
-  //
-  // Return the new instance of PythonDumpParser to Python.
-  //
-
-  return (PyObject *)Self;
-}
-
-//
-// Python Dump instance destruction.
-//   >>> del dump_instance
-//
-
-void DeleteDumpParser(PyObject *Object) {
-
-  //
-  // Release internal KernelDumpParser object.
-  //
-
-  auto *Self = (PythonDumpParser *)Object;
-
-  if (Self->DumpParser) {
-    delete Self->DumpParser;
-    Self->DumpParser = nullptr;
-  }
-
-  //
-  // Free type reference and self.
-  //
-
-  PyObject_Del(Object);
-}
-
-//
-// Python Dump instance method to retrieve the DumpType.
-//  >>> dump_instance.type() # return int
-//
-
-PyObject *DumpParserGetType(PyObject *Object, PyObject *) {
-
-  //
-  // Get the dump type (FullDump, KernelDump or BMPDump).
-  //
-
-  const auto *Self = (PythonDumpParser *)Object;
-  const auto DumpType = Self->DumpParser->GetDumpType();
-  return PyLong_FromUnsignedLong(static_cast<unsigned long>(DumpType));
-}
-
-//
-//  Python Dump instance method to retrieve the register context.
-//  >>> dump_instance.context() # return dict(str -> int)
-//
-
-PyObject *DumpParserGetContext(PyObject *Object, PyObject *) {
-
-  //
-  // Get the dump context (commons registers).
-  //
-
-  const auto *Self = (PythonDumpParser *)Object;
-  const auto *C = Self->DumpParser->GetContext();
-
-  //
-  // Create a Python dict object with lowercase register name and value.
-  //
-
-  PyObject *Context = PyDict_New();
-
-  PyDict_SetItemString(Context, "rax", PyLong_FromUnsignedLongLong(C->Rax));
-  PyDict_SetItemString(Context, "rbx", PyLong_FromUnsignedLongLong(C->Rbx));
-  PyDict_SetItemString(Context, "rcx", PyLong_FromUnsignedLongLong(C->Rcx));
-  PyDict_SetItemString(Context, "rdx", PyLong_FromUnsignedLongLong(C->Rdx));
-  PyDict_SetItemString(Context, "rsi", PyLong_FromUnsignedLongLong(C->Rsi));
-  PyDict_SetItemString(Context, "rdi", PyLong_FromUnsignedLongLong(C->Rdi));
-  PyDict_SetItemString(Context, "rip", PyLong_FromUnsignedLongLong(C->Rip));
-  PyDict_SetItemString(Context, "rsp", PyLong_FromUnsignedLongLong(C->Rsp));
-  PyDict_SetItemString(Context, "rbp", PyLong_FromUnsignedLongLong(C->Rbp));
-  PyDict_SetItemString(Context, "r8", PyLong_FromUnsignedLongLong(C->R8));
-  PyDict_SetItemString(Context, "r9", PyLong_FromUnsignedLongLong(C->R9));
-  PyDict_SetItemString(Context, "r10", PyLong_FromUnsignedLongLong(C->R10));
-  PyDict_SetItemString(Context, "r11", PyLong_FromUnsignedLongLong(C->R11));
-  PyDict_SetItemString(Context, "r12", PyLong_FromUnsignedLongLong(C->R12));
-  PyDict_SetItemString(Context, "r13", PyLong_FromUnsignedLongLong(C->R13));
-  PyDict_SetItemString(Context, "r14", PyLong_FromUnsignedLongLong(C->R14));
-  PyDict_SetItemString(Context, "r15", PyLong_FromUnsignedLongLong(C->R15));
-
-  //
-  //  Get the DirectoryTableBase from the dump and return the created dict to
-  //  Python.
-  //
-  PyDict_SetItemString(
-      Context, "dtb",
-      PyLong_FromUnsignedLongLong(Self->DumpParser->GetDirectoryTableBase()));
-
-  return Context;
-}
-
-//
-//  Python Dump instance method to retrieve the bugcheck parameters.
-//  >>> dump_instance.bugcheck() # return dict
-//
-
-PyObject *DumpParserGetBugCheckParameters(PyObject *Object, PyObject *) {
-
-  //
-  // Retrieve the bugcheck parameters.
-  //
-
-  const auto *Self = (PythonDumpParser *)Object;
-  const auto Parameters = Self->DumpParser->GetBugCheckParameters();
-
-  const uint64_t NumberParams = sizeof(Parameters.BugCheckCodeParameter) /
-                                sizeof(Parameters.BugCheckCodeParameter[0]);
-  PyObject *PythonParamsList = PyList_New(NumberParams);
-
-  for (uint64_t Idx = 0; Idx < NumberParams; Idx++) {
-    PyList_SetItem(
-        PythonParamsList, Idx,
-        PyLong_FromUnsignedLongLong(Parameters.BugCheckCodeParameter[Idx]));
-  }
-
-  //
-  // Create a Python dict object with code and parameters.
-  //
-
-  PyObject *PythonParams = PyDict_New();
-
-  PyDict_SetItemString(PythonParams, "code",
-                       PyLong_FromUnsignedLong(Parameters.BugCheckCode));
-  PyDict_SetItemString(PythonParams, "parameters", PythonParamsList);
-
-  return PythonParams;
-}
-
-//
-//  Python Dump instance method to get a physical page from a physical address.
-//  >>> dump_instance.get_physical_page(addr) # return bytes
-//
-
-PyObject *DumpParserGetPhysicalPage(PyObject *Object, PyObject *Args) {
-
-  //
-  // Parse Python argument (expect one unsigned long long integer).
-  //
-
-  const auto *Self = (PythonDumpParser *)Object;
-
-  uint64_t PhysicalAddress = 0;
-  if (!PyArg_ParseTuple(Args, "K", &PhysicalAddress)) {
-    return PyErr_Format(PyExc_TypeError,
-                        "get_physical_page() expected an integer");
-  }
-
-  //
-  // Get the physical page and return it as bytes.
-  //
-
-  const uint8_t *Page = Self->DumpParser->GetPhysicalPage(PhysicalAddress);
-  if (Page == nullptr) {
-    return PyErr_Format(PyExc_ValueError,
-                        "get_physical_page() invalid address");
-  }
-
-  return PyBytes_FromStringAndSize((char *)Page, kdmpparser::Page::Size);
-}
-
-//
-//  Python Dump instance method to perform address translation (physical to
-//  virtual).
-//  >>> dump_instance.virt_translate(addr, [dtb]) # return int
-//
-
-PyObject *DumpParserVirtTranslate(PyObject *Object, PyObject *Args) {
-
-  //
-  // Parse Python argument (expect one or two unsigned long long integer).
-  //
-
-  const auto *Self = (PythonDumpParser *)Object;
-
-  uint64_t VirtualAddress = 0;
-  uint64_t DirectoryTableBase = 0;
-  if (!PyArg_ParseTuple(Args, "K|K", &VirtualAddress, &DirectoryTableBase)) {
-    return PyErr_Format(PyExc_TypeError,
-                        "virt_translate() expected one or two integers");
-  }
-
-  //
-  // Retrieve the physical address (parse pages tables in the dump).
-  //
-
-  const auto &PhysicalAddress =
-      Self->DumpParser->VirtTranslate(VirtualAddress, DirectoryTableBase);
-
-  if (!PhysicalAddress) {
-    return PyErr_Format(PyExc_ValueError, "virt_translate() invalid address");
-  }
-
-  return PyLong_FromUnsignedLongLong(*PhysicalAddress);
-}
-
-//
-//  Python Dump instance method to get a page from a virtual address.
-//  >>> dump_instance.get_virtual_page(addr, [dtb]) # return bytes
-//
-
-PyObject *DumpParserGetVirtualPage(PyObject *Object, PyObject *Args) {
-
-  //
-  // Parse Python argument (expect one or two unsigned long long integer).
-  //
-
-  const auto *Self = (PythonDumpParser *)Object;
-
-  uint64_t VirtualAddress = 0;
-  uint64_t DirectoryTableBase = 0;
-  if (!PyArg_ParseTuple(Args, "K|K", &VirtualAddress, &DirectoryTableBase)) {
-    return PyErr_Format(PyExc_TypeError,
-                        "get_virtual_page() expected one or two integer");
-  }
-
-  const uint8_t *Page =
-      Self->DumpParser->GetVirtualPage(VirtualAddress, DirectoryTableBase);
-  if (Page == nullptr) {
-    return PyErr_Format(PyExc_ValueError, "get_virtual_page() invalid address");
-  }
-
-  return PyBytes_FromStringAndSize((char *)Page, kdmpparser::Page::Size);
-}
-
-//
-// KDMP Module initialization function.
-//
-
-PyMODINIT_FUNC PyInit_kdmp() {
-
-  //
-  // Initialize python.
-  //
-
-  Py_Initialize();
-
-  //
-  // Expose the kdmp module.
-  //
-
-  PyObject *M = PyModule_Create(&KDMPModule);
-  if (M == nullptr) {
-    return nullptr;
-  }
-
-  //
-  // Register PythonDumpParserType.
-  //
-
-  PyObject *Ty = PyType_FromSpec(&TySpec);
-  if (Ty == nullptr) {
-    return nullptr;
-  }
-
-  KDMPState(M)->PythonDumpParserType = (PyTypeObject *)Ty;
-
-  //
-  // Expose the PythonDumpParserType to Python in kdmp module.
-  //  >>> kdmp.Dump class
-  //
-
-  Py_INCREF(Ty);
-  if (PyModule_AddObject(M, "Dump", Ty) < 0) {
-    Py_DECREF(Ty);
-    Py_DECREF(M);
-    return nullptr;
-  }
-
-  //
-  // Expose DumpType constants to Python.
-  //  >>> kdmp.FullDump ...
-  //
-
-  PyModule_AddIntConstant(M, "FullDump",
-                          long(kdmpparser::DumpType_t::FullDump));
-  PyModule_AddIntConstant(M, "KernelDump",
-                          long(kdmpparser::DumpType_t::KernelDump));
-  PyModule_AddIntConstant(M, "BMPDump", long(kdmpparser::DumpType_t::BMPDump));
-  return M;
-}
diff --git a/src/python/python-kdmp.h b/src/python/python-kdmp.h
deleted file mode 100644
index 4a60dfd..0000000
--- a/src/python/python-kdmp.h
+++ /dev/null
@@ -1,104 +0,0 @@
-// Mastho - 2020
-// Axel '0vercl0k' Souchet - December 27 2020
-#pragma once
-
-//
-// PEP-384
-// """
-// Applications shall only include the header file Python.h (before including
-// any system headers).
-//
-
-#define Py_LIMITED_API
-#include <Python.h>
-
-#include "kdmp-parser.h"
-
-//
-// Python object handling all interactions with the library.
-//
-
-struct PythonDumpParser {
-  PyObject_HEAD kdmpparser::KernelDumpParser *DumpParser = nullptr;
-};
-
-//
-// Python Dump type functions declarations (class instance creation and instance
-// destruction).
-//
-
-PyObject *NewDumpParser(PyTypeObject *Type, PyObject *Args, PyObject *Kwds);
-void DeleteDumpParser(PyObject *Object);
-
-//
-// Python Dump object methods functions declarations.
-//
-
-PyObject *DumpParserGetType(PyObject *Object, PyObject *);
-PyObject *DumpParserGetContext(PyObject *Object, PyObject *);
-PyObject *DumpParserGetPhysicalPage(PyObject *Object, PyObject *Args);
-PyObject *DumpParserVirtTranslate(PyObject *Object, PyObject *Args);
-PyObject *DumpParserGetVirtualPage(PyObject *Object, PyObject *Args);
-PyObject *DumpParserGetBugCheckParameters(PyObject *Object, PyObject *);
-
-//
-// Object methods of Python Dump type.
-//
-
-PyMethodDef DumpObjectMethod[] = {
-    {"type", DumpParserGetType, METH_NOARGS,
-     "Show Dump Type (FullDump, KernelDump, BMPDump)"},
-    {"context", DumpParserGetContext, METH_NOARGS, "Get Register Context"},
-    {"get_physical_page", DumpParserGetPhysicalPage, METH_VARARGS,
-     "Get Physical Page Content"},
-    {"virt_translate", DumpParserVirtTranslate, METH_VARARGS,
-     "Translate Virtual to Physical Address"},
-    {"get_virtual_page", DumpParserGetVirtualPage, METH_VARARGS,
-     "Get Virtual Page Content"},
-    {"bugcheck", DumpParserGetBugCheckParameters, METH_NOARGS,
-     "Get BugCheck Parameters"},
-    {nullptr, nullptr, 0, nullptr}};
-
-//
-// Define Slots/Spec (name, initialization & destruction
-// functions and object methods).
-//
-
-PyType_Slot TySlots[] = {
-    {Py_tp_doc, (void *)"Dump object"},
-    {Py_tp_new, (void *)NewDumpParser},
-    {Py_tp_dealloc, (void *)DeleteDumpParser},
-    {Py_tp_methods, DumpObjectMethod},
-    {0, 0},
-};
-
-PyType_Spec TySpec = {"kdmp.Dump", sizeof(PythonDumpParser), 0,
-                      Py_TPFLAGS_DEFAULT, TySlots};
-
-//
-// KDMP Module definition.
-//
-
-struct KDMPState {
-  PyTypeObject *PythonDumpParserType = nullptr;
-};
-
-struct PyModuleDef KDMPModule = {
-    PyModuleDef_HEAD_INIT, /* m_base */
-    "kdmp",                /* m_name */
-    "KDMP module",         /* m_doc */
-    sizeof(KDMPState),     /* m_size */
-    nullptr,               /* m_methods */
-    nullptr,               /* m_slots */
-    nullptr,               /* m_traverse */
-    nullptr,               /* m_clear */
-    nullptr,               /* m_free */
-};
-
-//
-// KDMP Module initialization function.
-//
-
-PyMODINIT_FUNC PyInit_kdmp();
-
-#define KDMPState(o) ((KDMPState *)PyModule_GetState(o))
diff --git a/src/python/requirements.txt b/src/python/requirements.txt
new file mode 100644
index 0000000..f4b57e7
--- /dev/null
+++ b/src/python/requirements.txt
@@ -0,0 +1,4 @@
+setuptools
+wheel
+nanobind
+black
diff --git a/src/python/src/kdmp_parser.cc b/src/python/src/kdmp_parser.cc
new file mode 100644
index 0000000..c1bbe08
--- /dev/null
+++ b/src/python/src/kdmp_parser.cc
@@ -0,0 +1,291 @@
+//
+// This file is part of kdmp-parser project
+//
+// Released under MIT License, by 0vercl0k - 2023
+//
+// With contributions from:
+//  * hugsy - (github.com/hugsy)
+//
+
+#include "kdmp-parser.h"
+
+#include <nanobind/nanobind.h>
+#include <nanobind/stl/array.h>
+#include <nanobind/stl/bind_map.h>
+#include <nanobind/stl/filesystem.h>
+#include <nanobind/stl/optional.h>
+#include <nanobind/stl/pair.h>
+#include <nanobind/stl/string.h>
+#include <vector>
+
+namespace nb = nanobind;
+using namespace nb::literals;
+
+NB_MODULE(_kdmp_parser, m) {
+
+  m.doc() = "KDMP parser module";
+
+  nb::class_<kdmpparser::Version_t>(m, "version")
+      .def_ro_static("major", &kdmpparser::Version_t::Major)
+      .def_ro_static("minor", &kdmpparser::Version_t::Minor)
+      .def_ro_static("patch", &kdmpparser::Version_t::Patch)
+      .def_ro_static("release", &kdmpparser::Version_t::Release);
+
+  nb::class_<kdmpparser::uint128_t>(m, "uint128_t")
+      .def(nb::init<>())
+      .def_rw("Low", &kdmpparser::uint128_t::Low)
+      .def_rw("High", &kdmpparser::uint128_t::High);
+
+  nb::enum_<kdmpparser::DumpType_t>(m, "DumpType_t")
+      .value("FullDump", kdmpparser::DumpType_t::FullDump)
+      .value("KernelDump", kdmpparser::DumpType_t::KernelDump)
+      .value("BMPDump", kdmpparser::DumpType_t::BMPDump)
+      .export_values();
+
+  nb::class_<kdmpparser::PHYSMEM_RUN>(m, "PHYSMEM_RUN")
+      .def(nb::init<>())
+      .def_rw("BasePage", &kdmpparser::PHYSMEM_RUN::BasePage)
+      .def_rw("PageCount", &kdmpparser::PHYSMEM_RUN::PageCount)
+      .def("Show", &kdmpparser::PHYSMEM_RUN::Show, "Prefix"_a);
+
+  nb::class_<kdmpparser::PHYSMEM_DESC>(m, "PHYSMEM_DESC")
+      .def(nb::init<>())
+      .def_ro("NumberOfRuns", &kdmpparser::PHYSMEM_DESC::NumberOfRuns)
+      .def_ro("Padding0", &kdmpparser::PHYSMEM_DESC::Padding0)
+      .def_ro("NumberOfPages", &kdmpparser::PHYSMEM_DESC::NumberOfPages)
+      .def_ro("Run", &kdmpparser::PHYSMEM_DESC::Run)
+      .def("Show", &kdmpparser::PHYSMEM_DESC::Show, "Prefix"_a)
+      .def("LooksGood", &kdmpparser::PHYSMEM_DESC::LooksGood);
+
+  nb::class_<kdmpparser::BMP_HEADER64>(m, "BMP_HEADER64")
+      .def(nb::init<>())
+      .def_ro_static("ExpectedSignature",
+                     &kdmpparser::BMP_HEADER64::ExpectedSignature)
+      .def_ro_static("ExpectedSignature2",
+                     &kdmpparser::BMP_HEADER64::ExpectedSignature2)
+      .def_ro_static("ExpectedValidDump",
+                     &kdmpparser::BMP_HEADER64::ExpectedValidDump)
+      .def_ro("Signature", &kdmpparser::BMP_HEADER64::Signature)
+      .def_ro("ValidDump", &kdmpparser::BMP_HEADER64::ValidDump)
+      .def_ro("Padding0", &kdmpparser::BMP_HEADER64::Padding0)
+      .def_ro("FirstPage", &kdmpparser::BMP_HEADER64::FirstPage)
+      .def_ro("TotalPresentPages", &kdmpparser::BMP_HEADER64::TotalPresentPages)
+      .def_ro("Pages", &kdmpparser::BMP_HEADER64::Pages)
+      .def_ro("Bitmap", &kdmpparser::BMP_HEADER64::Bitmap)
+      .def("Show", &kdmpparser::PHYSMEM_DESC::Show, "Prefix"_a)
+      .def("LooksGood", &kdmpparser::PHYSMEM_DESC::LooksGood);
+
+  nb::class_<kdmpparser::CONTEXT>(m, "CONTEXT")
+      .def(nb::init<>())
+      .def_ro("P1Home", &kdmpparser::CONTEXT::P1Home)
+      .def_ro("P2Home", &kdmpparser::CONTEXT::P2Home)
+      .def_ro("P3Home", &kdmpparser::CONTEXT::P3Home)
+      .def_ro("P4Home", &kdmpparser::CONTEXT::P4Home)
+      .def_ro("P5Home", &kdmpparser::CONTEXT::P5Home)
+      .def_ro("P6Home", &kdmpparser::CONTEXT::P6Home)
+      .def_ro("ContextFlags", &kdmpparser::CONTEXT::ContextFlags)
+      .def_ro("MxCsr", &kdmpparser::CONTEXT::MxCsr)
+      .def_ro("SegCs", &kdmpparser::CONTEXT::SegCs)
+      .def_ro("SegDs", &kdmpparser::CONTEXT::SegDs)
+      .def_ro("SegEs", &kdmpparser::CONTEXT::SegEs)
+      .def_ro("SegFs", &kdmpparser::CONTEXT::SegFs)
+      .def_ro("SegGs", &kdmpparser::CONTEXT::SegGs)
+      .def_ro("SegSs", &kdmpparser::CONTEXT::SegSs)
+      .def_ro("EFlags", &kdmpparser::CONTEXT::EFlags)
+      .def_ro("Dr0", &kdmpparser::CONTEXT::Dr0)
+      .def_ro("Dr1", &kdmpparser::CONTEXT::Dr1)
+      .def_ro("Dr2", &kdmpparser::CONTEXT::Dr2)
+      .def_ro("Dr3", &kdmpparser::CONTEXT::Dr3)
+      .def_ro("Dr6", &kdmpparser::CONTEXT::Dr6)
+      .def_ro("Dr7", &kdmpparser::CONTEXT::Dr7)
+      .def_ro("Rax", &kdmpparser::CONTEXT::Rax)
+      .def_ro("Rcx", &kdmpparser::CONTEXT::Rcx)
+      .def_ro("Rdx", &kdmpparser::CONTEXT::Rdx)
+      .def_ro("Rbx", &kdmpparser::CONTEXT::Rbx)
+      .def_ro("Rsp", &kdmpparser::CONTEXT::Rsp)
+      .def_ro("Rbp", &kdmpparser::CONTEXT::Rbp)
+      .def_ro("Rsi", &kdmpparser::CONTEXT::Rsi)
+      .def_ro("Rdi", &kdmpparser::CONTEXT::Rdi)
+      .def_ro("R8", &kdmpparser::CONTEXT::R8)
+      .def_ro("R9", &kdmpparser::CONTEXT::R9)
+      .def_ro("R10", &kdmpparser::CONTEXT::R10)
+      .def_ro("R11", &kdmpparser::CONTEXT::R11)
+      .def_ro("R12", &kdmpparser::CONTEXT::R12)
+      .def_ro("R13", &kdmpparser::CONTEXT::R13)
+      .def_ro("R14", &kdmpparser::CONTEXT::R14)
+      .def_ro("R15", &kdmpparser::CONTEXT::R15)
+      .def_ro("Rip", &kdmpparser::CONTEXT::Rip)
+      .def_ro("ControlWord", &kdmpparser::CONTEXT::ControlWord)
+      .def_ro("StatusWord", &kdmpparser::CONTEXT::StatusWord)
+      .def_ro("TagWord", &kdmpparser::CONTEXT::TagWord)
+      .def_ro("Reserved1", &kdmpparser::CONTEXT::Reserved1)
+      .def_ro("ErrorOpcode", &kdmpparser::CONTEXT::ErrorOpcode)
+      .def_ro("ErrorOffset", &kdmpparser::CONTEXT::ErrorOffset)
+      .def_ro("ErrorSelector", &kdmpparser::CONTEXT::ErrorSelector)
+      .def_ro("Reserved2", &kdmpparser::CONTEXT::Reserved2)
+      .def_ro("DataOffset", &kdmpparser::CONTEXT::DataOffset)
+      .def_ro("DataSelector", &kdmpparser::CONTEXT::DataSelector)
+      .def_ro("Reserved3", &kdmpparser::CONTEXT::Reserved3)
+      .def_ro("MxCsr2", &kdmpparser::CONTEXT::MxCsr2)
+      .def_ro("MxCsr_Mask", &kdmpparser::CONTEXT::MxCsr_Mask)
+      .def_ro("FloatRegisters", &kdmpparser::CONTEXT::FloatRegisters)
+      .def_ro("Xmm0", &kdmpparser::CONTEXT::Xmm0)
+      .def_ro("Xmm1", &kdmpparser::CONTEXT::Xmm1)
+      .def_ro("Xmm2", &kdmpparser::CONTEXT::Xmm2)
+      .def_ro("Xmm3", &kdmpparser::CONTEXT::Xmm3)
+      .def_ro("Xmm4", &kdmpparser::CONTEXT::Xmm4)
+      .def_ro("Xmm5", &kdmpparser::CONTEXT::Xmm5)
+      .def_ro("Xmm6", &kdmpparser::CONTEXT::Xmm6)
+      .def_ro("Xmm7", &kdmpparser::CONTEXT::Xmm7)
+      .def_ro("Xmm8", &kdmpparser::CONTEXT::Xmm8)
+      .def_ro("Xmm9", &kdmpparser::CONTEXT::Xmm9)
+      .def_ro("Xmm10", &kdmpparser::CONTEXT::Xmm10)
+      .def_ro("Xmm11", &kdmpparser::CONTEXT::Xmm11)
+      .def_ro("Xmm12", &kdmpparser::CONTEXT::Xmm12)
+      .def_ro("Xmm13", &kdmpparser::CONTEXT::Xmm13)
+      .def_ro("Xmm14", &kdmpparser::CONTEXT::Xmm14)
+      .def_ro("Xmm15", &kdmpparser::CONTEXT::Xmm15)
+      .def_ro("VectorRegister", &kdmpparser::CONTEXT::VectorRegister)
+      .def_ro("VectorControl", &kdmpparser::CONTEXT::VectorControl)
+      .def_ro("DebugControl", &kdmpparser::CONTEXT::DebugControl)
+      .def_ro("LastBranchToRip", &kdmpparser::CONTEXT::LastBranchToRip)
+      .def_ro("LastBranchFromRip", &kdmpparser::CONTEXT::LastBranchFromRip)
+      .def_ro("LastExceptionToRip", &kdmpparser::CONTEXT::LastExceptionToRip)
+      .def_ro("LastExceptionFromRip",
+              &kdmpparser::CONTEXT::LastExceptionFromRip)
+      .def("Show", &kdmpparser::CONTEXT::Show, "Prefix"_a)
+      .def("LooksGood", &kdmpparser::CONTEXT::LooksGood);
+
+  nb::class_<kdmpparser::EXCEPTION_RECORD64>(m, "EXCEPTION_RECORD64")
+      .def(nb::init<>())
+      .def_ro("ExceptionCode", &kdmpparser::EXCEPTION_RECORD64::ExceptionCode)
+      .def_ro("ExceptionFlags", &kdmpparser::EXCEPTION_RECORD64::ExceptionFlags)
+      .def_ro("ExceptionRecord",
+              &kdmpparser::EXCEPTION_RECORD64::ExceptionRecord)
+      .def_ro("ExceptionAddress",
+              &kdmpparser::EXCEPTION_RECORD64::ExceptionAddress)
+      .def_ro("NumberParameters",
+              &kdmpparser::EXCEPTION_RECORD64::NumberParameters)
+      .def_ro("__unusedAlignment",
+              &kdmpparser::EXCEPTION_RECORD64::__unusedAlignment)
+      .def_ro("ExceptionInformation",
+              &kdmpparser::EXCEPTION_RECORD64::ExceptionInformation)
+      .def("Show", &kdmpparser::EXCEPTION_RECORD64::Show, "Prefix"_a);
+
+  nb::class_<kdmpparser::HEADER64>(m, "HEADER64")
+      .def(nb::init<>())
+      .def_ro_static("ExpectedSignature",
+                     &kdmpparser::HEADER64::ExpectedSignature)
+      .def_ro_static("ExpectedValidDump",
+                     &kdmpparser::HEADER64::ExpectedValidDump)
+
+      .def_ro("Signature", &kdmpparser::HEADER64::Signature)
+      .def_ro("ValidDump", &kdmpparser::HEADER64::ValidDump)
+      .def_ro("MajorVersion", &kdmpparser::HEADER64::MajorVersion)
+      .def_ro("MinorVersion", &kdmpparser::HEADER64::MinorVersion)
+      .def_ro("DirectoryTableBase", &kdmpparser::HEADER64::DirectoryTableBase)
+      .def_ro("PfnDatabase", &kdmpparser::HEADER64::PfnDatabase)
+      .def_ro("PsLoadedModuleList", &kdmpparser::HEADER64::PsLoadedModuleList)
+      .def_ro("PsActiveProcessHead", &kdmpparser::HEADER64::PsActiveProcessHead)
+      .def_ro("MachineImageType", &kdmpparser::HEADER64::MachineImageType)
+      .def_ro("NumberProcessors", &kdmpparser::HEADER64::NumberProcessors)
+      .def_ro("BugCheckCode", &kdmpparser::HEADER64::BugCheckCode)
+      .def_ro("Padding0", &kdmpparser::HEADER64::Padding0)
+      .def_ro("BugCheckCodeParameter",
+              &kdmpparser::HEADER64::BugCheckCodeParameter)
+      .def_ro("Padding1", &kdmpparser::HEADER64::Padding1)
+      .def_ro("KdDebuggerDataBlock", &kdmpparser::HEADER64::KdDebuggerDataBlock)
+      .def_ro("PhysicalMemoryBlockBuffer",
+              &kdmpparser::HEADER64::PhysicalMemoryBlockBuffer)
+      .def_ro("Padding2", &kdmpparser::HEADER64::Padding2)
+      .def_ro("ContextRecord", &kdmpparser::HEADER64::ContextRecord)
+      .def_ro("Padding3", &kdmpparser::HEADER64::Padding3)
+      .def_ro("Exception", &kdmpparser::HEADER64::Exception)
+      .def_ro("DumpType", &kdmpparser::HEADER64::DumpType)
+      .def_ro("Padding4", &kdmpparser::HEADER64::Padding4)
+      .def_ro("RequiredDumpSpace", &kdmpparser::HEADER64::RequiredDumpSpace)
+      .def_ro("SystemTime", &kdmpparser::HEADER64::SystemTime)
+      .def_ro("Comment", &kdmpparser::HEADER64::Comment)
+      .def_ro("SystemUpTime", &kdmpparser::HEADER64::SystemUpTime)
+      .def_ro("MiniDumpFields", &kdmpparser::HEADER64::MiniDumpFields)
+      .def_ro("SecondaryDataState", &kdmpparser::HEADER64::SecondaryDataState)
+      .def_ro("ProductType", &kdmpparser::HEADER64::ProductType)
+      .def_ro("SuiteMask", &kdmpparser::HEADER64::SuiteMask)
+      .def_ro("WriterStatus", &kdmpparser::HEADER64::WriterStatus)
+      .def_ro("Unused1", &kdmpparser::HEADER64::Unused1)
+      .def_ro("KdSecondaryVersion", &kdmpparser::HEADER64::KdSecondaryVersion)
+      .def_ro("Unused", &kdmpparser::HEADER64::Unused)
+      .def_ro("_reserved0", &kdmpparser::HEADER64::_reserved0)
+      .def_ro("BmpHeader", &kdmpparser::HEADER64::BmpHeader)
+      .def("Show", &kdmpparser::CONTEXT::Show, "Prefix"_a)
+      .def("LooksGood", &kdmpparser::CONTEXT::LooksGood);
+
+  m.attr("PageSize") = kdmpparser::Page::Size;
+  m.def("PageAlign", &kdmpparser::Page::Align, "Address"_a,
+        "Get the aligned value on the page for the given address.");
+  m.def("PageOffset", &kdmpparser::Page::Offset, "Address"_a,
+        "Get the offset to the page for the given address.");
+
+  nb::class_<kdmpparser::BugCheckParameters_t>(m, "BugCheckParameters_t")
+      .def(nb::init<>())
+      .def_ro("BugCheckCode", &kdmpparser::BugCheckParameters_t::BugCheckCode)
+      .def_ro("BugCheckCodeParameter",
+              &kdmpparser::BugCheckParameters_t::BugCheckCodeParameter);
+
+  nb::class_<kdmpparser::KernelDumpParser>(m, "KernelDumpParser")
+      .def(nb::init<>())
+      .def("Parse", &kdmpparser::KernelDumpParser::Parse, "PathFile"_a)
+      .def("GetContext", &kdmpparser::KernelDumpParser::GetContext)
+      .def("GetBugCheckParameters",
+           &kdmpparser::KernelDumpParser::GetBugCheckParameters)
+      .def("GetDumpType", &kdmpparser::KernelDumpParser::GetDumpType)
+      .def("GetPhysmem",
+           [](const kdmpparser::KernelDumpParser &Parser) {
+             const auto &PhysMem = Parser.GetPhysmem();
+             return nb::make_key_iterator(nb::type<std::vector<uint64_t>>(),
+                                          "it", PhysMem.cbegin(),
+                                          PhysMem.cend());
+           })
+      .def("ShowExceptionRecord",
+           &kdmpparser::KernelDumpParser::ShowExceptionRecord, "Prefix"_a = 0)
+      .def("ShowContextRecord",
+           &kdmpparser::KernelDumpParser::ShowContextRecord, "Prefix"_a = 0)
+      .def("ShowAllStructures",
+           &kdmpparser::KernelDumpParser::ShowAllStructures, "Prefix"_a = 0)
+      .def(
+          "GetPhysicalPage",
+          [](const kdmpparser::KernelDumpParser &Parser,
+             const uint64_t PhysicalAddress)
+              -> std::optional<kdmpparser::Page_t> {
+            const auto *Page = Parser.GetPhysicalPage(PhysicalAddress);
+            if (!Page) {
+              return std::nullopt;
+            }
+
+            kdmpparser::Page_t Out;
+            memcpy(Out.data(), Page, kdmpparser::Page::Size);
+            return Out;
+          },
+          "PhysicalAddress"_a)
+      .def("GetDirectoryTableBase",
+           &kdmpparser::KernelDumpParser::GetDirectoryTableBase)
+      .def("VirtTranslate", &kdmpparser::KernelDumpParser::VirtTranslate,
+           "VirtualAddress"_a, "DirectoryTableBase"_a)
+      .def(
+          "GetVirtualPage",
+          [](const kdmpparser::KernelDumpParser &Parser,
+             const uint64_t VirtualAddress,
+             const uint64_t DirectoryTableBase =
+                 0) -> std::optional<kdmpparser::Page_t> {
+            const auto *Page =
+                Parser.GetVirtualPage(VirtualAddress, DirectoryTableBase);
+            if (!Page) {
+              return std::nullopt;
+            }
+
+            kdmpparser::Page_t Out;
+            memcpy(Out.data(), Page, kdmpparser::Page::Size);
+            return Out;
+          },
+          "VirtualAddress"_a, "DirectoryTableBase"_a = 0);
+}


Test patch:
diff --git a/src/python/tests/pytest.ini b/src/python/tests/pytest.ini
new file mode 100644
index 0000000..8a8695c
--- /dev/null
+++ b/src/python/tests/pytest.ini
@@ -0,0 +1,15 @@
+[pytest]
+log_level = INFO
+minversion = 6.0
+required_plugins =
+    pytest-xdist
+    pytest-cov
+python_functions =
+    test_*
+    time_*
+python_files = *.py
+testpaths =
+    .
+markers =
+    slow: flag test as slow (deselect with '-m "not slow"')
+    online: flag test as requiring internet to work (deselect with '-m "not online"')
diff --git a/src/python/tests/requirements.txt b/src/python/tests/requirements.txt
new file mode 100644
index 0000000..eb9dfdd
--- /dev/null
+++ b/src/python/tests/requirements.txt
@@ -0,0 +1,4 @@
+pytest
+pytest-cov
+pytest-xdist
+pytest-forked
diff --git a/src/python/tests/test_page.py b/src/python/tests/test_page.py
new file mode 100644
index 0000000..e5e8049
--- /dev/null
+++ b/src/python/tests/test_page.py
@@ -0,0 +1,17 @@
+#
+# This file is part of kdmp-parser project
+#
+# Released under MIT License, by 0vercl0k - 2023
+#
+# With contributions from:
+# * masthoon - (github.com/masthoon)
+# * hugsy - (github.com/hugsy)
+#
+import kdmp_parser.page
+
+
+def test_page():
+    assert callable(kdmp_parser.page.align)
+    assert callable(kdmp_parser.page.offset)
+    assert isinstance(kdmp_parser.page.size, int)
+    assert kdmp_parser.page.size in kdmp_parser.page.VALID_PAGE_SIZES
diff --git a/src/python/tests/test_parser.py b/src/python/tests/test_parser.py
new file mode 100644
index 0000000..4933144
--- /dev/null
+++ b/src/python/tests/test_parser.py
@@ -0,0 +1,130 @@
+#
+# This file is part of kdmp-parser project
+#
+# Released under MIT License, by 0vercl0k - 2023
+#
+# With contributions from:
+# * masthoon - (github.com/masthoon)
+# * hugsy - (github.com/hugsy)
+#
+
+import pathlib
+import unittest
+
+import kdmp_parser
+
+REPO_ROOT = pathlib.Path(__file__).absolute().parent.parent.parent.parent
+
+class TestParserBasic(unittest.TestCase):
+    def setUp(self):
+        self.minidump_files: list[pathlib.Path] = [
+            REPO_ROOT / "bmp.dmp",
+            REPO_ROOT / "full.dmp",
+        ]
+
+        return super().setUp()
+
+    def tearDown(self) -> None:
+        return super().tearDown()
+
+    def test_parser_context(self):
+        expected_values = {
+            "bmp.dmp": {
+                "rip": 0xFFFFF805108776A0,
+                "rbp": 0xFFFFF80513568600,
+                "rsp": 0xFFFFF805135684F8,
+                "rax": 0x3,
+                "rbx": 0xFFFFF8050F4E9F70,
+                "rcx": 0x1,
+                "rdx": 0xFFFFF805135684D0,
+                "r8": 0x3,
+                "r9": 0xFFFFF805135684B8,
+                "r10": 0x0,
+                "r11": 0xFFFFA8848825E000,
+                "r12": 0xFFFFF8050F4E9F80,
+                "r13": 0xFFFFF80510C3C958,
+                "r14": 0x0,
+                "r15": 0x52,
+                "dtd": 0x6D4000,
+            },
+            "full.dmp": {
+                "rip": 0xFFFFF805108776A0,
+                "rbp": 0xFFFFF80513568600,
+                "rsp": 0xFFFFF805135684F8,
+                "rax": 0x3,
+                "rbx": 0xFFFFF8050F4E9F70,
+                "rcx": 0x1,
+                "rdx": 0xFFFFF805135684D0,
+                "r8": 0x3,
+                "r9": 0xFFFFF805135684B8,
+                "r10": 0x0,
+                "r11": 0xFFFFA8848825E000,
+                "r12": 0xFFFFF8050F4E9F80,
+                "r13": 0xFFFFF80510C3C958,
+                "r14": 0x0,
+                "r15": 0x52,
+                "dtd": 0x6D4000,
+            },
+        }
+
+        for md in self.minidump_files:
+            parser = kdmp_parser.KernelDumpParser(md)
+            assert parser.filepath == md
+
+            values = expected_values[md.name]
+            assert parser.context is not None
+            assert parser.context.Rip == values["rip"]
+            assert parser.context.Rbp == values["rbp"]
+            assert parser.context.Rsp == values["rsp"]
+            assert parser.context.Rax == values["rax"]
+            assert parser.context.Rbx == values["rbx"]
+            assert parser.context.Rcx == values["rcx"]
+            assert parser.context.Rdx == values["rdx"]
+            assert parser.context.R8 == values["r8"]
+            assert parser.context.R9 == values["r9"]
+            assert parser.context.R10 == values["r10"]
+            assert parser.context.R11 == values["r11"]
+            assert parser.context.R12 == values["r12"]
+            assert parser.context.R13 == values["r13"]
+            assert parser.context.R14 == values["r14"]
+            assert parser.context.R15 == values["r15"]
+
+            assert parser.directory_table_base == values["dtd"]
+
+    def test_parser_memory(self):
+        parser = kdmp_parser.KernelDumpParser(self.minidump_files[0])
+        assert parser.directory_table_base == 0x6D4000
+
+        page = parser.read_physical_page(0x5000)
+        assert page is not None
+        assert page[0x34:0x38] == b"MSFT"
+
+        assert parser.translate_virtual(0xFFFFF78000000000) == 0x0000000000C2F000
+        assert parser.translate_virtual(0xFFFFF80513370000) == 0x000000003D555000
+
+        assert parser.read_virtual_page(
+            0xFFFFF78000000000
+        ) == parser.read_physical_page(0x0000000000C2F000)
+        assert parser.read_virtual_page(
+            0xFFFFF80513370000
+        ) == parser.read_physical_page(0x000000003D555000)
+
+    def test_parser_page_iterator(self):
+        parser = kdmp_parser.KernelDumpParser(self.minidump_files[0])
+
+        page_addresses = list(parser.pages.keys())
+        page_values = list(parser.pages.values())
+
+        assert len(parser.pages) > 0
+        assert len(page_addresses) == len(parser.pages)
+        assert len(page_addresses) == len(page_values)
+
+        addr = page_addresses[0]
+        page = parser.pages[addr]
+        assert parser.read_physical_page(addr) == page
+
+        for addr, page in parser.pages.items():
+            assert addr in page_addresses
+            assert page in page_values
+            assert len(page) == kdmp_parser.page.size
+            assert parser.read_physical_page(addr) == page
diff --git a/src/python/tests/test_version.py b/src/python/tests/test_version.py
new file mode 100644
index 0000000..1ef483a
--- /dev/null
+++ b/src/python/tests/test_version.py
@@ -0,0 +1,17 @@
+#
+# This file is part of kdmp-parser project
+#
+# Released under MIT License, by 0vercl0k - 2023
+#
+# With contributions from:
+# * masthoon - (github.com/masthoon)
+# * hugsy - (github.com/hugsy)
+#
+import kdmp_parser
+
+
+def test_version():
+    assert isinstance(kdmp_parser.version.major, int)
+    assert isinstance(kdmp_parser.version.minor, int)
+    assert isinstance(kdmp_parser.version.patch, int)
+    assert isinstance(kdmp_parser.version.release, str)
diff --git a/src/testapp/CMakeLists.txt b/src/testapp/CMakeLists.txt
deleted file mode 100644
index 84c534e..0000000
--- a/src/testapp/CMakeLists.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# Axel '0vercl0k' Souchet - April 18 2020
-link_libraries(kdmp-parser)
-add_executable(testapp testapp.cc)
diff --git a/src/testapp/testapp.cc b/src/testapp/testapp.cc
deleted file mode 100644
index eb23ba1..0000000
--- a/src/testapp/testapp.cc
+++ /dev/null
@@ -1,170 +0,0 @@
-// Axel '0vercl0k' Souchet - February 15 2019
-#include "kdmp-parser.h"
-#include <cstring>
-
-int main(int argc, const char *argv[]) {
-  if (argc != 2) {
-    printf("test.exe <kdump path>\n");
-    return EXIT_FAILURE;
-  }
-
-  kdmpparser::KernelDumpParser Dmp;
-  if (!Dmp.Parse(argv[1])) {
-    return EXIT_FAILURE;
-  }
-
-  //
-  // kd> r
-  // rax=0000000000000003 rbx=fffff8050f4e9f70 rcx=0000000000000001
-  // rdx=fffff805135684d0 rsi=0000000000000100 rdi=fffff8050f4e9f80
-  // rip=fffff805108776a0 rsp=fffff805135684f8 rbp=fffff80513568600
-  // r8=0000000000000003  r9=fffff805135684b8 r10=0000000000000000
-  // r11=ffffa8848825e000 r12=fffff8050f4e9f80 r13=fffff80510c3c958
-  // r14=0000000000000000 r15=0000000000000052
-  // iopl=0         nv up ei pl nz na pe nc
-  // cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b efl=00040202
-  //
-
-  const kdmpparser::CONTEXT *C = Dmp.GetContext();
-  if (C->Rax != 0x0000000000000003ULL) {
-    printf("Rax(0x%016" PRIx64 ") does not match with 0x0000000000000003.",
-           C->Rax);
-    return EXIT_FAILURE;
-  }
-
-  if (C->Rbx != 0xfffff8050f4e9f70ULL) {
-    printf("Rbx(0x%016" PRIx64 ") does not match with 0xfffff8050f4e9f70.",
-           C->Rbx);
-    return EXIT_FAILURE;
-  }
-
-  if (C->Rcx != 0x0000000000000001ULL) {
-    printf("Rcx(0x%016" PRIx64 ") does not match with 0x0000000000000001.",
-           C->Rcx);
-    return EXIT_FAILURE;
-  }
-
-  if (C->Rdx != 0xfffff805135684d0ULL) {
-    printf("Rdx(0x%016" PRIx64 ") does not match with 0xfffff805135684d0.",
-           C->Rdx);
-    return EXIT_FAILURE;
-  }
-
-  if (C->Rsi != 0x0000000000000100ULL) {
-    printf("Rsi(0x%016" PRIx64 ") does not match with 0x0000000000000100.",
-           C->Rsi);
-    return EXIT_FAILURE;
-  }
-
-  if (C->Rdi != 0xfffff8050f4e9f80ULL) {
-    printf("Rdi(0x%016" PRIx64 ") does not match with 0xfffff8050f4e9f80.",
-           C->Rdi);
-    return EXIT_FAILURE;
-  }
-
-  if (C->Rip != 0xfffff805108776a0ULL) {
-    printf("Rip(0x%016" PRIx64 ") does not match with 0xfffff805108776a0.",
-           C->Rip);
-    return EXIT_FAILURE;
-  }
-
-  if (C->Rsp != 0xfffff805135684f8ULL) {
-    printf("Rsp(0x%016" PRIx64 ") does not match with 0xfffff805135684f8.",
-           C->Rsp);
-    return EXIT_FAILURE;
-  }
-
-  if (C->Rbp != 0xfffff80513568600ULL) {
-    printf("Rbp(0x%016" PRIx64 ") does not match with 0xfffff80513568600.",
-           C->Rbp);
-    return EXIT_FAILURE;
-  }
-
-  if (C->R8 != 0x0000000000000003ULL) {
-    printf("R8(0x%016" PRIx64 ") does not match with 0x0000000000000003.",
-           C->R8);
-    return EXIT_FAILURE;
-  }
-
-  if (C->R9 != 0xfffff805135684b8ULL) {
-    printf("R9(0x%016" PRIx64 ") does not match with 0xfffff805135684b8.",
-           C->R9);
-    return EXIT_FAILURE;
-  }
-
-  if (C->R10 != 0x0000000000000000ULL) {
-    printf("R10(0x%016" PRIx64 ") does not match with 0x0000000000000000.",
-           C->R10);
-    return EXIT_FAILURE;
-  }
-
-  if (C->R11 != 0xffffa8848825e000ULL) {
-    printf("R11(0x%016" PRIx64 ") does not match with 0xffffa8848825e000.",
-           C->R11);
-    return EXIT_FAILURE;
-  }
-
-  if (C->R12 != 0xfffff8050f4e9f80ULL) {
-    printf("R12(0x%016" PRIx64 ") does not match with 0xfffff8050f4e9f80.",
-           C->R12);
-    return EXIT_FAILURE;
-  }
-
-  if (C->R13 != 0xfffff80510c3c958ULL) {
-    printf("R13(0x%016" PRIx64 ") does not match with 0xfffff80510c3c958.",
-           C->R13);
-    return EXIT_FAILURE;
-  }
-
-  if (C->R14 != 0x0000000000000000ULL) {
-    printf("R14(0x%016" PRIx64 ") does not match with 0x0000000000000000.",
-           C->R14);
-    return EXIT_FAILURE;
-  }
-
-  if (C->R15 != 0x0000000000000052ULL) {
-    printf("R15(0x%016" PRIx64 ") does not match with 0x0000000000000052.",
-           C->R15);
-    return EXIT_FAILURE;
-  }
-
-  printf("GPRs matches the testdatas.\n");
-
-  const kdmpparser::DumpType_t Type = Dmp.GetDumpType();
-  const auto &Physmem = Dmp.GetPhysmem();
-  if (Type == kdmpparser::DumpType_t::BMPDump) {
-    if (Physmem.size() != 0x544b) {
-      printf("0x544b pages are expected but found %zd.\n", Physmem.size());
-      return EXIT_FAILURE;
-    }
-  } else if (Type == kdmpparser::DumpType_t::FullDump) {
-    if (Physmem.size() != 0x3fbe6) {
-      printf("0x3fbe6 pages are expected but found %zd.\n", Physmem.size());
-      return EXIT_FAILURE;
-    }
-  } else {
-    printf("Unknown dump.\n");
-    return EXIT_FAILURE;
-  }
-
-  const uint64_t Address = 0x6d4d22;
-  const uint64_t AddressAligned = Address & 0xfffffffffffff000;
-  const uint64_t AddressOffset = Address & 0xfff;
-  const uint8_t ExpectedContent[] = {0x6d, 0x00, 0x00, 0x00, 0x00, 0x0a,
-                                     0x63, 0x88, 0x75, 0x00, 0x00, 0x00,
-                                     0x00, 0x0a, 0x63, 0x98};
-  const uint8_t *Page = Dmp.GetPhysicalPage(AddressAligned);
-  if (Page == nullptr) {
-    printf("GetPhysicalPage failed for %p\n", (void *)Page);
-    return EXIT_FAILURE;
-  }
-
-  if (memcmp(Page + AddressOffset, ExpectedContent, sizeof(ExpectedContent)) !=
-      0) {
-    printf("Physical memory is broken.\n");
-    return EXIT_FAILURE;
-  }
-
-  printf("Physical memory page matches the testdatas.\n");
-  return EXIT_SUCCESS;
-}
\ No newline at end of file
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 8289ba9..2613707 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,11 +1,29 @@
-# Axel '0vercl0k' Souchet - July 25 2020
-find_package(Python3 COMPONENTS Interpreter)
-if(NOT Python3_Interpreter_FOUND)
-    message(FATAL_ERROR "Python3 interpreter could not be located")
-endif()
+# Axel '0vercl0k' Souchet - April 18 2020
+enable_testing()
 
-add_custom_target(
-    NativeTests ALL
-    COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests.py --bindir ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} --pymodule ${PYMODULE}
-    DEPENDS testapp ${PYMODULE}
-)
\ No newline at end of file
+#
+# Build and link Catch2 library dependency automatically if not already found
+#
+include(FetchContent)
+FetchContent_Declare(
+    Catch2
+    URL https://github.com/catchorg/Catch2/archive/refs/tags/v3.4.0.zip
+    URL_HASH MD5=c426e77d4ee0055410bc930182959ae5
+)
+FetchContent_MakeAvailable(Catch2)
+
+#
+# Build all test files
+#
+list(APPEND TEST_SOURCE_FILES
+    tests_version.cc
+    tests_parser.cc
+)
+
+foreach(SOURCE_FILE ${TEST_SOURCE_FILES})
+    cmake_path(GET SOURCE_FILE STEM TEST_EXECUTABLE_NAME)
+    add_executable(${TEST_EXECUTABLE_NAME} ${SOURCE_FILE})
+    target_link_libraries(${TEST_EXECUTABLE_NAME} PUBLIC kdmp-parser Catch2::Catch2WithMain)
+    add_test(NAME ${TEST_EXECUTABLE_NAME} COMMAND $<TARGET_FILE:${TEST_EXECUTABLE_NAME}>)
+    install(FILES $<TARGET_FILE:${TEST_EXECUTABLE_NAME}> DESTINATION tests)
+endforeach()
diff --git a/src/tests/tests.py b/src/tests/tests.py
deleted file mode 100644
index c1c7be1..0000000
--- a/src/tests/tests.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# Axel '0vercl0k' Souchet - July 25 2020
-import urllib.request
-import os
-import sys
-import zipfile
-import subprocess
-import itertools
-import platform
-import argparse
-
-testdatas_url = 'https://github.com/0vercl0k/kdmp-parser/releases/download/v0.1/testdatas.zip'
-
-def test(bin_dir, dmp_path):
-    cmd = (
-        os.path.join(bin_dir, 'testapp'),
-        dmp_path
-    )
-
-    print('Launching "{0}"..'.format(' '.join(cmd)))
-    return subprocess.call(cmd)
-
-def test_python(script_dir, bin_dir, dmp_path, pymodule):
-    py_exe = sys.executable
-    if pymodule == 'kdmp_d':
-        if py_exe.endswith('3.exe'):
-            py_exe = py_exe.replace('3.exe', '_d.exe')
-        else:
-            py_exe = py_exe.replace('.exe', '_d.exe')
-    cmd = (
-        py_exe,
-        os.path.join(script_dir, 'tests_bindings.py'),
-        os.path.abspath(bin_dir),
-        dmp_path
-    )
-
-    print('Launching "{0}"..'.format(' '.join(cmd)))
-    return subprocess.call(cmd)
-
-def main():
-    parser = argparse.ArgumentParser('Run test')
-    parser.add_argument('--bindir', required = True)
-    parser.add_argument('--pymodule', required = True, nargs = '?')
-    args = parser.parse_args()
-
-    script_dir = os.path.dirname(__file__)
-    if not os.path.isfile(os.path.join(script_dir, 'full.dmp')):
-        # Download the test datas off github.
-        print(f'Downloading {testdatas_url}..')
-        archive_path, _ = urllib.request.urlretrieve(testdatas_url)
-        print(f'Successfully downloaded the test datas in {archive_path}, extracting..')
-
-        # Unzip its content in the source directory so that we don't download the files
-        # for every targets we are building.
-        zipfile.ZipFile(archive_path).extractall(script_dir)
-
-        # Once we have extracted the archive content, we can delete it.
-        os.remove(archive_path)
-
-    # Build full path for both the full / bitmap dumps.
-    full = os.path.join(script_dir, 'full.dmp')
-    bmp = os.path.join(script_dir, 'bmp.dmp')
-    dmp_paths = (full, bmp)
-
-    # Now iterate through all the configurations and run every flavor of test.exe against
-    # both dumps.
-    for dmp_path in dmp_paths:
-        if test(args.bindir, dmp_path) != 0:
-            print(f'{args.bindir}/{dmp_path} test failed, bailing.')
-            return 1
-    
-        # Run python bindings tests
-        if args.pymodule:
-            if test_python(script_dir, args.bindir, dmp_path, args.pymodule) != 0:
-                print(f'{args.bindir}/{dmp_path} python test failed, bailing.')
-                return 1
-
-    print('All good!')
-    return 0
-
-if __name__ == '__main__':
-    sys.exit(main())
\ No newline at end of file
diff --git a/src/tests/tests_bindings.py b/src/tests/tests_bindings.py
deleted file mode 100644
index 8d65443..0000000
--- a/src/tests/tests_bindings.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import sys
-sys.path.append(sys.argv[1])
-
-from kdmp import Dump, FullDump, BMPDump
-
-dmp = Dump(sys.argv[2])
-assert(dmp.type() == FullDump or dmp.type() == BMPDump)
-
-ctx = dmp.context()
-dtb = ctx['dtb'] & ~0xfff # remove PCID
-
-assert(ctx['rip'] == 0xfffff805108776a0)
-assert(dtb == 0x6d4000)
-
-page = dmp.get_physical_page(0x5000)
-assert(page[0x34:0x38] == b'MSFT')
-
-assert(dmp.virt_translate(0xfffff78000000000) == 0x0000000000c2f000)
-assert(dmp.virt_translate(0xfffff80513370000) == 0x000000003d555000)
-
-assert(dmp.get_virtual_page(0xfffff78000000000) == dmp.get_physical_page(0x0000000000c2f000))
-assert(dmp.get_virtual_page(0xfffff80513370000) == dmp.get_physical_page(0x000000003d555000))
-
-v = 0xfffff80513568000
-assert(dmp.get_virtual_page(v) == dmp.get_physical_page(dmp.virt_translate(v)))
-
-print("Python tests: All good!")
-sys.exit(0)
\ No newline at end of file
diff --git a/src/tests/tests_parser.cc b/src/tests/tests_parser.cc
new file mode 100644
index 0000000..e608e4c
--- /dev/null
+++ b/src/tests/tests_parser.cc
@@ -0,0 +1,95 @@
+// Axel '0vercl0k' Souchet - 2023
+#define CATCH_CONFIG_MAIN
+
+#include "kdmp-parser.h"
+#include <array>
+#include <catch2/catch_test_macros.hpp>
+#include <cstring>
+#include <filesystem>
+
+const std::array g_TestDataFiles{
+    "full.dmp",
+    "bmp.dmp",
+};
+
+TEST_CASE("kdmp-parser", "parser") {
+  SECTION("Test minidump exists") {
+    for (const auto &File : g_TestDataFiles) {
+      REQUIRE(std::filesystem::exists(File));
+    }
+  }
+
+  SECTION("Basic parsing") {
+    for (const auto &File : g_TestDataFiles) {
+      kdmpparser::KernelDumpParser Dmp;
+      REQUIRE(Dmp.Parse(File));
+
+      const auto Type = Dmp.GetDumpType();
+      const auto &Physmem = Dmp.GetPhysmem();
+      const auto &Path = Dmp.GetDumpPath();
+      if (Path.filename() == "bmp.dmp") {
+        CHECK(Type == kdmpparser::DumpType_t::BMPDump);
+        CHECK(Physmem.size() == 0x54'4b);
+      } else if (Path.filename() == "full.dmp") {
+        CHECK(Type == kdmpparser::DumpType_t::FullDump);
+        CHECK(Physmem.size() == 0x03'fb'e6);
+      } else {
+        CHECK(false);
+      }
+    }
+  }
+
+  //
+  // kd> r
+  // rax=0000000000000003 rbx=fffff8050f4e9f70 rcx=0000000000000001
+  // rdx=fffff805135684d0 rsi=0000000000000100 rdi=fffff8050f4e9f80
+  // rip=fffff805108776a0 rsp=fffff805135684f8 rbp=fffff80513568600
+  // r8=0000000000000003  r9=fffff805135684b8 r10=0000000000000000
+  // r11=ffffa8848825e000 r12=fffff8050f4e9f80 r13=fffff80510c3c958
+  // r14=0000000000000000 r15=0000000000000052
+  // iopl=0         nv up ei pl nz na pe nc
+  // cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b efl=00040202
+  //
+
+  SECTION("Context values") {
+    for (const auto &File : g_TestDataFiles) {
+      kdmpparser::KernelDumpParser Dmp;
+      REQUIRE(Dmp.Parse(File));
+      const auto &Context = Dmp.GetContext();
+      CHECK(Context.Rax == 0x00000000'00000003ULL);
+      CHECK(Context.Rbx == 0xfffff805'0f4e9f70ULL);
+      CHECK(Context.Rcx == 0x00000000'00000001ULL);
+      CHECK(Context.Rdx == 0xfffff805'135684d0ULL);
+      CHECK(Context.Rsi == 0x00000000'00000100ULL);
+      CHECK(Context.Rdi == 0xfffff805'0f4e9f80ULL);
+      CHECK(Context.Rip == 0xfffff805'108776a0ULL);
+      CHECK(Context.Rsp == 0xfffff805'135684f8ULL);
+      CHECK(Context.Rbp == 0xfffff805'13568600ULL);
+      CHECK(Context.R8 == 0x00000000'00000003ULL);
+      CHECK(Context.R9 == 0xfffff805'135684b8ULL);
+      CHECK(Context.R10 == 0x00000000'00000000ULL);
+      CHECK(Context.R11 == 0xffffa884'8825e000ULL);
+      CHECK(Context.R12 == 0xfffff805'0f4e9f80ULL);
+      CHECK(Context.R13 == 0xfffff805'10c3c958ULL);
+      CHECK(Context.R14 == 0x00000000'00000000ULL);
+      CHECK(Context.R15 == 0x00000000'00000052ULL);
+    }
+  }
+
+  SECTION("Memory access") {
+    for (const auto &File : g_TestDataFiles) {
+      kdmpparser::KernelDumpParser Dmp;
+      REQUIRE(Dmp.Parse(File));
+      const uint64_t Address = 0x6d'4d'22;
+      const uint64_t AddressAligned = Address & 0xffffffff'fffff000;
+      const uint64_t AddressOffset = Address & 0xfff;
+      const uint8_t ExpectedContent[] = {0x6d, 0x00, 0x00, 0x00, 0x00, 0x0a,
+                                         0x63, 0x88, 0x75, 0x00, 0x00, 0x00,
+                                         0x00, 0x0a, 0x63, 0x98};
+      const uint8_t *Page = Dmp.GetPhysicalPage(AddressAligned);
+      CHECK(Page != nullptr);
+      CHECK(memcmp(Page + AddressOffset, ExpectedContent,
+                   sizeof(ExpectedContent)) == 0);
+    }
+  }
+}
diff --git a/src/tests/tests_version.cc b/src/tests/tests_version.cc
new file mode 100644
index 0000000..30e8802
--- /dev/null
+++ b/src/tests/tests_version.cc
@@ -0,0 +1,13 @@
+// Axel '0vercl0k' Souchet - 2023
+#define CATCH_CONFIG_MAIN
+
+#include <catch2/catch_test_macros.hpp>
+
+#include "kdmp-parser.h"
+
+TEST_CASE("kdmp-parser", "version") {
+  CHECK(kdmpparser::Version_t::Major == KDMPPARSER_VERSION_MAJOR);
+  CHECK(kdmpparser::Version_t::Minor == KDMPPARSER_VERSION_MINOR);
+  CHECK(kdmpparser::Version_t::Patch == KDMPPARSER_VERSION_PATCH);
+  CHECK(kdmpparser::Version_t::Release == KDMPPARSER_VERSION_RELEASE);
+}