Merge branch 'unicorn-upgrade-2' into unhandled-exceptions

This commit is contained in:
Maurice Heumann
2025-07-13 20:47:01 +02:00
245 changed files with 26970 additions and 2348 deletions

2
.clang-format-ignore Normal file
View File

@@ -0,0 +1,2 @@
**/*.hxx
deps/**/*

View File

@@ -4,9 +4,28 @@ updates:
directory: "/"
schedule:
interval: monthly
open-pull-requests-limit: 10
open-pull-requests-limit: 100
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
open-pull-requests-limit: 100
- package-ecosystem: "npm"
directory: "/page"
schedule:
interval: monthly
open-pull-requests-limit: 100
- package-ecosystem: "npm"
directory: "/mcp"
schedule:
interval: monthly
open-pull-requests-limit: 100
- package-ecosystem: "cargo"
directory: "/src/backends/icicle-emulator/icicle-bridge/"
schedule:
interval: monthly
open-pull-requests-limit: 100

View File

@@ -2,7 +2,7 @@ name: Build
on:
schedule:
- cron: '0 0 * * *'
- cron: "0 0 * * *"
push:
branches:
- "**"
@@ -21,39 +21,43 @@ on:
- "true"
- "false"
permissions:
contents: read
actions: write
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
clang-tidy:
name: Run Clang Tidy
runs-on: ubuntu-24.04
env:
LLVM_VERSION: 20
steps:
- name: Checkout Source
uses: actions/checkout@v4
with:
submodules: recursive
name: Run Clang Tidy
runs-on: ubuntu-24.04
env:
LLVM_VERSION: 20
steps:
- name: Checkout Source
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Ninja
uses: seanmiddleditch/gha-setup-ninja@v6
- name: Install Ninja
uses: seanmiddleditch/gha-setup-ninja@v6
- name: Install Clang
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh ${{ env.LLVM_VERSION }}
sudo apt install -y clang-tidy-${{ env.LLVM_VERSION }}
sudo apt install -y clang-${{ env.LLVM_VERSION }} lld-${{ env.LLVM_VERSION }}
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-${{ env.LLVM_VERSION }} 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-${{ env.LLVM_VERSION }} 100
sudo update-alternatives --set cc /usr/bin/clang-${{ env.LLVM_VERSION }}
sudo update-alternatives --set c++ /usr/bin/clang++-${{ env.LLVM_VERSION }}
- name: Install Clang
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh ${{ env.LLVM_VERSION }}
sudo apt install -y clang-tidy-${{ env.LLVM_VERSION }}
sudo apt install -y clang-${{ env.LLVM_VERSION }} lld-${{ env.LLVM_VERSION }}
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-${{ env.LLVM_VERSION }} 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-${{ env.LLVM_VERSION }} 100
sudo update-alternatives --set cc /usr/bin/clang-${{ env.LLVM_VERSION }}
sudo update-alternatives --set c++ /usr/bin/clang++-${{ env.LLVM_VERSION }}
- name: CMake Build
run: cmake --preset=release -DMOMO_ENABLE_CLANG_TIDY=On && cmake --build --preset=release
- name: CMake Build
run: cmake --preset=release -DMOMO_ENABLE_CLANG_TIDY=On && cmake --build --preset=release
verify-formatting:
name: Verify Formatting
@@ -65,8 +69,10 @@ jobs:
- name: Verify Formatting
uses: jidicula/clang-format-action@v4.15.0
with:
clang-format-version: '20'
check-path: 'src'
clang-format-version: "20"
- name: Verify Page Formatting
run: cd page && npx --yes prettier . --check
build-apiset-dumper:
name: Build API Set Dumper
@@ -92,6 +98,7 @@ jobs:
name: Temp API Set Dumper
working-directory: build/release/artifacts/
path: "*"
retention-days: 1
create-emulation-root:
name: Create Emulation Root
@@ -103,14 +110,14 @@ jobs:
platform:
- Windows 2025
- Windows 2022
- Windows 2019
#- Windows 2019
include:
- platform: Windows 2025
runner: windows-2025
- platform: Windows 2022
runner: windows-2022
- platform: Windows 2019
runner: windows-2019
#- platform: Windows 2019
# runner: windows-2019
steps:
- name: Checkout Source
uses: actions/checkout@v4
@@ -119,7 +126,7 @@ jobs:
run: curl --connect-timeout 20 --max-time 200 --retry 5 --retry-delay 2 --retry-max-time 200 -L -o directx_Jun2010_redist.exe https://download.microsoft.com/download/8/4/A/84A35BF1-DAFE-4AE8-82AF-AD2AE20B6B14/directx_Jun2010_redist.exe
- name: Extract DirectX Runtime
run: "cmd /c \"start /wait directx_Jun2010_redist.exe /Q /T:${{github.workspace}}/dxrt\""
run: 'cmd /c "start /wait directx_Jun2010_redist.exe /Q /T:${{github.workspace}}/dxrt"'
- name: Install DirectX Runtime
run: "cmd /c \"start /wait .\\dxrt\\dxsetup.exe /silent\""
@@ -151,7 +158,9 @@ jobs:
fail-fast: false
matrix:
platform:
- Windows x86
- Windows x86_64
- MinGW x86_64
- Linux x86_64 GCC
- Linux x86_64 GCC Sanitizer
- Linux x86_64 Clang
@@ -160,16 +169,27 @@ jobs:
- iOS arm64
- Android x86_64
- Android arm64-v8a
- Emscripten Web
- Emscripten Web Memory 64
- Emscripten Node.js
configuration:
- Debug
#- Debug
- Release
include:
- configuration: Debug
preset: debug
#- configuration: Debug
# preset: debug
- configuration: Release
preset: release
- platform: Windows x86
runner: windows-latest
devcmd_arch: x86
- platform: Windows x86_64
runner: windows-latest
devcmd_arch: x64
- platform: MinGW x86_64
runner: ubuntu-24.04
rust-target: x86_64-pc-windows-gnu
cmake-options: "-DCMAKE_TOOLCHAIN_FILE=$GITHUB_WORKSPACE/cmake/toolchain/mingw-w64.cmake"
- platform: Linux x86_64 GCC Sanitizer
runner: ubuntu-24.04
cmake-options: "-DMOMO_ENABLE_SANITIZER=On"
@@ -196,6 +216,15 @@ jobs:
abi: arm64-v8a
rust-target: aarch64-linux-android
cmake-options: "-DCMAKE_TOOLCHAIN_FILE=$GITHUB_WORKSPACE/cmake/toolchain/android-ndk.cmake"
- platform: Emscripten Web
runner: ubuntu-24.04
cmake-options: "-DMOMO_ENABLE_RUST_CODE=Off -DCMAKE_TOOLCHAIN_FILE=$(dirname $(which emcc))/cmake/Modules/Platform/Emscripten.cmake"
- platform: Emscripten Web Memory 64
runner: ubuntu-24.04
cmake-options: "-DMOMO_ENABLE_RUST_CODE=Off -DMOMO_EMSCRIPTEN_MEMORY64=On -DCMAKE_TOOLCHAIN_FILE=$(dirname $(which emcc))/cmake/Modules/Platform/Emscripten.cmake"
- platform: Emscripten Node.js
runner: ubuntu-24.04
cmake-options: "-DMOMO_EMSCRIPTEN_SUPPORT_NODEJS=On -DMOMO_ENABLE_RUST_CODE=Off -DCMAKE_TOOLCHAIN_FILE=$(dirname $(which emcc))/cmake/Modules/Platform/Emscripten.cmake"
steps:
- name: Checkout Source
uses: actions/checkout@v4
@@ -209,6 +238,10 @@ jobs:
if: "${{ matrix.rust-target }}"
run: rustup target add ${{ matrix.rust-target }}
- name: Install Emscripten
if: "${{ startsWith(matrix.platform, 'Emscripten') }}"
uses: mymindstorm/setup-emsdk@v14
- name: Install Clang
if: "${{ matrix.platform == 'Linux x86_64 Clang' }}"
run: |
@@ -221,9 +254,17 @@ jobs:
sudo update-alternatives --set cc /usr/bin/clang-${{ matrix.clang-version }}
sudo update-alternatives --set c++ /usr/bin/clang++-${{ matrix.clang-version }}
- name: Set up MinGW
uses: egor-tensin/setup-mingw@v2
if: "${{ startsWith(matrix.platform, 'MinGW') }}"
with:
platform: x64
- name: Enable Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1.13.0
if: ${{ startsWith(matrix.platform, 'Windows') }}
with:
arch: ${{ matrix.devcmd_arch }}
- uses: nttld/setup-ndk@v1
id: setup-ndk
@@ -232,7 +273,8 @@ jobs:
ndk-version: r26d
add-to-path: false
- name: Setup Environment Variables
- name: Setup Android Environment Variables
shell: bash
if: ${{ startsWith(matrix.platform, 'Android') }}
run: |
echo "ANDROID_NDK_ROOT=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
@@ -247,12 +289,69 @@ jobs:
name: ${{ matrix.platform }} ${{matrix.configuration}} Artifacts
working-directory: build/${{matrix.preset}}/artifacts/
path: "*"
retention-days: 1
- name: Upload Test Configuration
uses: actions/upload-artifact@v4.6.2
with:
name: Temp ${{ matrix.platform }} ${{matrix.configuration}} Test Config
path: "build/${{matrix.preset}}/**/CTestTestfile.cmake"
retention-days: 1
# Release is the same as Release from build step
# However, that way the win tests can start without
# waiting for other platforms
build-isolate:
name: Build Isolate
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
platform:
- Windows x86_64
configuration:
- Debug
- Release
include:
- configuration: Debug
preset: debug
- configuration: Release
preset: release
- platform: Windows x86_64
runner: windows-latest
devcmd_arch: x64
steps:
- name: Checkout Source
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Ninja
uses: seanmiddleditch/gha-setup-ninja@v6
- name: Enable Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1.13.0
if: ${{ startsWith(matrix.platform, 'Windows') }}
with:
arch: ${{ matrix.devcmd_arch }}
- name: CMake Build
run: cmake --preset=${{matrix.preset}} ${{matrix.cmake-options}} && cmake --build --preset=${{matrix.preset}}
- name: Upload Artifacts
uses: pyTooling/upload-artifact@v4
with:
name: Temp Isolate ${{ matrix.platform }} ${{matrix.configuration}} Artifacts
working-directory: build/${{matrix.preset}}/artifacts/
path: "*"
retention-days: 1
- name: Upload Test Configuration
uses: actions/upload-artifact@v4.6.2
with:
name: Temp Isolate ${{ matrix.platform }} ${{matrix.configuration}} Test Config
path: "build/${{matrix.preset}}/**/CTestTestfile.cmake"
retention-days: 1
test:
name: Test
@@ -262,6 +361,7 @@ jobs:
fail-fast: false
matrix:
platform:
- Windows x86
- Windows x86_64
- Linux x86_64 GCC
- Linux x86_64 GCC Sanitizer
@@ -276,13 +376,15 @@ jobs:
- Windows 2022
#- Windows 2019
configuration:
- Debug
#- Debug
- Release
include:
- configuration: Debug
preset: debug
#- configuration: Debug
# preset: debug
- configuration: Release
preset: release
- platform: Windows x86
runner: windows-latest
- platform: Windows x86_64
runner: windows-latest
- platform: Linux x86_64 GCC
@@ -302,13 +404,14 @@ jobs:
submodules: recursive
- name: Setup Environment Variables
shell: bash
run: |
echo "RUST_BACKTRACE=1" >> $GITHUB_ENV
echo "ASAN_OPTIONS=detect_odr_violation=0" >> $GITHUB_ENV
echo "EMULATOR_ICICLE=${{ matrix.emulator == 'Icicle' }}" >> $GITHUB_ENV
- name: Download Test Configuration
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: Temp ${{ matrix.platform }} ${{matrix.configuration}} Test Config
path: build/${{matrix.preset}}
@@ -325,7 +428,7 @@ jobs:
with:
name: Windows x86_64 Release Artifacts
path: build/${{matrix.preset}}/artifacts
- name: Download Emulation Root
uses: pyTooling/download-artifact@v4
with:
@@ -336,6 +439,7 @@ jobs:
run: cp build/${{matrix.preset}}/artifacts/test-sample.exe build/${{matrix.preset}}/artifacts/root/filesys/c/
- name: CMake Test
if: ${{ matrix.emulator != 'Icicle' || matrix.platform != 'Windows x86' }}
run: cd build/${{matrix.preset}} && ctest --verbose -j
env:
EMULATOR_ROOT: ${{github.workspace}}/build/${{matrix.preset}}/artifacts/root
@@ -345,17 +449,25 @@ jobs:
win-test:
name: Windows Test
runs-on: windows-latest
needs: [create-emulation-root, build]
needs: [create-emulation-root, build-isolate]
strategy:
fail-fast: false
matrix:
configuration:
- Debug
- Release
emulator:
- Unicorn
- Icicle
emulation-root:
- Windows 2025
- Windows 2022
- Windows 2019
#- Windows 2019
include:
- configuration: Debug
preset: debug
- configuration: Release
preset: release
steps:
- name: Checkout Source
uses: actions/checkout@v4
@@ -363,18 +475,52 @@ jobs:
submodules: recursive
- name: Setup Environment Variables
shell: bash
run: |
echo "RUST_BACKTRACE=1" >> $GITHUB_ENV
echo "ASAN_OPTIONS=detect_odr_violation=0" >> $GITHUB_ENV
echo "EMULATOR_ICICLE=${{ matrix.emulator == 'Icicle' }}" >> $GITHUB_ENV
- name: Download Test Configuration
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: Temp Windows x86_64 Release Test Config
path: build/release
name: Temp Isolate Windows x86_64 ${{ matrix.configuration}} Test Config
path: build/${{ matrix.preset }}
- name: Download Artifacts
uses: pyTooling/download-artifact@v4
with:
name: Temp Isolate Windows x86_64 ${{ matrix.configuration}} Artifacts
path: build/${{ matrix.preset }}/artifacts
- name: Download Emulation Root
uses: pyTooling/download-artifact@v4
with:
name: ${{ matrix.emulation-root }} Emulation Root
path: build/${{ matrix.preset }}/artifacts/root
- name: Copy Test Sample
run: cp build/${{ matrix.preset }}/artifacts/test-sample.exe build/${{ matrix.preset }}/artifacts/root/filesys/c/
- name: CMake Test
run: cd build/${{ matrix.preset }} && ctest --verbose -j
env:
EMULATOR_ROOT: ${{github.workspace}}/build/${{ matrix.preset }}/artifacts/root
EMULATOR_VERBOSE: ${{ github.event.inputs.verbose }}
ANALYSIS_SAMPLE: ${{github.workspace}}/build/${{ matrix.preset }}/artifacts/test-sample.exe
smoke-test-node:
name: Smoke Test Node.js
runs-on: ubuntu-24.04
needs: [create-emulation-root, build]
steps:
- name: Download Artifacts
uses: pyTooling/download-artifact@v4
with:
name: Emscripten Node.js Release Artifacts
path: build/release/artifacts
- name: Download Windows Artifacts
uses: pyTooling/download-artifact@v4
with:
name: Windows x86_64 Release Artifacts
@@ -383,19 +529,43 @@ jobs:
- name: Download Emulation Root
uses: pyTooling/download-artifact@v4
with:
name: ${{ matrix.emulation-root }} Emulation Root
name: Windows 2022 Emulation Root
path: build/release/artifacts/root
- name: Copy Test Sample
run: cp build/release/artifacts/test-sample.exe build/release/artifacts/root/filesys/c/
- name: CMake Test
run: cd build/release && ctest --verbose -j
run: cd build/release/artifacts && node ./windows-emulator-test.js
env:
EMULATOR_ROOT: ${{github.workspace}}/build/release/artifacts/root
EMULATOR_VERBOSE: ${{ github.event.inputs.verbose }}
ANALYSIS_SAMPLE: ${{github.workspace}}/build/release/artifacts/test-sample.exe
smoke-test-mingw:
name: Smoke Test MinGW x86_64
runs-on: windows-latest
needs: [create-emulation-root, build]
steps:
- name: Download Artifacts
uses: pyTooling/download-artifact@v4
with:
name: MinGW x86_64 Release Artifacts
path: build/release/artifacts
- name: Download Emulation Root
uses: pyTooling/download-artifact@v4
with:
name: Windows 2022 Emulation Root
path: build/release/artifacts/root
- name: Copy Test Sample
run: cp build/release/artifacts/test-sample.exe build/release/artifacts/root/filesys/c/
- name: CMake Test
run: cd build/release/artifacts && ./windows-emulator-test.exe && ./analyzer.exe -e root c:/test-sample.exe
env:
EMULATOR_ROOT: ${{github.workspace}}/build/release/artifacts/root
EMULATOR_VERBOSE: ${{ github.event.inputs.verbose }}
smoke-test-android:
name: Smoke Test Android
@@ -415,11 +585,11 @@ jobs:
- Windows 2022
#- Windows 2019
configuration:
- Debug
#- Debug
- Release
include:
- configuration: Debug
preset: debug
#- configuration: Debug
# preset: debug
- configuration: Release
preset: release
- architecture: x86_64
@@ -445,7 +615,7 @@ jobs:
with:
name: Windows x86_64 Release Artifacts
path: build/${{matrix.preset}}/artifacts
- name: Download Emulation Root
uses: pyTooling/download-artifact@v4
with:
@@ -460,17 +630,120 @@ jobs:
with:
api-level: 29
arch: ${{matrix.architecture}}
script: "adb push build/${{matrix.preset}}/artifacts/* /data/local/tmp && adb shell \"cd /data/local/tmp && export LD_LIBRARY_PATH=. && chmod +x ./analyzer && EMULATOR_ICICLE=${{ matrix.emulator == 'Icicle' }} ./analyzer -e ./root c:/test-sample.exe\""
script: 'adb push build/${{matrix.preset}}/artifacts/* /data/local/tmp && adb shell "cd /data/local/tmp && export LD_LIBRARY_PATH=. && chmod +x ./analyzer && EMULATOR_ICICLE=${{ matrix.emulator == ''Icicle'' }} ./analyzer -e ./root c:/test-sample.exe"'
build-page:
name: Build Page
runs-on: ubuntu-latest
needs: [create-emulation-root, build]
steps:
- name: Checkout Source
uses: actions/checkout@v4
with:
submodules: recursive
- name: Download Emscripten Web Artifacts
uses: pyTooling/download-artifact@v4
with:
name: Emscripten Web Release Artifacts
path: build/release/artifacts/32
- name: Download Emscripten Web Memory 64 Artifacts
uses: pyTooling/download-artifact@v4
with:
name: Emscripten Web Memory 64 Release Artifacts
path: build/release/artifacts/64
- name: Download Windows Artifacts
uses: pyTooling/download-artifact@v4
with:
name: Windows x86_64 Release Artifacts
path: build/release/artifacts
- name: Download Emulation Root
uses: pyTooling/download-artifact@v4
with:
name: Windows 2022 Emulation Root
path: build/release/artifacts/root
- name: Copy Sample
run: cp ./build/release/artifacts/test-sample.exe build/release/artifacts/root/filesys/c/
- name: Create Emulation Root zip
run: cd ./build/release/artifacts && zip -r "${{github.workspace}}/page/public/root.zip" ./root
- name: Copy Files
run: |
mkdir -p ./page/public/32/
mkdir -p ./page/public/64/
cp ./build/release/artifacts/32/analyzer.js ./page/public/32/
cp ./build/release/artifacts/32/analyzer.wasm ./page/public/32/
cp ./build/release/artifacts/64/analyzer.js ./page/public/64/
cp ./build/release/artifacts/64/analyzer.wasm ./page/public/64/
- name: Build Page
run: cd ./page && npm i && npm run build
- name: Upload Page Artifacts
uses: pyTooling/upload-artifact@v4
with:
name: Page Artifacts
working-directory: page/dist/
path: "*"
retention-days: 1
deploy-page:
name: Deploy Page
runs-on: ubuntu-latest
needs: [build-page, summary]
if: github.repository_owner == 'momo5502' && github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Download Page Artifacts
uses: pyTooling/download-artifact@v4
with:
name: Page Artifacts
path: ./page/dist/
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./page/dist
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
summary:
name: Pipeline Summary
runs-on: ubuntu-24.04
needs: [clang-tidy, build-apiset-dumper, smoke-test-android, create-emulation-root, build, test, win-test, verify-formatting]
needs:
[
build-page,
clang-tidy,
build-apiset-dumper,
smoke-test-node,
smoke-test-mingw,
smoke-test-android,
create-emulation-root,
build,
build-isolate,
test,
win-test,
verify-formatting,
]
if: always()
steps:
- uses: geekyeggo/delete-artifact@v5
continue-on-error: true
with:
name: 'Temp *'
name: "Temp *"
- name: Pipeline failed
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}

14
.gitignore vendored
View File

@@ -146,4 +146,16 @@ UpgradeLog*.htm
# User scripts
user*.bat
.vscode/
.vscode/
.cache/
compile_commands.json
page/public/analyzer.js
page/public/analyzer.wasm
page/public/debugger.js
page/public/debugger.wasm
page/public/root.zip
page/public/32/analyzer.js
page/public/32/analyzer.wasm
page/public/64/analyzer.js
page/public/64/analyzer.wasm

18
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "deps/unicorn"]
path = deps/unicorn
url = ../unicorn.git
url = https://github.com/momo5502/unicorn.git
shallow = true
branch = dev
[submodule "deps/reflect"]
@@ -10,11 +10,27 @@
[submodule "deps/googletest"]
path = deps/googletest
url = https://github.com/google/googletest.git
shallow = true
[submodule "deps/zlib"]
path = deps/zlib
url = https://github.com/madler/zlib.git
branch = develop
ignore = dirty
shallow = true
[submodule "deps/gtest-parallel"]
path = deps/gtest-parallel
url = https://github.com/google/gtest-parallel.git
shallow = true
[submodule "deps/flatbuffers"]
path = deps/flatbuffers
url = https://github.com/google/flatbuffers.git
shallow = true
[submodule "deps/base64"]
path = deps/base64
url = https://github.com/tobiaslocker/base64.git
shallow = true
[submodule "deps/minidump_cpp"]
path = deps/minidump_cpp
url = https://github.com/redthing1/minidump_cpp
shallow = true

View File

@@ -6,6 +6,8 @@ option(MOMO_ENABLE_AVX2 "Enable AVX2 support" ON)
option(MOMO_ENABLE_SANITIZER "Enable sanitizer" OFF)
option(MOMO_ENABLE_CLANG_TIDY "Enable clang-tidy checks" OFF)
option(MOMO_ENABLE_RUST_CODE "Enable code parts written in rust" ON)
option(MOMO_EMSCRIPTEN_MEMORY64 "Enable memory 64 support for emscripten builds" OFF)
option(MOMO_EMSCRIPTEN_SUPPORT_NODEJS "Enable Node.js filesystem for emscripten compilation" OFF)
option(MOMO_BUILD_AS_LIBRARY "Configure and Build the emulator as a shared library (without the samples and tests)" OFF)
set(MOMO_REFLECTION_LEVEL "0" CACHE STRING "Reflection level for the build")
@@ -28,7 +30,6 @@ endif()
# It doesn't support it, even if it thinks it does...
set(ENV{ARCHFLAGS} "nope")
##########################################
project(emulator LANGUAGES C CXX)

View File

@@ -23,7 +23,7 @@
"build"
],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{

View File

@@ -1,19 +1,22 @@
<img src="./docs/images/cover.png" />
<h1 align="center">
Windows User Space Emulator
Sogen
<br>
<a href="https://github.com/momo5502/emulator?tab=GPL-2.0-1-ov-file"><img src="https://img.shields.io/github/license/momo5502/emulator?color=00B0F8"/></a>
<a href="https://github.com/momo5502/emulator/actions"><img src="https://img.shields.io/github/actions/workflow/status/momo5502/emulator/build.yml?branch=main&label=build"/></a>
<a href="https://github.com/momo5502/emulator/issues"><img src="https://img.shields.io/github/issues/momo5502/emulator?color=F8B000"/></a>
<img src="https://img.shields.io/github/commit-activity/m/momo5502/emulator?color=FF3131"/>
<a href="https://github.com/momo5502/sogen?tab=GPL-2.0-1-ov-file"><img src="https://img.shields.io/github/license/momo5502/sogen?color=00B0F8"/></a>
<a href="https://github.com/momo5502/sogen/actions"><img src="https://img.shields.io/github/actions/workflow/status/momo5502/sogen/build.yml?branch=main&label=build"/></a>
<a href="https://github.com/momo5502/sogen/issues"><img src="https://img.shields.io/github/issues/momo5502/sogen?color=F8B000"/></a>
<img src="https://img.shields.io/github/commit-activity/m/momo5502/sogen?color=FF3131"/>
<a href="https://deepwiki.com/momo5502/sogen"><img src="https://deepwiki.com/badge.svg"/></a>
</h1>
A high-performance Windows process emulator that operates at syscall level, providing full control over process execution through comprehensive hooking capabilities.
Sogen is a high-performance Windows user space emulator that operates at syscall level, providing full control over process execution through comprehensive hooking capabilities.
Perfect for security research, malware analysis, and DRM research where fine-grained control over process execution is required.
Built in C++ and powered by the [Unicorn Engine](https://github.com/unicorn-engine/unicorn) (or the [icicle-emu](https://github.com/icicle-emu/icicle-emu) 🆕).
Try it out: <a href="https://sogen.dev">sogen.dev</a>
## Key Features
* 🔄 __Syscall-Level Emulation__
@@ -48,12 +51,12 @@ Click <a href="https://docs.google.com/presentation/d/1pha4tFfDMpVzJ_ehJJ21SA_HA
## Quick Start (Windows + Visual Studio)
> [!TIP]
> Checkout the [Wiki](https://github.com/momo5502/emulator/wiki) for more details on how to build & run the emulator on Windows, Linux, macOS, ...
> Checkout the [Wiki](https://github.com/momo5502/sogen/wiki) for more details on how to build & run the emulator on Windows, Linux, macOS, ...
1\. Checkout the code:
```bash
git clone --recurse-submodules https://github.com/momo5502/emulator.git
git clone --recurse-submodules https://github.com/momo5502/sogen.git
```
2\. Run the following command in an x64 Development Command Prompt in the cloned directory:
@@ -64,7 +67,7 @@ cmake --preset=vs2022
3\. Build the solution that was generated at `build/vs2022/emulator.sln`
4\. Create a registry dump by running the [grab-registry.bat](https://github.com/momo5502/emulator/blob/main/src/tools/grab-registry.bat) as administrator and place it in the artifacts folder next to the `analyzer.exe`
4\. Create a registry dump by running the [grab-registry.bat](https://github.com/momo5502/sogen/blob/main/src/tools/grab-registry.bat) as administrator and place it in the artifacts folder next to the `analyzer.exe`
5\. Run the program of your choice:

View File

@@ -22,7 +22,12 @@ cmake_policy(SET CMP0069 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
##########################################
if(NOT MINGW AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
endif()
##########################################
@@ -34,7 +39,14 @@ endif()
##########################################
if(MOMO_ENABLE_RUST_CODE)
set(MOMO_ENABLE_RUST OFF)
if(MOMO_ENABLE_RUST_CODE AND NOT MINGW AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten")
set(MOMO_ENABLE_RUST ON)
endif()
##########################################
if(MOMO_ENABLE_RUST)
add_compile_definitions(MOMO_ENABLE_RUST_CODE=1)
else()
add_compile_definitions(MOMO_ENABLE_RUST_CODE=0)
@@ -51,6 +63,21 @@ endif()
##########################################
if(MINGW)
add_link_options(
-static-libstdc++
-static-libgcc
-static
-lwinpthread
)
momo_add_c_and_cxx_compile_options(
-Wno-array-bounds
)
endif()
##########################################
if(LINUX)
add_link_options(
-Wl,--no-undefined
@@ -83,6 +110,57 @@ endif()
##########################################
if(CMAKE_SYSTEM_NAME MATCHES "Emscripten")
momo_add_c_and_cxx_compile_options(
-fexceptions
-ftrivial-auto-var-init=zero
-Wno-dollar-in-identifier-extension
)
add_link_options(
-fexceptions
-sALLOW_MEMORY_GROWTH=1
$<$<CONFIG:Debug>:-sASSERTIONS>
-sWASM_BIGINT
-sUSE_OFFSET_CONVERTER
#-sEXCEPTION_CATCHING_ALLOWED=[..]
-sEXIT_RUNTIME
-sASYNCIFY
)
if(MOMO_EMSCRIPTEN_MEMORY64)
momo_add_c_and_cxx_compile_options(
-sMEMORY64
)
add_link_options(
-sMAXIMUM_MEMORY=8gb
-sMEMORY64
)
else()
add_link_options(
-sMAXIMUM_MEMORY=4gb
)
endif()
if(MOMO_EMSCRIPTEN_SUPPORT_NODEJS)
add_link_options(
-lnodefs.js -sNODERAWFS=1
-sENVIRONMENT=node
--pre-js ${CMAKE_CURRENT_LIST_DIR}/misc/node-pre-script.js
)
else()
add_link_options(
-lidbfs.js
-sENVIRONMENT=worker
-sINVOKE_RUN=0
-sEXPORTED_RUNTIME_METHODS=['callMain']
)
endif()
endif()
##########################################
if(MSVC)
string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
string(REPLACE "/EHs" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
@@ -103,10 +181,6 @@ if(MSVC)
/INCREMENTAL:NO
)
momo_add_c_and_cxx_release_compile_options(
/Ob2
)
add_compile_definitions(
_CRT_SECURE_NO_WARNINGS
_CRT_NONSTDC_NO_WARNINGS
@@ -157,24 +231,6 @@ endif()
##########################################
set(OPT_DEBUG "-O0 -g")
set(OPT_RELEASE "-O3 -g")
if(MSVC)
set(OPT_DEBUG "/Od /Ob0 /Zi")
set(OPT_RELEASE "/O2 /Ob2 /Zi")
add_link_options(/DEBUG)
endif()
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${OPT_DEBUG}")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${OPT_DEBUG}")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} ${OPT_RELEASE}")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${OPT_RELEASE}")
##########################################
if(CMAKE_GENERATOR MATCHES "Visual Studio")
momo_add_c_and_cxx_compile_options(/MP)
endif()

View File

@@ -0,0 +1,3 @@
Module["preRun"] = () => {
ENV = process.env;
};

View File

@@ -0,0 +1,40 @@
set(CMAKE_SYSTEM_NAME Emscripten)
set(CMAKE_SYSTEM_VERSION 1)
# Specify the cross-compilers
set(CMAKE_C_COMPILER emcc)
set(CMAKE_CXX_COMPILER em++)
# Set the Emscripten root directory
set(EMSCRIPTEN_ROOT_PATH $ENV{EMSDK}/upstream/emscripten)
# Set the Emscripten toolchain file
set(CMAKE_SYSROOT ${EMSCRIPTEN_ROOT_PATH}/system)
# Set the Emscripten include directories
set(CMAKE_FIND_ROOT_PATH ${EMSCRIPTEN_ROOT_PATH}/system/include)
# Set the Emscripten library directories
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# Set the Emscripten linker
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s USE_SDL=2 -s USE_SDL_MIXER=2")
# Set the Emscripten runtime
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --shell-file shell_minimal.html")
# Set the Emscripten optimization flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
# Set the Emscripten debug flags
set(CMAKE_BUILD_TYPE Release)
# Set the Emscripten output format
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -o <TARGET>.html")
# Set the Emscripten file extensions
set(CMAKE_EXECUTABLE_SUFFIX ".js")
# Set the Emscripten runtime options
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORTED_FUNCTIONS='[_main]' -s EXPORTED_RUNTIME_METHODS='[\"cwrap\"]'")

View File

@@ -0,0 +1,38 @@
# cross compile
set(CMAKE_SYSTEM_NAME Windows)
set(MINGW_C_COMPILER_NAME "x86_64-w64-mingw32-gcc")
set(MINGW_CXX_COMPILER_NAME "x86_64-w64-mingw32-g++")
set(MINGW_WINDRES_COMPILER_NAME "x86_64-w64-mingw32-windres")
find_file(MINGW_C_COMPILER ${MINGW_C_COMPILER_NAME})
find_file(MINGW_CXX_COMPILER ${MINGW_CXX_COMPILER_NAME})
find_file(MINGW_WINDRES_COMPILER ${MINGW_WINDRES_COMPILER_NAME})
if (${MINGW_C_COMPILER} STREQUAL "MINGW_C_COMPILER-NOTFOUND")
message(FATAL_ERROR "mingw-w64 compiler not found: ${MINGW_C_COMPILER_NAME}")
endif()
if (${MINGW_CXX_COMPILER} STREQUAL "MINGW_CXX_COMPILER-NOTFOUND")
message(FATAL_ERROR "mingw-w64 compiler not found: ${MINGW_CXX_COMPILER_NAME}")
endif()
if (${MINGW_WINDRES_COMPILER} STREQUAL "MINGW_WINDRES_COMPILER-NOTFOUND")
message(FATAL_ERROR "mingw-w64 compiler not found: ${MINGW_WINDRES_COMPILER_NAME}")
endif()
# this macro is needed when compile `libwindows-emulator.a`
add_compile_definitions(NTDDI_VERSION=NTDDI_WIN10_MN)
# set the compiler
set(CMAKE_C_COMPILER ${MINGW_C_COMPILER})
set(CMAKE_CXX_COMPILER ${MINGW_CXX_COMPILER})
set(CMAKE_RC_COMPILER ${MINGW_WINDRES_COMPILER})
# set the compiler search path
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
# adjust the default behaviour of the FIND_XXX() commands:
# search headers and libraries in the target environment, search
# programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

View File

@@ -335,7 +335,7 @@ function(momo_strip_target target)
return()
endif()
if(MSVC OR MOMO_ENABLE_SANITIZER OR CMAKE_SYSTEM_NAME STREQUAL "iOS" OR CMAKE_SYSTEM_NAME STREQUAL "Android")
if(MSVC OR MOMO_ENABLE_SANITIZER OR CMAKE_SYSTEM_NAME STREQUAL "iOS" OR CMAKE_SYSTEM_NAME STREQUAL "Android" OR CMAKE_SYSTEM_NAME MATCHES "Emscripten")
return()
endif()

20
deps/CMakeLists.txt vendored
View File

@@ -1,9 +1,23 @@
set(UNICORN_ARCH "x86" CACHE STRING "")
add_subdirectory(unicorn)
##########################################
option(BASE64_ENABLE_TESTING "" OFF)
add_subdirectory(base64)
##########################################
option(FLATBUFFERS_BUILD_TESTS "" OFF)
option(FLATBUFFERS_INSTALL "" OFF)
add_subdirectory(flatbuffers)
if(MSVC)
target_compile_options(flatc PRIVATE /MD$<$<CONFIG:Debug>:d>)
endif()
##########################################
add_library(reflect INTERFACE)
target_include_directories(reflect INTERFACE
"${CMAKE_CURRENT_LIST_DIR}/reflect"
@@ -11,5 +25,9 @@ target_include_directories(reflect INTERFACE
##########################################
add_subdirectory(minidump_cpp)
##########################################
include(googletest.cmake)
include(zlib.cmake)

1
deps/base64 vendored Submodule

Submodule deps/base64 added at 0d0f5a8849

1
deps/flatbuffers vendored Submodule

Submodule deps/flatbuffers added at 64e5252b4e

1
deps/minidump_cpp vendored Submodule

Submodule deps/minidump_cpp added at df9ec209ce

2
deps/reflect vendored

2
deps/unicorn vendored

41
mcp/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Editor directories and files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

118
mcp/README.md Normal file
View File

@@ -0,0 +1,118 @@
# Sogen MCP Server
A Model Context Protocol (MCP) server that provides AI access to the sogen userspace emulator.
## Setup
1. Install dependencies:
```bash
npm install
```
2. Run the server:
```bash
npm start
```
Or for development with auto-restart:
```bash
npm run dev
```
## Structure
- `server.js` - Main server implementation
- `package.json` - Node.js project configuration
## Available Tools
### `list_applications`
Lists all available applications in the sogen userspace emulator.
**Parameters:** None
**Example usage:**
```json
{
"name": "list_applications",
"arguments": {}
}
```
### `run_application`
Executes a specific application from the allowed list in the sogen userspace emulator.
**Parameters:**
- `application` (string, required): The name of the application to run (use `list_applications` to see available apps)
- `args` (array of strings, optional): Arguments to pass to the application
- `timeout` (number, optional): Timeout in milliseconds (default: 5000)
**Example usage:**
```json
{
"name": "run_application",
"arguments": {
"application": "echo",
"args": ["Hello from sogen!"],
"timeout": 3000
}
}
```
## Adding More Tools
To add additional tools:
1. Add the tool definition to the `ListToolsRequestSchema` handler
2. Add the implementation to the `CallToolRequestSchema` handler
3. Create the corresponding method in the `MyMCPServer` class
## Execution Model
The server uses an **analyzer-based execution model**:
- Applications are not executed directly
- Instead, the server runs: `C:/analyer.exe -e C:/somedir <application_name> [args...]`
- The analyzer handles the actual execution within the sogen environment
- All output comes from the analyzer process
### Command Structure
```
C:/analyer.exe -e C:/somedir <application_name> [arguments...]
```
Where:
- `C:/analyer.exe` - The sogen analyzer executable
- `-e C:/somedir` - Analyzer flags and environment directory
- `<application_name>` - The application from `get_applications()`
- `[arguments...]` - Optional arguments passed to the application
## Implementation Required
You need to provide the implementation for the `get_applications()` method in `server.js`. This method should:
```javascript
async get_applications() {
// Return a Promise that resolves to a string array
// Example: return ['echo', 'ls', 'cat', 'grep'];
}
```
## Usage
This server allows AI assistants to:
1. **List available applications** using `list_applications`
2. **Execute specific applications** using `run_application` with validation
The server communicates over stdio and is designed for MCP-compatible clients.
### Example Workflow
1. Call `list_applications` to see what's available
2. Call `run_application` with a valid application name and arguments
## Next Steps
1. **Implement `get_applications()` method** - Provide the actual implementation
2. Add application-specific argument validation
3. Implement resource handling for file access
4. Add comprehensive logging and monitoring
5. Consider per-application sandboxing for enhanced security

1040
mcp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
mcp/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "sogen-mcp-server",
"version": "1.0.0",
"description": "MCP server for sogen userspace emulator",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.3"
},
"keywords": ["mcp", "server"],
"author": "",
"license": "MIT"
}

296
mcp/server.js Normal file
View File

@@ -0,0 +1,296 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { spawn } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class SogenMCPServer {
constructor() {
this.server = new Server(
{
name: "sogen-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
this.setupErrorHandling();
}
setupToolHandlers() {
// Handle tool listing
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_applications",
description:
"List all available applications in the sogen userspace emulator",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
},
{
name: "run_application",
description:
"Execute a specific application in the sogen userspace emulator",
inputSchema: {
type: "object",
properties: {
application: {
type: "string",
description:
"The name of the application to run (use list_applications to see available apps)",
},
args: {
type: "array",
items: {
type: "string",
},
description: "Arguments to pass to the application",
default: [],
},
timeout: {
type: "number",
description: "Timeout in milliseconds (default: 50000)",
default: 50000,
},
},
required: ["application"],
},
},
],
};
});
// Handle tool execution
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "list_applications":
return await this.listApplications();
case "run_application":
return await this.runApplication(
args.application,
args.args || [],
args.timeout || 50000
);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
}
async listApplications() {
try {
const applications = await this.get_applications();
return {
content: [
{
type: "text",
text: `Available applications in sogen:\n\n${applications
.map((app) => `${app}`)
.join("\n")}\n\nTotal: ${applications.length} application(s)`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error listing applications: ${error.message}`,
},
],
};
}
}
async runApplication(applicationName, args = [], timeout = 5000) {
try {
// First, get the list of available applications to validate
const availableApps = await this.get_applications();
if (!availableApps.includes(applicationName)) {
return {
content: [
{
type: "text",
text: `Error: Application '${applicationName}' is not available.\n\nAvailable applications:\n${availableApps
.map((app) => `${app}`)
.join("\n")}`,
},
],
};
}
return await this.executeApplication(applicationName, args, timeout);
} catch (error) {
return {
content: [
{
type: "text",
text: `Error running application '${applicationName}': ${error.message}`,
},
],
};
}
}
async executeApplication(applicationName, args, timeout) {
return new Promise((resolve, reject) => {
// Get the parent directory (emulator root)
const emulatorRoot = path.dirname(__dirname);
// Build the analyzer command: C:/analyer.exe -e C:/somedir <application_name>
const analyzerPath =
"C:\\Users\\mauri\\Desktop\\emulator\\build\\vs2022\\artifacts-relwithdebinfo\\analyzer.exe";
const analyzerArgs = [
"-c",
"-e",
"C:\\Users\\mauri\\Desktop\\emulator\\src\\tools\\root",
applicationName,
...args,
];
const child = spawn(analyzerPath, analyzerArgs, {
cwd: "C:\\Users\\mauri\\Desktop\\emulator\\build\\vs2022\\artifacts-relwithdebinfo",
shell: true,
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let timedOut = false;
// Set up timeout
const timer = setTimeout(() => {
timedOut = true;
child.kill("SIGTERM");
}, timeout);
// Collect stdout
child.stdout.on("data", (data) => {
stdout += data.toString();
});
// Collect stderr
child.stderr.on("data", (data) => {
stderr += data.toString();
});
// Handle process completion
child.on("close", (code) => {
clearTimeout(timer);
if (timedOut) {
resolve({
content: [
{
type: "text",
text: `Application '${applicationName}' timed out after ${timeout}ms\nAnalyzer command: ${analyzerPath} ${analyzerArgs.join(
" "
)}\nPartial stdout: ${stdout}\nPartial stderr: ${stderr}`,
},
],
});
} else {
const output = [];
if (stdout) {
output.push(`STDOUT:\n${stdout}`);
}
if (stderr) {
output.push(`STDERR:\n${stderr}`);
}
if (!stdout && !stderr) {
output.push("Application executed successfully with no output.");
}
output.push(`Exit code: ${code}`);
resolve({
content: [
{
type: "text",
text: `Application: ${applicationName}\nArguments: [${args.join(
", "
)}]\nAnalyzer command: ${analyzerPath} ${analyzerArgs.join(
" "
)}\n\n${output.join("\n\n")}`,
},
],
});
}
});
// Handle spawn errors
child.on("error", (error) => {
clearTimeout(timer);
resolve({
content: [
{
type: "text",
text: `Error executing application '${applicationName}' via analyzer: ${
error.message
}\nAnalyzer command: ${analyzerPath} ${analyzerArgs.join(" ")}`,
},
],
});
});
});
}
async get_applications() {
return [
"c:/test-sample.exe",
"c:/wukong/b1/Binaries/Win64/b1-Win64-Shipping.exe",
];
}
setupErrorHandling() {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("MCP Server running on stdio");
}
}
// Start the server
const server = new SogenMCPServer();
server.run().catch((error) => {
console.error("Failed to start server:", error);
process.exit(1);
});

24
page/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
page/.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
# Ignore artifacts:
dist
src/fb/

1
page/.prettierrc Normal file
View File

@@ -0,0 +1 @@
{}

21
page/components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

28
page/eslint.config.js Normal file
View File

@@ -0,0 +1,28 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
);

65
page/index.html Normal file
View File

@@ -0,0 +1,65 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="author" content="Maurice Heumann" />
<link
rel="icon"
type="image/png"
href="https://momo5502.com/emulator/pinata.png"
/>
<title>Sogen - Windows User Space Emulator</title>
<meta name="color-scheme" content="dark" />
<meta
name="description"
content="Sogen is a high-performance Windows user space emulator that can emulate windows processes. It is ideal for security-, DRM- or malware research."
data-react-helmet="true"
/>
<meta
property="og:site_name"
content="Sogen - Windows User Space Emulator"
data-react-helmet="true"
/>
<meta
property="og:title"
content="Sogen - Windows User Space Emulator"
data-react-helmet="true"
/>
<meta
property="og:description"
content="Sogen is a high-performance Windows user space emulator that can emulate windows processes. It is ideal for security-, DRM- or malware research."
data-react-helmet="true"
/>
<meta property="og:locale" content="en-us" data-react-helmet="true" />
<meta property="og:type" content="website" data-react-helmet="true" />
<meta
name="og:image"
content="https://repository-images.githubusercontent.com/842883987/9e56f43b-4afd-464d-85b9-d7e555751a39"
data-react-helmet="true"
/>
<meta name="twitter:card" content="summary" data-react-helmet="true" />
<meta
name="twitter:title"
content="Sogen - Windows User Space Emulator"
data-react-helmet="true"
/>
<meta
name="twitter:description"
content="Sogen is a high-performance Windows user space emulator that can emulate windows processes. It is ideal for security-, DRM- or malware research."
data-react-helmet="true"
/>
<meta
name="twitter:image"
content="https://repository-images.githubusercontent.com/842883987/9e56f43b-4afd-464d-85b9-d7e555751a39"
data-react-helmet="true"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6021
page/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

64
page/package.json Normal file
View File

@@ -0,0 +1,64 @@
{
"name": "emulator-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fontsource/inter": "^5.2.6",
"@irori/idbfs": "^0.5.1",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-context-menu": "^2.2.15",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"@types/react-window": "^1.8.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"flatbuffers": "^25.2.10",
"jszip": "^3.10.1",
"lucide-react": "^0.525.0",
"pe-library": "^1.0.1",
"react": "^19.0.0",
"react-bootstrap-icons": "^1.11.6",
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.8",
"react-helmet": "^6.1.0",
"react-resizable-panels": "^3.0.3",
"react-router-dom": "^7.6.3",
"react-window": "^1.8.11",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.4",
"vaul": "^1.1.2",
"wasm-feature-detect": "^1.8.0"
},
"devDependencies": {
"@eslint/js": "^9.30.0",
"@types/node": "^24.0.8",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-helmet": "^6.1.11",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.0",
"eslint-plugin-react-hooks": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"prettier": "3.6.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.0"
}
}

View File

@@ -0,0 +1,105 @@
var logLines = [];
var lastFlush = new Date().getTime();
var msgQueue = [];
onmessage = async (event) => {
const data = event.data;
const payload = data.data;
switch (data.message) {
case "run":
runEmulation(
payload.file,
payload.options,
payload.persist,
payload.wasm64,
payload.cacheBuster,
);
break;
case "event":
msgQueue.push(payload);
break;
}
};
function sendMessage(message, data) {
postMessage({ message, data });
}
function flushLines() {
const lines = logLines;
logLines = [];
lastFlush = new Date().getTime();
sendMessage("log", lines);
}
function logLine(text) {
logLines.push(text);
const now = new Date().getTime();
if (lastFlush + 15 < now) {
flushLines();
}
}
function notifyExit(code, persist) {
flushLines();
const finishExecution = () => {
sendMessage("end", code);
self.close();
};
if (persist) {
FS.syncfs(false, finishExecution);
} else {
finishExecution();
}
}
function handleMessage(message) {
sendMessage("event", message);
}
function getMessageFromQueue() {
if (msgQueue.length == 0) {
return "";
}
return msgQueue.shift();
}
function runEmulation(file, options, persist, wasm64, cacheBuster) {
const mainArguments = [...options, "-e", "./root", file];
globalThis.Module = {
arguments: mainArguments,
noInitialRun: true,
locateFile: (path, scriptDirectory) => {
const bitness = wasm64 ? "64" : "32";
return `${scriptDirectory}${bitness}/${path}?${cacheBuster}`;
},
onRuntimeInitialized: function () {
FS.mkdir("/root");
FS.mount(IDBFS, {}, "/root");
FS.syncfs(true, function (_) {
setTimeout(() => {
Module.callMain(mainArguments);
}, 0);
});
},
print: logLine,
printErr: logLine,
onAbort: () => notifyExit(null, persist),
onExit: (code) => notifyExit(code, persist),
postRun: flushLines,
};
if (wasm64) {
importScripts("./64/analyzer.js?" + cacheBuster);
} else {
importScripts("./32/analyzer.js?" + cacheBuster);
}
}

138
page/src/App.css Normal file
View File

@@ -0,0 +1,138 @@
html {
font-family: "Inter", sans-serif;
}
@media (pointer: fine) {
::-webkit-scrollbar {
width: 20px;
height: 20px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(97, 97, 97, 0.7);
border-radius: 20px;
border: 6px solid transparent;
background-clip: content-box;
transition: all 0.1s linear;
min-height: 50px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(97, 97, 97, 0.9);
}
::-webkit-scrollbar-corner {
background: transparent;
}
}
button {
cursor: pointer;
}
/*
button.fancy.bg-primary,
button.fancy.bg-secondary,
button.fancy.bg-destructive {
transition: all 0.2s ease;
}
button.fancy.bg-primary {
background: linear-gradient(
180deg,
rgba(38, 144, 255, 1) 0%,
rgba(0, 123, 255, 1) 100%
);
border: 1px solid rgb(18, 101, 236);
text-shadow: rgba(0, 0, 0, 0.2) 0px 1px;
}
button.fancy.bg-primary:hover {
background: linear-gradient(
180deg,
rgba(46, 151, 255, 1) 0%,
rgba(8, 130, 255, 1) 100%
);
border: 1px solid rgb(33, 33, 34);
}
button.fancy.bg-destructive {
background: linear-gradient(
180deg,
rgb(202, 23, 23) 0%,
rgb(176, 40, 9) 100%
);
border: 1px solid rgb(134, 30, 6);
text-shadow: rgba(0, 0, 0, 0.2) 0px 1px;
}
button.fancy.bg-destructive:hover {
background: linear-gradient(
180deg,
rgb(210, 30, 29) 0%,
rgb(184, 47, 15) 100%
);
border: 1px solid rgb(33, 33, 34);
}
button.fancy.bg-secondary {
background: linear-gradient(180deg, rgb(38, 38, 39) 0%, rgb(34, 34, 35) 100%);
border: 1px solid rgb(42, 42, 44);
}
button.fancy.bg-secondary:hover {
background: linear-gradient(180deg, rgb(42, 42, 43) 0%, rgb(38, 38, 39) 100%);
border: 1px solid rgb(33, 33, 34);
}
*/
/*.terminal-output span {
padding: 0px 16px;
}*/
.terminal-output {
font-weight: 600;
font-size: 1.05em;
font-family: monospace;
height: 100%;
}
.terminal-black {
color: #0c0c0c;
}
.terminal-red {
color: #ff3131;
}
.terminal-green {
color: #86c000;
}
.terminal-yellow {
color: #ffb940;
}
.terminal-blue {
color: #3a96dd;
}
.terminal-cyan {
color: #00adf7;
}
.terminal-pink {
color: #9750dd;
}
.terminal-white {
color: #ececec;
}
.terminal-dark-gray {
color: rgb(81, 81, 81);
}

35
page/src/App.tsx Normal file
View File

@@ -0,0 +1,35 @@
import { ThemeProvider } from "@/components/theme-provider";
import { TooltipProvider } from "@/components/ui/tooltip";
import { HashRouter, Route, Routes, Navigate } from "react-router-dom";
import { Playground } from "./playground";
import { LandingPage } from "./landing-page";
import "@fontsource/inter/100.css";
import "@fontsource/inter/200.css";
import "@fontsource/inter/300.css";
import "@fontsource/inter/400.css";
import "@fontsource/inter/500.css";
import "@fontsource/inter/600.css";
import "@fontsource/inter/700.css";
import "@fontsource/inter/800.css";
import "@fontsource/inter/900.css";
import "./App.css";
function App() {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<TooltipProvider>
<HashRouter>
<Routes>
<Route path="*" element={<Navigate to="/" replace />} />
<Route path="/" element={<LandingPage />} />
<Route path="/playground" element={<Playground />} />
</Routes>
</HashRouter>
</TooltipProvider>
</ThemeProvider>
);
}
export default App;

39
page/src/Header.tsx Normal file
View File

@@ -0,0 +1,39 @@
import { Helmet } from "react-helmet";
export interface HeaderProps {
title: string;
description: string;
preload?: string[];
}
const image =
"https://repository-images.githubusercontent.com/842883987/9e56f43b-4afd-464d-85b9-d7e555751a39";
export function Header(props: HeaderProps) {
return (
<Helmet>
<title>{props.title}</title>
<meta name="description" content={props.description} />
<meta property="og:site_name" content={props.title} />
<meta property="og:title" content={props.title} />
<meta property="og:description" content={props.description} />
<meta property="og:locale" content="en-us" />
<meta property="og:type" content="website" />
<meta name="og:image" content={image} />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content={props.title} />
<meta name="twitter:description" content={props.description} />
<meta name="twitter:image" content={image} />
{props.preload?.map((l) => (
<link
key={`link-${l}`}
rel="preload"
as={l.endsWith(".js") ? "script" : "fetch"}
crossOrigin=""
href={`${l}${l.indexOf("?") == -1 ? "?" : "&"}cb=${import.meta.env.VITE_BUILD_TIME}`}
/>
))}
</Helmet>
);
}

View File

@@ -0,0 +1,51 @@
import * as React from "react";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
/*SidebarMenu,
SidebarGroupContent,
SidebarMenuButton,
SidebarMenuItem,*/
SidebarRail,
} from "@/components/ui/sidebar";
// This is sample data.
const data = {
navMain: [
{
title: "TODO",
},
],
};
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar {...props}>
<SidebarHeader>Filesystem</SidebarHeader>
<SidebarContent>
{/* We create a SidebarGroup for each parent. */}
{data.navMain.map((item) => (
<SidebarGroup key={item.title}>
<SidebarGroupLabel>{item.title}</SidebarGroupLabel>
{/*<SidebarGroupContent>
<SidebarMenu>
{item.items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={item.isActive}>
<a href={item.url}>{item.title}</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>*/}
</SidebarGroup>
))}
</SidebarContent>
<SidebarRail />
</Sidebar>
);
}

View File

@@ -0,0 +1,5 @@
.emulation-summary {
box-shadow: 0px 0px 15px 4px rgba(255, 255, 255, 0.04);
/*border: 1px solid rgba(255, 255, 255, 0.2);*/
backdrop-filter: blur(7px) brightness(0.8) saturate(1.3);
}

View File

@@ -0,0 +1,48 @@
import { EmulationStatus } from "@/emulator";
import { TextTooltip } from "./text-tooltip";
import { BarChartSteps, CpuFill, FloppyFill } from "react-bootstrap-icons";
import "./emulation-summary.css";
export interface EmulationSummaryProps {
status?: EmulationStatus;
}
function formatMemory(value: BigInt): string {
const abbr = ["B", "KB", "MB", "GB", "PB"];
let num = Number(value);
let index = 0;
while (num >= 1024 && index < abbr.length - 1) {
num /= 1024;
index++;
}
return num.toFixed(2) + " " + abbr[index];
}
export function EmulationSummary(props: EmulationSummaryProps) {
if (!props.status) {
return <></>;
}
return (
<div className="emulation-summary items-center absolute z-49 right-0 m-6 rounded-xl min-w-[150px] p-3 text-whtie cursor-default font-medium text-right text-sm whitespace-nowrap leading-6 font-mono">
<TextTooltip tooltip={"Active threads"}>
{props.status.activeThreads}
<BarChartSteps className="inline ml-3" />
</TextTooltip>
<br />
<TextTooltip tooltip={"Application memory"}>
{formatMemory(props.status.committedMemory)}
<FloppyFill className="inline ml-3" />
</TextTooltip>
<br />
<TextTooltip tooltip={"Executed instructions"}>
{props.status.executedInstructions.toLocaleString()}
<CpuFill className="inline ml-3" />
</TextTooltip>
</div>
);
}

View File

@@ -0,0 +1,177 @@
import {
FolderFill,
FolderSymlinkFill,
FileEarmark,
FiletypeExe,
FileEarmarkBinary,
} from "react-bootstrap-icons";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
ContextMenuSeparator,
ContextMenuLabel,
} from "@/components/ui/context-menu";
import { TextTooltip } from "./text-tooltip";
export enum FolderElementType {
Folder = 0,
File,
}
export interface FolderElement {
name: string;
type: FolderElementType;
}
type ClickHandler = (element: FolderElement) => void;
type CreateFolderHandler = () => void;
type RemoveElementHandler = (element: FolderElement) => void;
type RenameElementHandler = (element: FolderElement) => void;
type AddFilesHandler = () => void;
type IconReader = (element: FolderElement) => string | null;
export interface FolderProps {
elements: FolderElement[];
iconReader: IconReader;
clickHandler: ClickHandler;
createFolderHandler: CreateFolderHandler;
removeElementHandler: RemoveElementHandler;
renameElementHandler: RenameElementHandler;
addFilesHandler: AddFilesHandler;
}
function elementComparator(e1: FolderElement, e2: FolderElement) {
if (e1.type != e2.type) {
return e1.type - e2.type;
}
return e1.name.localeCompare(e2.name);
}
function getIcon(
element: FolderElement,
iconReader: IconReader,
className: string = "",
) {
const icon = iconReader(element);
if (icon) {
return (
<div className={className}>
<div className="w-full h-full flex items-center">
<img className="rounded-sm" src={icon} />
</div>
</div>
);
}
switch (element.type) {
case FolderElementType.File:
if (element.name.endsWith(".dll")) {
return <FileEarmarkBinary className={className} />;
}
if (element.name.endsWith(".exe")) {
return <FiletypeExe className={className} />;
}
return <FileEarmark className={className} />;
case FolderElementType.Folder:
return element.name == ".." ? (
<FolderSymlinkFill className={className} />
) : (
<FolderFill className={className} />
);
default:
return <></>;
}
}
function renderIcon(element: FolderElement, iconReader: IconReader) {
let className = "w-11 h-11 flex-1";
return getIcon(element, iconReader, className);
}
function renderElement(element: FolderElement, props: FolderProps) {
return (
<div
onClick={() => props.clickHandler(element)}
className="folder-element cursor-default select-none flex flex-col gap-2 items-center text-center text-xs p-2 m-2 w-27 h-25 rounded-lg border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50"
>
{renderIcon(element, props.iconReader)}
<span className="whitespace-nowrap text-ellipsis overflow-hidden w-20">
{element.name}
</span>
</div>
);
}
export function trimFilename(filename: string, limit = 25) {
if (limit < 4) {
limit = 4;
}
if (filename.length < limit) {
return filename;
}
return filename.substring(0, limit - 3) + "...";
}
function renderElementWithContext(element: FolderElement, props: FolderProps) {
if (element.name == "..") {
return renderElement(element, props);
}
return (
<ContextMenu>
<ContextMenuTrigger>
<TextTooltip tooltip={element.name}>
{renderElement(element, props)}
</TextTooltip>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuLabel>{trimFilename(element.name)}</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => props.renameElementHandler(element)}>
Rename
</ContextMenuItem>
<ContextMenuItem onClick={() => props.removeElementHandler(element)}>
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
function renderElementWrapper(element: FolderElement, props: FolderProps) {
return (
<div key={`folder-element-${element.name}`}>
{renderElementWithContext(element, props)}
</div>
);
}
export function Folder(props: FolderProps) {
return (
<ContextMenu>
<ContextMenuTrigger>
<ScrollArea className="h-[50dvh]">
<div className="folder flex flex-wrap h-full text-neutral-300">
{props.elements
.sort(elementComparator)
.map((e) => renderElementWrapper(e, props))}
</div>
</ScrollArea>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={props.createFolderHandler}>
Create new Folder
</ContextMenuItem>
<ContextMenuItem onClick={props.addFilesHandler}>
Add Files
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}

View File

@@ -0,0 +1,282 @@
import React from "react";
import { FixedSizeList as List } from "react-window";
interface OutputProps {}
interface ColorState {
color: string;
}
interface OutputState extends ColorState {
lines: LogLine[];
}
enum SizeState {
Final,
Updating,
}
interface FullOutputState extends OutputState {
grouper: OutputGrouper;
height: number;
width: number;
state: SizeState;
}
interface LogLine {
text: string;
classNames: string;
}
function removeSubstringFromStart(str: string, substring: string): string {
if (str.startsWith(substring)) {
return str.slice(substring.length);
}
return str;
}
function removeSubstringFromEnd(str: string, substring: string): string {
if (str.endsWith(substring)) {
return str.slice(0, -substring.length);
}
return str;
}
function removeSpanFromStart(str: string, color: string) {
const pattern = /^<span class="(terminal-[a-z-]+)">/;
const match = str.match(pattern);
if (match) {
const terminalValue = match[1];
const cleanedString = str.replace(pattern, "");
return [cleanedString, terminalValue];
}
return [str, color];
}
function extractColor(line: string, colorState: ColorState) {
while (true) {
const newLine = removeSubstringFromStart(line, "</span>");
if (newLine == line) {
break;
}
line = newLine;
colorState.color = "";
}
const [nextLine, color] = removeSpanFromStart(line, colorState.color);
const finalLine = removeSubstringFromEnd(nextLine, "</span>");
if (finalLine != nextLine) {
colorState.color = "";
} else {
colorState.color = color;
}
return [finalLine, color];
}
function renderLine(line: string, colorState: ColorState) {
const [newLine, color] = extractColor(line, colorState);
return {
text: newLine,
classNames: "whitespace-nowrap block " + color,
};
}
function renderLines(lines: string[], color: string): OutputState {
var state: ColorState = {
color,
};
const resultLines = lines.map((line) => renderLine(line, state));
return {
lines: resultLines,
color: state.color,
};
}
function mergeLines(
previousState: OutputState,
newLines: string[],
): OutputState {
const result = renderLines(newLines, previousState.color);
return {
lines: previousState.lines.concat(result.lines),
color: result.color,
};
}
class OutputGrouper {
private lines: string[];
private flushQueued: boolean;
handler: (lines: string[]) => void;
constructor() {
this.lines = [];
this.flushQueued = false;
this.handler = () => {};
}
clear() {
this.lines = [];
this.flushQueued = false;
}
flush() {
const lines = this.lines;
this.lines = [];
this.handler(lines);
}
queueFlush() {
if (this.flushQueued) {
return false;
}
this.flushQueued = true;
requestAnimationFrame(() => {
if (!this.flushQueued) {
return;
}
this.flushQueued = false;
this.flush();
});
}
storeLines(lines: string[]) {
this.lines = this.lines.concat(lines);
this.queueFlush();
}
}
export class Output extends React.Component<OutputProps, FullOutputState> {
private outputRef: React.RefObject<HTMLDivElement | null>;
private listRef: React.RefObject<List | null>;
private resizeObserver: ResizeObserver;
constructor(props: OutputProps) {
super(props);
this.clear = this.clear.bind(this);
this.logLine = this.logLine.bind(this);
this.logLines = this.logLines.bind(this);
this.updateDimensions = this.updateDimensions.bind(this);
this.outputRef = React.createRef();
this.listRef = React.createRef();
this.resizeObserver = new ResizeObserver(this.updateDimensions);
this.state = {
lines: [],
color: "",
grouper: new OutputGrouper(),
height: 10,
width: 10,
state: SizeState.Final,
};
this.state.grouper.handler = (lines: string[]) => {
this.setState((s) => mergeLines(s, lines));
};
}
componentDidMount() {
this.updateDimensions();
if (this.outputRef.current) {
this.resizeObserver.observe(this.outputRef.current);
}
}
componentWillUnmount() {
this.resizeObserver.disconnect();
}
componentDidUpdate(_: OutputProps, prevState: FullOutputState) {
if (
prevState.lines.length == this.state.lines.length ||
!this.listRef.current
) {
return;
}
this.listRef.current.scrollToItem(this.state.lines.length - 1);
}
clear() {
this.state.grouper.clear();
this.setState({
lines: [],
color: "",
});
}
updateDimensions() {
if (!this.outputRef.current) {
return;
}
if (this.state.state == SizeState.Updating) {
this.setState({
width: this.outputRef.current.offsetWidth - 1,
height: this.outputRef.current.offsetHeight - 1,
state: SizeState.Final,
});
return;
}
this.setState(
{
width: 0,
height: 0,
state: SizeState.Updating,
},
this.triggerDimensionUpdate.bind(this),
);
}
triggerDimensionUpdate() {
requestAnimationFrame(() => {
this.updateDimensions();
});
}
logLines(lines: string[]) {
this.state.grouper.storeLines(lines);
}
logLine(line: string) {
this.logLines([line]);
}
render() {
return (
<div className="terminal-output" ref={this.outputRef}>
<List
ref={this.listRef}
width={this.state.width}
height={this.state.height}
itemCount={this.state.lines.length}
itemSize={20}
>
{({ index, style }) => {
const line = this.state.lines[index];
return (
<span className={line.classNames} style={style}>
{line.text}
</span>
);
}}
</List>
</div>
);
}
}

View File

@@ -0,0 +1,173 @@
import React from "react";
import { Checkbox } from "./ui/checkbox";
import { Label } from "./ui/label";
import { Settings } from "@/settings";
import { TextTooltip } from "./text-tooltip";
interface SettingsMenuProps {
settings: Settings;
allowWasm64: boolean;
onChange: (settings: Settings) => void;
}
interface SettingsLabelProps {
htmlFor?: string | undefined;
text: React.ReactNode;
tooltip: React.ReactNode;
}
function SettingsLabel(props: SettingsLabelProps) {
return (
<Label htmlFor={props.htmlFor}>
<TextTooltip tooltip={props.tooltip}>{props.text}</TextTooltip>
</Label>
);
}
export class SettingsMenu extends React.Component<SettingsMenuProps, Settings> {
constructor(props: SettingsMenuProps) {
super(props);
this.getSettings = this.getSettings.bind(this);
this.state = props.settings;
}
getSettings() {
return this.state;
}
updateSettings(settings: Settings) {
this.setState(() => settings);
}
componentDidUpdate(_: SettingsMenuProps, oldSettings: Settings) {
if (JSON.stringify(oldSettings) !== JSON.stringify(this.state)) {
this.props.onChange(this.state);
}
}
render() {
return (
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">Settings</h4>
<p className="text-sm text-muted-foreground">
Set the settings for the emulation.
</p>
</div>
<div className="flex gap-6">
<Checkbox
id="settings-verbose"
checked={this.state.verbose}
onCheckedChange={(checked: boolean) => {
this.setState({ verbose: checked });
}}
/>
<SettingsLabel
htmlFor="settings-verbose"
text={"Verbose Logging"}
tooltip={"Very detailed logging of all function call and accesses"}
/>
</div>
<div className="flex gap-6">
<Checkbox
id="settings-concise"
checked={this.state.concise}
onCheckedChange={(checked: boolean) => {
this.setState({ concise: checked });
}}
/>
<SettingsLabel
htmlFor="settings-concise"
text={"Concise Logging"}
tooltip={"Suppress logging until the application code runs"}
/>
</div>
<div className="flex gap-6">
<Checkbox
id="settings-silent"
checked={this.state.silent}
onCheckedChange={(checked: boolean) => {
this.setState({ silent: checked });
}}
/>
<SettingsLabel
htmlFor="settings-silent"
text={"Silent Logging"}
tooltip={"Suppress all logging except for stdout"}
/>
</div>
<div className="flex gap-6">
<Checkbox
id="settings-buffer"
checked={this.state.bufferStdout}
onCheckedChange={(checked: boolean) => {
this.setState({ bufferStdout: checked });
}}
/>
<SettingsLabel
htmlFor="settings-buffer"
text={"Buffer stdout"}
tooltip={
"Group stdout and print everything when the emulation ends"
}
/>
</div>
<div className="flex gap-6">
<Checkbox
id="settings-exec"
checked={this.state.execAccess}
onCheckedChange={(checked: boolean) => {
this.setState({ execAccess: checked });
}}
/>
<SettingsLabel
htmlFor="settings-exec"
text={"Log exec Memory Access"}
tooltip={"Log when the application reads/writes executable memory"}
/>
</div>
<div className="flex gap-6">
<Checkbox
id="settings-persist"
checked={this.state.persist}
onCheckedChange={(checked: boolean) => {
this.setState({ persist: checked });
}}
/>
<SettingsLabel
htmlFor="settings-persist"
text={"Persist filesystem"}
tooltip={
"Persist files and folders that were created, modified or deleted during the emulation"
}
/>
</div>
<div className="flex gap-6">
<Checkbox
id="settings-mem64"
disabled={!this.props.allowWasm64}
checked={this.state.wasm64}
onCheckedChange={(checked: boolean) => {
this.setState({ wasm64: checked });
}}
/>
<SettingsLabel
htmlFor="settings-mem64"
text={"64-Bit WebAssembly"}
tooltip={
"Use 64-bit WebAssembly which supports emulating applications that require more than 2gb of memory"
}
/>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,87 @@
import { Badge } from "@/components/ui/badge";
import { CircleFill } from "react-bootstrap-icons";
import { EmulationStatus, EmulationState as State } from "@/emulator";
function getStateName(state: State) {
switch (state) {
case State.Stopped:
return "Stopped";
case State.Paused:
return "Paused";
case State.Running:
return "Running";
case State.Failed:
return "Failed";
case State.Success:
return "Success";
default:
return "";
}
}
function getStateColor(state: State) {
switch (state) {
case State.Failed:
return "bg-orange-600";
case State.Paused:
return "bg-amber-500";
case State.Success:
return "bg-lime-600";
case State.Stopped:
return "bg-yellow-800";
case State.Running:
return "bg-sky-500";
default:
return "";
}
}
function getStateEmoji(state: State) {
switch (state) {
case State.Stopped:
return "🟤";
case State.Paused:
return "🟡";
case State.Running:
return "🔵";
case State.Failed:
return "🔴";
case State.Success:
return "🟢";
default:
return "";
}
}
function getFilename(path: string) {
const lastSlash = path.lastIndexOf("/");
if (lastSlash == -1) {
return path;
}
return path.substring(lastSlash + 1);
}
export interface StatusIndicatorProps {
state: State;
application: string | undefined;
}
export function StatusIndicator(props: StatusIndicatorProps) {
if (props.application && props.application.length > 0) {
document.title = `${getStateEmoji(props.state)} ${getFilename(props.application)} | Sogen`;
}
return (
<Badge variant="outline">
<CircleFill
className={
getStateColor(props.state) +
" rounded-full mr-1 n duration-200 ease-in-out"
}
color="transparent"
/>
{getStateName(props.state)}
</Badge>
);
}

View File

@@ -0,0 +1,21 @@
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
export interface TextTooltipProps {
children?: React.ReactNode;
tooltip: React.ReactNode;
}
export function TextTooltip(props: TextTooltipProps) {
return (
<Tooltip delayDuration={700} disableHoverableContent>
<TooltipTrigger asChild>
<span>{props.children}</span>
</TooltipTrigger>
<TooltipContent>{props.tooltip}</TooltipContent>
</Tooltip>
);
}

View File

@@ -0,0 +1,73 @@
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@@ -0,0 +1,46 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,109 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className,
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@@ -0,0 +1,59 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,92 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,30 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@@ -0,0 +1,250 @@
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
);
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
);
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@@ -0,0 +1,133 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@@ -0,0 +1,130 @@
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className,
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View File

@@ -0,0 +1,255 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,22 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@@ -0,0 +1,46 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -0,0 +1,54 @@
import * as React from "react";
import { GripVerticalIcon } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className,
)}
{...props}
/>
);
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@@ -0,0 +1,56 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

View File

@@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };

View File

@@ -0,0 +1,137 @@
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@@ -0,0 +1,724 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className,
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className,
)}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
);
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar();
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar();
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className,
)}
{...props}
/>
);
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
);
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
);
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className,
)}
{...props}
/>
);
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
);
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};

View File

@@ -0,0 +1,13 @@
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
);
}
export { Skeleton };

View File

@@ -0,0 +1,64 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className,
)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -0,0 +1,59 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

266
page/src/emulator.ts Normal file
View File

@@ -0,0 +1,266 @@
import { Settings, translateSettings } from "./settings";
import * as flatbuffers from "flatbuffers";
import * as fbDebugger from "@/fb/debugger";
type LogHandler = (lines: string[]) => void;
export enum EmulationState {
Stopped,
Paused,
Running,
Success,
Failed,
}
export interface EmulationStatus {
activeThreads: number;
reservedMemory: BigInt;
committedMemory: BigInt;
executedInstructions: BigInt;
}
function createDefaultEmulationStatus(): EmulationStatus {
return {
executedInstructions: BigInt(0),
activeThreads: 0,
reservedMemory: BigInt(0),
committedMemory: BigInt(0),
};
}
export function isFinalState(state: EmulationState) {
switch (state) {
case EmulationState.Stopped:
case EmulationState.Success:
case EmulationState.Failed:
return true;
default:
return false;
}
}
function base64Encode(uint8Array: Uint8Array): string {
let binaryString = "";
for (let i = 0; i < uint8Array.byteLength; i++) {
binaryString += String.fromCharCode(uint8Array[i]);
}
return btoa(binaryString);
}
function base64Decode(data: string) {
const binaryString = atob(data);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
function decodeEvent(data: string) {
const array = base64Decode(data);
const buffer = new flatbuffers.ByteBuffer(array);
const event = fbDebugger.DebugEvent.getRootAsDebugEvent(buffer);
return event.unpack();
}
type StateChangeHandler = (state: EmulationState) => void;
type StatusUpdateHandler = (status: EmulationStatus) => void;
export class Emulator {
logHandler: LogHandler;
stateChangeHandler: StateChangeHandler;
stautsUpdateHandler: StatusUpdateHandler;
terminatePromise: Promise<number | null>;
terminateResolve: (value: number | null) => void;
terminateReject: (reason?: any) => void;
worker: Worker;
state: EmulationState = EmulationState.Stopped;
exit_status: number | null = null;
constructor(
logHandler: LogHandler,
stateChangeHandler: StateChangeHandler,
stautsUpdateHandler: StatusUpdateHandler,
) {
this.logHandler = logHandler;
this.stateChangeHandler = stateChangeHandler;
this.stautsUpdateHandler = stautsUpdateHandler;
this.terminateResolve = () => {};
this.terminateReject = () => {};
this.terminatePromise = new Promise((resolve, reject) => {
this.terminateResolve = resolve;
this.terminateReject = reject;
});
const cacheBuster = import.meta.env.VITE_BUILD_TIME || Date.now();
this.worker = new Worker(
/*new URL('./emulator-worker.js', import.meta.url)*/ "./emulator-worker.js?" +
cacheBuster,
);
this.worker.onerror = this._onError.bind(this);
this.worker.onmessage = (e) => queueMicrotask(() => this._onMessage(e));
}
async start(settings: Settings, file: string) {
this._setState(EmulationState.Running);
this.stautsUpdateHandler(createDefaultEmulationStatus());
this.worker.postMessage({
message: "run",
data: {
file,
options: translateSettings(settings),
persist: settings.persist,
wasm64: settings.wasm64,
cacheBuster: import.meta.env.VITE_BUILD_TIME || Date.now(),
},
});
}
updateState() {
this.sendEvent(
new fbDebugger.DebugEventT(
fbDebugger.Event.GetStateRequest,
new fbDebugger.GetStateRequestT(),
),
);
}
getState() {
return this.state;
}
stop() {
this.worker.terminate();
this._setState(EmulationState.Stopped);
this.terminateResolve(null);
}
onTerminate() {
return this.terminatePromise;
}
sendEvent(event: fbDebugger.DebugEventT) {
const builder = new flatbuffers.Builder(1024);
fbDebugger.DebugEvent.finishDebugEventBuffer(builder, event.pack(builder));
const message = base64Encode(builder.asUint8Array());
this.worker.postMessage({
message: "event",
data: message,
});
}
pause() {
this.sendEvent(
new fbDebugger.DebugEventT(
fbDebugger.Event.PauseRequest,
new fbDebugger.PauseRequestT(),
),
);
this.updateState();
}
resume() {
this.sendEvent(
new fbDebugger.DebugEventT(
fbDebugger.Event.RunRequest,
new fbDebugger.RunRequestT(),
),
);
this.updateState();
}
logError(message: string) {
this.logHandler([`<span class="terminal-red">${message}</span>`]);
}
_onError(ev: ErrorEvent) {
try {
this.worker.terminate();
} catch (e) {}
this.logError(`Emulator encountered fatal error: ${ev.message}`);
this._setState(EmulationState.Failed);
this.terminateResolve(-1);
}
_onMessage(event: MessageEvent) {
if (event.data.message == "log") {
this.logHandler(event.data.data);
} else if (event.data.message == "event") {
this._onEvent(decodeEvent(event.data.data));
} else if (event.data.message == "end") {
this._setState(
this.exit_status === 0 ? EmulationState.Success : EmulationState.Failed,
);
this.terminateResolve(this.exit_status);
}
}
_onEvent(event: fbDebugger.DebugEventT) {
switch (event.eventType) {
case fbDebugger.Event.GetStateResponse:
this._handle_state_response(
event.event as fbDebugger.GetStateResponseT,
);
break;
case fbDebugger.Event.ApplicationExit:
this._handle_application_exit(
event.event as fbDebugger.ApplicationExitT,
);
break;
case fbDebugger.Event.EmulationStatus:
this._handle_emulation_status(
event.event as fbDebugger.EmulationStatusT,
);
break;
}
}
_setState(state: EmulationState) {
this.state = state;
this.stateChangeHandler(this.state);
}
_handle_application_exit(info: fbDebugger.ApplicationExitT) {
this.exit_status = info.exitStatus;
}
_handle_emulation_status(info: fbDebugger.EmulationStatusT) {
this.stautsUpdateHandler({
activeThreads: info.activeThreads,
executedInstructions: info.executedInstructions,
reservedMemory: info.reservedMemory,
committedMemory: info.committedMemory,
});
}
_handle_state_response(response: fbDebugger.GetStateResponseT) {
switch (response.state) {
case fbDebugger.State.None:
this._setState(EmulationState.Stopped);
break;
case fbDebugger.State.Paused:
this._setState(EmulationState.Paused);
break;
case fbDebugger.State.Running:
this._setState(EmulationState.Running);
break;
}
}
}

21
page/src/fb/debugger.ts Normal file
View File

@@ -0,0 +1,21 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
export { ApplicationExit, ApplicationExitT } from './debugger/application-exit.js';
export { DebugEvent, DebugEventT } from './debugger/debug-event.js';
export { EmulationStatus, EmulationStatusT } from './debugger/emulation-status.js';
export { Event } from './debugger/event.js';
export { GetStateRequest, GetStateRequestT } from './debugger/get-state-request.js';
export { GetStateResponse, GetStateResponseT } from './debugger/get-state-response.js';
export { PauseRequest, PauseRequestT } from './debugger/pause-request.js';
export { ReadMemoryRequest, ReadMemoryRequestT } from './debugger/read-memory-request.js';
export { ReadMemoryResponse, ReadMemoryResponseT } from './debugger/read-memory-response.js';
export { ReadRegisterRequest, ReadRegisterRequestT } from './debugger/read-register-request.js';
export { ReadRegisterResponse, ReadRegisterResponseT } from './debugger/read-register-response.js';
export { RunRequest, RunRequestT } from './debugger/run-request.js';
export { State } from './debugger/state.js';
export { WriteMemoryRequest, WriteMemoryRequestT } from './debugger/write-memory-request.js';
export { WriteMemoryResponse, WriteMemoryResponseT } from './debugger/write-memory-response.js';
export { WriteRegisterRequest, WriteRegisterRequestT } from './debugger/write-register-request.js';
export { WriteRegisterResponse, WriteRegisterResponseT } from './debugger/write-register-response.js';

View File

@@ -0,0 +1,86 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class ApplicationExit implements flatbuffers.IUnpackableObject<ApplicationExitT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ApplicationExit {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsApplicationExit(bb:flatbuffers.ByteBuffer, obj?:ApplicationExit):ApplicationExit {
return (obj || new ApplicationExit()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsApplicationExit(bb:flatbuffers.ByteBuffer, obj?:ApplicationExit):ApplicationExit {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ApplicationExit()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
exitStatus():number|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : null;
}
mutate_exit_status(value:number):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
static startApplicationExit(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addExitStatus(builder:flatbuffers.Builder, exitStatus:number) {
builder.addFieldInt32(0, exitStatus, null);
}
static endApplicationExit(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createApplicationExit(builder:flatbuffers.Builder, exitStatus:number|null):flatbuffers.Offset {
ApplicationExit.startApplicationExit(builder);
if (exitStatus !== null)
ApplicationExit.addExitStatus(builder, exitStatus);
return ApplicationExit.endApplicationExit(builder);
}
unpack(): ApplicationExitT {
return new ApplicationExitT(
this.exitStatus()
);
}
unpackTo(_o: ApplicationExitT): void {
_o.exitStatus = this.exitStatus();
}
}
export class ApplicationExitT implements flatbuffers.IGeneratedObject {
constructor(
public exitStatus: number|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return ApplicationExit.createApplicationExit(builder,
this.exitStatus
);
}
}

View File

@@ -0,0 +1,121 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { ApplicationExit, ApplicationExitT } from '../debugger/application-exit.js';
import { EmulationStatus, EmulationStatusT } from '../debugger/emulation-status.js';
import { Event, unionToEvent, unionListToEvent } from '../debugger/event.js';
import { GetStateRequest, GetStateRequestT } from '../debugger/get-state-request.js';
import { GetStateResponse, GetStateResponseT } from '../debugger/get-state-response.js';
import { PauseRequest, PauseRequestT } from '../debugger/pause-request.js';
import { ReadMemoryRequest, ReadMemoryRequestT } from '../debugger/read-memory-request.js';
import { ReadMemoryResponse, ReadMemoryResponseT } from '../debugger/read-memory-response.js';
import { ReadRegisterRequest, ReadRegisterRequestT } from '../debugger/read-register-request.js';
import { ReadRegisterResponse, ReadRegisterResponseT } from '../debugger/read-register-response.js';
import { RunRequest, RunRequestT } from '../debugger/run-request.js';
import { WriteMemoryRequest, WriteMemoryRequestT } from '../debugger/write-memory-request.js';
import { WriteMemoryResponse, WriteMemoryResponseT } from '../debugger/write-memory-response.js';
import { WriteRegisterRequest, WriteRegisterRequestT } from '../debugger/write-register-request.js';
import { WriteRegisterResponse, WriteRegisterResponseT } from '../debugger/write-register-response.js';
export class DebugEvent implements flatbuffers.IUnpackableObject<DebugEventT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):DebugEvent {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsDebugEvent(bb:flatbuffers.ByteBuffer, obj?:DebugEvent):DebugEvent {
return (obj || new DebugEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsDebugEvent(bb:flatbuffers.ByteBuffer, obj?:DebugEvent):DebugEvent {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new DebugEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
eventType():Event {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint8(this.bb_pos + offset) : Event.NONE;
}
event<T extends flatbuffers.Table>(obj:any):any|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__union(obj, this.bb_pos + offset) : null;
}
static startDebugEvent(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addEventType(builder:flatbuffers.Builder, eventType:Event) {
builder.addFieldInt8(0, eventType, Event.NONE);
}
static addEvent(builder:flatbuffers.Builder, eventOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, eventOffset, 0);
}
static endDebugEvent(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static finishDebugEventBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) {
builder.finish(offset);
}
static finishSizePrefixedDebugEventBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) {
builder.finish(offset, undefined, true);
}
static createDebugEvent(builder:flatbuffers.Builder, eventType:Event, eventOffset:flatbuffers.Offset):flatbuffers.Offset {
DebugEvent.startDebugEvent(builder);
DebugEvent.addEventType(builder, eventType);
DebugEvent.addEvent(builder, eventOffset);
return DebugEvent.endDebugEvent(builder);
}
unpack(): DebugEventT {
return new DebugEventT(
this.eventType(),
(() => {
const temp = unionToEvent(this.eventType(), this.event.bind(this));
if(temp === null) { return null; }
return temp.unpack()
})()
);
}
unpackTo(_o: DebugEventT): void {
_o.eventType = this.eventType();
_o.event = (() => {
const temp = unionToEvent(this.eventType(), this.event.bind(this));
if(temp === null) { return null; }
return temp.unpack()
})();
}
}
export class DebugEventT implements flatbuffers.IGeneratedObject {
constructor(
public eventType: Event = Event.NONE,
public event: ApplicationExitT|EmulationStatusT|GetStateRequestT|GetStateResponseT|PauseRequestT|ReadMemoryRequestT|ReadMemoryResponseT|ReadRegisterRequestT|ReadRegisterResponseT|RunRequestT|WriteMemoryRequestT|WriteMemoryResponseT|WriteRegisterRequestT|WriteRegisterResponseT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const event = builder.createObjectOffset(this.event);
return DebugEvent.createDebugEvent(builder,
this.eventType,
event
);
}
}

View File

@@ -0,0 +1,160 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class EmulationStatus implements flatbuffers.IUnpackableObject<EmulationStatusT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):EmulationStatus {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsEmulationStatus(bb:flatbuffers.ByteBuffer, obj?:EmulationStatus):EmulationStatus {
return (obj || new EmulationStatus()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsEmulationStatus(bb:flatbuffers.ByteBuffer, obj?:EmulationStatus):EmulationStatus {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new EmulationStatus()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
activeThreads():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0;
}
mutate_active_threads(value:number):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
reservedMemory():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
mutate_reserved_memory(value:bigint):boolean {
const offset = this.bb!.__offset(this.bb_pos, 6);
if (offset === 0) {
return false;
}
this.bb!.writeUint64(this.bb_pos + offset, value);
return true;
}
committedMemory():bigint {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
mutate_committed_memory(value:bigint):boolean {
const offset = this.bb!.__offset(this.bb_pos, 8);
if (offset === 0) {
return false;
}
this.bb!.writeUint64(this.bb_pos + offset, value);
return true;
}
executedInstructions():bigint {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
mutate_executed_instructions(value:bigint):boolean {
const offset = this.bb!.__offset(this.bb_pos, 10);
if (offset === 0) {
return false;
}
this.bb!.writeUint64(this.bb_pos + offset, value);
return true;
}
static startEmulationStatus(builder:flatbuffers.Builder) {
builder.startObject(4);
}
static addActiveThreads(builder:flatbuffers.Builder, activeThreads:number) {
builder.addFieldInt32(0, activeThreads, 0);
}
static addReservedMemory(builder:flatbuffers.Builder, reservedMemory:bigint) {
builder.addFieldInt64(1, reservedMemory, BigInt('0'));
}
static addCommittedMemory(builder:flatbuffers.Builder, committedMemory:bigint) {
builder.addFieldInt64(2, committedMemory, BigInt('0'));
}
static addExecutedInstructions(builder:flatbuffers.Builder, executedInstructions:bigint) {
builder.addFieldInt64(3, executedInstructions, BigInt('0'));
}
static endEmulationStatus(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createEmulationStatus(builder:flatbuffers.Builder, activeThreads:number, reservedMemory:bigint, committedMemory:bigint, executedInstructions:bigint):flatbuffers.Offset {
EmulationStatus.startEmulationStatus(builder);
EmulationStatus.addActiveThreads(builder, activeThreads);
EmulationStatus.addReservedMemory(builder, reservedMemory);
EmulationStatus.addCommittedMemory(builder, committedMemory);
EmulationStatus.addExecutedInstructions(builder, executedInstructions);
return EmulationStatus.endEmulationStatus(builder);
}
unpack(): EmulationStatusT {
return new EmulationStatusT(
this.activeThreads(),
this.reservedMemory(),
this.committedMemory(),
this.executedInstructions()
);
}
unpackTo(_o: EmulationStatusT): void {
_o.activeThreads = this.activeThreads();
_o.reservedMemory = this.reservedMemory();
_o.committedMemory = this.committedMemory();
_o.executedInstructions = this.executedInstructions();
}
}
export class EmulationStatusT implements flatbuffers.IGeneratedObject {
constructor(
public activeThreads: number = 0,
public reservedMemory: bigint = BigInt('0'),
public committedMemory: bigint = BigInt('0'),
public executedInstructions: bigint = BigInt('0')
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return EmulationStatus.createEmulationStatus(builder,
this.activeThreads,
this.reservedMemory,
this.committedMemory,
this.executedInstructions
);
}
}

View File

@@ -0,0 +1,86 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import { ApplicationExit, ApplicationExitT } from '../debugger/application-exit.js';
import { EmulationStatus, EmulationStatusT } from '../debugger/emulation-status.js';
import { GetStateRequest, GetStateRequestT } from '../debugger/get-state-request.js';
import { GetStateResponse, GetStateResponseT } from '../debugger/get-state-response.js';
import { PauseRequest, PauseRequestT } from '../debugger/pause-request.js';
import { ReadMemoryRequest, ReadMemoryRequestT } from '../debugger/read-memory-request.js';
import { ReadMemoryResponse, ReadMemoryResponseT } from '../debugger/read-memory-response.js';
import { ReadRegisterRequest, ReadRegisterRequestT } from '../debugger/read-register-request.js';
import { ReadRegisterResponse, ReadRegisterResponseT } from '../debugger/read-register-response.js';
import { RunRequest, RunRequestT } from '../debugger/run-request.js';
import { WriteMemoryRequest, WriteMemoryRequestT } from '../debugger/write-memory-request.js';
import { WriteMemoryResponse, WriteMemoryResponseT } from '../debugger/write-memory-response.js';
import { WriteRegisterRequest, WriteRegisterRequestT } from '../debugger/write-register-request.js';
import { WriteRegisterResponse, WriteRegisterResponseT } from '../debugger/write-register-response.js';
export enum Event {
NONE = 0,
PauseRequest = 1,
RunRequest = 2,
GetStateRequest = 3,
GetStateResponse = 4,
WriteMemoryRequest = 5,
WriteMemoryResponse = 6,
ReadMemoryRequest = 7,
ReadMemoryResponse = 8,
WriteRegisterRequest = 9,
WriteRegisterResponse = 10,
ReadRegisterRequest = 11,
ReadRegisterResponse = 12,
ApplicationExit = 13,
EmulationStatus = 14
}
export function unionToEvent(
type: Event,
accessor: (obj:ApplicationExit|EmulationStatus|GetStateRequest|GetStateResponse|PauseRequest|ReadMemoryRequest|ReadMemoryResponse|ReadRegisterRequest|ReadRegisterResponse|RunRequest|WriteMemoryRequest|WriteMemoryResponse|WriteRegisterRequest|WriteRegisterResponse) => ApplicationExit|EmulationStatus|GetStateRequest|GetStateResponse|PauseRequest|ReadMemoryRequest|ReadMemoryResponse|ReadRegisterRequest|ReadRegisterResponse|RunRequest|WriteMemoryRequest|WriteMemoryResponse|WriteRegisterRequest|WriteRegisterResponse|null
): ApplicationExit|EmulationStatus|GetStateRequest|GetStateResponse|PauseRequest|ReadMemoryRequest|ReadMemoryResponse|ReadRegisterRequest|ReadRegisterResponse|RunRequest|WriteMemoryRequest|WriteMemoryResponse|WriteRegisterRequest|WriteRegisterResponse|null {
switch(Event[type]) {
case 'NONE': return null;
case 'PauseRequest': return accessor(new PauseRequest())! as PauseRequest;
case 'RunRequest': return accessor(new RunRequest())! as RunRequest;
case 'GetStateRequest': return accessor(new GetStateRequest())! as GetStateRequest;
case 'GetStateResponse': return accessor(new GetStateResponse())! as GetStateResponse;
case 'WriteMemoryRequest': return accessor(new WriteMemoryRequest())! as WriteMemoryRequest;
case 'WriteMemoryResponse': return accessor(new WriteMemoryResponse())! as WriteMemoryResponse;
case 'ReadMemoryRequest': return accessor(new ReadMemoryRequest())! as ReadMemoryRequest;
case 'ReadMemoryResponse': return accessor(new ReadMemoryResponse())! as ReadMemoryResponse;
case 'WriteRegisterRequest': return accessor(new WriteRegisterRequest())! as WriteRegisterRequest;
case 'WriteRegisterResponse': return accessor(new WriteRegisterResponse())! as WriteRegisterResponse;
case 'ReadRegisterRequest': return accessor(new ReadRegisterRequest())! as ReadRegisterRequest;
case 'ReadRegisterResponse': return accessor(new ReadRegisterResponse())! as ReadRegisterResponse;
case 'ApplicationExit': return accessor(new ApplicationExit())! as ApplicationExit;
case 'EmulationStatus': return accessor(new EmulationStatus())! as EmulationStatus;
default: return null;
}
}
export function unionListToEvent(
type: Event,
accessor: (index: number, obj:ApplicationExit|EmulationStatus|GetStateRequest|GetStateResponse|PauseRequest|ReadMemoryRequest|ReadMemoryResponse|ReadRegisterRequest|ReadRegisterResponse|RunRequest|WriteMemoryRequest|WriteMemoryResponse|WriteRegisterRequest|WriteRegisterResponse) => ApplicationExit|EmulationStatus|GetStateRequest|GetStateResponse|PauseRequest|ReadMemoryRequest|ReadMemoryResponse|ReadRegisterRequest|ReadRegisterResponse|RunRequest|WriteMemoryRequest|WriteMemoryResponse|WriteRegisterRequest|WriteRegisterResponse|null,
index: number
): ApplicationExit|EmulationStatus|GetStateRequest|GetStateResponse|PauseRequest|ReadMemoryRequest|ReadMemoryResponse|ReadRegisterRequest|ReadRegisterResponse|RunRequest|WriteMemoryRequest|WriteMemoryResponse|WriteRegisterRequest|WriteRegisterResponse|null {
switch(Event[type]) {
case 'NONE': return null;
case 'PauseRequest': return accessor(index, new PauseRequest())! as PauseRequest;
case 'RunRequest': return accessor(index, new RunRequest())! as RunRequest;
case 'GetStateRequest': return accessor(index, new GetStateRequest())! as GetStateRequest;
case 'GetStateResponse': return accessor(index, new GetStateResponse())! as GetStateResponse;
case 'WriteMemoryRequest': return accessor(index, new WriteMemoryRequest())! as WriteMemoryRequest;
case 'WriteMemoryResponse': return accessor(index, new WriteMemoryResponse())! as WriteMemoryResponse;
case 'ReadMemoryRequest': return accessor(index, new ReadMemoryRequest())! as ReadMemoryRequest;
case 'ReadMemoryResponse': return accessor(index, new ReadMemoryResponse())! as ReadMemoryResponse;
case 'WriteRegisterRequest': return accessor(index, new WriteRegisterRequest())! as WriteRegisterRequest;
case 'WriteRegisterResponse': return accessor(index, new WriteRegisterResponse())! as WriteRegisterResponse;
case 'ReadRegisterRequest': return accessor(index, new ReadRegisterRequest())! as ReadRegisterRequest;
case 'ReadRegisterResponse': return accessor(index, new ReadRegisterResponse())! as ReadRegisterResponse;
case 'ApplicationExit': return accessor(index, new ApplicationExit())! as ApplicationExit;
case 'EmulationStatus': return accessor(index, new EmulationStatus())! as EmulationStatus;
default: return null;
}
}

View File

@@ -0,0 +1,56 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class GetStateRequest implements flatbuffers.IUnpackableObject<GetStateRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):GetStateRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsGetStateRequest(bb:flatbuffers.ByteBuffer, obj?:GetStateRequest):GetStateRequest {
return (obj || new GetStateRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsGetStateRequest(bb:flatbuffers.ByteBuffer, obj?:GetStateRequest):GetStateRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new GetStateRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static startGetStateRequest(builder:flatbuffers.Builder) {
builder.startObject(0);
}
static endGetStateRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createGetStateRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
GetStateRequest.startGetStateRequest(builder);
return GetStateRequest.endGetStateRequest(builder);
}
unpack(): GetStateRequestT {
return new GetStateRequestT();
}
unpackTo(_o: GetStateRequestT): void {}
}
export class GetStateRequestT implements flatbuffers.IGeneratedObject {
constructor(){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return GetStateRequest.createGetStateRequest(builder);
}
}

View File

@@ -0,0 +1,86 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { State } from '../debugger/state.js';
export class GetStateResponse implements flatbuffers.IUnpackableObject<GetStateResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):GetStateResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsGetStateResponse(bb:flatbuffers.ByteBuffer, obj?:GetStateResponse):GetStateResponse {
return (obj || new GetStateResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsGetStateResponse(bb:flatbuffers.ByteBuffer, obj?:GetStateResponse):GetStateResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new GetStateResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
state():State {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : State.None;
}
mutate_state(value:State):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
static startGetStateResponse(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addState(builder:flatbuffers.Builder, state:State) {
builder.addFieldInt32(0, state, State.None);
}
static endGetStateResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createGetStateResponse(builder:flatbuffers.Builder, state:State):flatbuffers.Offset {
GetStateResponse.startGetStateResponse(builder);
GetStateResponse.addState(builder, state);
return GetStateResponse.endGetStateResponse(builder);
}
unpack(): GetStateResponseT {
return new GetStateResponseT(
this.state()
);
}
unpackTo(_o: GetStateResponseT): void {
_o.state = this.state();
}
}
export class GetStateResponseT implements flatbuffers.IGeneratedObject {
constructor(
public state: State = State.None
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return GetStateResponse.createGetStateResponse(builder,
this.state
);
}
}

View File

@@ -0,0 +1,56 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class PauseRequest implements flatbuffers.IUnpackableObject<PauseRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):PauseRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsPauseRequest(bb:flatbuffers.ByteBuffer, obj?:PauseRequest):PauseRequest {
return (obj || new PauseRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsPauseRequest(bb:flatbuffers.ByteBuffer, obj?:PauseRequest):PauseRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new PauseRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static startPauseRequest(builder:flatbuffers.Builder) {
builder.startObject(0);
}
static endPauseRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createPauseRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
PauseRequest.startPauseRequest(builder);
return PauseRequest.endPauseRequest(builder);
}
unpack(): PauseRequestT {
return new PauseRequestT();
}
unpackTo(_o: PauseRequestT): void {}
}
export class PauseRequestT implements flatbuffers.IGeneratedObject {
constructor(){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return PauseRequest.createPauseRequest(builder);
}
}

View File

@@ -0,0 +1,110 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class ReadMemoryRequest implements flatbuffers.IUnpackableObject<ReadMemoryRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ReadMemoryRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsReadMemoryRequest(bb:flatbuffers.ByteBuffer, obj?:ReadMemoryRequest):ReadMemoryRequest {
return (obj || new ReadMemoryRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsReadMemoryRequest(bb:flatbuffers.ByteBuffer, obj?:ReadMemoryRequest):ReadMemoryRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ReadMemoryRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
address():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
mutate_address(value:bigint):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint64(this.bb_pos + offset, value);
return true;
}
size():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0;
}
mutate_size(value:number):boolean {
const offset = this.bb!.__offset(this.bb_pos, 6);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
static startReadMemoryRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addAddress(builder:flatbuffers.Builder, address:bigint) {
builder.addFieldInt64(0, address, BigInt('0'));
}
static addSize(builder:flatbuffers.Builder, size:number) {
builder.addFieldInt32(1, size, 0);
}
static endReadMemoryRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createReadMemoryRequest(builder:flatbuffers.Builder, address:bigint, size:number):flatbuffers.Offset {
ReadMemoryRequest.startReadMemoryRequest(builder);
ReadMemoryRequest.addAddress(builder, address);
ReadMemoryRequest.addSize(builder, size);
return ReadMemoryRequest.endReadMemoryRequest(builder);
}
unpack(): ReadMemoryRequestT {
return new ReadMemoryRequestT(
this.address(),
this.size()
);
}
unpackTo(_o: ReadMemoryRequestT): void {
_o.address = this.address();
_o.size = this.size();
}
}
export class ReadMemoryRequestT implements flatbuffers.IGeneratedObject {
constructor(
public address: bigint = BigInt('0'),
public size: number = 0
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return ReadMemoryRequest.createReadMemoryRequest(builder,
this.address,
this.size
);
}
}

View File

@@ -0,0 +1,123 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class ReadMemoryResponse implements flatbuffers.IUnpackableObject<ReadMemoryResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ReadMemoryResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsReadMemoryResponse(bb:flatbuffers.ByteBuffer, obj?:ReadMemoryResponse):ReadMemoryResponse {
return (obj || new ReadMemoryResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsReadMemoryResponse(bb:flatbuffers.ByteBuffer, obj?:ReadMemoryResponse):ReadMemoryResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ReadMemoryResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
address():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
mutate_address(value:bigint):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint64(this.bb_pos + offset, value);
return true;
}
data(index: number):number|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint8(this.bb!.__vector(this.bb_pos + offset) + index) : 0;
}
dataLength():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
dataArray():Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? new Uint8Array(this.bb!.bytes().buffer, this.bb!.bytes().byteOffset + this.bb!.__vector(this.bb_pos + offset), this.bb!.__vector_len(this.bb_pos + offset)) : null;
}
static startReadMemoryResponse(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addAddress(builder:flatbuffers.Builder, address:bigint) {
builder.addFieldInt64(0, address, BigInt('0'));
}
static addData(builder:flatbuffers.Builder, dataOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, dataOffset, 0);
}
static createDataVector(builder:flatbuffers.Builder, data:number[]|Uint8Array):flatbuffers.Offset {
builder.startVector(1, data.length, 1);
for (let i = data.length - 1; i >= 0; i--) {
builder.addInt8(data[i]!);
}
return builder.endVector();
}
static startDataVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(1, numElems, 1);
}
static endReadMemoryResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createReadMemoryResponse(builder:flatbuffers.Builder, address:bigint, dataOffset:flatbuffers.Offset):flatbuffers.Offset {
ReadMemoryResponse.startReadMemoryResponse(builder);
ReadMemoryResponse.addAddress(builder, address);
ReadMemoryResponse.addData(builder, dataOffset);
return ReadMemoryResponse.endReadMemoryResponse(builder);
}
unpack(): ReadMemoryResponseT {
return new ReadMemoryResponseT(
this.address(),
this.bb!.createScalarList<number>(this.data.bind(this), this.dataLength())
);
}
unpackTo(_o: ReadMemoryResponseT): void {
_o.address = this.address();
_o.data = this.bb!.createScalarList<number>(this.data.bind(this), this.dataLength());
}
}
export class ReadMemoryResponseT implements flatbuffers.IGeneratedObject {
constructor(
public address: bigint = BigInt('0'),
public data: (number)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const data = ReadMemoryResponse.createDataVector(builder, this.data);
return ReadMemoryResponse.createReadMemoryResponse(builder,
this.address,
data
);
}
}

View File

@@ -0,0 +1,85 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class ReadRegisterRequest implements flatbuffers.IUnpackableObject<ReadRegisterRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ReadRegisterRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsReadRegisterRequest(bb:flatbuffers.ByteBuffer, obj?:ReadRegisterRequest):ReadRegisterRequest {
return (obj || new ReadRegisterRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsReadRegisterRequest(bb:flatbuffers.ByteBuffer, obj?:ReadRegisterRequest):ReadRegisterRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ReadRegisterRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
register():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0;
}
mutate_register(value:number):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
static startReadRegisterRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addRegister(builder:flatbuffers.Builder, register:number) {
builder.addFieldInt32(0, register, 0);
}
static endReadRegisterRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createReadRegisterRequest(builder:flatbuffers.Builder, register:number):flatbuffers.Offset {
ReadRegisterRequest.startReadRegisterRequest(builder);
ReadRegisterRequest.addRegister(builder, register);
return ReadRegisterRequest.endReadRegisterRequest(builder);
}
unpack(): ReadRegisterRequestT {
return new ReadRegisterRequestT(
this.register()
);
}
unpackTo(_o: ReadRegisterRequestT): void {
_o.register = this.register();
}
}
export class ReadRegisterRequestT implements flatbuffers.IGeneratedObject {
constructor(
public register: number = 0
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return ReadRegisterRequest.createReadRegisterRequest(builder,
this.register
);
}
}

View File

@@ -0,0 +1,123 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class ReadRegisterResponse implements flatbuffers.IUnpackableObject<ReadRegisterResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ReadRegisterResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsReadRegisterResponse(bb:flatbuffers.ByteBuffer, obj?:ReadRegisterResponse):ReadRegisterResponse {
return (obj || new ReadRegisterResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsReadRegisterResponse(bb:flatbuffers.ByteBuffer, obj?:ReadRegisterResponse):ReadRegisterResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ReadRegisterResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
register():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0;
}
mutate_register(value:number):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
data(index: number):number|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint8(this.bb!.__vector(this.bb_pos + offset) + index) : 0;
}
dataLength():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
dataArray():Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? new Uint8Array(this.bb!.bytes().buffer, this.bb!.bytes().byteOffset + this.bb!.__vector(this.bb_pos + offset), this.bb!.__vector_len(this.bb_pos + offset)) : null;
}
static startReadRegisterResponse(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addRegister(builder:flatbuffers.Builder, register:number) {
builder.addFieldInt32(0, register, 0);
}
static addData(builder:flatbuffers.Builder, dataOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, dataOffset, 0);
}
static createDataVector(builder:flatbuffers.Builder, data:number[]|Uint8Array):flatbuffers.Offset {
builder.startVector(1, data.length, 1);
for (let i = data.length - 1; i >= 0; i--) {
builder.addInt8(data[i]!);
}
return builder.endVector();
}
static startDataVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(1, numElems, 1);
}
static endReadRegisterResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createReadRegisterResponse(builder:flatbuffers.Builder, register:number, dataOffset:flatbuffers.Offset):flatbuffers.Offset {
ReadRegisterResponse.startReadRegisterResponse(builder);
ReadRegisterResponse.addRegister(builder, register);
ReadRegisterResponse.addData(builder, dataOffset);
return ReadRegisterResponse.endReadRegisterResponse(builder);
}
unpack(): ReadRegisterResponseT {
return new ReadRegisterResponseT(
this.register(),
this.bb!.createScalarList<number>(this.data.bind(this), this.dataLength())
);
}
unpackTo(_o: ReadRegisterResponseT): void {
_o.register = this.register();
_o.data = this.bb!.createScalarList<number>(this.data.bind(this), this.dataLength());
}
}
export class ReadRegisterResponseT implements flatbuffers.IGeneratedObject {
constructor(
public register: number = 0,
public data: (number)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const data = ReadRegisterResponse.createDataVector(builder, this.data);
return ReadRegisterResponse.createReadRegisterResponse(builder,
this.register,
data
);
}
}

View File

@@ -0,0 +1,85 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class RunRequest implements flatbuffers.IUnpackableObject<RunRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):RunRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsRunRequest(bb:flatbuffers.ByteBuffer, obj?:RunRequest):RunRequest {
return (obj || new RunRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsRunRequest(bb:flatbuffers.ByteBuffer, obj?:RunRequest):RunRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new RunRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
singleStep():boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
mutate_single_step(value:boolean):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeInt8(this.bb_pos + offset, +value);
return true;
}
static startRunRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addSingleStep(builder:flatbuffers.Builder, singleStep:boolean) {
builder.addFieldInt8(0, +singleStep, +false);
}
static endRunRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createRunRequest(builder:flatbuffers.Builder, singleStep:boolean):flatbuffers.Offset {
RunRequest.startRunRequest(builder);
RunRequest.addSingleStep(builder, singleStep);
return RunRequest.endRunRequest(builder);
}
unpack(): RunRequestT {
return new RunRequestT(
this.singleStep()
);
}
unpackTo(_o: RunRequestT): void {
_o.singleStep = this.singleStep();
}
}
export class RunRequestT implements flatbuffers.IGeneratedObject {
constructor(
public singleStep: boolean = false
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return RunRequest.createRunRequest(builder,
this.singleStep
);
}
}

View File

@@ -0,0 +1,9 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
export enum State {
None = 0,
Running = 1,
Paused = 2
}

View File

@@ -0,0 +1,123 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class WriteMemoryRequest implements flatbuffers.IUnpackableObject<WriteMemoryRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):WriteMemoryRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsWriteMemoryRequest(bb:flatbuffers.ByteBuffer, obj?:WriteMemoryRequest):WriteMemoryRequest {
return (obj || new WriteMemoryRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsWriteMemoryRequest(bb:flatbuffers.ByteBuffer, obj?:WriteMemoryRequest):WriteMemoryRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new WriteMemoryRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
address():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
mutate_address(value:bigint):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint64(this.bb_pos + offset, value);
return true;
}
data(index: number):number|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint8(this.bb!.__vector(this.bb_pos + offset) + index) : 0;
}
dataLength():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
dataArray():Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? new Uint8Array(this.bb!.bytes().buffer, this.bb!.bytes().byteOffset + this.bb!.__vector(this.bb_pos + offset), this.bb!.__vector_len(this.bb_pos + offset)) : null;
}
static startWriteMemoryRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addAddress(builder:flatbuffers.Builder, address:bigint) {
builder.addFieldInt64(0, address, BigInt('0'));
}
static addData(builder:flatbuffers.Builder, dataOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, dataOffset, 0);
}
static createDataVector(builder:flatbuffers.Builder, data:number[]|Uint8Array):flatbuffers.Offset {
builder.startVector(1, data.length, 1);
for (let i = data.length - 1; i >= 0; i--) {
builder.addInt8(data[i]!);
}
return builder.endVector();
}
static startDataVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(1, numElems, 1);
}
static endWriteMemoryRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createWriteMemoryRequest(builder:flatbuffers.Builder, address:bigint, dataOffset:flatbuffers.Offset):flatbuffers.Offset {
WriteMemoryRequest.startWriteMemoryRequest(builder);
WriteMemoryRequest.addAddress(builder, address);
WriteMemoryRequest.addData(builder, dataOffset);
return WriteMemoryRequest.endWriteMemoryRequest(builder);
}
unpack(): WriteMemoryRequestT {
return new WriteMemoryRequestT(
this.address(),
this.bb!.createScalarList<number>(this.data.bind(this), this.dataLength())
);
}
unpackTo(_o: WriteMemoryRequestT): void {
_o.address = this.address();
_o.data = this.bb!.createScalarList<number>(this.data.bind(this), this.dataLength());
}
}
export class WriteMemoryRequestT implements flatbuffers.IGeneratedObject {
constructor(
public address: bigint = BigInt('0'),
public data: (number)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const data = WriteMemoryRequest.createDataVector(builder, this.data);
return WriteMemoryRequest.createWriteMemoryRequest(builder,
this.address,
data
);
}
}

View File

@@ -0,0 +1,135 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class WriteMemoryResponse implements flatbuffers.IUnpackableObject<WriteMemoryResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):WriteMemoryResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsWriteMemoryResponse(bb:flatbuffers.ByteBuffer, obj?:WriteMemoryResponse):WriteMemoryResponse {
return (obj || new WriteMemoryResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsWriteMemoryResponse(bb:flatbuffers.ByteBuffer, obj?:WriteMemoryResponse):WriteMemoryResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new WriteMemoryResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
address():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
mutate_address(value:bigint):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint64(this.bb_pos + offset, value);
return true;
}
size():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0;
}
mutate_size(value:number):boolean {
const offset = this.bb!.__offset(this.bb_pos, 6);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
success():boolean {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
mutate_success(value:boolean):boolean {
const offset = this.bb!.__offset(this.bb_pos, 8);
if (offset === 0) {
return false;
}
this.bb!.writeInt8(this.bb_pos + offset, +value);
return true;
}
static startWriteMemoryResponse(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addAddress(builder:flatbuffers.Builder, address:bigint) {
builder.addFieldInt64(0, address, BigInt('0'));
}
static addSize(builder:flatbuffers.Builder, size:number) {
builder.addFieldInt32(1, size, 0);
}
static addSuccess(builder:flatbuffers.Builder, success:boolean) {
builder.addFieldInt8(2, +success, +false);
}
static endWriteMemoryResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createWriteMemoryResponse(builder:flatbuffers.Builder, address:bigint, size:number, success:boolean):flatbuffers.Offset {
WriteMemoryResponse.startWriteMemoryResponse(builder);
WriteMemoryResponse.addAddress(builder, address);
WriteMemoryResponse.addSize(builder, size);
WriteMemoryResponse.addSuccess(builder, success);
return WriteMemoryResponse.endWriteMemoryResponse(builder);
}
unpack(): WriteMemoryResponseT {
return new WriteMemoryResponseT(
this.address(),
this.size(),
this.success()
);
}
unpackTo(_o: WriteMemoryResponseT): void {
_o.address = this.address();
_o.size = this.size();
_o.success = this.success();
}
}
export class WriteMemoryResponseT implements flatbuffers.IGeneratedObject {
constructor(
public address: bigint = BigInt('0'),
public size: number = 0,
public success: boolean = false
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return WriteMemoryResponse.createWriteMemoryResponse(builder,
this.address,
this.size,
this.success
);
}
}

View File

@@ -0,0 +1,123 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class WriteRegisterRequest implements flatbuffers.IUnpackableObject<WriteRegisterRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):WriteRegisterRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsWriteRegisterRequest(bb:flatbuffers.ByteBuffer, obj?:WriteRegisterRequest):WriteRegisterRequest {
return (obj || new WriteRegisterRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsWriteRegisterRequest(bb:flatbuffers.ByteBuffer, obj?:WriteRegisterRequest):WriteRegisterRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new WriteRegisterRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
register():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0;
}
mutate_register(value:number):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
data(index: number):number|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint8(this.bb!.__vector(this.bb_pos + offset) + index) : 0;
}
dataLength():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
dataArray():Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? new Uint8Array(this.bb!.bytes().buffer, this.bb!.bytes().byteOffset + this.bb!.__vector(this.bb_pos + offset), this.bb!.__vector_len(this.bb_pos + offset)) : null;
}
static startWriteRegisterRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addRegister(builder:flatbuffers.Builder, register:number) {
builder.addFieldInt32(0, register, 0);
}
static addData(builder:flatbuffers.Builder, dataOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, dataOffset, 0);
}
static createDataVector(builder:flatbuffers.Builder, data:number[]|Uint8Array):flatbuffers.Offset {
builder.startVector(1, data.length, 1);
for (let i = data.length - 1; i >= 0; i--) {
builder.addInt8(data[i]!);
}
return builder.endVector();
}
static startDataVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(1, numElems, 1);
}
static endWriteRegisterRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createWriteRegisterRequest(builder:flatbuffers.Builder, register:number, dataOffset:flatbuffers.Offset):flatbuffers.Offset {
WriteRegisterRequest.startWriteRegisterRequest(builder);
WriteRegisterRequest.addRegister(builder, register);
WriteRegisterRequest.addData(builder, dataOffset);
return WriteRegisterRequest.endWriteRegisterRequest(builder);
}
unpack(): WriteRegisterRequestT {
return new WriteRegisterRequestT(
this.register(),
this.bb!.createScalarList<number>(this.data.bind(this), this.dataLength())
);
}
unpackTo(_o: WriteRegisterRequestT): void {
_o.register = this.register();
_o.data = this.bb!.createScalarList<number>(this.data.bind(this), this.dataLength());
}
}
export class WriteRegisterRequestT implements flatbuffers.IGeneratedObject {
constructor(
public register: number = 0,
public data: (number)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const data = WriteRegisterRequest.createDataVector(builder, this.data);
return WriteRegisterRequest.createWriteRegisterRequest(builder,
this.register,
data
);
}
}

View File

@@ -0,0 +1,135 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class WriteRegisterResponse implements flatbuffers.IUnpackableObject<WriteRegisterResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):WriteRegisterResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsWriteRegisterResponse(bb:flatbuffers.ByteBuffer, obj?:WriteRegisterResponse):WriteRegisterResponse {
return (obj || new WriteRegisterResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsWriteRegisterResponse(bb:flatbuffers.ByteBuffer, obj?:WriteRegisterResponse):WriteRegisterResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new WriteRegisterResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
register():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0;
}
mutate_register(value:number):boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
size():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0;
}
mutate_size(value:number):boolean {
const offset = this.bb!.__offset(this.bb_pos, 6);
if (offset === 0) {
return false;
}
this.bb!.writeUint32(this.bb_pos + offset, value);
return true;
}
success():boolean {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
mutate_success(value:boolean):boolean {
const offset = this.bb!.__offset(this.bb_pos, 8);
if (offset === 0) {
return false;
}
this.bb!.writeInt8(this.bb_pos + offset, +value);
return true;
}
static startWriteRegisterResponse(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addRegister(builder:flatbuffers.Builder, register:number) {
builder.addFieldInt32(0, register, 0);
}
static addSize(builder:flatbuffers.Builder, size:number) {
builder.addFieldInt32(1, size, 0);
}
static addSuccess(builder:flatbuffers.Builder, success:boolean) {
builder.addFieldInt8(2, +success, +false);
}
static endWriteRegisterResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createWriteRegisterResponse(builder:flatbuffers.Builder, register:number, size:number, success:boolean):flatbuffers.Offset {
WriteRegisterResponse.startWriteRegisterResponse(builder);
WriteRegisterResponse.addRegister(builder, register);
WriteRegisterResponse.addSize(builder, size);
WriteRegisterResponse.addSuccess(builder, success);
return WriteRegisterResponse.endWriteRegisterResponse(builder);
}
unpack(): WriteRegisterResponseT {
return new WriteRegisterResponseT(
this.register(),
this.size(),
this.success()
);
}
unpackTo(_o: WriteRegisterResponseT): void {
_o.register = this.register();
_o.size = this.size();
_o.success = this.success();
}
}
export class WriteRegisterResponseT implements flatbuffers.IGeneratedObject {
constructor(
public register: number = 0,
public size: number = 0,
public success: boolean = false
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return WriteRegisterResponse.createWriteRegisterResponse(builder,
this.register,
this.size,
this.success
);
}
}

5
page/src/fb/events.ts Normal file
View File

@@ -0,0 +1,5 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
export * as Debugger from './debugger.js';

View File

@@ -0,0 +1,656 @@
import React from "react";
import {
Folder,
FolderElement,
FolderElementType,
trimFilename,
} from "./components/folder";
import { Filesystem } from "./filesystem";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "./components/ui/button";
import { Input } from "./components/ui/input";
import { DialogDescription } from "@radix-ui/react-dialog";
import Dropzone from "react-dropzone";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { HouseFill } from "react-bootstrap-icons";
import { parsePeIcon } from "./pe-icon-parser";
export interface FilesystemExplorerProps {
filesystem: Filesystem;
runFile: (file: string) => void;
resetFilesys: () => void;
path: string[];
iconCache: Map<string, string | null>;
}
export interface FilesystemExplorerState {
path: string[];
createFolder: boolean;
resetFilesys: boolean;
errorText: string;
removeFile: string;
renameFile: string;
}
function makeFullPath(path: string[]) {
return "/root/filesys/" + path.join("/");
}
function makeFullPathAndJoin(path: string[], element: string) {
return makeFullPath([...path, element]);
}
function makeFullPathWithState(
state: FilesystemExplorerState,
element: string,
) {
return makeFullPathAndJoin(state.path, element);
}
function relativePathToWindowsPath(fullPath: string) {
if (fullPath.length == 0) {
return fullPath;
}
const drive = fullPath.substring(0, 1);
const rest = fullPath.substring(1);
return `${drive}:${rest}`;
}
function makeRelativePathWithState(
state: FilesystemExplorerState,
element: string,
) {
return [...state.path, element].join("/");
}
function makeWindowsPathWithState(
state: FilesystemExplorerState,
element: string,
) {
const fullPath = makeRelativePathWithState(state, element);
return relativePathToWindowsPath(fullPath);
}
function getFolderElements(filesystem: Filesystem, path: string[]) {
const fullPath = makeFullPath(path);
const files = filesystem.readDir(fullPath);
return files
.filter((f) => {
if (f == ".") {
return false;
}
if (path.length == 0 && f == "..") {
return false;
}
return true;
})
.map((f) => {
const element: FolderElement = {
name: f,
type: filesystem.isFolder(`${fullPath}/${f}`)
? FolderElementType.Folder
: FolderElementType.File,
};
return element;
});
}
interface FileWithData {
file: File;
data: ArrayBuffer;
}
function readFile(file: File): Promise<FileWithData> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (reader.readyState === FileReader.DONE) {
resolve({
file,
data: reader.result as ArrayBuffer,
});
}
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
async function readFiles(files: FileList | File[]): Promise<FileWithData[]> {
const promises = [];
for (let i = 0; i < files.length; i++) {
promises.push(readFile(files[i]));
}
return Promise.all(promises);
}
function selectFiles(): Promise<FileList> {
return new Promise((resolve) => {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".exe";
fileInput.addEventListener("change", function (event) {
const files = (event as any).target.files as FileList;
resolve(files);
});
fileInput.click();
});
}
function getPeIcon(
filesystem: Filesystem,
file: string,
cache: Map<string, string | null>,
) {
if (!file || !file.endsWith(".exe")) {
return null;
}
const cachedValue = cache.get(file);
if (cachedValue) {
return cachedValue;
}
const data = filesystem.readFile(file);
const icon = parsePeIcon(data);
cache.set(file, icon);
return icon;
}
interface BreadcrumbElement {
node: React.ReactNode;
targetPath: string[];
}
function isGoodPath(path: any) {
return typeof path === "string" && path.length > 0;
}
function trimLeadingSlash(path: string) {
if (path.startsWith("/")) {
return path.substring(1);
}
return path;
}
function getFileName(file: File) {
const fileObj = file as any;
const properties = ["relativePath", "webkitRelativePath", "name"];
for (let i = 0; i < properties.length; ++i) {
const prop = properties[i];
if (prop in fileObj) {
const relativePath = fileObj[prop];
if (isGoodPath(relativePath)) {
return trimLeadingSlash(relativePath);
}
}
}
return file.name;
}
function generateBreadcrumbElements(path: string[]): BreadcrumbElement[] {
const elements = path.map((p, index) => {
const e: BreadcrumbElement = {
node: p,
targetPath: path.slice(0, index + 1),
};
return e;
});
elements.unshift({
node: <HouseFill />,
targetPath: [],
});
return elements;
}
export class FilesystemExplorer extends React.Component<
FilesystemExplorerProps,
FilesystemExplorerState
> {
constructor(props: FilesystemExplorerProps) {
super(props);
this._onAddFiles = this._onAddFiles.bind(this);
this._uploadFiles = this._uploadFiles.bind(this);
this._onElementSelect = this._onElementSelect.bind(this);
this.state = {
path: this.props.path,
createFolder: false,
resetFilesys: false,
errorText: "",
removeFile: "",
renameFile: "",
};
}
_showError(errorText: string) {
this.setState({ errorText });
}
_onElementSelect(element: FolderElement) {
if (element.type != FolderElementType.Folder) {
if (element.name.endsWith(".exe")) {
const file = makeWindowsPathWithState(this.state, element.name);
this.props.runFile(file);
}
return;
}
this.setState((s) => {
const path = [...s.path];
if (element.name == "..") {
path.pop();
} else {
path.push(element.name);
}
return {
path,
};
});
}
async _onFileRename(file: string, newFile: string) {
const oldPath = makeFullPathWithState(this.state, file);
const newPath = makeFullPathWithState(this.state, newFile);
this.setState({ renameFile: "" });
this._removeFromCache(file);
this._removeFromCache(newFile);
await this.props.filesystem.rename(oldPath, newPath);
this.forceUpdate();
}
async _onAddFiles() {
const files = await selectFiles();
await this._uploadFiles(files);
}
async _onFolderCreate(name: string) {
this.setState({ createFolder: false });
name = name.toLowerCase();
if (name.length == 0) {
return;
}
if (name.includes("/") || name.includes("\\")) {
this._showError("Folder must not contain special characters");
return;
}
if (this.state.path.length == 0 && name.length > 1) {
this._showError("Drives must be a single letter");
return;
}
const fullPath = makeFullPathWithState(this.state, name);
await this.props.filesystem.createFolder(fullPath);
this.forceUpdate();
}
async _uploadFiles(files: FileList | File[]) {
if (files.length == 0) {
return;
}
if (this.state.path.length == 0) {
this._showError("Files must be within a drive");
return;
}
const fileData = (await readFiles(files)).map((f) => {
const name = getFileName(f.file);
return {
name: makeFullPathWithState(this.state, name.toLowerCase()),
data: f.data,
};
});
fileData.forEach((d) => {
this._removeFromCache(d.name);
});
await this.props.filesystem.storeFiles(fileData);
this.forceUpdate();
}
_renderCreateFolderDialog() {
return (
<Dialog
open={this.state.createFolder}
onOpenChange={(open) => this.setState({ createFolder: open })}
>
<DialogContent className="sm:max-w-[425px]">
<form
onSubmit={(e) => {
const folderName = (e.target as any).elements.name.value;
this._onFolderCreate(folderName);
e.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>Create new folder</DialogTitle>
<DialogDescription className="hidden">
Create new folder
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Input id="name" defaultValue="New Folder" />
</div>
<DialogFooter>
<Button type="submit" className="fancy rounded-lg">
Create
</Button>
<DialogClose asChild>
<Button variant="secondary" className="fancy rounded-lg">
Cancel
</Button>
</DialogClose>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
_renderRenameDialog() {
return (
<Dialog
open={this.state.renameFile.length > 0}
onOpenChange={(open) => (open ? {} : this.setState({ renameFile: "" }))}
>
<DialogContent className="sm:max-w-[425px]">
<form
onSubmit={(e) => {
const newName = (e.target as any).elements.name.value;
this._onFileRename(this.state.renameFile, newName);
e.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>
Rename {trimFilename(this.state.renameFile)}
</DialogTitle>
<DialogDescription className="hidden">
Rename {this.state.renameFile}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Input id="name" defaultValue={this.state.renameFile} />
</div>
<DialogFooter>
<Button type="submit" className="fancy rounded-lg">
Rename
</Button>
<DialogClose asChild>
<Button variant="secondary" className="fancy rounded-lg">
Cancel
</Button>
</DialogClose>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
_renderErrorDialog() {
return (
<Dialog
open={this.state.errorText.length > 0}
onOpenChange={(open) => (open ? {} : this.setState({ errorText: "" }))}
>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Error</DialogTitle>
<DialogDescription className="hidden">
Error: {this.state.errorText}
</DialogDescription>
</DialogHeader>
<div className="py-4">{this.state.errorText}</div>
<DialogFooter>
<Button
variant="destructive"
className="fancy rounded-lg"
onClick={() => this.setState({ errorText: "" })}
>
Ok
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
_renderRemoveDialog() {
return (
<Dialog
open={this.state.removeFile.length > 0}
onOpenChange={(open) => (open ? {} : this.setState({ removeFile: "" }))}
>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>
Delete {trimFilename(this.state.removeFile)}?
</DialogTitle>
<DialogDescription className="hidden">
Delete {this.state.removeFile}
</DialogDescription>
</DialogHeader>
<div className="py-4">
Are you sure you want to delete{" "}
<b className="break-all">
{makeWindowsPathWithState(this.state, this.state.removeFile)}
</b>
</div>
<DialogFooter>
<Button
variant="destructive"
className="fancy rounded-lg"
onClick={() => {
const file = makeFullPathWithState(
this.state,
this.state.removeFile,
);
this.setState({ removeFile: "" });
this._removeFromCache(file);
this.props.filesystem
.unlink(file)
.then(() => this.forceUpdate());
}}
>
Delete
</Button>
<Button
variant="secondary"
className="fancy rounded-lg"
onClick={() => {
this.setState({ removeFile: "" });
}}
>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
_renderResetDialog() {
return (
<Dialog
open={this.state.resetFilesys}
onOpenChange={(open) => this.setState({ resetFilesys: open })}
>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Reset filesystem</DialogTitle>
<DialogDescription className="hidden">
Reset filesystem
</DialogDescription>
</DialogHeader>
<div className="py-4">
Are you sure you want to reset the filesystem?
</div>
<DialogFooter>
<Button
variant="destructive"
className="fancy rounded-lg"
onClick={() => {
this.setState({ resetFilesys: false });
this.props.iconCache.clear();
this.props.resetFilesys();
}}
>
Reset
</Button>
<Button
variant="secondary"
className="fancy rounded-lg"
onClick={() => {
this.setState({ resetFilesys: false });
}}
>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
_renderBreadcrumbElements() {
const elements = generateBreadcrumbElements(this.state.path);
return elements.map((e, index) => {
if (index == this.state.path.length) {
return (
<BreadcrumbItem key={`breadcrumb-item-${index}`}>
<BreadcrumbPage key={`breadcrumb-page-${index}`}>
{e.node}
</BreadcrumbPage>
</BreadcrumbItem>
);
}
const navigate = () => this.setState({ path: e.targetPath });
return [
<BreadcrumbItem key={`breadcrumb-item-${index}`}>
<BreadcrumbLink key={`breadcrumb-link-${index}`} onClick={navigate}>
{e.node}
</BreadcrumbLink>
</BreadcrumbItem>,
<BreadcrumbSeparator key={`breadcrumb-separator-${index}`} />,
];
});
}
_renderBreadCrumb() {
return (
<Breadcrumb>
<BreadcrumbList>{this._renderBreadcrumbElements()}</BreadcrumbList>
</Breadcrumb>
);
}
_removeFromCache(file: string) {
this.props.iconCache.delete(file);
}
render() {
const elements = getFolderElements(this.props.filesystem, this.state.path);
return (
<>
{this._renderCreateFolderDialog()}
{this._renderRenameDialog()}
{this._renderErrorDialog()}
{this._renderRemoveDialog()}
{this._renderResetDialog()}
<div className="flex flex-row w-full items-center gap-3">
<div className="whitespace-nowrap">{this._renderBreadCrumb()}</div>
<div className="flex-1 text-right">
<Button
onClick={() => this.setState({ resetFilesys: true })}
variant="destructive"
size="sm"
className="fancy rounded-lg"
>
Reset
</Button>
</div>
</div>
<Dropzone onDrop={this._uploadFiles} noClick={true}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
<Folder
elements={elements}
clickHandler={this._onElementSelect}
createFolderHandler={() =>
this.setState({ createFolder: true })
}
removeElementHandler={(e) =>
this.setState({ removeFile: e.name })
}
renameElementHandler={(e) =>
this.setState({ renameFile: e.name })
}
addFilesHandler={this._onAddFiles}
iconReader={(e) =>
getPeIcon(
this.props.filesystem,
makeFullPathWithState(this.state, e.name),
this.props.iconCache,
)
}
/>
</div>
)}
</Dropzone>
</>
);
}
}

243
page/src/filesystem.ts Normal file
View File

@@ -0,0 +1,243 @@
import { parseZipFile, ProgressHandler } from "./zip-file";
import idbfsModule, { MainModule } from "@irori/idbfs";
type DownloadProgressHandler = (
receivedBytes: number,
totalBytes: number,
) => void;
function fetchFilesystemZip(progressCallback: DownloadProgressHandler) {
return fetch("./root.zip", {
method: "GET",
headers: {
"Content-Type": "application/octet-stream",
},
}).then((response) => {
const maybeReader = response.body?.getReader();
if (!maybeReader) {
throw new Error("Bad reader");
}
const reader = maybeReader;
const contentLength = parseInt(
response.headers?.get("Content-Length") || "0",
);
let receivedLength = 0;
let chunks: Uint8Array<ArrayBufferLike>[] = [];
function processData(
res: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>,
): Promise<ArrayBuffer> {
if (res.value) {
chunks.push(res.value);
receivedLength += res.value.length;
}
progressCallback(receivedLength, contentLength);
if (!res.done) {
return reader.read().then(processData);
}
const chunksAll = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
chunksAll.set(new Uint8Array(chunk), position);
position += chunk.length;
}
return Promise.resolve(chunksAll.buffer);
}
return reader.read().then(processData);
});
}
async function fetchFilesystem(
progressHandler: ProgressHandler,
downloadProgressHandler: DownloadProgressHandler,
) {
const filesys = await fetchFilesystemZip(downloadProgressHandler);
return await parseZipFile(filesys, progressHandler);
}
function synchronizeIDBFS(idbfs: MainModule, populate: boolean) {
return new Promise<void>((resolve, reject) => {
idbfs.FS.syncfs(populate, function (err: any) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
async function initializeIDBFS() {
const idbfs = await idbfsModule();
idbfs.FS.mkdir("/root");
idbfs.FS.mount(idbfs.IDBFS, {}, "/root");
await synchronizeIDBFS(idbfs, true);
return idbfs;
}
export interface FileWithData {
name: string;
data: ArrayBuffer;
}
function deleteDatabase(dbName: string) {
return new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase(dbName);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Error deleting database ${dbName}.`));
};
request.onblocked = () => {
reject(new Error(`Deletion of database ${dbName} blocked.`));
};
});
}
function filterPseudoDir(e: string) {
return e != "." && e != "..";
}
export class Filesystem {
private idbfs: MainModule;
constructor(idbfs: MainModule) {
this.idbfs = idbfs;
}
_storeFile(file: FileWithData) {
if (file.name.includes("/")) {
const folder = file.name.split("/").slice(0, -1).join("/");
this._createFolder(folder);
}
const buffer = new Uint8Array(file.data);
this.idbfs.FS.writeFile(file.name, buffer);
}
readFile(file: string): Uint8Array {
return this.idbfs.FS.readFile(file);
}
async storeFiles(files: FileWithData[]) {
files.forEach((f) => {
this._storeFile(f);
});
await this.sync();
}
_unlinkRecursive(element: string) {
if (!this.isFolder(element)) {
this.idbfs.FS.unlink(element);
return;
}
this.readDir(element) //
.filter(filterPseudoDir)
.forEach((e) => {
this._unlinkRecursive(`${element}/${e}`);
});
this.idbfs.FS.rmdir(element);
}
async rename(oldFile: string, newFile: string) {
this.idbfs.FS.rename(oldFile, newFile);
await this.sync();
}
async unlink(file: string) {
this._unlinkRecursive(file);
await this.sync();
}
_createFolder(folder: string) {
this.idbfs.FS.mkdirTree(folder, 0o777);
}
async createFolder(folder: string) {
this._createFolder(folder);
await this.sync();
}
async sync() {
await synchronizeIDBFS(this.idbfs, false);
}
readDir(dir: string): string[] {
return this.idbfs.FS.readdir(dir);
}
stat(file: string) {
return this.idbfs.FS.stat(file, false);
}
isFolder(file: string) {
return (this.stat(file).mode & 0x4000) != 0;
}
async delete() {
this.readDir("/root") //
.filter(filterPseudoDir) //
.forEach((e) => {
try {
this._unlinkRecursive(e);
} catch (_) {}
});
await this.sync();
try {
await deleteDatabase("/root");
} catch (e) {}
}
}
export async function setupFilesystem(
progressHandler: ProgressHandler,
downloadProgressHandler: DownloadProgressHandler,
) {
const idbfs = await initializeIDBFS();
const fs = new Filesystem(idbfs);
if (idbfs.FS.analyzePath("/root/api-set.bin", false).exists) {
return fs;
}
const filesystem = await fetchFilesystem(
progressHandler,
downloadProgressHandler,
);
filesystem.forEach((e) => {
if (idbfs.FS.analyzePath("/" + e.name, false).exists) {
return;
}
if (e.name.endsWith("/")) {
idbfs.FS.mkdir("/" + e.name.slice(0, -1));
} else {
const buffer = new Uint8Array(e.data);
idbfs.FS.writeFile("/" + e.name, buffer);
}
});
await fs.sync();
return fs;
}

View File

@@ -0,0 +1,21 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

122
page/src/index.css Normal file
View File

@@ -0,0 +1,122 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.5rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.64 0.192 253.32);
--primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.623 0.214 259.815);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.623 0.214 259.815);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.623 0.214 259.815);
}
:root,
.dark {
--background: oklch(0.19 0.004 286.01);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(62.358% 0.20636 255.147);
--primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.69 0.1953 33.18);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.488 0.243 264.376);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.546 0.245 262.881);
--sidebar-primary-foreground: oklch(0.379 0.146 265.522);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.488 0.243 264.376);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

365
page/src/landing-page.tsx Normal file
View File

@@ -0,0 +1,365 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Shield,
FileCode,
Layers,
Cpu,
Database,
Terminal,
ExternalLink,
Github,
Play,
ArrowRight,
Code,
Zap,
Target,
Users,
BookOpen,
Download,
} from "lucide-react";
import { Header } from "./Header";
export function LandingPage() {
const features = [
{
icon: <Cpu className="h-8 w-8" />,
title: "Syscall-Level Emulation",
description:
"Operates at syscall level, leveraging existing system DLLs instead of reimplementing Windows APIs",
accent: "from-cyan-500 to-blue-500",
},
{
icon: <Database className="h-8 w-8" />,
title: "Advanced Memory Management",
description:
"Supports Windows-specific memory types including reserved, committed, built on top of Unicorn's memory management",
accent: "from-purple-500 to-pink-500",
},
{
icon: <FileCode className="h-8 w-8" />,
title: "Complete PE Loading",
description:
"Handles executable and DLL loading with proper memory mapping, relocations, and TLS",
accent: "from-lime-400 to-green-500",
},
{
icon: <Shield className="h-8 w-8" />,
title: "Exception Handling",
description:
"Implements Windows structured exception handling (SEH) with proper exception dispatcher and unwinding support",
accent: "from-orange-400 to-red-500",
},
{
icon: <Layers className="h-8 w-8" />,
title: "Threading Support",
description: "Provides a scheduled (round-robin) threading model",
accent: "from-teal-500 to-blue-500",
},
{
icon: <Terminal className="h-8 w-8" />,
title: "Debugging Interface",
description:
"Implements GDB serial protocol for integration with common debugging tools",
accent: "from-purple-500 to-indigo-500",
},
];
const useCases = [
{
icon: <Shield className="h-6 w-6" />,
title: "Security Research",
description:
"Analyze malware and security vulnerabilities in a controlled environment",
},
{
icon: <Code className="h-6 w-6" />,
title: "DRM Research",
description:
"Study digital rights management systems and protection mechanisms",
},
{
icon: <Target className="h-6 w-6" />,
title: "Malware Analysis",
description:
"Reverse engineer malicious software with full process control",
},
];
const stats = [
{ value: "100%", label: "Open Source" },
{ value: "C++", label: "High Performance" },
{ value: "GDB", label: "Debug Protocol" },
{ value: "64 bit", label: "Native PE Loading" },
];
return (
<>
<Header
title="Sogen - Windows User Space Emulator"
description="Sogen is a high-performance Windows user space emulator that can emulate windows processes. It is ideal for security-, DRM- or malware research."
/>
<div className="flex flex-col min-h-screen bg-gradient-to-br from-zinc-900 via-neutral-900 to-black">
{/* Hero Section with Animated Background */}
<section className="relative overflow-hidden">
{/* Animated Background Elements */}
<div className="absolute inset-0">
<div className="absolute top-20 left-10 w-72 h-72 bg-blue-500/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute top-40 right-20 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-pulse delay-1000"></div>
<div className="absolute bottom-20 left-1/3 w-80 h-80 bg-cyan-500/10 rounded-full blur-3xl animate-pulse delay-2000"></div>
</div>
<div className="relative container mx-auto min-h-[100dvh] p-4 flex items-center xl:min-h-0 xl:px-6 xl:py-32">
<div className="text-center space-y-8 max-w-4xl mx-auto">
{/* Main Headline */}
<h1 className="text-5xl md:text-7xl font-bold bg-gradient-to-r from-white via-blue-100 to-cyan-200 bg-clip-text text-transparent leading-tight">
Sogen
</h1>
<p className="text-xl md:text-2xl text-neutral-300 font-light leading-relaxed">
Revolutionary Windows user space emulator.
<br />
<span className="text-blue-400">
Perfect for security research, malware analysis, and DRM
research.
</span>
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-8">
<a href="#/playground">
<Button
size="lg"
className="bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white border-0 px-8 py-6 text-lg font-semibold group transition-all duration-300 transform hover:scale-105"
>
<Play className="mr-2 h-5 w-5 group-hover:scale-110 transition-transform" />
Try Online
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</Button>
</a>
<a href="https://github.com/momo5502/sogen" target="_blank">
<Button
size="lg"
variant="outline"
className="border-gray-600 text-gray-300 hover:bg-gray-800/50 px-8 py-6 text-lg font-semibold group transition-all duration-300"
>
<Github className="mr-2 h-5 w-5 group-hover:scale-110 transition-transform" />
View Source
<ExternalLink className="ml-2 h-4 w-4" />
</Button>
</a>
</div>
{/* Stats */}
<div className="flex justify-center gap-8 pt-12">
{stats.map((stat, index) => (
<div key={index} className="text-center">
<div className="text-2xl font-bold text-white">
{stat.value}
</div>
<div className="text-sm text-gray-400">{stat.label}</div>
</div>
))}
</div>
</div>
</div>
</section>
{/* Features Section with Hover Effects */}
<section className="py-24 relative">
<div className="container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl md:text-5xl font-bold text-white mb-6">
Powerful Features
</h2>
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
Built from the ground up for performance and accuracy in Windows
process emulation
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((feature, index) => (
<Card
key={index}
className="bg-gray-800/50 border-gray-700 hover:bg-gray-800/80 transition-all duration-200 group hover:shadow-2xl"
>
<CardHeader className="pb-4">
<div
className={`w-16 h-16 rounded-xl bg-gradient-to-br ${feature.accent} p-4 mb-4 transition-transform duration-200`}
>
<div className="text-white">{feature.icon}</div>
</div>
<CardTitle className="text-white text-xl font-semibold group-hover:text-blue-300 transition-colors">
{feature.title}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-300 leading-relaxed">
{feature.description}
</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
{/* Use Cases */}
<section className="py-24 bg-gray-900/50">
<div className="container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-white mb-6">
Perfect For Your Research
</h2>
<p className="text-xl text-gray-400">
Designed for researchers who need precise control over Windows
process execution
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-4xl mx-auto">
{useCases.map((useCase, index) => (
<div
key={index}
className="text-center p-8 rounded-2xl bg-gray-800/50 border border-gray-700 hover:border-blue-500/50 transition-all duration-300 group"
>
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-500 p-3 group-hover:scale-110 transition-transform">
<div className="text-white">{useCase.icon}</div>
</div>
<h3 className="text-xl font-semibold text-white mb-3">
{useCase.title}
</h3>
<p className="text-gray-400">{useCase.description}</p>
</div>
))}
</div>
</div>
</section>
{/* Video Section with Modern Design */}
<section className="py-24">
<div className="container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-white mb-6">
See Sogen in Action
</h2>
<p className="text-xl text-gray-400 max-w-3xl mx-auto">
Watch a comprehensive overview of the emulator's capabilities
and discover how it can accelerate your research workflow.
</p>
</div>
<div className="max-w-5xl mx-auto">
<div className="relative group">
<div className="absolute -inset-4 bg-gradient-to-r from-neutral-500/10 to-neutral-500/10 rounded-3xl blur-xl group-hover:blur-2xl transition-all duration-300"></div>
<div className="relative aspect-video rounded-2xl overflow-hidden border border-gray-700">
<iframe
className="w-full h-full"
src="https://www.youtube.com/embed/wY9Q0DhodOQ?si=Lm_anpeBU6Txl5AW"
title="Sogen Emulator Overview"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
></iframe>
</div>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-24 bg-gradient-to-r from-gray-900 to-zinc-900">
<div className="container mx-auto px-6 text-center">
<h2 className="text-4xl font-bold text-white mb-6">
Ready to Start Emulating?
</h2>
<p className="text-xl text-gray-300 mb-8 max-w-2xl mx-auto">
Join researchers worldwide who trust Sogen for their Windows
emulation needs.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href="#/playground">
<Button
size="lg"
className="bg-white text-blue-900 hover:bg-blue-50 px-8 py-6 text-lg font-semibold"
>
<Play className="mr-2 h-5 w-5" />
Launch Playground
</Button>
</a>
<a href="https://github.com/momo5502/sogen" target="_blank">
<Button
size="lg"
variant="outline"
className="border-white text-white hover:bg-white/10 px-8 py-6 text-lg font-semibold"
>
<Download className="mr-2 h-5 w-5" />
Download Source
</Button>
</a>
</div>
</div>
</section>
{/* Footer */}
<footer className="py-16 border-t border-gray-800">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-8 md:mb-0 text-center md:text-left">
<h2 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
Sogen
</h2>
<p className="mt-2 text-gray-400 text-lg">
Windows User Space Emulator
</p>
<p className="mt-1 text-gray-500 text-sm">
Built by{" "}
<a
href="https://momo5502.com"
className="underline"
target="_blank"
>
momo5502
</a>{" "}
with lots of help of{" "}
<a
href="https://github.com/momo5502/sogen/graphs/contributors"
className="underline"
target="_blank"
>
the community
</a>
.
</p>
</div>
<div className="flex items-center space-x-6">
<a
href="https://github.com/momo5502/sogen"
target="_blank"
className="text-gray-400 hover:text-blue-400 transition-colors p-2 rounded-lg hover:bg-gray-800/50"
>
<Github className="h-6 w-6" />
</a>
<a
href="#/playground"
className="text-gray-400 hover:text-blue-400 transition-colors p-2 rounded-lg hover:bg-gray-800/50"
>
<Play className="h-6 w-6" />
</a>
<a
href="https://github.com/momo5502/sogen/wiki"
target="_blank"
className="text-gray-400 hover:text-blue-400 transition-colors p-2 rounded-lg hover:bg-gray-800/50"
>
<BookOpen className="h-6 w-6" />
</a>
</div>
</div>
</div>
</footer>
</div>
</>
);
}

6
page/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

10
page/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);

241
page/src/pe-icon-parser.tsx Normal file
View File

@@ -0,0 +1,241 @@
import * as PE from "pe-library";
function patchExeFile(exe: PE.NtExecutable) {
// The PE library doesn't support parsing resources if other sections follow
// This might make sense, as the library will have issues rewriting the PE file.
// As we only care about parsing though, just kill the other sections.
const rsrc = exe.getSectionByEntry(PE.Format.ImageDirectoryEntry.Resource);
const orig = exe.getAllSections.bind(exe);
exe.getAllSections = function () {
let x = { skip: false };
return orig().filter((s) => {
if (x.skip) {
return false;
}
if (s == rsrc) {
x.skip = true;
}
return true;
});
};
}
function arrayBufferToBase64(bytes: Uint8Array) {
let binary = "";
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
function isPng(buffer: Uint8Array) {
if (buffer.length < 4) {
return false;
}
return buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71;
}
function generateDataURL(arrayBuffer: Uint8Array, contentType: string) {
const base64 = arrayBufferToBase64(arrayBuffer);
return `data:${contentType};base64,${base64}`;
}
interface IconEntry {
width: number;
height: number;
colorCount: number;
reserved: number;
planes: number;
bitCount: number;
bytesInRes: number;
id: number;
}
interface IconGroup {
reserved: number;
type: number;
icons: IconEntry[];
}
function writeUint8(buffer: Uint8Array, offset: number, value: number) {
buffer[offset] = value;
}
function writeUint16(buffer: Uint8Array, offset: number, value: number) {
writeUint8(buffer, offset + 0, value & 0xff);
writeUint8(buffer, offset + 1, (value >> 8) & 0xff);
}
function writeUint32(buffer: Uint8Array, offset: number, value: number) {
writeUint16(buffer, offset + 0, value & 0xffff);
writeUint16(buffer, offset + 2, (value >> 16) & 0xffff);
}
function readUInt8(buffer: Uint8Array, offset: number) {
return buffer[offset];
}
function readUInt16(buffer: Uint8Array, offset: number) {
return readUInt8(buffer, offset) | (readUInt8(buffer, offset + 1) << 8);
}
function readUInt32(buffer: Uint8Array, offset: number) {
return readUInt16(buffer, offset) | (readUInt16(buffer, offset + 2) << 16);
}
function parseIconGroup(buffer: Uint8Array): IconGroup {
const reserved = readUInt16(buffer, 0);
const type = readUInt16(buffer, 2);
const count = readUInt16(buffer, 4);
const icons: IconEntry[] = [];
for (let i = 0; i < count; ++i) {
const start = 6 + i * 14;
const width = readUInt8(buffer, start + 0);
const height = readUInt8(buffer, start + 1);
const colorCount = readUInt8(buffer, start + 2);
const reserved2 = readUInt8(buffer, start + 3);
const planes = readUInt16(buffer, start + 4);
const bitCount = readUInt16(buffer, start + 6);
const bytesInRes = readUInt32(buffer, start + 8);
const id = readUInt16(buffer, start + 12);
icons.push({
width,
height,
colorCount,
reserved: reserved2,
planes,
bitCount,
bytesInRes,
id,
});
}
return {
reserved,
type,
icons,
};
}
function mergeArrayBuffers(
buffer1: ArrayBuffer,
buffer2: ArrayBuffer,
): ArrayBuffer {
const mergedBuffer = new ArrayBuffer(buffer1.byteLength + buffer2.byteLength);
const view1 = new Uint8Array(buffer1);
const view2 = new Uint8Array(buffer2);
const mergedView = new Uint8Array(mergedBuffer);
mergedView.set(view1, 0);
mergedView.set(view2, buffer1.byteLength);
return mergedBuffer;
}
function generateIcoHeader(icon: IconEntry) {
const headerSize = 0x16;
const header = new Uint8Array(headerSize);
writeUint8(header, 2, 1); // Image type -> ico
writeUint8(header, 4, 1); // Image count
const start = 6;
writeUint8(header, start + 0, icon.width);
writeUint8(header, start + 1, icon.height);
writeUint8(header, start + 2, icon.colorCount);
writeUint16(header, start + 4, icon.planes);
writeUint16(header, start + 6, icon.bitCount);
writeUint32(header, start + 8, icon.bytesInRes);
writeUint32(header, start + 12, headerSize);
return header;
}
function isMaxResIcon(icon: IconEntry) {
return icon.width == 0 && icon.height == 0;
}
function getBiggestIcon(group: IconGroup) {
if (group.icons.length == 0) {
return null;
}
var biggest = group.icons[0];
if (isMaxResIcon(biggest)) {
return biggest;
}
for (let i = 1; i < group.icons.length; ++i) {
let current = group.icons[i];
if (isMaxResIcon(current)) {
return current;
}
if (current.width * current.height > biggest.width * biggest.height) {
biggest = current;
}
}
return biggest;
}
function getPeResources(data: Uint8Array) {
const exe = PE.NtExecutable.from(data, { ignoreCert: true });
patchExeFile(exe);
return PE.NtExecutableResource.from(exe, true);
}
function getIconDataUrl(iconEntry: IconEntry, iconData: ArrayBuffer) {
let contentType = "image/png";
if (!isPng(new Uint8Array(iconData))) {
contentType = "image/x-icon";
const header = generateIcoHeader(iconEntry);
iconData = mergeArrayBuffers(header, iconData);
}
return generateDataURL(new Uint8Array(iconData), contentType);
}
function tryParsePeIcon(data: Uint8Array) {
const res = getPeResources(data);
const icons = res.entries.filter((e) => e.type == 3);
const iconGroups = res.entries.filter((e) => e.type == 14);
if (iconGroups.length == 0 || icons.length == 0) {
return null;
}
const groupData = new Uint8Array(iconGroups[0].bin);
const group = parseIconGroup(groupData);
const iconEntry = getBiggestIcon(group);
if (!iconEntry) {
return null;
}
const icon = icons.find((i) => i.id == iconEntry.id);
if (!icon) {
return null;
}
return getIconDataUrl(iconEntry, icon.bin);
}
export function parsePeIcon(data: Uint8Array) {
try {
return tryParsePeIcon(data);
} catch (e) {
// console.error(e);
return null;
}
}

352
page/src/playground.tsx Normal file
View File

@@ -0,0 +1,352 @@
import React from "react";
import { Output } from "@/components/output";
import { Emulator, EmulationState, isFinalState } from "./emulator";
import { Filesystem, setupFilesystem } from "./filesystem";
import { memory64 } from "wasm-feature-detect";
import "./App.css";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Settings, loadSettings, saveSettings } from "./settings";
import { SettingsMenu } from "@/components/settings-menu";
import {
PlayFill,
StopFill,
GearFill,
PauseFill,
HouseFill,
} from "react-bootstrap-icons";
import { StatusIndicator } from "@/components/status-indicator";
import { Header } from "./Header";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer";
import { FilesystemExplorer } from "./filesystem-explorer";
import { EmulationStatus } from "./emulator";
import { TextTooltip } from "./components/text-tooltip";
import { EmulationSummary } from "./components/emulation-summary";
interface PlaygroundProps {}
interface PlaygroundState {
settings: Settings;
filesystemPromise?: Promise<Filesystem>;
filesystem?: Filesystem;
emulator?: Emulator;
emulationStatus?: EmulationStatus;
application?: string;
drawerOpen: boolean;
allowWasm64: boolean;
}
function makePercentHandler(
handler: (percent: number) => void,
): (current: number, total: number) => void {
const progress = {
tracked: 0,
};
return (current, total) => {
if (total == 0) {
return;
}
const percent = Math.floor((current * 100) / total);
const sanePercent = Math.max(Math.min(percent, 100), 0);
if (sanePercent + 1 > progress.tracked) {
progress.tracked = sanePercent + 1;
handler(sanePercent);
}
};
}
export class Playground extends React.Component<
PlaygroundProps,
PlaygroundState
> {
private output: React.RefObject<Output | null>;
private iconCache: Map<string, string | null> = new Map();
constructor(props: PlaygroundProps) {
super(props);
this.output = React.createRef();
this.start = this.start.bind(this);
this.resetFilesys = this.resetFilesys.bind(this);
this.createEmulator = this.createEmulator.bind(this);
this.toggleEmulatorState = this.toggleEmulatorState.bind(this);
this.state = {
settings: loadSettings(),
drawerOpen: false,
allowWasm64: false,
};
}
componentDidMount(): void {
memory64().then((allowWasm64) => {
this.setState({ allowWasm64 });
});
}
async resetFilesys() {
if (!this.state.filesystem) {
return;
}
await this.state.filesystem.delete();
this.setState({
filesystemPromise: undefined,
filesystem: undefined,
drawerOpen: false,
});
this.output.current?.clear();
location.reload();
}
_onEmulatorStatusChanged(s: EmulationStatus) {
this.setState({ emulationStatus: s });
}
_onEmulatorStateChanged(s: EmulationState, persistFs: boolean) {
if (isFinalState(s) && persistFs) {
this.setState({ filesystemPromise: undefined, filesystem: undefined });
this.initFilesys(true);
} else {
this.forceUpdate();
}
}
initFilesys(force: boolean = false) {
if (!force && this.state.filesystemPromise) {
return this.state.filesystemPromise;
}
const promise = new Promise<Filesystem>((resolve) => {
if (!force) {
this.logLine("Loading filesystem...");
}
setupFilesystem(
(current, total, file) => {
this.logLine(`Processing filesystem (${current}/${total}): ${file}`);
},
makePercentHandler((percent) => {
this.logLine(`Downloading filesystem: ${percent}%`);
}),
).then(resolve);
});
promise.then((filesystem) => this.setState({ filesystem }));
this.setState({ filesystemPromise: promise });
return promise;
}
setDrawerOpen(drawerOpen: boolean) {
this.setState({ drawerOpen });
}
async start() {
await this.initFilesys();
this.setDrawerOpen(true);
}
logLine(line: string) {
this.output.current?.logLine(line);
}
logLines(lines: string[]) {
this.output.current?.logLines(lines);
}
isEmulatorPaused() {
return (
this.state.emulator &&
this.state.emulator.getState() == EmulationState.Paused
);
}
toggleEmulatorState() {
if (this.isEmulatorPaused()) {
this.state.emulator?.resume();
} else {
this.state.emulator?.pause();
}
}
async createEmulator(userFile: string) {
this.state.emulator?.stop();
this.output.current?.clear();
this.setDrawerOpen(false);
this.logLine("Starting emulation...");
if (this.state.filesystemPromise) {
await this.state.filesystemPromise;
}
const persistFs = this.state.settings.persist;
const new_emulator = new Emulator(
(l) => this.logLines(l),
(s) => this._onEmulatorStateChanged(s, persistFs),
(s) => this._onEmulatorStatusChanged(s),
);
//new_emulator.onTerminate().then(() => this.setState({ emulator: null }));
this.setState({ emulator: new_emulator, application: userFile });
new_emulator.start(this.state.settings, userFile);
}
render() {
return (
<>
<Header
title="Sogen - Playground"
description="Playground to test and run Sogen, the Windows user space emulator, right in your browser."
preload={
[
/*"./emulator-worker.js", "./analyzer.js", "./analyzer.wasm"*/
]
}
/>
<div className="h-[100dvh] flex flex-col">
<header className="flex shrink-0 items-center gap-2 border-b p-2 overflow-y-auto">
<a href="#/">
<Button size="sm" variant="secondary" className="fancy">
<HouseFill />
</Button>
</a>
<Button size="sm" className="fancy" onClick={this.start}>
<PlayFill /> <span>Start</span>
</Button>
<Button
disabled={
!this.state.emulator ||
isFinalState(this.state.emulator.getState())
}
size="sm"
variant="secondary"
className="fancy"
onClick={() => this.state.emulator?.stop()}
>
<StopFill /> <span className="hidden sm:inline">Stop</span>
</Button>
<Button
size="sm"
disabled={
!this.state.emulator ||
isFinalState(this.state.emulator.getState())
}
variant="secondary"
className="fancy"
onClick={this.toggleEmulatorState}
>
{this.isEmulatorPaused() ? (
<>
<PlayFill /> <span className="hidden sm:inline">Resume</span>
</>
) : (
<>
<PauseFill /> <span className="hidden sm:inline">Pause</span>
</>
)}
</Button>
<Popover>
<PopoverTrigger asChild>
<Button size="sm" variant="secondary" className="fancy">
<GearFill />{" "}
<span className="hidden sm:inline">Settings</span>
</Button>
</PopoverTrigger>
<PopoverContent>
<SettingsMenu
settings={this.state.settings}
allowWasm64={this.state.allowWasm64}
onChange={(s) => {
saveSettings(s);
this.setState({ settings: s });
}}
/>
</PopoverContent>
</Popover>
{!this.state.filesystem ? (
<></>
) : (
<Drawer
open={this.state.drawerOpen}
onOpenChange={(o) => this.setState({ drawerOpen: o })}
>
<DrawerContent className="!will-change-auto">
<DrawerHeader>
<DrawerTitle className="hidden">
Filesystem Explorer
</DrawerTitle>
<DrawerDescription className="hidden">
Filesystem Explorer
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<FilesystemExplorer
filesystem={this.state.filesystem}
iconCache={this.iconCache}
runFile={this.createEmulator}
resetFilesys={this.resetFilesys}
path={["c"]}
/>
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
{/* Separator */}
<div className="flex-1"></div>
<div className="text-right items-center">
<StatusIndicator
application={this.state.application}
state={
this.state.emulator
? this.state.emulator.getState()
: EmulationState.Stopped
}
/>
</div>
</header>
<div className="flex flex-1">
<EmulationSummary status={this.state.emulationStatus} />
<div className="flex flex-1 flex-col pl-1 overflow-auto">
<Output ref={this.output} />
</div>
</div>
</div>
</>
);
}
}

73
page/src/settings.ts Normal file
View File

@@ -0,0 +1,73 @@
export interface Settings {
verbose: boolean;
concise: boolean;
silent: boolean;
bufferStdout: boolean;
persist: boolean;
execAccess: boolean;
wasm64: boolean;
}
export function createDefaultSettings(): Settings {
return {
verbose: false,
concise: false,
silent: false,
bufferStdout: true,
persist: false,
execAccess: true,
wasm64: false,
};
}
export function loadSettings(): Settings {
const defaultSettings = createDefaultSettings();
const settingsStr = localStorage.getItem("settings");
if (!settingsStr) {
return defaultSettings;
}
try {
const userSettings = JSON.parse(settingsStr);
const keys = Object.keys(defaultSettings);
keys.forEach((k) => {
if (k in userSettings) {
(defaultSettings as any)[k] = userSettings[k];
}
});
} catch (e) {}
return defaultSettings;
}
export function saveSettings(settings: Settings) {
localStorage.setItem("settings", JSON.stringify(settings));
}
export function translateSettings(settings: Settings): string[] {
const switches: string[] = [];
if (settings.verbose) {
switches.push("-v");
}
if (settings.concise) {
switches.push("-c");
}
if (settings.silent) {
switches.push("-s");
}
if (settings.bufferStdout) {
switches.push("-b");
}
if (settings.execAccess) {
switches.push("-x");
}
return switches;
}

1
page/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

42
page/src/zip-file.ts Normal file
View File

@@ -0,0 +1,42 @@
import JSZip from "jszip";
export type ProgressHandler = (
processed: number,
total: number,
filename: string,
) => void;
export interface FileEntry {
name: string;
data: ArrayBuffer;
}
export async function parseZipFile(
arrayBuffer: ArrayBuffer,
progressHandler?: ProgressHandler,
) {
const zip = await JSZip.loadAsync(arrayBuffer);
const files: Promise<FileEntry>[] = [];
const progress = {
files: 0,
processed: 0,
};
zip.forEach(function (relativePath, zipEntry) {
progress.files += 1;
files.push(
zipEntry.async("arraybuffer").then((data) => {
progress.processed += 1;
if (progressHandler) {
progressHandler(progress.processed, progress.files, relativePath);
}
return { name: relativePath, data };
}),
);
});
return await Promise.all(files);
}

30
page/tsconfig.app.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

Some files were not shown because too many files have changed in this diff Show More